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
Browse files Browse the repository at this point in the history
  • Loading branch information
x1B committed Nov 25, 2016
1 parent e55ea55 commit f97828d
Show file tree
Hide file tree
Showing 20 changed files with 1,748 additions and 1,618 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Last Changes

- [#381](https://github.com/LaxarJS/laxar/issues/381): flow: clearly distinguish places from their targets and patterns
+ **BREAKING CHANGE:** see ticket for details
- [#402](https://github.com/LaxarJS/laxar/issues/402): project: polyfill `Object.assign`
- [#395](https://github.com/LaxarJS/laxar/issues/395): cleanup: removed `object.extend` and `object.deepFreeze`
+ **BREAKING CHANGE:** see ticket for details
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

## Why LaxarJS?

Find out [why](docs/why_laxar.md) you would use LaxarJS and if it's the right tool for you.
Then, explore the [core concepts](docs/concepts.md) and browse the [manuals](docs/manuals/index.md) in the [documentation](docs).
Find out [why](//laxarjs.org/docs/laxar-latest/why_laxar/) you would use LaxarJS and if it's the right tool for you.
Then, explore the [core concepts](http://laxarjs.org/docs/laxar-latest/concepts/) and browse the [manuals](//laxarjs.org/docs/laxar-latest/manuals/) in the [documentation](//laxarjs.org/docs/laxar-latest/).

Have a look at the [LaxarJS homepage](http://laxarjs.org) for demos and more information.

Expand Down
160 changes: 89 additions & 71 deletions docs/manuals/flow_and_places.md

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions docs/manuals/widget_services.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ The complete feature configuration for this instance, with defaults filled in fr
### `axFlowService`

Offers the API of the flow service to widgets.
For widgets this API provides methods to e.g. create bookmarkable URLs to flow targets for use as `href` in an `a` tag.
So whenever real links are required instead of programmatical navigation in a LaxarJS application, this is the way to create the URLs.
This API provides a method *constructAbsoluteUrl* to create bookmarkable URLs to flow targets, also encoding place parameters as needed.
The generated URLs can then be used as `href` attribute in `a` tags.
So whenever real links are desired instead of programmatic, event-initiated navigation in a LaxarJS application, this is the way to create the URLs.


### `axGlobalEventBus`
Expand Down
4 changes: 2 additions & 2 deletions laxar.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,12 @@ export function bootstrap(
whenDocumentReady( () => {
log.trace( `LaxarJS loading Flow: ${flowName}` );
services.pageService.createControllerFor( anchorElement );
services.flowService.controller()
services.flowController
.loadFlow()
.then( () => {
log.trace( 'Flow loaded' );
}, err => {
log.fatal( 'Failed to load' );
log.fatal( 'LaxarJS failed to load flow.' );
log.fatal( 'Error [0].\nStack: [1]', err, err && err.stack );
} );
} );
Expand Down
296 changes: 296 additions & 0 deletions lib/runtime/flow_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
/**
* Copyright 2016 aixigo AG
* Released under the MIT license.
* http://laxarjs.org/license
*/
import assert from '../utilities/assert';
import { create as createJsonValidator } from '../utilities/json_validator';
import { forEach, setPath } from '../utilities/object';
import flowSchema from '../../static/schemas/flow';

const SESSION_KEY_TIMER = 'navigationTimer';
const DEFAULT_PLACE = '';

export const TARGET_SELF = '_self';

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

/**
* Module providing the flow controller factory.
*
* @module flow_controller
* @private
*/

/**
* Creates a flow controller to load a flow definition, to setup routes, and to navigate between places. The
* flow controller triggers handles router-initiated navigation as well as `navigateRequest` events and
* triggers instantiation/destruction of the associated pages.
*
* @param {ArtifactProvider} artifactProvider
* an artifact provider, needed to fetch the flow definition
* @param {Configuration} configuration
* a configuration instance, to determine the name of the flow to load
* @param {EventBus} eventBus
* an event bus instance, used to subscribe to navigateRequest events, and to publish will/did-responses
* @param {Logger} log
* a logger that is used for reporting flow validation and navigation problems
* @param {PageService} pageService
* the page service to use for actual page transitions (setup, teardown) during navigation
* @param {Router} router
* router to register places with, and to use for URL construction
* @param {Timer} timer
* timer to use for measuring page transitions
*
* @return {FlowController}
* a flow controller instance
*/
export function create( artifactProvider, configuration, eventBus, log, pageService, router, timer ) {

const COLLABORATOR_ID = 'AxFlowController';
const availablePlaces = {};

let activeParameters = {};
let activePlace;
let navigationInProgress = false;
let requestedTarget = null;

eventBus.subscribe( 'navigateRequest', ( { target, data } ) => {
if( navigationInProgress ) { return; }
requestedTarget = target;
navigateToTarget( target, { ...activeParameters, ...data } );
}, { subscriber: COLLABORATOR_ID } );

return {
constructAbsoluteUrl,
loadFlow
};

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

/**
* Starts loading the configured flow definition and configures the router.
*
* @return {Promise}
* a promise that is resolved when all routes have been registered
*/
function loadFlow() {
const flowName = configuration.ensure( 'flow.name' );
return artifactProvider.forFlow( flowName ).definition()
.then( flow => {
validateFlowJson( flow );
router.registerRoutes(
assembleRoutes( flow ),
createFallbackHandler( flow )
);
} );
}

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

/**
* Get the place definition for a given target or place. If the provided identifier is a target of the
* current place, the definition of the referenced place is returned. Otherwise, the current place is
* returned.
*
* @param {String} targetOrPlaceId
* a string identifying the target or place to obtain a definition for
* @param {Object} place
* the corresponding place definition
*
* @return {Object}
* a place definition with `targets` and `patterns` as specified in the flow definition, plus `id`
*/
function placeForTarget( targetOrPlaceId, place = activePlace ) {
let placeId = place ? place.targets[ targetOrPlaceId ] : null;
if( placeId == null ) {
placeId = targetOrPlaceId;
}
assert.state(
placeId in availablePlaces,
`Unknown target or place "${targetOrPlaceId}". Current place: "${place.id}"`
);
return availablePlaces[ placeId ];
}

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

/**
* Constructs an absolute URL to the given target or place using the given parameters. If a target is
* given as first argument, it is resolved using the currently active place.
*
* @param {String} targetOrPlace
* the target or place ID to construct a URL for
* @param {Object} [optionalParameters]
* optional map of place parameters. Missing parameters are filled base on the parameters that were
* passed to the currently active place
*
* @return {String}
* the generated absolute URL
*
* @memberof FlowService
*/
function constructAbsoluteUrl( targetOrPlace, optionalParameters ) {
const place = placeForTarget( targetOrPlace );
return router.constructAbsoluteUrl(
place.patterns,
withoutRedundantParameters( place, optionalParameters )
);
}

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

function navigateToTarget( targetOrPlaceId, parameters, redirectFrom ) {
const place = placeForTarget( targetOrPlaceId, redirectFrom );
router.navigateTo(
place.patterns,
withoutRedundantParameters( place, parameters ),
!!redirectFrom
);
}

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

function handleRouteChange( place, routerParameters ) {
const parameters = { ...place.defaultParameters, ...routerParameters };
if( activePlace && place.id === activePlace.id && equals( parameters, activeParameters ) ) {
navigationInProgress = false;
log.trace( `Canceling navigation to "${place.id}". Already there with same parameters.` );
return Promise.resolve();
}
if( navigationInProgress ) {
log.trace( `Canceling navigation to "${place.id}". Navigation already in progress.` );
return Promise.resolve();
}
navigationInProgress = true;

const fromPlace = activePlace ? activePlace.targets[ TARGET_SELF ] : '';
const navigationTimer = timer.started( {
label: `navigation (${fromPlace} -> ${place.targets[ TARGET_SELF ]})`,
persistenceKey: SESSION_KEY_TIMER
} );

const event = {
target: requestedTarget || place.id,
place: place.id,
data: parameters
};
requestedTarget = null;

const options = { sender: COLLABORATOR_ID };
return eventBus.publish( `willNavigate.${event.target}`, event, options )
.then( () => {
if( activePlace && place.id === activePlace.id ) {
activeParameters = parameters;
return Promise.resolve();
}

return pageService.controller().tearDownPage()
.then( () => {
log.setTag( 'PLCE', place.id );
activeParameters = parameters;
activePlace = place;
return pageService.controller().setupPage( place.page );
} );
} )
.then( () => {
navigationInProgress = false;
navigationTimer.stopAndLog( 'didNavigate' );
return eventBus.publish( `didNavigate.${event.target}`, event, options );
} )
.catch( err => {
log.error( `Failed to navigate to place "${place.id}". Error: [0]\n`, err.stack );
return Promise.reject( err );
} );
}

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

function createFallbackHandler( flow ) {
const { redirectOn, places } = flow;
return path => {
log.warn( `Received request for unknown route "${path}".` );
if( redirectOn.unknownPlace in places ) {
log.trace( `- Redirecting to error place ("${redirectOn.unknownPlace}").` );
handleRouteChange( places[ redirectOn.unknownPlace ], {} );
}
else if( DEFAULT_PLACE in places ) {
log.trace( `- Redirecting to default place ("${DEFAULT_PLACE}").` );
handleRouteChange( places[ DEFAULT_PLACE ], {} );
}
else {
log.trace( '- Got no unknownPlace redirect and no default place. Doing nothing.' );
}
};
}

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

function assembleRoutes( { places } ) {
const routeMap = {};
forEach( places, ( place, placeId ) => {
place.id = placeId;
place.patterns = place.patterns || [ `/${placeId}` ];
setPath( place, `targets.${TARGET_SELF}`, place.id );

const { id, patterns, page, redirectTo } = place;
availablePlaces[ id ] = place;

if( redirectTo ) {
patterns.forEach( pattern => {
routeMap[ pattern ] = parameters => {
navigateToTarget( redirectTo, parameters, place );
};
} );
return;
}

if( !page ) {
log.error( `flow: invalid empty place: ${id}` );
return;
}

patterns.forEach( pattern => {
routeMap[ pattern ] = parameters => {
handleRouteChange( place, parameters );
};
} );
} );
return routeMap;
}

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

function validateFlowJson( flowJson ) {
const errors = createJsonValidator( flowSchema ).validate( flowJson );
if( errors.length ) {
log.error(
'LaxarJS flow controller: Failed validating flow definition:\n[0]',
errors.map( ({ message }) => ` - ${message}` ).join( '\n' )
);
throw new Error( 'Illegal flow.json format' );
}
}

}

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

function withoutRedundantParameters( place, parameters ) {
const { defaultParameters = {} } = place;
const remainingParameters = {};
forEach( parameters, ( value, key ) => {
if( !( key in defaultParameters ) || defaultParameters[ key ] !== value ) {
remainingParameters[ key ] = value;
}
} );
return remainingParameters;
}

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

function equals( a, b ) {
const aKeys = Object.keys( a );
const bKeys = Object.keys( b );
return aKeys.length === bKeys.length && aKeys.every( key => key in b && a[ key ] === b[ key ] );
}
Loading

0 comments on commit f97828d

Please sign in to comment.