Skip to content

Aurelia

Components

In Au UI component is composed of view and view-model pairs. NB! The component name, derived from the file name, must contain a hyphen when working with Shadow DOM.

Import component into other views via <import from="component-name"/>

Use component as new html tag <component-name></component-name>

Components, todo-input.ts/html

1
2
3
4
5
<form class="add-items d-flex" name="todo-input" submit.trigger="submitForm($event)">
    <input value.bind="todoDescription" type="text" 
    class="form-control todo-list-input" placeholder.one-time="placeholder">
    <button class="add btn btn-primary font-weight-bold todo-list-add-btn">Add</button>
</form>
1
2
3
4
5
6
7
8
9
export class TodoInput {

public todoDescription: string = '';

public placeholder: string = "What do you need to do today?"
    submitForm(event: Event) {
        event.preventDefault();
    }
}
1
2
<import from="components/todo-input"/>
<todo-input></todo-input>

Binding to components 1

Sending data to components - @bindable Custom component must specify the properties that will be bindable as attributes from the outside Default binding is one-way – from parent to child.

1
2
3
4
5
import {bindable} from 'aurelia';

export class TodoInput {
    @bindable public placeholder: string = "What do you need to do today?";
}
1
<todo-input placeholder.bind="placeholder"></todo-input>

Binding to components 2

Change component default binding behaviour

1
@bindable({ defaultBindingMode: bindingMode.twoWay }) public todoDescription: string = '';

Possible values are:

  • oneTime
  • fromView
  • toView / oneWay (default)
  • twoWay

Messaging - Overview

  • It’s not recommended to use two-way binding between components – it gets complicated fast.
  • One way to solve it, is to use messaging mechanism import { EventAggregator, IDisposable } from 'aurelia’;
  • Use @autoinject decorator and constructor parameters to get automatic DI constructor(private eventAggregator: EventAggregator) {
  • Send messages with this.eventAggregator.publish("eventId", this.todoDescription);
  • Subscribe to receive messages with this.eventAggregator.subscribe("eventID", (data) => { this.handleMsg(data) })
  • Remove subscriptions when view is removed...

Code example - publishing

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { bindable, bindingMode } from 'aurelia';
import { EventAggregator, IDisposable } from 'aurelia';

export class TodoInput {

    private subscriptions: IDisposable[] = [];
    @bindable public placeholder: string = "What do you need to do today?"
    public todoDescription: string = '';

    constructor(private eventAggregator: EventAggregator) {
    }

    detached() {
        // remove all the listeners
        this.subscriptions.forEach(subscription => {
            subscription.dispose();
        });
        this.subscriptions = [];
    }
    submitForm(event: Event) {
        this.eventAggregator.publish("newTodo", this.todoDescription);
        this.todoDescription = "";
        event.preventDefault();
    }
}

Code example - subscribing

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { EventAggregator, IDisposable } from 'aurelia-event-aggregator';
import { ITodo } from './domain/ITodo';

export class App {
    private subscriptions: IDisposable[] = [];
    public todos: ITodo[] = [];
    public placeholder: string = "Add something?"

    constructor(private eventAggregator: EventAggregator) {
        this.subscriptions.push(
        this.eventAggregator.subscribe("newTodo", (descr: string) => this.      newTodoReceived(descr)));
    }

    detached() {
        this.subscriptions.forEach(subscription => {
            subscription.dispose();
        });
        this.subscriptions = [];
    }

    newTodoReceived(description: any){
        if (typeof(description) == 'string' && description.length > 0){
            this.todos.push({ description: description, done: false });
        }
    }

    removeTodo(index: string) {
        this.todos.splice(Number(index), 1);
    }
}

Dependency injection

Use constructor parameters with access modifier to get automatic DI

1
2
3
4
5
import { EventAggregator } from 'aurelia-event-aggregator';

export class App {
    constructor(private eventAggregator: EventAggregator) {
    }

Everything is an application-level singleton, lazily instantiated.

DAL/BLL

Functional style, immutable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { ITodo } from "domain/ITodo";

export class AppState {
    private todos: readonly ITodo[] = [];

    getTodos = (): readonly ITodo[] => [...this.todos];

    addTodo = (description: string): readonly ITodo[] =>
    this.todos = [...this.todos, { description: description, done: false }];

    removeTodo = (todoIndex: number): readonly ITodo[] =>
    this.todos = this.todos.filter((_, index) => index !== todoIndex);

    countTodos = () => this.todos.length;
}

DAL/BLL - Usage

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { bindable } from 'aurelia-framework';
import { AppState } from 'state/app-state';

export class TodoInput {
    @bindable public placeholder: string = "What do you need to do today?"
    public todoDescription: string = '';

    constructor(private appState: AppState) {
    }

    submitForm(event: Event) {
        this.appState.addTodo(this.todoDescription);
        this.todoDescription = "";
        event.preventDefault();
    }
}

DAL/BLL - Usage 2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { ITodo } from './domain/ITodo';
import { AppState } from 'state/app-state';

export class App {
    public placeholder: string = "Add something?"

    constructor(private appState: AppState) {
    }

    removeTodo(index: string) {
        this.appState.removeTodo(Number(index));
    }
}

Routing

  • Typically your app will not contain just one page/with components
  • You need some navigation between different sections of your app


  • Your component view must have a <au-viewport></au-viewport> element.

  • Aurelia supports 3 different routing methods

    • Direct, Component configured, Configured (classical)

Enabling routing

Enable routing in main.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import Aurelia, { ColorOptions, ConsoleSink, LoggerConfiguration, LogLevel, RouterConfiguration } from 'aurelia';
import { MyApp } from './my-app';

Aurelia
  .register(
    LoggerConfiguration.create(
      {
        sinks: [ConsoleSink],
        level: LogLevel.debug,
        colorOptions:
          ColorOptions.colors
      }
    ),
    RouterConfiguration)
  .app(MyApp)
  .start();

Routing, classical

Define routes - MyApp

1
<au-viewport></au-viewport>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { IRouter, ILogger, route } from 'aurelia';

@route({
  routes: [
    {
      id: 'main',
      path: '',
      component: import('./views/index'),
      title: 'Main',
    },
    {
      id: 'results',
      path: '/results/:foo',
      component: import('./views/results'),
      title: 'Results',
    },
  ]
})

export class MyApp {
  constructor(
    @IRouter private router: IRouter,
    @ILogger private logger: ILogger
  ) {
    this.logger = logger.scopeTo("MyApp");
  }
}

Default view - Index

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { ILogger, IRouter, inject } from 'aurelia';

@inject()
export class Index {

    constructor(
        @IRouter private router: IRouter,
        @ILogger private logger: ILogger) {
        this.logger = logger.scopeTo("Index");
    }

    attached() {
        this.logger.debug("attached");
    }

    async go(event: PointerEvent) {

        event.preventDefault();
        this.logger.debug("GO");
        await this.router.load('results/somevalue',
            {
                title: 'My product',
                queryParams: {
                    bar: 'one'
                }
            } 
        );
    }

}
1
2
index
<button click.trigger="go($event)">Go to results</button>

Results view

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import { ILogger, IRouter, inject, Params } from 'aurelia';

@inject()
export class Results {

    constructor(
        @IRouter private router: IRouter,
        @ILogger private logger: ILogger) {
        this.logger = logger.scopeTo("Results");
    }

    attached() {
        this.logger.debug("attached");
    }

    async load(params: Params) {
        this.logger.debug("params", params);
    }
}

Routing, direct

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<import from="./views/home-view" />

<li class="nav-item">
    <a class="nav-link text-dark" load="home-view">Home</a>
</li>

<!-- ... -->

<main role="main" class="pb-3">
    <au-viewport></au-viewport>
</main>

Routing, direct, default route

Typically we need to have default page (home-view) loaded initially.

<au-viewport default="home"></au-viewport>

Direct routing, parameters

Sending parameters to view

1
2
3
export class MyApp {
    private data = "some_value";
}
1
<a class="nav-link text-dark" load="test-view(${data})">Home</a>

Receiving parameters in view

1
2
3
4
5
6
7
import { IRouteViewModel } from "aurelia";

export class TestView implements IRouteViewModel {
    load(parameters) {
        console.log(parameters); // {0: 'some_value'} 
    }
}

Direct routing, named parameters

Inline named route parameters

1
<a class="nav-link text-dark" load="test-view(foo=${data})">Home</a>

{foo: ‘some_data}


Or named route parameters - (didn't work...)

Add static class property paramaters – string array of your parameter names

1
2
3
4
5
6
7
8
9
import { IRouteViewModel } from "aurelia";

export class TestView implements IRouteViewModel {
    static parameters = ['bar'];

    load(parameters) {
        console.log(parameters);
    }
}

Direct routing

  • When loading the new view from inside <au-viewport>
  • Use the SLASH in front of the view element
  • Views have to be imported on the same view where the viewport is!
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<tr repeat.for="item of data">
    <td>
        ${item.id}
    </td>
    <td>
        ${item.contactTypeValue}
    </td>
    <td>
        <a href="#" load="/contacttype-details(${item.id})">Details</a>
    </td>
</tr>

Routing from code

Inject IRouter into constructor.

1
2
constructor(
  @IRouter private router: IRouter,

Navigate from code.

1
await this.router.load('/home-index');

With parameters.

1
await this.router.load('/home-index(12)');

Rest client 1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { HttpClient } from 'aurelia';
import { ITodo } from 'domain/ITodo';

export class TodoService {
    constructor(private httpClient: HttpClient) {

    } 

    getTodos(): Promise<ITodo[]> {
        return this.httpClient.fetch('https://127.0.0.1:5001/api/todos', {  cache: "no-store" })
        .then(response => response.json())
        .then((data: ITodo[]) => {
            return data;
        });
    }
}

Rest client 2

https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API

Promise? Async/await?