Highway App
import HighwayApp, { rerunMqs, updateActiveLinks } from '@bbc/front-end-kit/js/components/HighwayApp';
Extends App with build-in Highway.
The main purpose of the App class that extends from App is to centralize main initialisations and keep it as clear as possible. It also uses the InstanceManager to reduce the many reoccuring logic to initialise the many components of the application or website.
Adding Highway to App can bring a lot of extra logic to the scope which in turn would hold back the aim for the existence of that App structure.
The HighwayApp class aims to reinforce the main purpose of the App by being the extra layer between the original App class and the project App class that adds all the Highway logic in a structured way so it is generic enough to create consistency in implementing Highway in all future projects.
Getting started
Just like App, it is meant to be extended and use that child class as the base of the project that is being developed
class MainApp extends HighwayApp {
// constructor
constructor (args = {}) {
super({
highway: { /* HighwayApp configuration */},
/* Media query definitions */
});
}
// methods...
}
Initialisations
Highway splits the general scope into 2 sub scopes. Simple page refreshes have 1 scope of looking for initialisations on page load which is the whole page (aka. Global scope) as the whole page is totally new on each page change. In Highway's case, there is a part that needs to initialise on page load (Global scope) and on view load (View scope). Page changes managed by Highway prevent the global scope to be renewed.
The supposed MainApp class of the project which extends from HighwayApp would need 1 init & destroy method for the Global scope and 1 init & destroy method for the View scope, both per defined media query.
class MainApp extends HighwayApp {
// ...
initReadyGlobal(args = {}) {
}
destroyReadyGlobal(args = {}) {
}
initReadyView(args = {}) {
}
destroyReadyView(args = {}) {
}
// ...
}
...Global: Inits on full page load (once) on media query change...View: Inits on full page load, on media query chnge and after every Highway page change!
From now on these methods will also run multiple times. But thanks to the added context in the args object, it is possible to know when to initialise what. Keys prefixed with highway: tells developers that a Highway action is responsible for the call. Keys it uses are:
highway:navigate-in: When a view is navigating inhighway:navigate-out: When a view is navigating outhighway:navigate-end: When a the transition between views has ended.
The base App class will also still run these methods but the keys will either start with init or destroy
initReadyView(args = {}) {
if (args.key === 'highway:navigate-in') {
// init new view stuff while it is transitioning in
}
}
destroyReadyView(args = {}) {
if (args.key === 'highway:navigate-end') {
// destroy old instances from the previous view
}
}
An easy way to combine keys for overlapping scenarios: This example runs both when Highway is navigating in or when the original app instance is Resizing
initReadyView(args = {}) {
if (![
'init:resize', // on resize
'highway:navigate-in' // after highway's navigate-in is started running
].includes(args.key))) {
// init new view stuff while it is transitioning in
}
}
Highway renderers and transitions
HighwayApp will pass all Highway transitions and renderers directly to the Highway.Core constructor. This way it is still possible to make great use of their great transition structure.
class MainApp extends HighwayApp {
// constructor
constructor (args = {}) {
super({
highway: {
// add renderers
renderers: {
index: MyCustomPageRenderers
},
// add transitions
transitions: {
index: MyCustomPageTransition
}
}
// ...
});
}
}
These renderes and transitions will be passed to the Highway Core constructor directly. Further documentation about how they work, can be found here
Tips
Make sure to remove the old view at the end of the 'out' animation of the Higway transition.
Addition to JS API
App uses a $el property in which it stores the elment it is initialised upon. If no element was provided during initialisation, it will probably be the body elmeent. Higway app adds 2 extra methods to that $el instance only! They are accessed in the same way as querySelector and querySelectorAll.
Query inside Highway view
Query for DOM element inside the current view of the Highway router and ignore all the elments outside the view DOM element. It adds tome extra strings to the selector to make this possible. This way developers don't need to come up with these selectors every time they need them.
queryInsideHW (selector, all = false)
Parameters
selector: Selector string that will be used to add to a larger selector so it makes sure the lookup will only happen inside the Highway view componentall: Boolean to define which querySelector method to use:querySelectorAllorquerySelector(default:false);
this.$el.queryInsideHW('[selector]', true); // uses querySelectorAll
this.$el.queryInsideHW('[selector]'); // uses querySelector
more info
It takes the given selector and wraps it inside another one through string concatination.
function queryInsideHW(selector, all = false) {
return (all ? this.$el.querySelectorAll : this.$el.querySelector).call(this.$el, `[data-router-view="${this.currentView}"] ${selector}`);
}
Query outside Highway view
Query form DOM elements outside the current view of the Highway router and ignore all the elmenets inside the view DOM element. It adds tome extra strings to the selector to make this possible. This way developers don't need to come up with these selectors every time they need them.
queryOutsideHW (selector, all = false)
Parameters
selector: Selector string that will be used to add to a larger selector so it makes sure the lookup will only happen outside the Highway view componentall: Boolean to define which querySelector method to use:querySelectorAllorquerySelector(default:false);
this.$el.queryOutsideHW('[selector]', true); // uses querySelectorAll
this.$el.queryOutsideHW('[selector]'); // uses querySelector
more info
It takes the given selector and wraps it inside another one through string concatination.
function queryOutsideHW(selector, all = false) {
return (all ? this.$el.querySelectorAll : this.$el.querySelector).call(this.$el, `${selector}:not([data-router-view="${this.currentView}"] ${selector})`);
}
API
constructor(args = {})
The constructor method initialises the new HighwayApp instance.
Parameters
args: The configuration object used by the constructor to initialise the HighwayApp in the desired way.
Getters & Setters
highway
Access to the Highway.Core instance in _highway
currentView
The name of the current view of the Highway router
higwayPageChange
Boolean to detect if highway has already changed pages, which helps to define if the change caused a full page reload or not.
Known typo
The getter is named higwayPageChange (missing h) in the source. Use that exact name when accessing it directly.
Private properties
The HighwayApp class uses some extra private properties on top of those present in App that will be present in the scope of your app class but aren't really meant to be accessed directly.
_highway
Contains the Highway.Core instance. It initialises during contstruction
_currentView
Contains the name of the current view Highway is displaying. It returns the value found in data-router-view of the active view element.
_highwayPageChange
A boolean that starts out false and turns true the moment Highway router performs a page change. This is a great indicator for detecting the differens between full page reloads or router view loads.
Default Highway configuration
If no highway arg is provided (or a partial one), HighwayApp merges caller config over these built-in defaults:
{
transitions: {
default: FadeSwapTransition,
contextual: {
instant: InstantTransition,
},
},
renderers: {}
}
FadeSwapTransition— fades out the old view, removes it, then fades in the new view.InstantTransition— removes the old view immediately with no animation. Triggered viadata-transition="instant"on a link.
Events
HighwayApp dispatches the following events on the instance (via EventDispatcher). Each carries a details payload containing the raw Highway event args.
navigate-in
Fired when a new view is navigating in. Payload: { details: <highway args> }.
navigate-out
Fired when the current view is navigating out. Payload: { details: <highway args> }.
navigate-end
Fired when the transition between views has ended. Payload: { details: <highway args> }.
Exported methods
rerunMqs(extraParams = {})
Forces all media queries to re-evaluate in the current scope (this). Merges extraParams into the checkMediaQueries call with force: true. Must be called with .call(this, ...) to bind the correct scope.
updateActiveLinks()
Scans all <a> elements (excluding [data-router-disabled]) and toggles the --active class based on whether location.href includes the link's href. Also updates --active on matching header nav.menu a entries.
External dependencies
This class uses some external dependencies that may be necessary to install using NPM.
- Highway.js: Dogstudio's Higway.js javascript router
- lodash: For some quick and usefull utility methods like:
capitalize: To capitalize strings so thatinit[name]can be written in camelCasedefaultsDeep: Used to provide objects with default values.
Todo
- Provide raw API documentation