I wrote this web app that splits a URL graphically so that its parts can be customized to create a new URL. https://github.com/ibrahim-islam/urlpad. The app currently works as expected for usual URls but I am not so sure about the code structure.
module UrlPad {
const map = (o: OmniboxFull): CustomOmniboxMap => {
return {
protocol: o.protocol,
host: o.host,
port: o.port,
pathnames: o.pathname.split('/').filter(char => char),
hash: o.hash,
query: o.query
};
}
class FormBuilder {
private template: string[] = [];
private for: string = '';
constructor(forClass?: string) {
this.for = forClass;
this.template.push('<div class="form-group">');
}
label(title: string) {
this.template.push(`<label class="col-sm-2">${title}</label>`);
return this;
}
input(value: string, id?: string, size: number = 10) {
let id_att = id ? 'id="'+ id +'"' : '';
this.template.push(`<div class="col-sm-${size}">
<input ${id_att} spellcheck="false" type="text" class="form-control ${this.for}" value="${value}"></div>`);
return this;
}
build() {
this.template.push('</div>')
return this.template.join('');
}
}
export function parse(url: string): string {
let o = map(omnibox.parse(url));
return Object.keys(o)
.filter((key, index) => o[key])
.map((key, index) => {
let value = o[key];
switch (key) {
case 'query':
return Object.keys(value)
.map((key, index) => new FormBuilder('query').label(`Querystring #${index}`).input(key, '', 5).input(value[key], '', 5).build())
.join('');
case 'pathnames':
return value
.map((value, index) => new FormBuilder('path').label(`Pathname #${index}`).input(value).build())
.join('');
default:
return new FormBuilder().label(key).input(value, key).build();
}
})
.join('');
}
export function make(o: CustomOmniboxMap): string {
let querystrings = Object.keys(o.query)
.map((key, index) => `${key}=${o.query[key]}`)
.join('&'),
paths = o.pathnames.join('/');
return `${o.protocol}://${o.host}${o.port ? ':' + o.port : ''}/${paths}${querystrings ? '?' + querystrings : ''}${o.hash ? o.hash : ''}`;
}
}
window["UrlPad"] = UrlPad;
I made the app related functionality into a
module
, should it have been aclass
?map
function is only used insideparse
function, should it have been a local function?When testing this app should I test only the exported functions or any class or function I wrote as well?
Any other observations or suggestions to improve the code is also welcomed.
1 Answer 1
For the most part, this looks good. However, there are a few things I'd recommend changing.
Don't use
module
.namespace
should be used instead. From the Typescript Handbook:A note about terminology: It’s important to note that in TypeScript 1.5, the nomenclature has changed. "Internal modules" are now "namespaces". "External modules" are now simply "modules", as to align with ECMAScript 2015’s terminology, (namely that
module X {
is equivalent to the now-preferrednamespace X {
).You do not properly escape special characters. If a double quote appears anywhere in the URL, the form will break. Try the url
https://example.com/?a=" >Broke
to see an example.Related to the above, building HTML with strings is an incredibly error prone process. If at all possible, avoid it. It is a better idea to use the
<template>
tag.You don't need
omnibox
browsers have a built inURL
api which covers everything thatomnibox
does and more. If you need to support IE, you can use the url-polyfill package - and gain the advantage that newer browsers don't need to load it.Consider using
Object.entries
instead ofObject.keys
as it can slightly simplify some of your code.const querystrings = Object.entries(o.query) .map(([key, value]) => `${key}=${value}`) .join('&')
For your specific questions:
A
(削除) module (削除ここまで)namespace is a reasonable choice for the app. I personally never use them, preferring to use ES Modules. A larger concern is that you export the module to the global scope unnecessarily. Instead of usingonclick="parse()"
in your HTML, it would be better to add the event handler in yourindex.ts
file and have this module in it's own file.I don't believe you even need the
map
function, it wouldn't be that big of a deal to check forpathname
instead ofpathnames
in the switch statement and split the string there. If you do keep the map function, it should be renamed to something more descriptive. When I sawmap
I expected it to be roughly equivalent toArray.prototype.map
.For tests, it depends on who you ask. Since you aren't parsing the URL yourself (good!) there isn't really too much that can go wrong here. I'd probably only write a few integration tests for this app as a whole.
make()
is exported because it will be invoked from UI. If you browseindex.html
it will make sense. I did not understand what you meant by "like view controller". Please elaborate. \$\endgroup\$