Skip to content
This repository has been archived by the owner on Mar 28, 2023. It is now read-only.

Commit

Permalink
(#381) work-in-progress: factor-out routing: wip
Browse files Browse the repository at this point in the history
  • Loading branch information
x1B committed Nov 25, 2016
1 parent f97828d commit 4552a54
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 27 deletions.
38 changes: 29 additions & 9 deletions docs/manuals/flow_and_places.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ Preliminary readings:
* [Configuration](configuration.md)
* [Writing Pages](writing_pages.md)


## The Flow

The flow ties together the parts of a LaxarJS application: it defines what pages are reachable, which also determines the set of widgets and controls to load as part of an application.
Each LaxarJS application has only a single flow, but when bootstrapping your application you can decide which flow to use, so that you can easily break it up into several segments, and only load one of these at a time.
Once bootstrapped, a LaxarJS application uses only a single flow.
But at bootstrapping time, you can decide which flow to use.
This way, you can easily create several "perspectives" onto your application that share pages and widgets as needed.
For example, you could have a flow to present to new visitors, a second flow for registered users, and a third flow to implement a back-office tool.

A flow is specified using a definition in JSON format, and it primarily consists of a set of named *places*.
Each flow is specified using a *flow definition file* in JSON format, and it primarily consists of a set of named *places*.


## Places
Expand Down Expand Up @@ -61,22 +65,38 @@ In the example, the place *entry* has a single pattern (`/`), while the place *d
If no patterns are specified, a place with ID `$some-id` will automatically be assigned the patterns list `[ "/$some-id" ]`, which will only match a slash followed by the exact place ID.

The syntax for URL patterns ultimately depends on what the router (page.js) deems valid.
Note that regular-expression patterns, while in principle supported by page.js, are currently not available for use in a LaxarJS flow definition.
It is *strongly recommended* to always start patterns with a leading slash, as relative paths will break down quickly in most setups.
Also note that each list of pattern should begin with a *reversible* pattern, which contains only constant parts and named parameters.
The pattern `*` that matches any path is not reversible, for example.
It is *strongly recommended* to always start patterns with a *leading slash*, as relative paths will quickly break down in most setups.
Also note that each list of patterns should start with a *reversible* pattern, as explained in the next section.
Note that regular-expression patterns, while in principle supported by page.js, are currently not available for use in a LaxarJS flow definition, both because they are not reversible, and because there is no JSON notation for them.

Apart from its patterns, a place has either a `page` entry, or a `redirectTo` entry.
The former determines that the corresponding page will be looked up relative to the pages directory of your application and instantiated when entering the place, while the latter makes it a redirect to another place specified by ID.
In the example, the place *entry* specifies a redirect to the place *details*.
You can use redirects to support legacy URLs in your application and to forward them to actual pages.

Application may also enable *query-strings* using the configuration key `router.query.enabled`.
Query parameters are never used for routing, but carry *optional parameter values* that may be useful to widgets on a page.
Because query parameters are optional, each place may specify an object containing `defaultParameters`, that are published with navigation events if no matching query parameter was passed.
Note that regular place parameters always override query parameters of the same name.


### Reverse Routing

The declarative routing configuration is more restrictive than free-form programmatic routing.
On the other hand, this notation allows LaxarJS to automatically generate URLs to any place, from just its ID and possibly a set of named parameters.
The widgets and activities in your application do not need to know about the URL patterns associated with their respective place, which makes them portable across pages and even application.
The declarative routing configuration used by LaxarJS is a bit more restrictive than free-form programmatic routing.
On the other hand, this notation allows applications to automatically generate URLs to any place, just from an ID and possibly a set of named parameters.
The widgets and activities within your application do not need to know about the URL patterns associated with their respective place, which makes them portable across pages and even application.

To make use of reverse routing, it is important that the first pattern for each place is *reversible*.
Specifically, any wildcard parts of the URL pattern must be *named*, so that they can be substituted for the actual parameter names by the router.
The pattern `*` that matches any path is not reversible, for example.
Also, page.js regular expression patterns are not reversible, because JavaScript does not support named capturing groups in regular expressions.
However, their syntax is not supported by the JSON flow definition anyway, so applications cannot use them by mistake.
The following pattern styles are known to work with reverse routing:

* verbatim: `/some/path`
* named parameter segments `/some/:param/:other-param`

If query parameters are enabled, any additional parameters that are not part of the pattern to reverse will be encoded into query parameters, except if the parameter value to be encoded equals the default value of the target place.


### Initiating navigation
Expand Down
39 changes: 23 additions & 16 deletions lib/runtime/pagejs_router.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/
import { forEach } from '../utilities/object';

const ROUTE_PARAM_MATCHER = /\/:([^\/]+)/g;
const ROUTE_PARAM_MATCHER = /\/:([^\/\?\(]+)(\(\.\*\)|\?)?/g;
const TRAILING_SEGMENTS_MATCHER = /\/(_\/)*_?$/;

export function create( pagejs, browser, configuration ) {
Expand All @@ -29,7 +29,7 @@ export function create( pagejs, browser, configuration ) {
pagejs.base( base );
forEach( routeMap, ( handler, pattern ) => {
pagejs( pattern, context => {
handler( collectParameters( context ) );
handler( collectParameters( pattern, context ) );
} );
} );
pagejs( '*', context => {
Expand Down Expand Up @@ -57,8 +57,8 @@ export function create( pagejs, browser, configuration ) {
function constructPath( patterns, parameters ) {
const bestPattern = patterns[ 0 ];
const path = bestPattern
.replace( ROUTE_PARAM_MATCHER, ( $0, $param ) => {
const replacement = encodeSegment( parameters[ $param ] );
.replace( ROUTE_PARAM_MATCHER, ( $0, $param, $modifier ) => {
const replacement = encodeSegment( parameters[ $param ], $modifier === '(.*)' );
delete parameters[ $param ];
return `/${replacement}`;
} )
Expand Down Expand Up @@ -88,7 +88,7 @@ export function create( pagejs, browser, configuration ) {

///////////////////////////////////////////////////////////////////////////////////////////////////////////

function collectParameters( context ) {
function collectParameters( pattern, context ) {
const { querystring = '', params = {} } = context;
const parameters = {};
if( queryEnabled && querystring.length ) {
Expand All @@ -98,16 +98,17 @@ export function create( pagejs, browser, configuration ) {
parameters[ key ] = value !== undefined ? value : true;
} );
}
forEach( params, (value, key) => {
parameters[ key ] = decodeSegment( value );
forEach( params, ( value, key ) => {
const isMultiSegment = pattern.indexOf( `/:${key}(.*)` ) !== -1;
parameters[ key ] = decodeSegment( value, isMultiSegment );
} );
return parameters;
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
* Encode a parameter value for use as a path segment in routing.
* Encode a parameter value for use as path segment(s) in routing.
*
* Usually, values are simply URL-encoded, but there are special cases:
*
Expand All @@ -122,15 +123,18 @@ export function create( pagejs, browser, configuration ) {
*
* @param {*} value
* the parameter to encode
* @param {Boolean} [isMultiSegment=false]
* determines if encoded value may contain slashes (true) or if slashes are double-encoded so that the
* parameter can always be matched by a single path segment (false)
* @return {String}
* the encoded value, for use as a path segment in URLs
*
* @private
*/
function encodeSegment( value ) {
return value == null ?
'_' :
encodeURIComponent( value ).replace( /%2F/g, '%252F' ).replace( /_/g, '%255F' );
function encodeSegment( value, isMultiSegment ) {
if( value == null ) { return '_'; }
const urlSegments = encodeURIComponent( value ).replace( /_/g, '%255F' );
return isMultiSegment ? urlSegments : urlSegments.replace( /%2F/g, '%252F' );
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////
Expand All @@ -147,15 +151,18 @@ export function create( pagejs, browser, configuration ) {
*
* @param {String} value
* the encoded parameter segment to decode
* @param {Boolean} [isMultiSegment=false]
* determines if url-encoded slashes in the value were part of the original input (true) or if slashes
* in the given value were double-encoded by {#encodeSegment} and need additional decoding (false)
* @return {String}
* the decoded value, for use as a path segment in URLs
*
* @private
*/
function decodeSegment( value ) {
return value === '_' || value == null ?
null :
value.replace( /%2F/g, '/' ).replace( /%5F/g, '_' );
function decodeSegment( value, isMultiSegment ) {
if( value === '_' || value == null ) { return null; }
const segments = value.replace( /%5F/g, '_' );
return isMultiSegment ? segments : segments.replace( /%2F/g, '/' )
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////
Expand Down
81 changes: 79 additions & 2 deletions lib/runtime/spec/pagejs_router_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ describe( 'A page.js router', () => {
let spyOneBinary;
let spyOneUnary;
let spyTwo;
let spyMultiSegment;

let baseRouteMap;
let fallbackHandlerSpy;

Expand All @@ -33,11 +35,13 @@ describe( 'A page.js router', () => {
spyOneBinary = jasmine.createSpy( 'spyOneBinary' );
spyOneUnary = jasmine.createSpy( 'spyOneUnary' );
spyTwo = jasmine.createSpy( 'spyTwo' );
spyMultiSegment = jasmine.createSpy( 'spyMultiSegment' );

baseRouteMap = {
'/prefixOne/:paramA/:paramB': spyOneBinary,
'/prefixOne/:p': spyOneUnary,
'/prefixTwo': spyTwo
'/prefixTwo': spyTwo,
'/prefixOne/:a/and/:multi-param(.*)/plus/:x(.*)': spyMultiSegment
};
fallbackHandlerSpy = jasmine.createSpy( 'fallbackHandlerSpy' );

Expand Down Expand Up @@ -216,6 +220,45 @@ describe( 'A page.js router', () => {
).toEqual( 'https://server:4711/the-base///' );
} );

/////////////////////////////////////////////////////////////////////////////////////////////////////

it( 'does not harm empty strings by stripping empty segments', () => {
expect(
router.constructAbsoluteUrl( [ '//:a/:b/' ], { a: '', b: null } )
).toEqual( 'https://server:4711/the-base///' );
} );

/////////////////////////////////////////////////////////////////////////////////////////////////////

it( 'single-encodes slashes in multi-segment parameters', () => {
expect(
router.constructAbsoluteUrl( [ '/:some-path(.*)/segment/:x' ], {
'some-path': 'le&ave/m?e/al#one',
x: 'help/me'
} )
).toEqual( 'https://server:4711/the-base/le%26ave%2Fm%3Fe%2Fal%23one/segment/help%252Fme' );
} );

/////////////////////////////////////////////////////////////////////////////////////////////////////

it( 'encodes missing multi-segment parameters using the underscore (_)', () => {
expect(
router.constructAbsoluteUrl( [ '/:some-path(.*)/segment/:x(.*)/end' ], {
x: '_'
} )
).toEqual( 'https://server:4711/the-base/_/segment/%255F/end' );
} );

/////////////////////////////////////////////////////////////////////////////////////////////////////

it( 'treats optional single-segment parameters like regular single-segment parameters', () => {
expect(
router.constructAbsoluteUrl( [ '/:some-arg?/segment/:x?' ], {
x: 'help/me'
} )
).toEqual( 'https://server:4711/the-base/_/segment/help%252Fme' );
} );

} );

////////////////////////////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -383,6 +426,40 @@ describe( 'A page.js router', () => {

////////////////////////////////////////////////////////////////////////////////////////////////////////

describe( 'when page.js handles a route change, passing multi-segment parameter values', () => {

let decodedParams;

beforeEach( () => {
pagejsMock.triggerRoute(
'/prefixOne/:a/and/:multi-param(.*)/plus/:x(.*)',
{ params: { a: '%2F', 'multi-param': 'a%2Fwidget%2Fencoded%2Fpath%5F', x: '_' } }
);
decodedParams = spyMultiSegment.calls.mostRecent().args[ 0 ];
} );

/////////////////////////////////////////////////////////////////////////////////////////////////////

it( 'does not double-decode slashes in the parameter value', () => {
expect( decodedParams[ 'multi-param' ] ).toEqual( 'a%2Fwidget%2Fencoded%2Fpath_' );
} );

/////////////////////////////////////////////////////////////////////////////////////////////////////

it( 'double-decodes underscores in the parameter value', () => {
expect( decodedParams.x ).toEqual( null );
} );

/////////////////////////////////////////////////////////////////////////////////////////////////////

it( 'reverts double-encoding in regular parameter values', () => {
expect( decodedParams.a ).toEqual( '/' );
} );

} );

////////////////////////////////////////////////////////////////////////////////////////////////////////

describe( 'when page.js handles a route change, passing query parameters', () => {

let decodedParams;
Expand Down Expand Up @@ -449,7 +526,7 @@ describe( 'A page.js router', () => {

////////////////////////////////////////////////////////////////////////////////////////////////////////

describe( 'asked to navigate based on a list of patterns, substituting paremeters', () => {
describe( 'asked to navigate based on a list of patterns, substituting parameters', () => {

beforeEach( () => {
router.navigateTo( [ '/prefixOne/:param', '/prefixTwo' ], {
Expand Down

0 comments on commit 4552a54

Please sign in to comment.