Bootstrapping Aurelia
Most platforms have a "main" or entry point for code execution. Aurelia is no different. If you've read the Quick Start, then you've seen the aurelia-app
attribute. Simply place this on an HTML element and Aurelia's bootstrapper will load an app${context.language.fileExtension} and app.html, databind them together and inject them into the DOM element on which you placed that attribute.
Often times you want to configure the framework or run some code prior to displaying anything to the user though. So chances are, as your project progresses, you will migrate towards needing some startup configuration. In order to do this, you can provide a value for the aurelia-app
attribute that points to a configuration module. This module should export a single function named configure
. Aurelia invokes your configure
function, passing it the Aurelia object which you can then use to configure the framework yourself and decide what, when, and where to display your UI. Here's an example configuration file showing the standard configuration, the same configuration that is equivalent to what you would get when using aurelia-app
without a value:
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging();
aurelia.start().then(() => aurelia.setRoot());
}
import {Aurelia} from 'aurelia-framework';
export function configure(aurelia: Aurelia): void {
aurelia.use
.standardConfiguration()
.developmentLogging();
aurelia.start().then(() => aurelia.setRoot());
}
So, if you want to keep all the default settings, it's really easy. Just call standardConfiguration()
to configure the standard set of plugins. Then call developmentLogging()
to turn on logging in debug mode, output to the console
.
Some Aurelia modules will not behave correctly if they are imported before the Aurelia Platform Abstraction Layer (PAL) is initialized, which happens during aurelia.start()
. If you need these modules in your configuration module, you need to initialize the PAL yourself before the file is loaded.
This Github issue
describes how you can manually initialize the PAL before starting the bootstrapping process.
The use
property on the aurelia
instance is an instance of FrameworkConfiguration
. It has many helper methods for configuring Aurelia. For example, if you wanted to manually configure all the standard plugins without using the standardConfiguration()
helper method to do so and you wanted to configure logging without using the helper method for that, this is how you would utilize the FrameworkConfiguration
instance:
import {LogManager} from 'aurelia-framework';
import {ConsoleAppender} from 'aurelia-logging-console';
LogManager.addAppender(new ConsoleAppender());
LogManager.setLevel(LogManager.logLevel.debug);
export function configure(aurelia) {
aurelia.use
.defaultBindingLanguage()
.defaultResources()
.history()
.router()
.eventAggregator();
aurelia.start().then(() => aurelia.setRoot());
}
import {Aurelia, LogManager} from 'aurelia-framework';
import {ConsoleAppender} from 'aurelia-logging-console';
LogManager.addAppender(new ConsoleAppender());
LogManager.setLevel(LogManager.logLevel.debug);
export function configure(aurelia: Aurelia): void {
aurelia.use
.defaultBindingLanguage()
.defaultResources()
.history()
.router()
.eventAggregator();
aurelia.start().then(() => aurelia.setRoot());
}
You can see that this code configures the default data-binding language (.bind, .trigger, etc.), the default set of view resources (repeat, if, compose, etc.) the history module (integration with the browser's history API), the router (mapping routes to components) and the event aggregator (app-wide pub/sub messaging). If, for example, you were building an app that didn't need to use the router or event aggregator, but did want debug logging, you could do that very easily with this configuration:
export function configure(aurelia) {
aurelia.use
.defaultBindingLanguage()
.defaultResources()
.developmentLogging();
aurelia.start().then(() => aurelia.setRoot());
}
import {Aurelia} from 'aurelia-framework';
export function configure(aurelia: Aurelia): void {
aurelia.use
.defaultBindingLanguage()
.defaultResources()
.developmentLogging();
aurelia.start().then(() => aurelia.setRoot());
}
Once you've configured the framework, you need to start things up by calling aurelia.start()
. This API returns a promise. Once it's resolved, the framework is ready, including all plugins, and it is now safe to interact with the services and begin rendering.
Rendering the Root Component
The root component is set by calling aurelia.setRoot()
. If no values are provided, this defaults to treating the element with the aurelia-app
attribute as the DOM host for your app and app${context.language.fileExtension}
/app.html
as the source for the root component. However, you can specify whatever you want, just like this:
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging();
aurelia.start().then(() => aurelia.setRoot('my-root', document.getElementById('some-element'));
}
import {Aurelia} from 'aurelia-framework';
export function configure(aurelia: Aurelia): void {
aurelia.use
.standardConfiguration()
.developmentLogging();
aurelia.start().then(() => aurelia.setRoot('my-root', document.getElementById('some-element'));
}
This causes the my-root${context.language.fileExtension}
/my-root.html
to be loaded as the root component and injected into the some-element
HTML element.
The content of the app host element, the one marked with aurelia-app
or passed to Aurelia.prototype.setRoot
, will be replaced when Aurelia.prototype.setRoot
completes.
When using the
<body>
element as the app host, bear in mind that any content added prior to the completion of Aurelia.prototype.setRoot
will be removed.
Bootstrapping Older Browsers
Aurelia was originally designed for Evergreen Browsers. This includes Chrome, Firefox, IE11 and Safari 8. However, we also support IE9 and above through the use of additional polyfills. To support these earlier browsers, you need the
requestAnimationFrame Polyfill
and the
MutationObserver polyfill
. Once you have installed these (via npm install -D raf mutationobserver-shim
), you'll need to adjust your code to load them before Aurelia is initialized.
In case you are using Webpack, create a file, e.g. ie-polyfill.js
:
import 'mutationobserver-shim/MutationObserver'; // IE10 MutationObserver polyfill
import 'raf/polyfill'; // IE9 requestAnimationFrame polyfill
After you have created the file, add it as the first file in your aurelia-bootstrapper
bundle. You can find bundle configuration in the webpack.config.js
file, something like:
entry: {
'app': ['./ie-polyfill', 'aurelia-bootstrapper'],
If you are using JSPM change your index.html
startup code as follows:
<!doctype html>
<html>
<head>
<title>My App</title>
</head>
<body>
<script src="jspm_packages/system.js"></script>
<script src="config.js"></script>
<script>
SystemJS.import('raf/polyfill').then(function() {
return SystemJS.import('aurelia-polyfills');
}).then(function() {
return SystemJS.import('mutationobserver-shim/MutationObserver');
}).then(function() {
SystemJS.import('aurelia-bootstrapper');
});
</script>
</body>
</html>
Module Loaders and Bundlers
The code in this article demonstrates loading via SystemJS. However, these techniques can be accomplished with other module loaders just as readily. Be sure to lookup the appropriate APIs for your chosen loader or bundler in order to translate these samples into the required code for your own app.
Promises in Edge
Currently, the Edge browser has a serious performance problem with its Promise implementation. This deficiency can greatly increase startup time of your app. If you are targeting the Edge browser, it is highly recommended that you use the bluebird promise library to replace Edge's native implementation. You can do this by simply referencing the library prior to loading other libraries.
Manual Bootstrapping
So far, we've been bootstrapping our app declaratively by using the aurelia-app
attribute. That's not the only way though. You can manually bootstrap the framework as well. In case of JSPM, here's how you would change your HTML file to use manual bootstrapping:
<!doctype html>
<html>
<head>
<title>My App</title>
</head>
<body>
<script src="jspm_packages/system.js"></script>
<script src="config.js"></script>
<script>
SystemJS.import('aurelia-bootstrapper').then(bootstrapper => {
bootstrapper.bootstrap(function(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging();
aurelia.start().then(() => aurelia.setRoot('app', document.body));
});
});
</script>
</body>
</html>
In case you use Webpack, you can replace the aurelia-bootstrapper-webpack
package with the ./src/main
entry file in the aurelia-bootstrapper
bundle defined inside of webpack.config.js
, and call the bootstrapper manually:
import {bootstrap} from 'aurelia-bootstrapper';
bootstrap(async aurelia => {
aurelia.use
.standardConfiguration()
.developmentLogging();
await aurelia.start();
aurelia.setRoot(PLATFORM.moduleName('app'), document.body);
});
import {Aurelia} from 'aurelia-framework';
import {bootstrap} from 'aurelia-bootstrapper';
bootstrap(async (aurelia: Aurelia) => {
aurelia.use
.standardConfiguration()
.developmentLogging();
await aurelia.start();
aurelia.setRoot(PLATFORM.moduleName('app'), document.body);
});
The function you pass to the bootstrap
method is the same as the configure
function from the examples above.
Making Resources Global
When you create a view in Aurelia, it is completely encapsulated. In the same way that you must import
modules into an ES2015/TypeScript module, you must also import or require
components into an Aurelia view. However, certain components are used so frequently across views that it can become very tedious to import them over and over again. To solve this problem, Aurelia lets you explicitly declare certain "view resources" as global. In fact, the configuration helper method defaultResources()
mentioned above does just that. It takes the default set of view resources, such as repeat
, if
, compose
, etc, and makes them globally usable in every view. You can do the same with your own components. Here's how we could make the my-component
custom element, located in a resources subfolder of your project, globally available in all views.
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging()
.globalResources('resources/my-component');
aurelia.start().then(() => aurelia.setRoot());
}
import {Aurelia} from 'aurelia-framework';
export function configure(aurelia: Aurelia): void {
aurelia.use
.standardConfiguration()
.developmentLogging()
.globalResources('resources/my-component');
aurelia.start().then(() => aurelia.setRoot());
}
Organizing Your App with Features
Sometimes you have whole group of components or related functionality that collectively form a "feature". This "feature" may even be owned by a particular set of developers on your team. You want these developers to be able to manage the configuration and resources of their own feature, without interfering with the other parts of the app. For this scenario, Aurelia provides the "feature" feature.
Imagine, as above, that we have a my-component
component. Imagine that that was then one of a dozen components that formed a logical feature in your app called my-feature
. Rather than place the feature's configuration logic inside the app's configuration module, we can place the feature's configuration inside its own feature configuration module.
To create a "feature", simply create a folder in your app; in the case of our example: my-feature
. Inside that folder, place all the components and other code that pertain to that feature. Finally, create an index${context.language.fileExtension}
file at the root of the my-feature
folder. The index${context.language.fileExtension}
file should export a single configure
function. Here's what our code might look like for our hypothetical my-feature
feature:
export function configure(config) {
config.globalResources(['./my-component', './my-component-2', 'my-component-3', 'etc.']);
}
import {FrameworkConfiguration} from 'aurelia-framework';
export function configure(config: FrameworkConfiguration): void {
config.globalResources(['./my-component', './my-component-2', 'my-component-3', 'etc.']);
}
The configure
method receives an instance of the same FrameworkConfiguration
object as the aurelia.use
property. So, the feature can configure your app in any way it needs. An important note is that resources should be configured using paths relative to the index${context.language.fileExtension}
itself.
How then do we turn this feature on in our app? Here's an app configuration file that shows:
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging()
.feature('my-feature');
aurelia.start().then(() => aurelia.setRoot());
}
import {Aurelia} from 'aurelia-framework';
export function configure(aurelia: Aurelia): void {
aurelia.use
.standardConfiguration()
.developmentLogging()
.feature('my-feature');
aurelia.start().then(() => aurelia.setRoot());
}
Features with Webpack
When using Webpack, the syntax for enabling a feature is a little different. Instead of calling .feature('my-feature');
, you will want to use the PLATFORM.moduleName(...)
helper that allows the Aurelia Webpack plugin to understand dynamic module references. In this case, your syntax will look like .feature(PLATFORM.moduleName('my-feature/index'));
Notice that in addition to the use of the PLATFORM.moduleName(...)
helper, the index file must be directly referenced.
Installing Plugins
Similar to features, you can install 3rd party plugins. The main difference is that a "feature" is provided internally by your application, while a plugin is installed from a 3rd party source through your package manager.
To use a plugin, you first install the package. For example jspm install my-plugin
would use jspm to install the my-plugin
package. Once the package is installed, you must configure it in your application. Here's some code that shows how that works.
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging()
.plugin('my-plugin', pluginConfiguration);
aurelia.start().then(() => aurelia.setRoot());
}
import {Aurelia} from 'aurelia-framework';
export function configure(aurelia: Aurelia): void {
aurelia.use
.standardConfiguration()
.developmentLogging()
.plugin('my-plugin', pluginConfiguration);
aurelia.start().then(() => aurelia.setRoot());
}
Simply provide the same name used during installation to the plugin API. Some plugins may require configuration (see the plugin's documentation for details). If so, pass the configuration object or configuration callback function as the second parameter of the plugin
API.
While all plugins work in a similar manner, consider the real-world example of adding and configuring the dialog plugin by using a configuration callback. The configuration parameter in this case is a type of DialogConfiguration
and the above code would become:
export function configure(aurelia) {s
aurelia.use
.standardConfiguration()
.developmentLogging()
.plugin('aurelia-dialog', config => {
config.useDefaults();
config.settings.lock = true;
config.settings.centerHorizontalOnly = false;
config.settings.startingZIndex = 5;
config.settings.keyboard = true;
});
aurelia.start().then(() => aurelia.setRoot());
}
export function configure(aurelia: Aurelia): void {
aurelia.use
.standardConfiguration()
.developmentLogging()
.plugin('aurelia-dialog', config => {
config.useDefaults();
config.settings.lock = true;
config.settings.centerHorizontalOnly = false;
config.settings.startingZIndex = 5;
config.settings.keyboard = true;
});
aurelia.start().then(() => aurelia.setRoot());
}
Leveraging Progressive Enhancement
So far you've seen Aurelia replacing a portion of the DOM with a root component. However, that's not the only way to render with Aurelia. Aurelia can also progressively enhance existing HTML.
Imagine that you want to generate your home page on the server, including using your server-side templating engine to render out HTML. Perhaps you've got custom components you created with Aurelia, but you want to render the custom elements on the server with some content, in order to make things a bit more SEO friendly. Or perhaps you have an existing, traditional web app, that you want to incrementally start adding Aurelia to. When the HTML is rendered in the browser, you want to progressively enhance that HTML and "bring it to life" by activating all the Aurelia component's rich behavior.
All this is possible with Aurelia, using a single method call: enhance
. Instead of using aurelia-app
let's use manual bootstrapping for this example. To progressively enhance the entire body
of your HTML page, you can do something like this (JSPM-based example):
<!doctype html>
<html>
<head>
<title>My App</title>
</head>
<body>
<my-component message="Enhance Me"></my-component>
<script src="jspm_packages/system.js"></script>
<script src="config.js"></script>
<script>
SystemJS.import('aurelia-bootstrapper').then(bootstrapper => {
bootstrapper.bootstrap(function(aurelia){
aurelia.use
.defaultBindingLanguage()
.defaultResources()
.developmentLogging()
.globalResources('resources/my-component');
aurelia.start().then(() => aurelia.enhance());
});
});
</script>
</body>
</html>
It's important to note that, in order for enhance
to identify components to enhance in your HTML page, you need to declare those components as global resources, as we have above with the my-component
component.
Optionally, you can provide an object instance to use as the data-binding context for the enhancement, or provide a specific part of the DOM to enhance. Here's an example that shows both (JSPM-based):
<!doctype html>
<html>
<head>
<title>My App</title>
</head>
<body>
<my-component message.bind="message"></my-component>
<script src="jspm_packages/system.js"></script>
<script src="config.js"></script>
<script>
SystemJS.import('aurelia-bootstrapper').then(bootstrapper => {
bootstrapper.bootstrap(function(aurelia){
aurelia.use
.defaultBindingLanguage()
.defaultResources()
.developmentLogging()
.globalResources('resources/my-component');
var viewModel = {
message: 'Enhanced'
};
aurelia.start().then(() => aurelia.enhance(viewModel, document.body));
});
});
</script>
</body>
</html>
But what if you need to enhance multiple elements on a page that do not have a direct parent/child relationship? For example, suppose you have an existing application written on a non-Aurelia framework that you need to refactor component by component.
You can't use the aurelia.enhance
method multiple times on the same content. It was not designed for that. Instead you can use the templating engine's enhance
method directly.
<!doctype html>
<html>
<head>
<title>My App</title>
</head>
<body>
<my-component message="Enhance Me"></my-component>
<div class="42">Some legacy code that you don't want to enhance</div>
<your-component message.bind="message"></your-component>
</body>
</html>
import {TemplatingEngine} from 'aurelia-framework';
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging()
.globalResources('resources/my-component', 'resources/your-component');
aurelia.start().then(a => {
let templatingEngine = a.container.get(TemplatingEngine);
templatingEngine.enhance({
container: a.container,
element: document.querySelector('my-component'),
resources: a.resources
});
templatingEngine.enhance({
container: a.container,
element: document.querySelector('your-component'),
resources: a.resources,
bindingContext: {
message: 'Enhance Me as well'
}
});
});
}
Customizing Conventions
There are many things you may want to customize or configure as part of your application's bootstrap process. Once you have your main configure
method in place and aurelia-app
is pointing to that module, you can do just about anything you want. One of the most common aspects of Aurelia that developers may want to customize, is its conventions.
Configuring the View Location Convention
Aurelia uses a View Strategy to locate the view that is associated with a particular component's view-model. If the component doesn't specify its own view strategy, then Aurelia's ViewLocator
service will use a fallback view strategy. The fallback strategy that is used is named ConventionalViewStrategy
. This strategy uses the view-model's module id to conventionally map to its view id. For example, if the module id is "welcome${context.language.fileExtension}" then this strategy will look for the view at "welcome.html". The conventional strategy's mapping logic can be changed if a different convention is desired. To do this, during bootstrap, import the ViewLocator
and replace its convertOriginToViewUrl
method with your own implementation. Here's some example code:
import {ViewLocator} from 'aurelia-framework';
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging();
ViewLocator.prototype.convertOriginToViewUrl = (origin) => {
let moduleId = origin.moduleId;
...
return "view.html";
};
aurelia.start().then(a => a.setRoot());
}
import {ViewLocator, Aurelia, Origin} from 'aurelia-framework';
export function configure(aurelia: Aurelia): void {
aurelia.use
.standardConfiguration()
.developmentLogging();
ViewLocator.prototype.convertOriginToViewUrl = (origin: Origin): string => {
let moduleId = origin.moduleId;
...
return "view.html";
};
aurelia.start().then(a => a.setRoot());
}
In this example, you would simply replace "..." with your own mapping logic and return the resulting view path that was desired.
If you're using Webpack with a HTML templating engine such as Jade, you'd have to configure Aurelia to look for the .jade
extension instead of .html
. This is due to Webpack keeping the original sourcemaps and lets loader plugins take care of transpiling the source. Here's the code to configure Aurelia's ViewLocator
for Jade:
import {ViewLocator} from 'aurelia-framework';
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging();
ViewLocator.prototype.convertOriginToViewUrl = (origin) => {
let moduleId = origin.moduleId;
let id = (moduleId.endsWith('.js') || moduleId.endsWith('.ts')) ? moduleId.substring(0, moduleId.length - 3) : moduleId;
return id + '.jade';
};
aurelia.start().then(a => a.setRoot());
}
import {ViewLocator, Aurelia, Origin} from 'aurelia-framework';
export function configure(aurelia: Aurelia): void {
aurelia.use
.standardConfiguration()
.developmentLogging();
ViewLocator.prototype.convertOriginToViewUrl = (origin: Origin): string => {
let moduleId = origin.moduleId;
let id = (moduleId.endsWith('.js') || moduleId.endsWith('.ts')) ? moduleId.substring(0, moduleId.length - 3) : moduleId;
return id + '.jade';
};
aurelia.start().then(a => a.setRoot());
}
Configuring the Fallback View Location Strategy
In addition to customizing the mapping logic of the ConventionalViewStrategy
you can also replace the entire fallback view strategy. To do this, replace the createFallbackViewStrategy
of the ViewLocator
with your own implementation. Here's some sample code for that:
import {ViewLocator} from 'aurelia-framework';
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging();
ViewLocator.prototype.createFallbackViewStrategy = (origin) => {
return new CustomViewStrategy(origin);
};
aurelia.start().then(a => a.setRoot());
}
import {ViewLocator, Aurelia, Origin} from 'aurelia-framework';
export function configure(aurelia: Aurelia): void {
aurelia.use
.standardConfiguration()
.developmentLogging();
ViewLocator.prototype.createFallbackViewStrategy = (origin: Origin) => {
return new CustomViewStrategy(origin);
};
aurelia.start().then(a => a.setRoot());
}
Logging
Aurelia has a simple logging abstraction that the framework itself uses. By default it is a no-op. The configuration in the above examples shows how to install an appender which will take the log data and output it to the console. Here's the code again, for convenience:
import {LogManager} from 'aurelia-framework';
import {ConsoleAppender} from 'aurelia-logging-console';
LogManager.addAppender(new ConsoleAppender());
LogManager.setLevel(LogManager.logLevel.debug);
export function configure(aurelia) {
aurelia.use
.standardConfiguration;
aurelia.start().then(() => aurelia.setRoot());
}
import {LogManager, Aurelia} from 'aurelia-framework';
import {ConsoleAppender} from 'aurelia-logging-console';
LogManager.addAppender(new ConsoleAppender());
LogManager.setLevel(LogManager.logLevel.debug);
export function configure(aurelia: Aurelia): void {
aurelia.use
.standardConfiguration;
aurelia.start().then(() => aurelia.setRoot());
}
You can also see how to set the log level. Values for the logLevel
include: none
, error
, warn
, info
and debug
.
The above example uses our provided ConsoleAppender
, but you can easily create your own appenders. Simply implement a class that matches the Appender
interface from the logging library.