How do I calculate relative URL using standard browser APIs - javascript

The standard URL object can be used to calculate an absolute URL from a relative URL and a base URL as follows.
const base = 'http://example.com/'
const relative = '/foo/bar?quux=123'
const absolute = new URL(relative, base).href
console.assert(absolute === 'http://example.com/foo/bar?quux=123')
However, I could not figure out how to use the URL object to do the reverse.
const base = 'http://example.com/'
const absolute = 'http://example.com/foo/bar?quux=123'
const relative = '???'
console.assert(relative === '/foo/bar?quux=123')
Do the browser APIs provide a standardised way for constructing relative URLs or do I need to use a 3rd party solution?

Do the browser APIs provide a standardised way for constructing
relative URLs?
Yes, they do. You already used it, URL
Alternatively, you can create a temporary <a>-element and get the values from that. A freshly created <a>-element or URL both implement location, so you can extract location-properties:
// use <a href ...>
const absolute = `http://example.com/foo/bar?quux=123`;
const hrefTmp = document.createElement(`a`);
hrefTmp.href = absolute;
console.log(`Absolute from <a>: ${hrefTmp.href}`);
console.log(`Relative from <a>: ${hrefTmp.pathname}${hrefTmp.search}`);
// using URL
const url = new URL(absolute);
console.log(`Absolute from url: ${url.href}`);
console.log(`Relative from url: ${url.pathname}${url.search}`);
// using URL with a different base path
const baseOther = `http://somewhere.eu`;
const urlOther = new URL(`${url.pathname}${url.search}`, baseOther );
console.log(`Absolute from urlOther: ${urlOther.href}`);
console.log(`Relative from urlOther: ${urlOther.pathname}${urlOther.search}`);
.as-console-wrapper { top: 0; max-height: 100% !important; }

I ended up doing the following.
const base = 'http://example.com/'
const absolute = 'http://example.com/foo/bar?quux=123'
const relative = ((temp) => {
return absolute.startsWith(base) ? temp.pathname.concat(temp.search) : temp.href
})(new URL(absolute, base))
console.assert(relative === '/foo/bar?quux=123')

There's an npm module called relateurl that works well but its dependency on url (note lower-case) causes mild trouble in the latest Webpack and React. I published another called relativize-url that uses URL (shouty-caps), which is supported everywhere. It's pretty minimal so you can install it or just steal the code from index.js.
const components = [
{name: 'protocol', write: u => u.protocol },
{name: 'hostname', write: u => '//' + u.hostname },
{name: 'port', write: u => u.port === '' ? '' : (':' + u.port) },
{name: 'pathname', write: (u, frm, relativize) => {
if (!relativize) return u.pathname;
const f = frm.pathname.split('/').slice(1);
const t = u.pathname.split('/').slice(1);
const maxDepth = Math.max(f.length, t.length);
let start = 0;
while(start < maxDepth && f[start] === t[start]) ++start;
const rel = f.slice(start+1).map(c => '..')
.concat(t.slice(start)).join('/');
return rel.length <= u.pathname.length ? rel : u.pathname
}},
{name: 'search', write: u => u.search },
{name: 'hash', write: u => u.hash},
];
function relativize (rel, base, opts = {}) { // opts not yet used
const from = new URL(base);
const to = new URL(rel, from);
let ret = '';
for (let component of components) {
if (ret) { // force abs path if e.g. host was diffferent
ret += component.write(to, from, false);
} else if (from[component.name] !== to[component.name]) {
ret = component.write(to, from, true);
}
}
return ret;
}
The pathname handler has extra code in it to give you nice minimal relative paths. Give it some exercise:
const base = 'http://a.example/b/e/f?g=h#i'
const target = 'http://a.example/b/c/d?j=k#l'
console.log(relativize(target, base))
// got '../c/d'; let's check it:
console.log(new URL('../c/d', base).href === target)
// true
console.log(relativize('http://a.example/b?a=b','http://a.example/b?c=d'))
// ?a=b
console.log(relativize('http://a.example/b#asdf', 'http://a.example/b'))
// #asdf
console.log(relativize('http://a.example/b', 'http://c.example/d'))
// //a.example/b
Please report bugs in https://github.com/ericprud/relativize-url/issues .

Related

check value of parameters in URL with Cypress

In our tool we create a url which quiet a few parameters with values. And I want Cypress to check the contents of this url.
The example url is:
http://someUrl.com/sap/?action=create&type=sw&notifno=70432&repby=TRL&repres=ABC&geo=017&startloc=12345&notiftp=2021-06-15T08:06:42.379Z&scen=1.0&refno=1234567&awsrt=-&vrst=&sbst=&objtp=art&objtxt=&objfc=&tel=084123456&prio=4 Niet urgent&priost=&prioen=&wbi=&facts=&bgeb=AB-CD&bequi=
I have stored the url in 'href' variable but how i can now check all the attr and their values? I really don't have a clue.
I'd parse it into an object and then use .wrap(), .its(), and .should() commands:
const url = "http://someUrl.com/sap/?action=create&type=sw&notifno=70432&repby=TRL&repres=ABC&geo=017&startloc=12345&notiftp=2021-06-15T08:06:42.379Z&scen=1.0&refno=1234567&awsrt=-&vrst=&sbst=&objtp=art&objtxt=&objfc=&tel=084123456&prio=4 Niet urgent&priost=&prioen=&wbi=&facts=&bgeb=AB-CD&bequi=";
const arr = url.split('/?')[1].split('&');
const paramObj = {};
arr.forEach(param => {
const [ key, value ] = param.split('=');
paramObj[key] = value;
});
cy
.wrap(paramObj)
.its('tel')
.should('eq', '084123456');
or if you want to assert more properties:
cy
.wrap(paramObj)
.then(obj => {
expect(obj.notifno).to.eq('70432');
expect(obj.tel).to.eq('084123456');
});
My colleague came with this solution, now the Cucumber line included:
Given('I expect the parameter {string} of the SAP-link on dossier {string} to equal {string}',(parameter:string, dossier:string, value:string) => {
cy.get('selector').each(ele => {
if(ele.text().trim().indexOf(dossier) == 0) {
cy.get('selector')
.parents('selector')
.find('selector').should('have.attr', 'href').then((sapUrl: JQuery<HTMLElement>) => {
cy.log(sapUrl.toString());
const queryParamString: string = sapUrl.toString().split('?')[1];
cy.log(queryParamString);
const queryParamArray: string[] = queryParamString.split('&');
var params: {} = {};
queryParamArray.forEach((keyValueString: string) => {
const currentParamArray: string[] = keyValueString.split('=');
params[currentParamArray[0]] = currentParamArray[1];
});
// Actual param check
expect(params[parameter]).to.equal(value);
});
}
});
});

How can I extract path parameters from url?

Let's say I have an object that contains the values:
const pathParams = { schoolId :'12', classroomId: 'j3'}
and I have a path: school/:schoolId/classroom/:classroomId
I would like to extract: 'schoolId' and 'classroomId' from the path so that I can later replace them with their corresponding values from the pathParams object. Rather than iterating from the pathParam object keys, I want to do the other way around, check what keys the path needs and then check their values in the object.
I currently have this:
function addPathParams(path, paramsMap) {
const pathParamsRegex = /(:[a-zA-Z]+)/g;
const params = path.match(pathParamsRegex); // eg. school/:schoolId/classroom/:classroomId -> [':schoolId', ':classroomId']
console.log('--params--', params)
let url = path;
params.forEach((param) => {
const paramKey = param.substring(1); // remove ':'
url = addPathParam(url, param, paramsMap[paramKey]);
});
return url;
}
function addPathParam(path, param, value) {
return path.replace(`${param}`, value);
}
Does this look robust enough to you? Is there any other case I should be considering?
Here's a couple of tests I did:
Result:

RegEx that match react router path declaration

I have a map of routes with react router path as keys, eg:
const routes = [
{
page: "mySettings",
label: "pages.mySettings",
path: "/professionels/mes-reglages.html",
exact: true
},
{
page: "viewUser",
label: "pages.viewUser",
path: "/users/:id/view.html",
exact: true
}
];
I want from a location retrieved with useHistory().location.pathname, to match all the path that match the key in react-router terms, eg:
(pathname) => get(routesMap, "/professionels/mes-reglages.html") => should match routesMap.get('/professionels/mes-reglages.html')
(pathname) => get(routesMap, "/users/11/view.html") => should match routesMap.get('/users/:id/view.html')
and all react-router paths so this should work too:
(pathname) => get(routesMap, "/users/11/settings/10/items/24/view.html") => should match routesMap.get('/users/:userId/settings/:settingId/items/:id/view.html')
I have started here, any idea how I can do that with a regexp?
https://codesandbox.io/s/youthful-wing-fjgm1
Based on your comments i adjusted the code a bit and wrote a rapper function for your lookup.
The following rules you have to watch out for when creating the urls:
The last id always gets replaced by {id}
All other ids get replaced by url part to id without plural and "Id" attached ("/users/111" -> "/users/{userId}")
This would be the function:
const getRouteFromPath = (map, url) => {
if (url.match(/\/\d+\//g).length > 1) {
let allowedUrlPart = getAllowedIdQualifier(map);
let urlParts = url.match(/(?<=\/)\w+(?=\/\d+\/)/g);
urlParts.forEach(val => {
if (!allowedUrlPart.includes(val)) {
urlParts = urlParts.slice(urlParts.indexOf(val), 1);
}
});
urlParts.forEach((val, key, arr) => {
if (key === arr.length - 1) {
let regex = new RegExp("(?<=/" + val + "/)\\d+", "g");
let replacement = ":id";
url = url.replace(regex, replacement);
} else {
let regex = new RegExp("(?<=/" + val + "/)\\d+", "g");
let replacement = ":" + val.slice(0, -1) + "Id";
url = url.replace(regex, replacement);
}
});
return map.get(url);
} else {
url = url.replace(/\/\d+\//g, "/:id/");
return map.get(url);
}
};
const getAllowedIdQualifier = map => {
let allowedQualifiers = [];
map.forEach(val => {
let allowed = val.path.match(/(?<=\/)\w+(?=\/:)/g);
allowed.forEach(e => {
if (!allowedQualifiers.includes(e)) {
allowedQualifiers.push(e);
}
});
});
return allowedQualifiers;
};
export default getRouteFromPath;
As parameter you pass in the url to match as first parameter and the map of routes as the second paramter and call the function getRoute() instead of the direct map.get() call you where using before.
Here is the example with the urls adjusted to follow the rules, since you need some rules to be able to apply RegEx.
EDIT:
I adjusted the script, so that it reads the map first and determines the allowed paths which accept a id and then check the possible ids from an actual url against it.
https://codesandbox.io/s/kind-moon-9oyj9?fontsize=14&hidenavigation=1&theme=dark

Let statement in Explorer?

My project (with HTML and JavaScript) doesn't work in Internet Explorer and I think that is because I use a let statement in a JS method. Can I add some external libraries that makes it possible to read the let statements? Or can I rewrite the function? I don't really know how the let method works so can anyone tell me how to rewrite these lines:
initializeData = function()
{
//Check URL and QueryString
var url = window.location.href.toString();
var queryString = (url.split("?"))[1];
if (queryString == undefined || queryString=="" || queryString==null)
resetData();
else //filter data based on URL QueryString
{
const query = decodeURI(queryString);
const result = query.split('&');
result.forEach(function(item){
const [cat, values] = item.split('='); //ERROR
const isArray = cat.endsWith('[]');
let pair;
if (isArray)
{
const p = values;
pair = { cat, 'values': [values] };
}
else
{
pair = {cat, values };
}
currentFilters.push(pair);
});
RunFilter();
}
}
I need pair to be on the same structure since I am using it in the RunFilter() method.

How to build query string with Javascript

Just wondering if there is anything built-in to Javascript that can take a Form and return the query parameters, eg: "var1=value&var2=value2&arr[]=foo&arr[]=bar..."
I've been wondering this for years.
The URLSearchParams API is available in all modern browsers. For example:
const params = new URLSearchParams({
var1: "value",
var2: "value2",
arr: "foo",
});
console.log(params.toString());
//Prints "var1=value&var2=value2&arr=foo"
2k20 update: use Josh's solution with URLSearchParams.toString().
Old answer:
Without jQuery
var params = {
parameter1: 'value_1',
parameter2: 'value 2',
parameter3: 'value&3'
};
var esc = encodeURIComponent;
var query = Object.keys(params)
.map(k => esc(k) + '=' + esc(params[k]))
.join('&');
For browsers that don't support arrow function syntax which requires ES5, change the .map... line to
.map(function(k) {return esc(k) + '=' + esc(params[k]);})
If you're using jQuery you might want to check out jQuery.param() http://api.jquery.com/jQuery.param/
Example:
var params = {
parameter1: 'value1',
parameter2: 'value2',
parameter3: 'value3'
};
var query = $.param(params);
console.log(query);
This will print out:
parameter1=value1&parameter2=value2&parameter3=value3
This doesn't directly answer your question, but here's a generic function which will create a URL that contains query string parameters. The parameters (names and values) are safely escaped for inclusion in a URL.
function buildUrl(url, parameters){
var qs = "";
for(var key in parameters) {
var value = parameters[key];
qs += encodeURIComponent(key) + "=" + encodeURIComponent(value) + "&";
}
if (qs.length > 0){
qs = qs.substring(0, qs.length-1); //chop off last "&"
url = url + "?" + qs;
}
return url;
}
// example:
var url = "http://example.com/";
var parameters = {
name: "George Washington",
dob: "17320222"
};
console.log(buildUrl(url, parameters));
// => http://www.example.com/?name=George%20Washington&dob=17320222
Create an URL object and append the values to seachParameters
let stringUrl = "http://www.google.com/search";
let url = new URL(stringUrl);
let params = url.searchParams;
params.append("q", "This is seach query");
console.log(url.toString());
The output will be
http://www.google.com/search?q=This+is+seach+query
With jQuery you can do this by $.param
$.param({ action: 'ship', order_id: 123, fees: ['f1', 'f2'], 'label': 'a demo' })
// -> "action=ship&order_id=123&fees%5B%5D=f1&fees%5B%5D=f2&label=a+demo"
ES2017 (ES8)
Making use of Object.entries(), which returns an array of object's [key, value] pairs. For example, for {a: 1, b: 2} it would return [['a', 1], ['b', 2]]. It is not supported (and won't be) only by IE.
Code:
const buildURLQuery = obj =>
Object.entries(obj)
.map(pair => pair.map(encodeURIComponent).join('='))
.join('&');
Example:
buildURLQuery({name: 'John', gender: 'male'});
Result:
"name=John&gender=male"
querystring can help.
So, you can
const querystring = require('querystring')
url += '?' + querystring.stringify(parameters)
No, I don't think standard JavaScript has that built in, but Prototype JS has that function (surely most other JS frameworks have too, but I don't know them), they call it serialize.
I can reccomend Prototype JS, it works quite okay. The only drawback I've really noticed it it's size (a few hundred kb) and scope (lots of code for ajax, dom, etc.). Thus if you only want a form serializer it's overkill, and strictly speaking if you only want it's Ajax functionality (wich is mainly what I used it for) it's overkill. Unless you're careful you may find that it does a little too much "magic" (like extending every dom element it touches with Prototype JS functions just to find elements) making it slow on extreme cases.
If you don't want to use a library, this should cover most/all of the same form element types.
function serialize(form) {
if (!form || !form.elements) return;
var serial = [], i, j, first;
var add = function (name, value) {
serial.push(encodeURIComponent(name) + '=' + encodeURIComponent(value));
}
var elems = form.elements;
for (i = 0; i < elems.length; i += 1, first = false) {
if (elems[i].name.length > 0) { /* don't include unnamed elements */
switch (elems[i].type) {
case 'select-one': first = true;
case 'select-multiple':
for (j = 0; j < elems[i].options.length; j += 1)
if (elems[i].options[j].selected) {
add(elems[i].name, elems[i].options[j].value);
if (first) break; /* stop searching for select-one */
}
break;
case 'checkbox':
case 'radio': if (!elems[i].checked) break; /* else continue */
default: add(elems[i].name, elems[i].value); break;
}
}
}
return serial.join('&');
}
You can do that nowadays with FormData and URLSearchParams without the need to loop over anything.
const formData = new FormData(form);
const searchParams = new URLSearchParams(formData);
const queryString = searchParams.toString();
Older browsers will need a polyfill, though.
Might be a bit redundant but the cleanest way i found which builds on some of the answers here:
const params: {
key1: 'value1',
key2: 'value2',
key3: 'value3',
}
const esc = encodeURIComponent;
const query = Object.keys(params)
.map(k => esc(k) + '=' + esc(params[k]))
.join('&');
return fetch('my-url', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: query,
})
Source
I'm not entirely certain myself, I recall seeing jQuery did it to an extent, but it doesn't handle hierarchical records at all, let alone in a php friendly way.
One thing I do know for certain, is when building URLs and sticking the product into the dom, don't just use string-glue to do it, or you'll be opening yourself to a handy page breaker.
For instance, certain advertising software in-lines the version string from whatever runs your flash. This is fine when its adobes generic simple string, but however, that's very naive, and blows up in an embarrasing mess for people whom have installed Gnash, as gnash'es version string happens to contain a full blown GPL copyright licences, complete with URLs and <a href> tags. Using this in your string-glue advertiser generator, results in the page blowing open and having imbalanced HTML turning up in the dom.
The moral of the story:
var foo = document.createElement("elementnamehere");
foo.attribute = allUserSpecifiedDataConsideredDangerousHere;
somenode.appendChild(foo);
Not:
document.write("<elementnamehere attribute=\""
+ ilovebrokenwebsites
+ "\">"
+ stringdata
+ "</elementnamehere>");
Google need to learn this trick. I tried to report the problem, they appear not to care.
You don't actually need a form to do this with Prototype. Just use Object.toQueryString function:
Object.toQueryString({ action: 'ship', order_id: 123, fees: ['f1', 'f2'], 'label': 'a demo' })
// -> 'action=ship&order_id=123&fees=f1&fees=f2&label=a%20demo'
I know this is very late answer but works very well...
var obj = {
a:"a",
b:"b"
}
Object.entries(obj).map(([key, val])=>`${key}=${val}`).join("&");
note: object.entries will return key,values pairs
output from above line will be a=a&b=b
Hope its helps someone.
Happy Coding...
The UrlSearchParams API is a great suggestion, but I can't believe nobody mentioned the incredibly useful .get and .set methods. They can be used to manipulate the query string and not only they're very easy to use, they also solve a number of issues you might encounter. For example, in my case I wanted to build a query string without duplicate keys. .set solves this problem for you. Quoting from the MDN docs:
URLSearchParams.set()
Sets the value associated with a given search parameter to the given value. If there are several values, the others are deleted.
Example (from MDN):
let url = new URL('https://example.com?foo=1&bar=2');
let params = new URLSearchParams(url.search);
// Add a third parameter
params.set('baz', 3);
params.toString(); // "foo=1&bar=2&baz=3"
Alternative, shorter syntax:
let url = new URL('https://example.com?foo=1&bar=2');
// Add a third parameter
url.searchParams.set('baz', 3);
url.searchParams.toString(); // "foo=1&bar=2&baz=3"
As Stein says, you can use the prototype javascript library from http://www.prototypejs.org.
Include the JS and it is very simple then, $('formName').serialize() will return what you want!
For those of us who prefer jQuery, you would use the form plugin: http://plugins.jquery.com/project/form, which contains a formSerialize method.
Is is probably too late to answer your question.
I had the same question and I didn't like to keep appending strings to create a URL. So, I started using $.param as techhouse explained.
I also found a URI.js library that creates the URLs easily for you. There are several examples that will help you: URI.js Documentation.
Here is one of them:
var uri = new URI("?hello=world");
uri.setSearch("hello", "mars"); // returns the URI instance for chaining
// uri == "?hello=mars"
uri.setSearch({ foo: "bar", goodbye : ["world", "mars"] });
// uri == "?hello=mars&foo=bar&goodbye=world&goodbye=mars"
uri.setSearch("goodbye", "sun");
// uri == "?hello=mars&foo=bar&goodbye=sun"
// CAUTION: beware of arrays, the following are not quite the same
// If you're dealing with PHP, you probably want the latter…
uri.setSearch("foo", ["bar", "baz"]);
uri.setSearch("foo[]", ["bar", "baz"]);`
These answers are very helpful, but i want to add another answer, that may help you build full URL.
This can help you concat base url, path, hash and parameters.
var url = buildUrl('http://mywebsite.com', {
path: 'about',
hash: 'contact',
queryParams: {
'var1': 'value',
'var2': 'value2',
'arr[]' : 'foo'
}
});
console.log(url);
You can download via npm https://www.npmjs.com/package/build-url
Demo:
;(function () {
'use strict';
var root = this;
var previousBuildUrl = root.buildUrl;
var buildUrl = function (url, options) {
var queryString = [];
var key;
var builtUrl;
var caseChange;
// 'lowerCase' parameter default = false,
if (options && options.lowerCase) {
caseChange = !!options.lowerCase;
} else {
caseChange = false;
}
if (url === null) {
builtUrl = '';
} else if (typeof(url) === 'object') {
builtUrl = '';
options = url;
} else {
builtUrl = url;
}
if(builtUrl && builtUrl[builtUrl.length - 1] === '/') {
builtUrl = builtUrl.slice(0, -1);
}
if (options) {
if (options.path) {
var localVar = String(options.path).trim();
if (caseChange) {
localVar = localVar.toLowerCase();
}
if (localVar.indexOf('/') === 0) {
builtUrl += localVar;
} else {
builtUrl += '/' + localVar;
}
}
if (options.queryParams) {
for (key in options.queryParams) {
if (options.queryParams.hasOwnProperty(key) && options.queryParams[key] !== void 0) {
var encodedParam;
if (options.disableCSV && Array.isArray(options.queryParams[key]) && options.queryParams[key].length) {
for(var i = 0; i < options.queryParams[key].length; i++) {
encodedParam = encodeURIComponent(String(options.queryParams[key][i]).trim());
queryString.push(key + '=' + encodedParam);
}
} else {
if (caseChange) {
encodedParam = encodeURIComponent(String(options.queryParams[key]).trim().toLowerCase());
}
else {
encodedParam = encodeURIComponent(String(options.queryParams[key]).trim());
}
queryString.push(key + '=' + encodedParam);
}
}
}
builtUrl += '?' + queryString.join('&');
}
if (options.hash) {
if(caseChange)
builtUrl += '#' + String(options.hash).trim().toLowerCase();
else
builtUrl += '#' + String(options.hash).trim();
}
}
return builtUrl;
};
buildUrl.noConflict = function () {
root.buildUrl = previousBuildUrl;
return buildUrl;
};
if (typeof(exports) !== 'undefined') {
if (typeof(module) !== 'undefined' && module.exports) {
exports = module.exports = buildUrl;
}
exports.buildUrl = buildUrl;
} else {
root.buildUrl = buildUrl;
}
}).call(this);
var url = buildUrl('http://mywebsite.com', {
path: 'about',
hash: 'contact',
queryParams: {
'var1': 'value',
'var2': 'value2',
'arr[]' : 'foo'
}
});
console.log(url);
var params = { width:1680, height:1050 };
var str = jQuery.param( params );
console.log(str)
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
Remove undefined params 💪😃
urlParams = obj =>{
const removeUndefined = JSON.parse(JSON.stringify(obj))
const result = new URLSearchParams(removeUndefined).toString();
return result ? `?${result}`: '';
}
console.log(urlParams({qwe: undefined, txt: 'asd'})) // '?txt=asd'
console.log(urlParams({qwe: undefined})) // ''

Categories