app.js
and app.html
, databind them together and inject them into the DOM element on which you placed that attribute.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());
}
standardConfiguration()
to configure the standard set of plugins. Then call developmentLogging()
to turn on logging in debug mode, output to the console
.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());
}​
feature
" is provided internally by your application, while a plugin is installed from a 3rd party source through your package manager.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());
}
plugin
API.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());
}
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());
}
logLevel
include : none
, error
, warn
, info
and debug
.hello.js
for our view-model and hello.html
for our view. Here's the source for each :export class Hello {
constructor() {
this.firstName = 'John';
this.lastName = 'Doe';
}
sayHello() {
alert(`Hello ${this.firstName} ${this.lastName}. Nice to meet you.`);
}
}
export class Hello {
firstName: string = 'John';
lastName: string = 'Doe';
sayHello() {
alert(`Hello ${this.firstName} ${this.lastName}. Nice to meet you.`);
}
}
<template>
<input value.bind="firstName">
<input value.bind="lastName">
<button click.trigger="sayHello()">Say Hello</button>
</template>
constructor()
: The view-model's constructor is called first.created(owningView: View, myView: View)
: If the view-model implements the created
callback it is invoked next. At this point in time, the view has also been created and both the view-model and the view are connected to their controller. The created callback will receive the instance of the "owningView". This is the view that the component is declared inside of. If the component itself has a view, this will be passed second.bind(bindingContext: Object, overrideContext: Object)
: Databinding is then activated on the view and view-model. If the view-model has a bind
callback, it will be invoked at this time. The "binding context" to which the component is being bound will be passed first. An "override context" will be passed second. The override context contains information used to traverse the parent hierarchy and can also be used to add any contextual properties that the component wants to add.attached()
: Next, the component is attached to the DOM (in document). If the view-model has an attached
callback, it will be invoked at this time.detached()
: If defined on your view-model - is invoked after the component has been removed from the DOM. Due to navigating away or other reasons.unbind()
: After a component is detached, it's usually unbound. If your view-model has the unbind
callback, it will be invoked during this process.unbind
. The same goes for attached
and detached
, but again, it isn't mandatory.re-assembling
" the smaller parts again at runtime. This is what a dependency injection container can do for you, using simple declarative hints.CustomerEditScreen
that needs to load a Customer
entity by ID from a web service
. We wouldn't want to place all the details of our AJAX implementation inside our CustomerEditScreen
class. Instead, we would want to factor that into a CustomerService
class that our CustomerEditScreen
, or any other class, can use when it needs to load a Customer
. Aurelia's dependency injection container lets you accomplish this by declaring that the CustomerEditScreen
needs to have a CustomerService injected
at creation time.import {CustomerService} from 'backend/customer-service';
import {inject} from 'aurelia-framework';
@inject(CustomerService)
export class CustomerEditScreen {
constructor(private customerService: CustomerService) {
this.customer = null;
}
activate(params) {
return this.customerService.getCustomerById(params.customerId)
.then(customer => this.customer = customer);
}
}
lifetime
". There are three lifetime behaviors that are typical :A
, is instantiated when it is first needed by the DI container. The container then holds a reference to class A
's instance so that even if no other objects reference it, the container will keep it in memory. When any other class needs to inject A
, the container will return the exact same instance. Thus, the instance of A
has its lifetime connected to the container instance. It will not be garbage collected until the container itself is disposed and no other classes hold a reference to it.child DI
containers created from parent containers. Each of these child containers inherits the services of the parent, but can override them with their own registrations. Every application has a root DI
container from which all classes and child containers are created. An application singleton is just like a container singleton, except that the instance is referenced by the root DI
container in the application. This means that the root and all child containers will return the same singleton instance, provided that a child container doesn't explicitly override it with its own registration.Any DI
container can create transient instances. These instances are created each time they are needed. The container holds no references to them and always creates a new instance for each request.Container
named root
and we call root.createChild()
to create a child container named child. Then, we invoke child.get(A)
to resolve an instance of A
. First
, child
checks for a Resolver for A. If none is found, then it calls get(A)
on its parent which is the root container
from which it was created. root
then checks to see if it has a Resolver. If not, it auto-registers A in root
and then immediately calls the Resolver to get
an instance of A
. root
. If a developer (or Aurelia) invokes root.get(A)
to resolve an instance of A
, the root will first check to see if it has a Resolver for A
. If one is found
, the Resolver is used to get the instance, which is then returned to the developer. If one is not found, the container will auto-register
a Resolver for A
. This resolver
is configured with a singleton lifetime behavior. Immediately after auto-registration
, the Resolver is used to get the instance of A
which is returned to the developer. Subsequent calls to root.get(A)
will now immediately find a Resolver for A
which will return the singleton instance. Resolvers
internally to provide all instances. When explicitly configuring
the container, you are actually specifying what Resolver
should be associated with a particular lookup key. However, there's a second way that resolvers
are useful. Instead of supplying a key as part of the inject
decorator, you can provide a Resolver
instead.resolver
then communicates
with the container to provide special resolution behavior, specific to the injection
. Here's a list of the resolvers
you can use in this capacity:Lazy.of(HttpClient)
All
: Injects an array of all services registered with the provided key.All.of(Plugin)
Optional
: Injects an instance of a class only if it already exists in the container; null otherwise.Optional.of(LoggedInUser)
Parent
: Skips starting dependency resolution from the current container and instead begins the lookup process on the parent container.Parent.of(MyCustomElement)
Factory
: Used to allow injecting dependencies, but also passing data to the constructor.Factory.of(CustomClass)
NewInstance
: Used to inject a new instance of a dependency, without regard for existing instances in the container.NewInstance.of(CustomClass).as(Another)
TypeScript
, keep in mind that @autoinject
won't allow you to use Resolvers. You also can use inject as argument decorator for your own custom resolvers, eg constructor(@inject(NewInstance.of(HttpClient)) public client: HttpClient){...}
. Available build-in function parameter decorators are :lazy(key)
all(key)
optional(checkParent?)
parent
factory(key)
newInstance(asKey?, dynamicDependencies: [any])
dependency
on HttpClient
that we may or may not actually need to use, depending on runtime scenarios :Using Resolvers : TypeScript
import {lazy} from 'aurelia-framework';
import {HttpClient} from 'aurelia-fetch-client';
export class CustomerDetail {
constructor(@lazy(HttpClient) private getHTTP: () => HttpClient){ }
}
Lazy
resolver doesn't actually provide an instance of HttpClient
directly. Instead, it provides a function that can be invoked at some point in the future to obtain an instance of HttpClient
if needed..bind
: Uses the default binding. One-way binding
for everything but form controls, which use two-way binding
..one-way
: Flows data one direction: from the view-model
to the view
..two-way
: Flows data both ways: from view-model
to view
and from view
to view-model
..one-time
: Renders data once, but does not synchronize
changes after the initial render.<template>
<input type="text" value.bind="firstName">
<input type="text" value.two-way="lastName">
<a href.one-way="profileUrl">View Profile</a>
</template>
.trigger
: Attaches an event handler directly to the element. When the event fires, the expression will be invoked..delegate
: Attaches a single event handler to the document (or nearest shadow DOM boundary) which handles all events of the specified type, properly dispatching them back to their original targets for invocation of the associated expression.<template>
<button click.trigger="save()">Save</button>
<button click.delegate="save($event)">Save</button>
</template>
import {RouterConfiguration, Router} from 'aurelia-router';
export class App {
configureRouter(config: RouterConfiguration, router: Router): void {
this.router = router;
config.title = 'Aurelia';
config.map([
{ route: ['', 'home'], name: 'home', moduleId: 'home/index' },
{ route: 'users', name: 'users', moduleId: 'users/index', nav: true },
{ route: 'users/:id/detail', name: 'userDetail', moduleId: 'users/detail' },
{ route: 'files*path', name: 'files', moduleId: 'files/index', href:'#files', nav: true }
]);
}
}
ie 'home'
: Matches the string exactly.ie 'users/:id/detail'
: Matches the string and then parses an id parameter. Your view-model's activate callback will be called with an object that has an id property set to the value that was extracted from the url.ie 'files*path'
: Matches the string and then anything that follows it. Your view-model's activate callback will be called with an object that has a path property set to the wildcard's value.canActivate(params, routeConfig, navigationInstruction)
: Implement this hook if you want to control whether or not your view-model can be navigated to. Return a boolean value, a promise for a boolean value, or a navigation command.activate(params, routeConfig, navigationInstruction)
: Implement this hook if you want to perform custom logic just before your view-model is displayed. You can optionally return a promise to tell the router to wait to bind and attach the view until after you finish your work.canDeactivate()
: Implement this hook if you want to control whether or not the router can navigate away from your view-model when moving to a new route. Return a boolean value, a promise for a boolean value, or a navigation command.deactivate()
: Implement this hook if you want to perform custom logic when your view-model is being navigated away from. You can optionally return a promise to tell the router to wait until after you finish your work.aurelia-event-aggregator
plugin using "basicConfiguration
" or "standardConfiguration
" then the singleton EventAggregator's API
will be also present on the Aurelia
object. You can also create additional instances of the EventAggregator
, if needed, and "merge
" them into any object. To do this, import includeEventsIn
and invoke it with the object you wish to turn into an event aggregator
. For example includeEventsIn(myObject)
. Now my object has publish
and subscribe
methods and can be used in the same way as the global event aggregator
, detailed below.import {autoinject} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
@autoinject
export class APublisher {
constructor(private eventAggregator: EventAggregator) { }
publish(): void {
var payload = {};
this.eventAggregator.publish('channel name here', payload);
}
}
import {autoinject} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
@autoinject
export class ASubscriber {
constructor(private eventAggregator: EventAggregator) { }
subscribe(): void {
this.eventAggregator.subscribe('channel name here', payload => {
...
});
}
}
//some-message.ts
export class SomeMessage{ }
//a-publisher.ts
import {autoinject} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
import {SomeMessage} from './some-message';
@autoinject
export class APublisher {
constructor(private eventAggregator: EventAggregator) { }
publish(): void {
this.eventAggregator.publish(new SomeMessage());
}
}
import {autoinject} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
import {SomeMessage} from './some-message';
@autoinject
export class ASubscriber {
constructor(private eventAggregator: EventAggregator) { }
subscribe(): void {
this.eventAggregator.subscribe(SomeMessage, message => {
...
});
}
}
ref="expression"
. When the view is data-bound the specified expression will be assigned the DOM element. <template>
<input type="text" ref="nameInput"> ${nameInput.value}
</template>
element.ref="expression"
: create a reference to the DOM element (same as ref="expression"
).attribute-name.ref="expression"
: create a reference to a custom attribute's view-model.view-model.ref="expression"
: create a reference to a custom element's view-model.view.ref="expression"
: create a reference to a custom element's view instance (not an HTML Element).controller.ref="expression"
: create a reference to a custom element's controller instance.<span textcontent.bind="'Hello' + firstName"></span>
<span>Hello ${firstName}</span>
ES2015
.to-view
binding. By default, the mode of an interpolation binding is to-view
and the result of the expression is always coerced to a string. Results that are null
or undefined
will result in an empty string.$this
: The binding context (the view-model).$parent
: Explicitly accesses the outer scope from within a compose or repeat template. You may need this when a property on the current scope masks a property on the outer scope. Chainable- eg $parent.$parent.foo
is supported.$event
: The DOM Event in delegate or trigger bindings.$index
: In a repeat template, the index of the item in the collection.$first
: In a repeat template, is true if the item is the first item in the array.$last
: In a repeat template, is true if the item is the last item in the array.$even
: In a repeat template, is true if the item has an even numbered index.$odd
: In a repeat template, is true if the item has an odd numbered index..bind/.to-view
.<template>
<div class="foo ${isActive ? 'active' : ''} bar"></div>
<div class.bind="isActive ? 'active' : ''"></div>
<div class.one-time="isActive ? 'active' : ''"></div>
</template>
(eg via classList.add(...))
are preserved. This "safe by default" behavior comes at a small cost but can be noticeable in benchmarks or other performance critical situations like repeats with lots of elements. You can opt out of the default behavior by binding directly to the element's className property using class-name.bind="...."
or class-name.one-time="..."
. This will be marginally faster but can add up over a lot of bindings.style
attribute. Use css
custom attribute when doing string interpolation in your view to ensure your application is compatible with Internet Explorer and Edge. If you don't use interpolation in css
- it won't get processed, so if you are just using inline style - use the proper style attribute of HTMLElement. export class StyleData {
styleString: string;
styleObject: any;
constructor() {
this.styleString = 'color: red; background-color: blue';
this.styleObject = {
color: 'red',
'background-color': 'blue'
};
}
}
<template>
<div style.bind="styleString"></div>
<div style.bind="styleObject"></div>
</template>
Person
class that exposes
a fullName
property that computes it's value using the firstName
and lastName
properties. export class Person {
firstName: string = 'John';
lastName: string = 'Doe';
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}
fullName
. The binding system will examine the property's descriptor , determine that the value of the property is computed by a function and choose the dirty checking observation strategy. Dirty checking means the binding system will periodically check the property's value for changes and update the view as-needed. This means your property's getter function will be executed many times, approximately once every 120 milliseconds
. Most of the time this isn't an issue, however, if you're using a lot of computed properties or if your getter functions are sufficiently complex you may want to consider giving the binding system hints on what to observe so that it doesn't need to use dirty checking. This is where the @computedFrom
decorator comes in: import {computedFrom} from 'aurelia-framework';
export class Person {
firstName: string = 'John';
lastName: string = 'Doe';
@computedFrom('firstName', 'lastName')
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}
@computedFrom
tells the binding system which expressions to observe. When those expressions change, the binding system will re-evaluate the property (execute the getter). This eliminates the need for dirty checking and can improve performance. The @computedFrom
parameters can be simple property names as shown above or more complex expressions like @computedFrom('event.startDate', 'event.endDate')
.animator-velocity()
in Aurelia is used to make things further lively and interesting. This is when the element you will be using will be more exciting performing certain animations as you like to. Here in the Aurelia animation solutions, we can use simply the same library. Here it actually uses a very simple interface and has great flexibility in performing or creating animations. DefaultLinkHandler
skips
click hijacking
in following situations.Alt/Ctrl/Meta/Shift
keys is pressed.<a>
element starts with # (link to local hash)
, or it's a full url like https://... or ftp://....
<a>
has a target attribute and it is not targeting the current window. <a href="/some/link" target="_blank">Skip Hijacking</a>
<a href="/some/link">Does Not Skip</a>
<a href="/some/link" target="_self">Does Not Skip</a>
<a href="/some/link" target="name-of-current-window">Does Not Skip</a>​
<a>
has special attribute download, or router-ignore
, or data-router-ignore
. <a href="/some/link" download>Skip Hijacking</a>
<a href="/some/link" download="">Skip Hijacking</a>
<a href="/some/link" router-ignore>Skip Hijacking</a>
<a href="/some/link" router-ignore="">Skip Hijacking</a>
<a href="/some/link" data-router-ignore>Skip Hijacking</a>
<a href="/some/link" data-router-ignore="">Skip Hijacking</a>​
<a href="/some/link" data-router-ignore.bind="condition || null">Conditional Skip Hijacking</a>
add/remove attribute data-router-ignore
when condition changes. Note || null
is necessary because Aurelia only removes the data attribute when the bound value becomes null
or undefined
, it doesn't remove the data attribute when bound value is 0
or ""
or even false.<a href="/some/link" router-ignore.bind="condition || null">Does not work</a>
does NOT add/remove
attribute router-ignore
. Aurelia only does that for data-
or aria-
attributes.fluent rule API
and minimal changes to your templates.aurelia-validation
using npm install aurelia-validation
or jspm
install aurelia-validation. Afterwards
, add .plugin(PLATFORM.moduleName('aurelia-validation'))
to the configuration in your main.js to ensure the plugin is loaded at application startup..ensure
.required
, .matches
, etc.withMessage
, .when
, etc.then
.on
validate
binding behavior enables quick and easy validation for two-way data-bindings. The behavior registers the binding instance with a controller
, enabling
the system
to validate the binding's associated property when the validate trigger occurs (blur / change). The binding behavior is able to identify the object and property name to validate in all sorts of binding expressions : <input type="text" value.bind="firstName & validate">
<input type="text" value.bind="person.firstName & validate">
<input type="text" value.bind="person['firstName'] | upperCase & validate">
<input type="text" value.bind="currentEntity[p] & debounce & validate">​
validate
accepts a couple of optional arguments enabling you to explicitly specify the rules and controller instance: <input type="text" value.bind="firstName & validate:personController">
<input type="text" value.bind="firstName & validate:personRules">
<input type="text" value.bind="firstName & validate:personController:personRules">​
validate
binding behavior obeys the associated controller's validateTrigger (blur, focusout, change, changeOrBlur, changeOrFocusout, manual)
. If you'd like to use a different validateTrigger
in a particular binding use one of the following binding behaviors in place of & validate
:& validateOnBlur
: the DOM blur event triggers validation.& validateOnFocusout
: the DOM focusout event triggers validation.& validateOnChange
: data entry that changes the model triggers validation.& validateOnChangeOrBlur
: DOM blur or data entry triggers validation.& validateOnChangeOrFocusout
: DOM focusout or data entry triggers validation.& validateManually
: the binding is not validated automatically when the associated element is blurred or changed by the user.ValidateResult
instances. These are the results of validating individual rules
.ValidateResult
instances whose valid property is false.validation
. <form>
<ul if.bind="controller.errors">
<li repeat.for="error of controller.errors">
${error.message}
</li>
</ul>
...
...
</form>
import {inject, NewInstance} from 'aurelia-framework';
import {ValidationController, ValidationRules} from 'aurelia-validation';
@inject(NewInstance.of(ValidationController), NewInstance.of(ValidationController))
export class ItalianRestaurant {
pizza;
pasta;
pizzaValidationController;
pastaValidationController
constructor(pizzaValidationController, pastaValidationController) {
this.pizzaValidationController = pizzaValidationController;
this.pastaValidationController = pastaValidationController;
const pizzaRules = ValidationRules
.ensure((res: ItalianRestaurant) => res.pizza).required()
.rules;
this.pizzaValidationController.addObject(this, pizzaRules);
const pastaRules = ValidationRules
.ensure((res: ItalianRestaurant) => res.pasta).required()
.rules;
this.pastaValidationController.addObject(this, pastaRules);
}
orderPizza() {
this.pizzaValidationController.validate()
.then(result => {
if (result.valid) {
alert("Ordering this pizza: " + this.pizza);
}
});
}
orderPasta() {
this.pastaValidationController.validate()
.then(result => {
if (result.valid) {
alert("Ordering this pasta: " + this.pasta);
}
});
}
}
In your view you need to take care to associate each input with the correct validation controller :
I18N View :
<template>
<form validation-errors="errors.bind: pizzaErrors; controller.bind: pizzaValidationController"
submit.delegate="orderPizza()">
<label for="pizza">Choose a pizza:</label>
<input id="pizza" value.bind="pizza & validateManually:pizzaValidationController">
<span class="help-block" repeat.for="errorInfo of pizzaErrors">
${errorInfo.error.message}
</span>
<input type="submit" value="Order pizza!">
</form>
<form validation-errors="errors.bind: pastaErrors; controller.bind: pastaValidationController"
submit.delegate="orderPasta()">
<label for="pasta">Choose a pasta:</label>
<input id="pasta" value.bind="pasta & validateManually:pastaValidationController">
<span class="help-block" repeat.for="errorInfo of pastaErrors">
${errorInfo.error.message}
</span>
<input type="submit" value="Order pasta!">
</form>
</template>
In the forms above you can see that each validation-errors
attribute and each validateManually
binding behavior is bound to the appropriate validation controller.
UI Virtualization
plugin allows you to virtually scroll lists comprised of many items, it also provides an infinite scroll
attribute which allows you to fetch more items when the user scrolls the container. The infinite-scroll-next
attribute accepts a callback function in your view which receives three arguments when fired.<template></template>
is not supported as a root element of a virtual repeat template, because virtualization requires item heights to be calculatable. With <template></template>
, there is no easy and performant way to acquire this value.virtual-repeat
, unlike repeat i.e: built-in
template controllers: with
, if
, replaceable
cannot be used with virtual-repeat
. You can easily work around this constraint by nesting other template controllers inside the repeated element, with <template></template>
element.app.html
<template>
<h1>${message}</h1>
<div virtual-repeat.for="person of persons">
<template with.bind="person">
${Name}
</template>
</div>
</template>
nth-child
and similar selectors. Virtualization requires appropriate removing and inserting visible items, based on scroll position. This means DOM elements order will not stay the same, thus creating unexpected :nth-child CSS selector behavior
. To work around this, you can use contextual properties $index
, $odd
, $even
etc... to determine an item position, and apply CSS classes/styles
against it, like the following example : <template>
<div virtual-repeat.for="person of persons" class="${$odd ? 'odd' : 'even'}-row">
${person.name}
</div>
</template>​
(bind, attached)
or removing (unbind, detached)
ComponentTester
exposes a set of properties that can be handy when doing asserts or to stage a component in a specific way. Here's a list of what is available :element
: The HTML element that gets rendered.viewModel
: The view-model for the component.configure
: The ComponentTester's configure method can be overwritten in order to set it up with custom configuration or get a reference to the container instance.dispose
: Cleans up the DOM after a test has run.bind
: Manually handles bind.unbind
: Manually handles unbind.attached
: Manually handles attached.detached
: Manually handles detached.waitForElement and waitForElements
: Waits until one or several elements are present / absent. See below.End-To-End (E2E)
testing is all about testing your application against specific scenarios. From a requirements-engineering-perspective you'd call those User Stories. Essentially, these stories describe a series of actions a user performs to achieve a certain goal. So by assembling a bunch of these - referred to as a test suite - you are able to verify that your web app acts as expected. The key to success, of course, lies in the amount and quality of tests written.E2E
testing you are not interacting with the app's code per se, but with the app's interface. This is different than unit tests, which take care of isolated parts of the application - called units - by verifying them through the removal or mocking of other parts. It's important to note that one method of testing does not replace the other, so don't take this article as an excuse to skip unit testing.aurelia_project
directory there is a file called aurelia.json. This file is the centralized settings for all the gulp tasks like build and test. We will show you more details of this file when talking about various customization. Aurelia CLI
, you are presented with a wizard to select a bundler, a module loader, CSS preprocessor and more.Webpack
(the default bundler) or CLI's built-in bundler (the alternative bundler)..moduleName()
for any module reference in the code, except for the import statements. PLATFORM can be imported using import {PLATFORM}
from 'aurelia-pal
';.main.js
or main.ts
do aurelia.setRoot(PLATFORM.moduleName('app'))
.{ ...., name: 'users', moduleId: PLATFORM.moduleName('./users'), ... }
@useView(PLATFORM.moduleName('my-view.html'))
.resources(PLATFORM.moduleName('resources/index'))
aurelia_project/aurelia.json
file. This is different for Webpack, where the bundle configuration is inside webpack.config.js
. Webpack projects created by the CLI use Aurelia's Webpack Plugin, which you'll find in webpack.config.js
.SASS
or LESS
, then you are going to have to import the stylesheets differently. When you include stylesheets and use the Aurelia CLI bundler, then your <require>
statements will have the .css extension: <require from="styles/my-stylesheet.css"></require>
. When you use Webpack you have to change this into the .sass
or .less extension: <require from="styles/my-stylesheet.sass"></require>
.Aurelia CLI
to a newer version. In this chapter, these steps will be explained.Aurelia CLI
is actually installed twice, once globally and once locally. The globally installed Aurelia CLI (npm install aurelia-cli -g)
can be found in your user profile and the local one lives inside the node_modules
folder of your project. The global Aurelia CLI
is used when you're not inside a project directory, for example when you create a new Aurelia CLI
project using au new. Inside a project directory, however, the local version of Aurelia CLI
is used. That allows you to use different versions of the Aurelia CLI
per project.npm
install aurelia-cli@latest -g
to update the CLI globally, and run npm install -D
aurelia-cli@latest
from the project directory to update the local Aurelia CLI.Gulp tasks
. So when you want to update an existing project, you'll want to update the Gulp tasks as well. The recommended way to do this is to update the CLI globally and create a new project using au new
. Then you can copy over the tasks in the aurelia_project/tasks
directory to your own project.