# Routing (components + resources) Links: [Home](../index.md) A static routing pattern for pages built with the framework: an array of **routes** (a string pattern, a RegExp, a matcher function or `null` for the fallback) names the **component** modules to render and the shared **resource** modules they need; `load()` resolves the current URL, loads every resource once, renders the components of every matching route in order and logs each step (`[L]` load, `[R]` render, `[D]` done, `[A]` async) with timings. Written by SGApps.IO ("Static Routing Framework"); it is a small application skeleton rather than a UI module, so it is **not** registered in `lib.js`. ## Loading the Module The routing module is required by **path**; the components and resources it loads are resolved relative to its own folder, so an application built on it keeps them inside `application-patterns/routing/`: ``` application-patterns/ routing.js <-- module.exports = { components(routes), resources(list) } routing/ components.js <-- the manager: matches routes, loads resources, renders components resources.js <-- the resource loader (cache + dependency resolution) routes.js <-- an example routes array components/ none.js <-- example component (does nothing) .js <-- 'your-component' in a route's components list header/menu.js <-- 'header/menu' resources/ token.js <-- example resource: the access token in localStorage request.js <-- example resource: an authenticated request() factory (depends on token) .js ``` ```js App.require('/application-prototype-modules/application-patterns/routing.js').then(function (routing) { var routes = [ { path: /.*/, components: ['header/menu'], resources: ['token'] }, // every page { path: '/profile', components: ['profile'], resources: ['request'] }, { path: '/user/:id', components: ['user'], resources: ['request'] }, { path: null, components: ['not-found'], resources: [] } // fallback ]; var manager = routing.components(routes); manager.on('ready', function () { console.log('page rendered'); }); manager.load(); }); ``` **Dependencies:** the framework's `async` (sequential flows) and `request/params-parser` (string patterns); the example `request` resource uses `request`. ## API ### `routing.components(routes)` Builds and returns the **manager** (an `ApplicationPrototype`) for a routes array, already wired to the resource loader. Anything but an array throws `Error("routes not present")` after printing the example routes of `routing/routes.js` with `console.warn`. #### Route object | Key | Type | Description | |---|---|---| | `path` | string | A pattern for the framework's `params-parser`: literal text with `:name` placeholders (`/user/:id`), matched against `location.href` **without query string and hash**, anchored at the end only (`fixedEnd`) -- so `/profile` matches `https://host/profile`. The placeholders become `route.params` | | | `RegExp` | `href.match(path)` -- no params | | | `function (url, route)` | Returns the params object for a match, anything falsy for no match | | | `null` | A **fallback** route: used (all of them) only when no other route matched | | `components` | string[] | Component module names, rendered **in order**, one after the other; resolved as `application-patterns/routing/components/.js` | | `resources` | string[] | Resource module names needed by every component of the route (merged with each component's own `resources` list, duplicates removed); resolved as `application-patterns/routing/resources/.js` | Every route whose `path` matches is applied, in array order -- routes are additive, not first-match. #### Manager methods and events | Member | Returns | Description | |---|---|---| | `load()` | `Promise` | Reads the current URL, computes the matching routes (or the fallback ones), creates a resource manager, then for each route in sequence and each of its components in sequence: requires the component module, requires its resources (route + component lists), calls the component. Resolves when every route is done, then emits `ready` on the next tick. Each call renders again from scratch (nothing is unmounted) | | `whenLoaded()` | `Promise` | The promise of the last `load()`, or a fresh `load()` when none was made | | `handleResourceLoader(fn)` | -- | Replaces the resource loader factory (`routing.components()` sets the bundled one) | | `on('ready', fn)` | -- | Emitted after `load()` resolved | ### Component module A component is a module under `routing/components/` exporting: ```js // routing/components/profile.js module.exports = { resources : ['request'], // extra resources for this component (merged with the route's) skipErrors : false, // true: render even when a resource failed to load async : false, // true: do not wait for done() before the next component timeout : 15000, // ms before "Timeout Render Error" (default 15 s) component : function (resources, done, resourceManager) { // this === the manager; resources = { request: , ... } resources.request('/api/profile').response('json').then(function (profile) { document.getElementById('main').textContent = profile.name; done(); // or done(err) }, done); } }; ``` | Key | Default | Description | |---|---|---| | `component(resources, done, resourceManager)` | required | Called with `this` = the manager once the resources are ready. Call `done()` (or `done(err)`) when rendered; the next component waits for it | | `resources` | `[]` | Resource names loaded before the component runs, in addition to the route's list | | `skipErrors` | `false` | When a resource rejects, the component is skipped (`[L] COMPONENT ... can't use resource`) unless `skipErrors` is `true`, in which case it runs with the missing resources absent from the object | | `async` | `false` | `true` announces `[A]` and continues with the next component immediately; a later `done()` is ignored | | `timeout` | `15000` | If `done()` has not been called in time, an error is logged and the flow continues | A component that throws is logged as `[R] COMPONENT [err]` and the flow continues. ### Resource module A resource is a module under `routing/resources/` exporting a factory; its value is loaded **once per page** (module-level cache, shared by every manager) and its own `resources` are loaded first: ```js // routing/resources/request.js (bundled example) module.exports = { resources : ['token'], // dependencies, injected by name resource : function (resources, resourceManager) { // must return a promise return new Application.Promise(function (resolve, reject) { Application.require('request').then(function (request) { resolve(function requestWithToken(path, method) { var req = new request(); req.method(method || 'get'); req.url(path || '/'); req.async(true); req.open(); req.json = function (data) { // helper: send a JSON body req.header('Content-Type', 'application/json'); req.send(JSON.stringify(data, null, ' ')); return req; }; req.header('Access-Token', resources.token.token()); return req; }); }, reject); }); } }; ``` The bundled `token` resource resolves to `{ token() -> localStorage['access-token'], update(token) }`; the bundled `none` component does nothing. #### The resource manager Passed to every component as the third argument and to every resource factory as the second: | Method | Returns | Description | |---|---|---| | `require(name)` | promise | The (cached or newly started) load of one resource | | `require([names])` | `{ name: promise }` | Several at once | | `require()` | `{ name: promise }` | Every resource loaded so far | | `drop(name or [names])` | -- | Forgets cached resources so that the next `require()` loads them again (e.g. after a login changed the token) | ### `routing.resources(list)` Starts loading the given resource names right away (a warm-up), without waiting for a route. Returns nothing; the values are picked up from the cache by the components later. ## Use Cases ### A page with a shared header and one component per route ```js App.require('/application-prototype-modules/application-patterns/routing.js').then(function (routing) { routing.components([ { path: /.*/, components: ['header/menu', 'footer'], resources: ['token'] }, { path: '/', components: ['home'] }, { path: '/articles/:slug', components: ['article'], resources: ['request'] }, { path: null, components: ['not-found'] } ]).load().then(function () { document.body.classList.add('ready'); }); }); ``` ```js // routing/components/article.js -- the params are not passed to the component: read the URL yourself module.exports = { resources: ['request'], component: function (resources, done) { var slug = location.pathname.split('/').pop(); resources.request('/api/articles/' + slug).response('json').then(function (article) { document.getElementById('main').innerHTML = article.html; done(); }, done); } }; ``` ### A non-blocking analytics component ```js // routing/components/analytics.js module.exports = { async: true, // the next component does not wait for the beacon component: function (resources, done) { navigator.sendBeacon('/hit', location.pathname); done(); } }; ``` ### Refreshing a resource after login ```js // inside a component resources.token.update(newToken); resourceManager.drop(['token', 'request']); // the next load() rebuilds both this.load(); // this === the manager ``` ## Notes - **Static**: `load()` reads `location.href` once; there is no listener on `popstate` / `hashchange` and no unmounting -- call `load()` again after a navigation you handle yourself, and let components clean up their previous DOM. - The matched `params` are stored on the internal route objects only (they appear in the `[D] ROUTE` log); components read the URL themselves. - Names are resolved relative to `routing/components/` and `routing/resources/` by the framework's `module.require()`, which is why the pattern ships as a folder to copy or extend rather than as a registered module. - Every step is logged with `console.info` / `console.error` (the banner, `[L]` / `[R]` / `[D]` / `[A]` lines with durations) -- useful in development, noisy in production. - No playground page: the pattern needs component and resource files of its own, which a single-page example cannot provide; the [Getting Started](../getting-started.md) page shows how the modules are booted.