Adam Dawkins

📔 📄

The Hunt for the missing /: A story of anchors, location and Internet Explorer's pathname

The nice trick with anchors and location

There’s a really nice way to get the pathname off URLs that aren’t the current window.location. If you set them as the href of an anchor element, the anchor element actually implements the same location Protocol as window.

// on https://adamdawkins.uk/articles
window.location.pathname // => “/articles”

var url = document.createElement('a');
url.href = "https://adamdawkins.uk/articles";
url.pathname; // => "/articles"

Where can this be useful? On any site where you need to manipulate or analyse URLs client-side, or, as in my case, when you’re using browser.pushState but still want to read state from a location object in the same way regardless.

On a project recently I used it like this:

//    getState :: Location -> Object
const getState = (location) => ({
    page: location.pathname,
    ...getQueryParams(location)
})

I’d call getState(window.location) on page load, but, when I changed the URL with history.pushState I needed to pass that URL into my getState function as well. Instead of trying to change the simplicity of getState (takes a location, returns an object, lovely) - we can use the fact that anchors implement the location protocol to get something we can pass to this.

 //   toLocation :: URL -> <a>
const toLocation = (url) => {
  const anchor = document.createElement('a');
  anchor.href = url;
	
  return anchor
}

const newURL = setQueryParams({foo: 'bar'}) // returns the whole URL, e.g. "https://adamdawkins.uk/articles?foo=bar"
const newState = getState(toLocation(newURL))

Note the the anchor DOM Element itself implements the Location protocol, not anchor.location.

Internet Explorer and relative pathnames

Lovely stuff. Enter Internet Explorer, stage right.

Internet Explorer takes relative URLs and does not treat the initial / as part of the pathname

Using our code above:

// All other browsers 
toLocation("/articles?foo=bar").pathname // => "/articles"
// Internet Explorer
toLocation("/articles?foo=bar").pathname // => "articles"

Great, thanks.

Maybe I was just unlucky, but I was matching on that / to check which page I was on:

if (state.page.match(/\/checkout/delivery\//)) {
 // …
}

And it would work, unless I pushed state in Internet Explorer, now state.page is “checkout/delivery”.

What to fix?

There’s a dilemma here. The easiest fix is to loosen my regex on the page, and not check for a preceding /, but the problem is - that is what I mean - I do want to match /checkout/delivery, and it feels wrong to change that because IE has anxiety issues about the initial /.

The fix I went for was to create a simple pathname() function that would replace the / if it had been lopped off by IE:

//    pathname :: Location -> String
const pathname = (location) =>  (
    // in IE location.pathname strips the first `/` from relative URLs, here we add it back
    location.pathname.match(/^\//) ? location.pathname : `/${location.pathname}`
)

Phew.