| OLD | NEW |
| (Empty) |
| 1 (function(window, document) { | |
| 2 // <app-route path="/path" [import="/page/cust-el.html"] [element="cust-el"] [
template]></app-route> | |
| 3 document.registerElement('app-route', { | |
| 4 prototype: Object.create(HTMLElement.prototype) | |
| 5 }); | |
| 6 | |
| 7 // <active-route></active-route> holds the active route's content when `shadow
` is not enabled | |
| 8 document.registerElement('active-route', { | |
| 9 prototype: Object.create(HTMLElement.prototype) | |
| 10 }); | |
| 11 | |
| 12 // <app-router [shadow] [trailingSlash="strict|ignore"] [init="auto|manual"]><
/app-router> | |
| 13 var router = Object.create(HTMLElement.prototype); | |
| 14 | |
| 15 var importedURIs = {}; | |
| 16 var isIE = 'ActiveXObject' in window; | |
| 17 | |
| 18 // fire(type, detail, node) - Fire a new CustomEvent(type, detail) on the node | |
| 19 // | |
| 20 // listen with document.querySelector('app-router').addEventListener(type, fun
ction(event) { | |
| 21 // event.detail, event.preventDefault() | |
| 22 // }) | |
| 23 function fire(type, detail, node) { | |
| 24 // create a CustomEvent the old way for IE9/10 support | |
| 25 var event = document.createEvent('CustomEvent'); | |
| 26 | |
| 27 // initCustomEvent(type, bubbles, cancelable, detail) | |
| 28 event.initCustomEvent(type, false, true, detail); | |
| 29 | |
| 30 // returns false when event.preventDefault() is called, true otherwise | |
| 31 return node.dispatchEvent(event); | |
| 32 } | |
| 33 | |
| 34 // Initial set up when attached | |
| 35 router.attachedCallback = function() { | |
| 36 if(this.getAttribute('init') !== 'manual') { | |
| 37 this.init(); | |
| 38 } | |
| 39 }; | |
| 40 | |
| 41 // Initialize the router | |
| 42 router.init = function() { | |
| 43 if (this.isInitialized) { | |
| 44 return; | |
| 45 } | |
| 46 this.isInitialized = true; | |
| 47 this.activeRoute = document.createElement('app-route'); | |
| 48 | |
| 49 // Listen for URL change events. | |
| 50 this.stateChangeHandler = this.go.bind(this); | |
| 51 window.addEventListener('popstate', this.stateChangeHandler, false); | |
| 52 if (isIE) { | |
| 53 // IE is truly special! A hashchange is supposed to trigger a popstate, ma
king popstate the only | |
| 54 // even you should need to listen to. Not the case in IE! Make another eve
nt listener for it! | |
| 55 window.addEventListener('hashchange', this.stateChangeHandler, false); | |
| 56 } | |
| 57 | |
| 58 // set up an <active-route> element for the active route's content | |
| 59 this.activeRouteContent = document.createElement('active-route'); | |
| 60 this.appendChild(this.activeRouteContent); | |
| 61 if (this.hasAttribute('shadow')) { | |
| 62 this.activeRouteContent = this.activeRouteContent.createShadowRoot(); | |
| 63 } | |
| 64 | |
| 65 // load the web component for the active route | |
| 66 this.go(); | |
| 67 }; | |
| 68 | |
| 69 // clean up global event listeners | |
| 70 router.detachedCallback = function() { | |
| 71 window.removeEventListener('popstate', this.stateChangeHandler, false); | |
| 72 if (isIE) { | |
| 73 window.removeEventListener('hashchange', this.stateChangeHandler, false); | |
| 74 } | |
| 75 }; | |
| 76 | |
| 77 // go() - Find the first <app-route> that matches the current URL and change t
he active route | |
| 78 router.go = function() { | |
| 79 var urlPath = this.parseUrlPath(window.location.href); | |
| 80 var eventDetail = { | |
| 81 path: urlPath | |
| 82 }; | |
| 83 if (!fire('state-change', eventDetail, this)) { | |
| 84 return; | |
| 85 } | |
| 86 var routes = this.querySelectorAll('app-route'); | |
| 87 for (var i = 0; i < routes.length; i++) { | |
| 88 if (this.testRoute(routes[i].getAttribute('path'), urlPath, this.getAttrib
ute('trailingSlash'), routes[i].hasAttribute('regex'))) { | |
| 89 this.activateRoute(routes[i], urlPath); | |
| 90 return; | |
| 91 } | |
| 92 } | |
| 93 fire('not-found', eventDetail, this); | |
| 94 }; | |
| 95 | |
| 96 // activateRoute(route, urlPath) - Activate the route | |
| 97 router.activateRoute = function(route, urlPath) { | |
| 98 var eventDetail = { | |
| 99 path: urlPath, | |
| 100 route: route, | |
| 101 oldRoute: this.activeRoute | |
| 102 }; | |
| 103 if (!fire('activate-route-start', eventDetail, this)) { | |
| 104 return; | |
| 105 } | |
| 106 if (!fire('activate-route-start', eventDetail, route)) { | |
| 107 return; | |
| 108 } | |
| 109 | |
| 110 this.activeRoute.removeAttribute('active'); | |
| 111 route.setAttribute('active', 'active'); | |
| 112 this.activeRoute = route; | |
| 113 | |
| 114 var importUri = route.getAttribute('import'); | |
| 115 var routePath = route.getAttribute('path'); | |
| 116 var isRegExp = route.hasAttribute('regex'); | |
| 117 var elementName = route.getAttribute('element'); | |
| 118 var isTemplate = route.hasAttribute('template'); | |
| 119 var isElement = !isTemplate; | |
| 120 | |
| 121 // import custom element | |
| 122 if (isElement && importUri) { | |
| 123 this.importAndActivateCustomElement(importUri, elementName, routePath, url
Path, isRegExp, eventDetail); | |
| 124 } | |
| 125 // pre-loaded custom element | |
| 126 else if (isElement && !importUri && elementName) { | |
| 127 this.activateCustomElement(elementName, routePath, urlPath, isRegExp, even
tDetail); | |
| 128 } | |
| 129 // import template | |
| 130 else if (isTemplate && importUri) { | |
| 131 this.importAndActivateTemplate(importUri, route, eventDetail); | |
| 132 } | |
| 133 // pre-loaded template | |
| 134 else if (isTemplate && !importUri) { | |
| 135 this.activateTemplate(route, eventDetail); | |
| 136 } | |
| 137 }; | |
| 138 | |
| 139 // importAndActivateCustomElement(importUri, elementName, routePath, urlPath,
isRegExp, eventDetail) - Import the custom element then replace the active route | |
| 140 // with a new instance of the custom element | |
| 141 router.importAndActivateCustomElement = function(importUri, elementName, route
Path, urlPath, isRegExp, eventDetail) { | |
| 142 if (!importedURIs.hasOwnProperty(importUri)) { | |
| 143 importedURIs[importUri] = true; | |
| 144 var elementLink = document.createElement('link'); | |
| 145 elementLink.setAttribute('rel', 'import'); | |
| 146 elementLink.setAttribute('href', importUri); | |
| 147 document.head.appendChild(elementLink); | |
| 148 } | |
| 149 this.activateCustomElement(elementName || importUri.split('/').slice(-1)[0].
replace('.html', ''), routePath, urlPath, isRegExp, eventDetail); | |
| 150 }; | |
| 151 | |
| 152 // activateCustomElement(elementName, routePath, urlPath, isRegExp, eventDetai
l) - Replace the active route with a new instance of the custom element | |
| 153 router.activateCustomElement = function(elementName, routePath, urlPath, isReg
Exp, eventDetail) { | |
| 154 var resourceEl = document.createElement(elementName); | |
| 155 var routeArgs = this.routeArguments(routePath, urlPath, window.location.href
, isRegExp); | |
| 156 for (var arg in routeArgs) { | |
| 157 if (routeArgs.hasOwnProperty(arg)) { | |
| 158 resourceEl[arg] = routeArgs[arg]; | |
| 159 } | |
| 160 } | |
| 161 this.activeElement(resourceEl, eventDetail); | |
| 162 }; | |
| 163 | |
| 164 // importAndActivateTemplate(importUri, route, eventDetail) - Import the templ
ate then replace the active route with a clone of the template's content | |
| 165 router.importAndActivateTemplate = function(importUri, route, eventDetail) { | |
| 166 if (importedURIs.hasOwnProperty(importUri)) { | |
| 167 // previously imported. this is an async operation and may not be complete
yet. | |
| 168 var previousLink = document.querySelector('link[href="' + importUri + '"]'
); | |
| 169 if (previousLink.import) { | |
| 170 // the import is complete | |
| 171 this.activeElement(document.importNode(previousLink.import.querySelector
('template').content, true), eventDetail); | |
| 172 } else { | |
| 173 // wait for `onload` | |
| 174 previousLink.onload = function() { | |
| 175 if (route.hasAttribute('active')) { | |
| 176 this.activeElement(document.importNode(previousLink.import.querySele
ctor('template').content, true), eventDetail); | |
| 177 } | |
| 178 }.bind(this); | |
| 179 } | |
| 180 } else { | |
| 181 // template hasn't been loaded yet | |
| 182 importedURIs[importUri] = true; | |
| 183 var templateLink = document.createElement('link'); | |
| 184 templateLink.setAttribute('rel', 'import'); | |
| 185 templateLink.setAttribute('href', importUri); | |
| 186 templateLink.onload = function() { | |
| 187 if (route.hasAttribute('active')) { | |
| 188 this.activeElement(document.importNode(templateLink.import.querySelect
or('template').content, true), eventDetail); | |
| 189 } | |
| 190 }.bind(this); | |
| 191 document.head.appendChild(templateLink); | |
| 192 } | |
| 193 }; | |
| 194 | |
| 195 // activateTemplate(route, eventDetail) - Replace the active route with a clon
e of the template's content | |
| 196 router.activateTemplate = function(route, eventDetail) { | |
| 197 var clone = document.importNode(route.querySelector('template').content, tru
e); | |
| 198 this.activeElement(clone, eventDetail); | |
| 199 }; | |
| 200 | |
| 201 // activeElement(element, eventDetail) - Replace the active route's content wi
th the new element | |
| 202 router.activeElement = function(element, eventDetail) { | |
| 203 while (this.activeRouteContent.firstChild) { | |
| 204 this.activeRouteContent.removeChild(this.activeRouteContent.firstChild); | |
| 205 } | |
| 206 this.activeRouteContent.appendChild(element); | |
| 207 fire('activate-route-end', eventDetail, this); | |
| 208 fire('activate-route-end', eventDetail, eventDetail.route); | |
| 209 }; | |
| 210 | |
| 211 // urlPath(url) - Parses the url to get the path | |
| 212 // | |
| 213 // This will return the hash path if it exists or return the real path if no h
ash path exists. | |
| 214 // | |
| 215 // Example URL = 'http://domain.com/other/path?queryParam3=false#/example/path
?queryParam1=true&queryParam2=example%20string' | |
| 216 // path = '/example/path' | |
| 217 // | |
| 218 // Note: The URL must contain the protocol like 'http(s)://' | |
| 219 router.parseUrlPath = function(url) { | |
| 220 // The relative URI is everything after the third slash including the third
slash | |
| 221 // Example relativeUri = '/other/path?queryParam3=false#/example/path?queryP
aram1=true&queryParam2=example%20string' | |
| 222 var splitUrl = url.split('/'); | |
| 223 var relativeUri = '/' + splitUrl.splice(3, splitUrl.length - 3).join('/'); | |
| 224 | |
| 225 // The path is everything in the relative URI up to the first ? or # | |
| 226 // Example path = '/other/path' | |
| 227 var path = relativeUri.split(/[\?#]/)[0]; | |
| 228 | |
| 229 // The hash is everything from the first # up to the the search starting wit
h ? if it exists | |
| 230 // Example hash = '#/example/path' | |
| 231 var hashIndex = relativeUri.indexOf('#'); | |
| 232 if (hashIndex !== -1) { | |
| 233 var hash = relativeUri.substring(hashIndex).split('?')[0]; | |
| 234 if (hash.substring(0, 2) === '#/') { | |
| 235 // Hash path | |
| 236 path = hash.substring(1); | |
| 237 } else if (hash.substring(0, 3) === '#!/') { | |
| 238 // Hashbang path | |
| 239 path = hash.substring(2); | |
| 240 } | |
| 241 } | |
| 242 | |
| 243 return path; | |
| 244 }; | |
| 245 | |
| 246 // router.testRoute(routePath, urlPath, trailingSlashOption, isRegExp) - Test
if the route's path matches the URL's path | |
| 247 // | |
| 248 // Example routePath: '/example/*' | |
| 249 // Example urlPath = '/example/path' | |
| 250 router.testRoute = function(routePath, urlPath, trailingSlashOption, isRegExp)
{ | |
| 251 // This algorithm tries to fail or succeed as quickly as possible for the mo
st common cases. | |
| 252 | |
| 253 // handle trailing slashes (options: strict (default), ignore) | |
| 254 if (trailingSlashOption === 'ignore') { | |
| 255 // remove trailing / from the route path and URL path | |
| 256 if(urlPath.slice(-1) === '/') { | |
| 257 urlPath = urlPath.slice(0, -1); | |
| 258 } | |
| 259 if(routePath.slice(-1) === '/' && !isRegExp) { | |
| 260 routePath = routePath.slice(0, -1); | |
| 261 } | |
| 262 } | |
| 263 | |
| 264 if (isRegExp) { | |
| 265 // parse HTML attribute path="/^\/\w+\/\d+$/i" to a regular expression `ne
w RegExp('^\/\w+\/\d+$', 'i')` | |
| 266 // note that 'i' is the only valid option. global 'g', multiline 'm', and
sticky 'y' won't be valid matchers for a path. | |
| 267 if (routePath.charAt(0) !== '/') { | |
| 268 // must start with a slash | |
| 269 return false; | |
| 270 } | |
| 271 routePath = routePath.slice(1); | |
| 272 var options = ''; | |
| 273 if (routePath.slice(-1) === '/') { | |
| 274 routePath = routePath.slice(0, -1); | |
| 275 } | |
| 276 else if (routePath.slice(-2) === '/i') { | |
| 277 routePath = routePath.slice(0, -2); | |
| 278 options = 'i'; | |
| 279 } | |
| 280 else { | |
| 281 // must end with a slash followed by zero or more options | |
| 282 return false; | |
| 283 } | |
| 284 return new RegExp(routePath, options).test(urlPath); | |
| 285 } | |
| 286 | |
| 287 // If the urlPath is an exact match or '*' then the route is a match | |
| 288 if (routePath === urlPath || routePath === '*') { | |
| 289 return true; | |
| 290 } | |
| 291 | |
| 292 // Look for wildcards | |
| 293 if (routePath.indexOf('*') === -1 && routePath.indexOf(':') === -1) { | |
| 294 // No wildcards and we already made sure it wasn't an exact match so the t
est fails | |
| 295 return false; | |
| 296 } | |
| 297 | |
| 298 // Example urlPathSegments = ['', example', 'path'] | |
| 299 var urlPathSegments = urlPath.split('/'); | |
| 300 | |
| 301 // Example routePathSegments = ['', 'example', '*'] | |
| 302 var routePathSegments = routePath.split('/'); | |
| 303 | |
| 304 // There must be the same number of path segments or it isn't a match | |
| 305 if (urlPathSegments.length !== routePathSegments.length) { | |
| 306 return false; | |
| 307 } | |
| 308 | |
| 309 // Check equality of each path segment | |
| 310 for (var i = 0; i < routePathSegments.length; i++) { | |
| 311 // The path segments must be equal, be a wildcard segment '*', or be a pat
h parameter like ':id' | |
| 312 var routeSegment = routePathSegments[i]; | |
| 313 if (routeSegment !== urlPathSegments[i] && routeSegment !== '*' && routeSe
gment.charAt(0) !== ':') { | |
| 314 // The path segment wasn't the same string and it wasn't a wildcard or p
arameter | |
| 315 return false; | |
| 316 } | |
| 317 } | |
| 318 | |
| 319 // Nothing failed. The route matches the URL. | |
| 320 return true; | |
| 321 }; | |
| 322 | |
| 323 // router.routeArguments(routePath, urlPath, url, isRegExp) - Gets the path va
riables and query parameter values from the URL | |
| 324 router.routeArguments = function routeArguments(routePath, urlPath, url, isReg
Exp) { | |
| 325 var args = {}; | |
| 326 | |
| 327 // Example urlPathSegments = ['', example', 'path'] | |
| 328 var urlPathSegments = urlPath.split('/'); | |
| 329 | |
| 330 if (!isRegExp) { | |
| 331 // Example routePathSegments = ['', 'example', '*'] | |
| 332 var routePathSegments = routePath.split('/'); | |
| 333 | |
| 334 // Get path variables | |
| 335 // urlPath '/customer/123' | |
| 336 // routePath '/customer/:id' | |
| 337 // parses id = '123' | |
| 338 for (var index = 0; index < routePathSegments.length; index++) { | |
| 339 var routeSegment = routePathSegments[index]; | |
| 340 if (routeSegment.charAt(0) === ':') { | |
| 341 args[routeSegment.substring(1)] = urlPathSegments[index]; | |
| 342 } | |
| 343 } | |
| 344 } | |
| 345 | |
| 346 // Get the query parameter values | |
| 347 // The search is the query parameters including the leading '?' | |
| 348 var searchIndex = url.indexOf('?'); | |
| 349 var search = ''; | |
| 350 if (searchIndex !== -1) { | |
| 351 search = url.substring(searchIndex); | |
| 352 var hashIndex = search.indexOf('#'); | |
| 353 if (hashIndex !== -1) { | |
| 354 search = search.substring(0, hashIndex); | |
| 355 } | |
| 356 } | |
| 357 // If it's a hash URL we need to get the search from the hash | |
| 358 var hashPathIndex = url.indexOf('#/'); | |
| 359 var hashBangPathIndex = url.indexOf('#!/'); | |
| 360 if (hashPathIndex !== -1 || hashBangPathIndex !== -1) { | |
| 361 var hash = ''; | |
| 362 if (hashPathIndex !== -1) { | |
| 363 hash = url.substring(hashPathIndex); | |
| 364 } else { | |
| 365 hash = url.substring(hashBangPathIndex); | |
| 366 } | |
| 367 searchIndex = hash.indexOf('?'); | |
| 368 if (searchIndex !== -1) { | |
| 369 search = hash.substring(searchIndex); | |
| 370 } | |
| 371 } | |
| 372 | |
| 373 var queryParameters = search.substring(1).split('&'); | |
| 374 // split() on an empty string has a strange behavior of returning [''] inste
ad of [] | |
| 375 if (queryParameters.length === 1 && queryParameters[0] === '') { | |
| 376 queryParameters = []; | |
| 377 } | |
| 378 for (var i = 0; i < queryParameters.length; i++) { | |
| 379 var queryParameter = queryParameters[i]; | |
| 380 var queryParameterParts = queryParameter.split('='); | |
| 381 args[queryParameterParts[0]] = queryParameterParts.splice(1, queryParamete
rParts.length - 1).join('='); | |
| 382 } | |
| 383 | |
| 384 // Parse the arguments into unescaped strings, numbers, or booleans | |
| 385 for (var arg in args) { | |
| 386 var value = args[arg]; | |
| 387 if (value === 'true') { | |
| 388 args[arg] = true; | |
| 389 } else if (value === 'false') { | |
| 390 args[arg] = false; | |
| 391 } else if (!isNaN(value) && value !== '') { | |
| 392 // numeric | |
| 393 args[arg] = +value; | |
| 394 } else { | |
| 395 // string | |
| 396 args[arg] = decodeURIComponent(value); | |
| 397 } | |
| 398 } | |
| 399 | |
| 400 return args; | |
| 401 }; | |
| 402 | |
| 403 document.registerElement('app-router', { | |
| 404 prototype: router | |
| 405 }); | |
| 406 })(window, document); | |
| OLD | NEW |