Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
New to Javascript, Node.js and front-end development in general? Don't worry, we got you.
Welcome to the magical world of Javascript development. This guide is for any newcomer to front-end development who isn't that experienced with modern tooling or Javascript frameworks.
For the purposes of this tutorial and as a general rule for any modern framework like Aurelia, you will be using a terminal of some sort. On Windows, this can be the Command Prompt or Powershell. On macOS, it'll be Terminal (or any other Terminal alternative), the same thing with Linux.
To work with Aurelia, you will need to install Node.js. If you are new to Node.js, it is used by almost every tool in the front-end ecosystem now, from Webpack to other niche bundlers and tools. It underpins the front-end ecosystem.
The easiest way to install Node.js is from the official website here. Download the installer for your operating system and then follow the prompts.
To write code, you need an editor that will help you. The most popular choice for Javascript development is Visual Studio Code. It is a completely free and open-source code editor made by Microsoft, which has great support for Aurelia applications and Node.js.
We will be following the instructions in the Quick install guide to bootstrap a new Aurelia application. After installing Node.js, that's it. You don't need to install anything else to create a new Aurelia application, here's how we do it.
Open up a Terminal/Command Prompt window and run the following:
You are going to be presented with a few options when you run this command. Don't worry, we'll go through each screen step by step.
You will be asked to enter a name for your project, this can be anything you want. If you can't think of a name just enter my-app
and then hit enter.
In step 2 you will be presented with three options.
Option one: "Default ESNext Aurelia 2 App" this is a basic Aurelia 2 Javascript application using Babel for transpiling and Webpack for the bundler.
Option two: "Default Typescript Aurelia 2 App" this is a basic Aurelia 2 TypeScript application with Webpack for the bundler.
Option three: "Custom Aurelia 2 App" no defaults, you choose everything.
In this guide, we are going to go with the most straightforward option, option #1.
You are going to be asked if you want to install the Npm dependencies and the answer is yes. For this guide we are using Npm, so select option #2.
Depending on your internet connection speed, this can take a while.
After the installation is finished you should see a little block of text with the heading, "Get Started" follow the instructions. Firstly, cd my-app
to go into the directory where we installed our app. Then run npm start
to run our example app.
Your web browser should open automatically and point to http://localhost:9000
Any changes you make to the files in the src
directory of your app will cause the dev server to refresh the page with your new changes. Edit my-app.html
and save it to see the browser update. Cool!
In the last section we created a new application and ran the development server, but in the "real world" you will build and deploy your site for production.
Run the Npm build command by running the following in your Terminal or Command Prompt window:
This will build your application for production and create a new folder called dist
.
Before you can build an application with Aurelia, you need to meet a couple of prerequisites.
Aurelia was designed to make building web applications and rich UI's easy. Living by the mantra of; Simple, Powerful and Unobtrusive, you have everything you need right out of the box to start building feature-rich web applications.
This section aims to give you a brief technical overview of how Aurelia works, but it is not intended to be a tutorial or reference. If you are looking for a nice gentle introduction to Aurelia that will only take a few minutes of your time the Hello World tutorial is a great place to start.
The core concepts of Aurelia are simple; HTML, CSS and Javascript. While Aurelia does introduce its own templating language you will need to become familiar with, it is enhanced HTML, so the syntax and concepts will feel familiar to you already.
Underpinning Aurelia is the view-model and view convention. Out-of-the-box, Aurelia assumes that your components are made up of both a view model (.js or .ts file) as well as an accompanying view with a .html file extension.
If you have worked with .NET or other frameworks that use similar concepts, the view-model and view paradigm will be familiar with you. The view-model contains the business logic and data, the view is responsible for displaying it.
Like all concepts in Aurelia, these are default conventions and can be changed as needed.
Much like other fully-featured Javascript frameworks and once again, .NET, Aurelia features a robust dependency injection system that allows you to manage your in-app dependencies. You will learn about the benefits of dependency injection in other parts of the documentation, but it plays a fundamental part in Aurelia.
To create new Aurelia applications using the Aurelia CLI via Makes, you will need to ensure you have Node.js installed. The latest version of Node is always recommended and preferable. While Node can be installed through different methods, the easiest is to obtain a distributable from the Node.js website.
Aurelia does not require you to install any global Node packages, instead of using the Makes scaffolding tool, new Aurelia applications can be generated by running the following command.
You will then be presented with the Aurelia CLI screen which will first ask for a project name, then guide you through some options. The fastest way to get started is by choosing either the ESNext or TypeScript default options (1 and 2).
We highly recommend choosing TypeScript for any new Aurelia applications. With TypeScript you get the benefits of intellisense and type safety.
Once the CLI process is finished and you have installed the dependencies for your new app, run npm start
in the project directory, a browser window will open up with the new Aurelia application you just generated running.
Take a look at our beginner-friendly obligatory hello world introduction to Aurelia here. And if you want to dive even deeper (perhaps you've dabbled in Aurelia before) we have a fantastic selection of tutorials here.
Learn the basics of Aurelia by building an interactive Hello, World! application
This guide will take you through creating a hello world app using Aurelia and briefly explain its main concepts. We will be building a simple hello, world example with a text field you can enter a name into and watch the view update. We assume you are familiar with JavaScript, HTML, and CSS.
Here's what you'll learn...
How to set up a new Aurelia project.
Creating components from view-models and views.
The basics of templating and binding.
Where to go from here.
When you complete this guide, you'll be ready to take your first steps building your own Aurelia app and, you'll have the resources you need to continue into more advanced topics. So, what are you waiting for? Let's get started!
A view-model is where your business logic will live for your components. Follow along as you create your first view-model.
Aurelia as a default convention works on the premise of a view and view-model, both of which are tied together. The view-model is where your business logic lives and, if you have ever worked with .NET before (or other frameworks), you might refer to this as the controller.
Navigate to the src
directory where your application code lives and open up the my-app(.ts/.js
file. This is the main entry point for the application and this file is where business logic would live. As you can see, there is not a lot going on at the moment.
The class property message
contains a string and within our view, we are displaying it using interpolation.
We are now going to create a new component which will be a smarter hello component. Inside of src
create a new file called hello-name
and use the appropriate file extension depending on whether you are using TypeScript or not. So, hello-name.ts
or hello-name.js
.
Notice how our new component doesn't look much different than the generated one? That's intentional. We are going to bind to this name property using two-way
binding from within our view. We don't need any callback functions to update this value either.
Nice one! You just created a custom element view-model. It might not look like much, but you just learned a very core fundamental concept of building Aurelia applications. A view-model can be as simple and complex as you want it to be.
In the next chapter, we will hook this up with our view and allow a text input to change this value.
Learn to use Aurelia's project scaffold tooling to create your first project setup.
There are various ways that you can set up an Aurelia project, including everything from adding a simple script tag to your HTML page to creating a custom Webpack configuration. One of the easiest and most powerful ways to get started is by using the makes
tool.
Before you run makes
, you will need a recent version of Node.js installed on your machine. If you do not have Node.js, you can get it here. Please ensure that Node.js is up-to-date if you already have it installed.
Next, using npx
, a tool distributed with Node.js, we'll create a new Aurelia app. Open a command prompt and run the following command:
makes
will then start the Aurelia project wizard, asking you a few questions to help you get things set up properly. When prompted, give your project the name "hello-world" and then select a default setup, either ESNext (Javascript) or TypeScript, depending on your preference. Finally, choose "yes" to install the project dependencies.
You have now created a hello world app without writing a single line code, well done. However, we'll be updating our app to allow for text input so we can make it say, "Hello" to whoever we want in the next section.
You now have an Aurelia setup ready for you to run, debug, or deploy. To ensure that everything is working properly, cd
into your project folder and run npm start
. Your project will then build and a web browser will open, displaying the message "Hello World".
Get acquainted with Aurelia, the documentation, and how to get started.
Welcome to Aurelia, the future-oriented, open-source JavaScript platform for crafting extraordinary web, mobile, and desktop applications. At its core, Aurelia is more than just a framework; it believes in the unbridled power of open web standards. With Aurelia, your development experience is streamlined, intuitive, and remarkably flexible.
To start, jump to our Quick Start Guide or read on for an overview.
Aurelia emerges as a beacon of innovation and power in front-end development in a sea of frameworks. Here's why Aurelia might be the perfect choice for your next project:
All-in-One Ecosystem: Aurelia is your Swiss Army knife, offering everything from dependency injection to routing, eliminating the need to juggle multiple libraries. Its ecosystem extends to plugins for internationalization, validation, and more, supported by tools like a CLI and a VS Code plugin for seamless development. And with Aurelia, flexibility is key; you can customize every aspect, including the templating/binding engine.
Unparalleled Performance: Speed and efficiency are in Aurelia's DNA. It consistently outperforms competitors in benchmarks thanks to its sophisticated batched rendering and efficient memory usage.
Data-Binding Done Right: Aurelia harmonizes the safety of uni-directional data flow with data-binding productivity, ensuring both performance and ease of development.
Stability in a Dynamic World: Since our 1.0 release in 2016, Aurelia has maintained API stability, innovating without breaking changes, a testament to our commitment to developer trust.
Adherence to High Standards: Aurelia is a paragon of standards compliance, focusing on modern JavaScript and Web Components without unnecessary abstractions.
Empowering Developers: Aurelia's philosophy is simple: empower developers with plain JavaScript/TypeScript and get out of the way. This results in cleaner, more maintainable code.
Ease of Learning and Use: Aurelia's design is intuitive and consistent, minimizing the learning curve and maximizing productivity with solid, simple conventions.
Seamless Integration: Aurelia plays well with others. Its adherence to web standards makes it a breeze to integrate with any third-party library or framework.
A Vibrant Open-Source Community: Embrace the open-source ethos with Aurelia, licensed under MIT, without restrictive clauses. Join a community that thrives on collaboration and innovation.
A Welcoming and Supportive Community: Our active core team and Discord server embody a culture of welcome and support, where every developer is a valued member of the Aurelia family.
While it's true that Aurelia's ecosystem and community might seem modest compared to some of its more populous counterparts, this perceived underdog status is, in fact, one of its greatest strengths. Aurelia represents a boutique choice in web frameworks, prioritizing quality over quantity and fostering a tight-knit, highly engaged community. This approach translates to a more focused, consistent, and dedicated development experience, both in terms of the framework itself and the support you receive from the community.
Direct Impact: In a smaller community, each member's voice and contribution have a more significant impact. Developers have direct access to the core team, fostering a collaborative environment where feedback and contributions influence the framework's evolution.
Quality Over Quantity: Aurelia focuses on delivering a high-quality, refined development experience. This means that each plugin, library, and tool in the ecosystem has been carefully crafted and thoroughly vetted to meet high standards.
Nurturing Innovation: A smaller ecosystem doesn't mean limited capabilities. On the contrary, it often leads to innovative solutions and creative problem-solving, as developers are encouraged to think outside the box.
Stronger Connections: A smaller community fosters closer relationships, leading to more meaningful interactions, collaborations, and a sense of belonging. This environment is conducive to a supportive and helpful atmosphere where developers at all levels can thrive.
The size of a community or ecosystem should not be the sole criterion for its effectiveness. In many cases, the quality of interactions, the dedication of its members, and the framework's alignment with your project's needs are far more critical factors. Aurelia's commitment to standards, modern best practices, and empowering approach offers a unique and rewarding development experience that stands on its own merits. Aurelia isn't trying to reinvent the wheel or make your life a misery by changing its API with every major release.
In a landscape where Virtual DOM is often touted as a necessity, Aurelia confidently takes a different path. Aurelia's choice not to use a Virtual DOM is a deliberate and strategic decision grounded in a philosophy that prioritizes efficiency, simplicity, and alignment with web standards.
Directness and Efficiency: Aurelia interacts directly and efficiently with the DOM. By avoiding the overhead of a Virtual DOM, Aurelia ensures faster rendering and updates, leading to a smoother user experience.
Simplicity and Predictability: Without the abstraction layer of a Virtual DOM, Aurelia offers a more straightforward and predictable development experience. Developers work closer to the web platform, leveraging native browser capabilities for DOM manipulation.
Optimal Performance: Aurelia's approach to DOM updates is highly optimized. It intelligently manages DOM changes, ensuring minimal performance overhead. This results in applications that are not only fast but also more resource-efficient. Let's say Aurelia isn't going to drain your phone or Macbook Pro battery.
Aligned with Web Standards: Aurelia's commitment to standards means that it leverages the native capabilities of modern browsers, ensuring compatibility and future-proofing your applications. If the browser can do it, Aurelia won't try one-upping it by rolling its own implementation.
Less Complexity, More Control: By avoiding a Virtual DOM, Aurelia reduces the complexity of your application's architecture. This direct approach gives developers more control and understanding of how their application interacts with the browser.
Tailored for Modern Web Development: Aurelia is designed to meet the needs of modern web applications. Its efficient, standards-based approach is perfectly suited for today's dynamic, interactive web experiences.
The decision to not use a Virtual DOM is a reflection of Aurelia's overarching philosophy: to provide a powerful, efficient, and straightforward framework that stays true to the nature of the web. This approach encourages developers to build applications that are both performant and maintainable, leveraging the full potential of the web platform.
Your journey with Aurelia begins here in our meticulously crafted documentation. Start with the Quick Start Guide to grasp the basics of building an Aurelia application. The "Getting Started" section is your next stop, followed by "App Basics" for a deep dive into regular Aurelia usage.
As your comfort with Aurelia grows, explore "Advanced Scenarios" for insights into performance optimization and large-scale project management. While you may find less need for raw API documentation due to Aurelia's convention-based approach, our "API" section is comprehensive. Don't miss the "Examples" section, where practical code samples for common scenarios are found. And, our "Resources" section is a treasure trove of FAQs, framework comparisons, and much more.
Did you find something amiss in our documentation? Every page includes an "Edit on GitHub" link, making it easy to contribute corrections or improvements.
You're in the right place. If you are starting with Aurelia, we highly recommend reading through our Quick Start Guide. It will show you how to create a project and build your first components using Aurelia's powerful templating system.
Use a declarative approach to describe how your component should be rendered.
In the previous section, we created a basic view model, and you are probably sceptical that we are now going to create a view with a text input that updates this value without writing any more Javascript.
Inside the src
directory create a new file called hello-name.html
this will complicate the view model you already have (hello-name.ts
or hello-name.js
).
Firstly, let's write the code to display the value. Notice how we are using interpolation to print the value like the default generated my-app.html
file was? ${name}
the value inside the curly braces references the class property we defined in the previous section, by the name of name
.
I would say run the app and see it in action, but we haven't imported our custom element just yet. Let's do that now.
Inside of my-app.html
replace the entire file with the following:
We use the import
element to include our newly created component. Take note there is a lack of file extension. This is because Aurelia will know to include our view-model and, in turn, include our view and any styles.
We then reference our custom element by its name. Aurelia knows using default conventions to take the file name, strip the file extension and use that as the tag name (this is configurable but beyond the scope of this tutorial).
If you were to run the app using npm start
you would then see your application rendering your new component. You should see the text, Hello, Person!
rendering in the view. Once you have the app running, leave it running as any changes you make will automatically be detected and re-rendered.
We have a functional custom element, but we promised we would be able to update the name with any value we want. Inside of hello-name.html
add the following beneath the existing heading.
You should now see a text input with the value Person
inside of it. By adding value.bind
to the input, Aurelia will use two-way binding by default. This means it will show the value from the view-model inside of the view, and if it changes in either view or view-model, it will be updated. Think of a two-way binding as syncing the value.
Type something into the text input and you should see the heading value change as you type. This works because Aurelia binds to the native value
property of the text input and keeps track of the changes for you without needing to use callbacks or anything else.
As you might have noticed, there really is not that much code here. Leveraging Aurelia's conventions and defaults allows you to write Aurelia applications without the verbosity and without writing tens of lines of code to do things like binding or printing values.
Congratulations! You just ran your first Aurelia app. Now, let's get building.
Aurelia isn't just feature-rich; it's a testament to high performance and extensive extensibility, offering a component-oriented design that's fully testable. It's built for developers who know the value of simplicity and the importance of conventions that don't obstruct but enhance creativity.
As you become more and more familiar with Aurelia, perhaps you will even come up with something that you would like to share with the community. The Aurelia team welcomes any contributions you feel would benefit others in the community. Check out the "Community Contribution" section if that is the case. We also welcome any feedback or suggestions that will help improve Aurelia and enrich the community. Check out the "Contributor Guide" for details about the contributing process and how to contact us.
Understand what areas of the framework to learn next, and how to proceed from here on out.
In our hello world example we touched upon some very basic and light Aurelia concepts. The most important of those is the view/view-model relationship. However, please note that you can write Aurelia applications in a wide variety of different ways, including custom elements without view models.
It is highly recommended that you have a read through additional sections in the "Getting Started" section to become familiar with the templating syntax, building components with bindable properties, looping through collections and more.
Once you are familiar with Aurelia's templating syntax and conventions, you will discover that you end up applying these templating basics to every app that you build.
Aurelia 2's templating system is the cornerstone of crafting rich, interactive user interfaces for your web applications. It transcends the limitations of static HTML, empowering you to create truly dynamic views that respond intelligently to both application data and user interactions. At its core, Aurelia templating establishes a fluid and intuitive connection between your HTML templates and your application logic written in JavaScript or TypeScript, resulting in a responsive and data-driven UI development experience.
Forget static HTML pages. Aurelia 2 templates are living, breathing views that actively engage with your application's underlying code. They react in real-time to data modifications and user actions, ensuring your UI is always in sync and providing a seamless user experience. This deep integration not only streamlines your development workflow but also significantly reduces boilerplate, allowing you to build sophisticated UIs with greater clarity and efficiency.
From the moment you initiate an Aurelia 2 project, you'll find yourself working with templates that are both comfortably familiar in their HTML structure and remarkably powerful in their extended capabilities. Whether you're structuring the layout for a complex component or simply displaying data within your HTML, Aurelia 2's templating syntax is meticulously designed to be both highly expressive and exceptionally developer-friendly, making UI development a truly enjoyable and productive process.
Aurelia's templating engine is packed with features designed to enhance your UI development workflow and capabilities:
Effortless Two-Way Data Binding: Experience truly seamless synchronization between your application's data model and the rendered view. Aurelia's robust two-way data binding automatically keeps your model and UI in perfect harmony, eliminating manual DOM manipulation and ensuring data consistency with minimal effort.
Extendable HTML with Custom Elements and Attributes: Break free from standard HTML limitations by creating your own reusable components and HTML attributes. Encapsulate complex UI logic and behavior into custom elements and attributes, promoting modularity, code reuse, and a more maintainable codebase. This allows you to tailor HTML to the specific needs of your application.
Adaptive Dynamic Composition for Flexible UIs: Build truly dynamic and adaptable user interfaces with Aurelia's dynamic composition. Render different components and templates on-the-fly based on your application's state, user interactions, or any dynamic condition. This enables you to create flexible layouts and UI structures that respond intelligently to changing requirements.
Expressive and Intuitive Templating Syntax: Harness the power of Aurelia's rich templating syntax to handle common UI patterns with ease. From iterating over lists of data and conditionally rendering UI elements to effortlessly managing user events, Aurelia's syntax is designed to be both powerful and remarkably intuitive, reducing complexity and boosting productivity.
Simplified Data Integration with Expressions and Interpolation: Seamlessly integrate your application data into your templates using Aurelia's straightforward expression syntax. Effortlessly bind data to HTML elements and manipulate attributes directly within your templates using interpolation, making data display and interaction a breeze.
Aurelia 2's templating system is more than just a way to write HTML; it's a comprehensive toolkit for building modern, dynamic web applications with efficiency and elegance. By embracing its features, you'll unlock a more productive and enjoyable UI development experience.
If you followed along with our tutorial, you would now have a functional hello world application. The application will function like the one below. Typing into the text input field should dynamically display the value you entered.
Text interpolation allows you to display dynamic values in your views. By wrapping an expression with ${}
, you can render variables, object properties, function results, and more within your HTML. This is conceptually similar to JavaScript template literals.
Interpolation can display the values of view model properties, object fields, and any valid expression. As an example, consider the following code:
Here, the template references the same property name, myName
, that is defined in the view model. Aurelia automatically replaces ${myName}
with "Aurelia" at runtime. Any property you define on your class can be directly accessed inside your templates.
Expressions inside ${}
can perform operations such as arithmetic, function calls, or ternaries:
You can call functions defined on your view model. For example:
You can also use ternary operations:
This will display either "True" or "False" depending on the boolean value of isTrue
.
Aurelia supports the following optional chaining and nullish coalescing operators in templates:
??
?.
?.()
?.[]
Note that ??=
is not supported.
You can use these operators to safely handle null or undefined values:
This helps avoid lengthy if-statements or ternary checks in your view model when dealing with potentially undefined data.
While template interpolation is powerful, there are a few limitations to keep in mind:
You cannot chain expressions using ;
or ,
.
You cannot use certain primitives or operators such as Boolean
, String
, instanceof
, or typeof
.
The pipe character |
is reserved for Aurelia value converters and cannot be used as a bitwise operator inside interpolation.
Attribute binding in Aurelia is a powerful feature that allows you to dynamically bind data from your view model to any native HTML attribute within your templates. This enables real-time updates to element attributes such as classes, styles, src
, alt
, and other standard HTML attributes, enhancing the interactivity and responsiveness of your applications.
The fundamental syntax for binding to attributes in Aurelia is simple and intuitive:
attribute-name.bind="value"
: The binding declaration.
attribute-name
: The target HTML attribute you want to bind to.
.bind
: The binding command indicating a two-way binding by default.
value
: The expression or property from the view model to bind.
You can bind to virtually any attribute listed in the HTML Attributes Reference.
title
AttributeResult: The div
will have a title
attribute with the value "This is a tooltip". Hovering over the div will display the tooltip.
Aurelia provides multiple methods for attribute binding, each tailored for specific use cases and offering different levels of data flow control.
Interpolation allows embedding dynamic values directly within strings. This is useful for concatenating strings with dynamic data.
Example: Binding the id
Attribute Using Interpolation
Result: The h1
element will have an id
attribute set to "main-heading".
Aurelia supports several binding keywords that define the direction and frequency of data flow between the view model and the view:
.one-time
: Updates the view from the view model only once. Subsequent changes in the view model do not affect the view.
.to-view
/ .one-way
: Continuously updates the view from the view model.
.from-view
: Updates the view model based on changes in the view.
.two-way
: Establishes a two-way data flow, keeping both the view and view model in sync.
.bind
: Automatically determines the appropriate binding mode. Defaults to .two-way
for form elements (e.g., input
, textarea
) and .to-view
for most other elements.
Result: The input fields and links will reflect the bound properties with varying degrees of reactivity based on the binding keyword used.
Binding image attributes such as src
and alt
ensures that images update dynamically based on the view model data.
Example: Dynamic Image Binding
Result: The img
element will display the image from the specified src
and use the provided alt
text.
Dynamically enabling or disabling form elements enhances user interaction and form validation.
Example: Binding the disabled
Attribute
Result: The Submit
button starts as disabled, and the input field is enabled. Calling toggleButton()
or toggleInput()
will toggle their disabled states.
innerHTML
and textContent
Choose between innerHTML
for rendering HTML content and textContent
for rendering plain text to control how content is displayed within elements.
Example: Rendering HTML vs. Text
Result:
The first div
will render the bold text as HTML.
The second div
will display the HTML tags as plain text.
Explore more sophisticated binding scenarios to handle complex data interactions and ensure seamless attribute management.
Aurelia employs a mapping function to translate view model properties to corresponding HTML attributes. This typically involves converting kebab-case
attribute names to camelCase
property names. However, not all properties directly map to attributes, especially custom or non-standard attributes.
Example: Automatic Mapping
Result: The input
element's value
attribute is bound to the userName
property. Changes in userName
update the input
value and vice versa.
.attr
Binding CommandWhen automatic mapping fails or when dealing with non-standard attributes, use the .attr
binding command to ensure proper attribute binding.
Example: Binding a Custom Attribute
Result: The input
element will have a my-custom-attr
attribute set to "Custom Attribute Value".
.bind
with Attribute Binding BehaviorYou can specify binding behaviors to fine-tune how bindings operate. For instance, using .bind
with the attr
binding behavior ensures that the binding targets the attribute rather than the property.
Example: Explicit Attribute Binding
Result: The input
element's pattern
attribute is bound to patternProp
, ensuring that it reflects directly as an attribute in the DOM.
To better illustrate attribute bindings, here are several practical scenarios showcasing different binding techniques.
Example: Toggling CSS Classes
Result: The div
will have the class active
when isActive
is true
and inactive
when false
. Calling toggleStatus()
toggles the class.
Example: Binding Inline Styles
Result: The div
's background color reflects the current value of bgColor
. Invoking changeColor('coral')
will update the background to coral.
Example: Conditionally Setting the required
Attribute
Result: The input
field will be required based on the isEmailRequired
property. Toggling this property will add or remove the required
attribute.
While attribute binding in Aurelia is versatile and robust, there are certain syntactical nuances and limitations to be aware of to prevent unexpected behavior.
Expression Syntax Restrictions
No Chaining with ;
or ,
: Expressions within ${}
cannot be chained using semicolons ;
or commas ,
. Each interpolation expression should represent a single, complete expression.
Restricted Primitives and Operators: Certain JavaScript primitives and operators cannot be used within interpolation expressions. These include:
Boolean
String
instanceof
typeof
Bitwise operators (except for the pipe |
used with value converters)
Usage of Pipe |
: The pipe character |
is reserved exclusively for Aurelia's value converters within bindings and cannot be used as a bitwise operator.
Attribute Targeting Syntax
The presence of both .bind
and .attr
syntaxes can be confusing. Here's why both exist:
Property vs. Attribute Binding: .bind
targets the DOM property, which is suitable for standard attributes that have corresponding DOM properties. However, for custom or non-standard attributes that do not have direct property mappings, .attr
is necessary to bind directly to the attribute itself.
Example: Binding id
Using Property and Attribute
Result:
Using .bind
, Aurelia binds to the id
property of the input
element.
Using .attr
, Aurelia binds directly to the id
attribute in the DOM.
Choosing Between Interpolation and Keyword Binding
Both interpolation and keyword binding can achieve similar outcomes. The choice between them often comes down to preference and specific use case requirements.
Performance and Features: There is no significant performance difference between the two. Both are equally efficient and offer similar capabilities.
Readability and Maintainability: Interpolation can be more readable for simple string concatenations, while keyword bindings offer more explicit control for complex bindings.
Binding with a Value Converter
Result: Displays the totalPrice
formatted as currency, e.g., "$199.99".
Attribute binding in Aurelia offers a flexible and powerful means to synchronize data between your view model and the DOM. By understanding and utilizing the various binding commands and techniques, you can create dynamic, responsive, and maintainable user interfaces. Always consider the specific needs of your project when choosing between different binding strategies, and leverage Aurelia's features to their fullest to enhance your application's interactivity and user experience.
Aurelia 2 allows you to managed variables directly within your view templates: the <let>
custom element. This element allows you to declare and initialize variables inline in your HTML, making your templates more dynamic and readable. <let>
is incredibly versatile, supporting a range of value assignments, from simple strings and interpolation to complex expressions and bindings to your view model. This capability significantly enhances template flexibility and reduces the need for excessive view model code for simple template-specific logic.
<let>
The <let>
element provides a straightforward syntax for declaring variables directly within your templates. The basic structure is as follows:
<let>
: The custom element tag that signals the declaration of a template variable.
variable-name
: The name you choose for your template variable. In templates, you will reference this variable name in its camelCase form (e.g., variableName
).
"variable value"
: The initial value assigned to the variable. This can be a string literal, an interpolation expression, a binding expression, or any valid JavaScript expression that Aurelia can evaluate within the template context.
You can assign simple string literals to <let>
variables:
To display the value of this variable in your template, use interpolation with the camelCase version of the variable name:
This will render:
<let>
variables are not limited to static strings. You can use binding expressions to assign dynamic values that are calculated or updated based on your view model or other template logic.
Example: Simple Mathematical Expression
Now, you can display the result of this calculation:
This will output:
Example: Binding to View Model Properties
You can bind a <let>
variable to properties defined in your view model, making template variables reactive to changes in your data:
In this example, both ${userName}
interpolations will display "John Doe". If you update the userName
property in your view model, both interpolations will dynamically reflect the change.
Example: Using Template Expressions
<let>
variables can also be assigned values derived from template expressions, including function calls, ternary operators, and more:
Here, isEvening
will be a boolean value based on the current hour, and timeOfDayMessage
will be dynamically set to either "Good evening" or "Good day" based on the value of isEvening
.
<let>
variables are scoped to the template in which they are declared. This means a variable declared with <let>
is only accessible within the template block where it's defined. This scoping helps prevent naming conflicts and keeps your templates organized and predictable.
Example: Scoped Variables in repeat.for
When using <let>
within a repeat.for
loop, each iteration of the loop will have its own instance of the <let>
variable, ensuring that variables are correctly associated with each repeated item.
In this example, itemIndex
is scoped to each <li>
element within the repeat.for
loop, correctly displaying the index for each item in the list.
<let>
<let>
is incredibly useful in various template scenarios. Here are a few common use cases:
When you have complex expressions that are used multiple times within a template, you can use <let>
to assign the result of the expression to a variable, improving readability and maintainability.
Before using <let>
:
After using <let>
:
Using <let subtotal.bind="quantity * price">
makes the template cleaner and easier to understand, especially if the calculation is more complex.
You can use <let>
in conjunction with conditional attributes like if.bind
or else
to manage template variables based on conditions.
Here, showDetails
is used to control both the button text and the visibility of the details section, simplifying the conditional logic within the template.
You can perform simple data transformations directly within your templates using <let>
, although for more complex transformations, value converters are generally recommended.
Example: Formatting a Date
This example formats the current date using toLocaleDateString()
and stores it in formattedDate
for display.
While not its primary purpose, <let>
can indirectly contribute to creating reusable template snippets by encapsulating logic and variables within a specific section of your template. Combined with custom elements or template parts, <let>
helps in modularizing your view templates.
<let>
Keep it Simple: While <let>
is powerful, it's best used for template-specific variables and simple logic. For complex data manipulation or business logic, keep that in your view model.
Readability: Use descriptive variable names for <let>
to maintain template readability.
Scoping: Be mindful of the scope of <let>
variables. They are limited to the template in which they are declared.
Alternatives: For complex data transformations or reusable formatting logic, consider using Aurelia's value converters, which are designed for these purposes and promote better separation of concerns.
Learn how Aurelia 2 handles global variables in templates, the built-in list of accessible globals, and when to use them effectively.
By design, Aurelia templates limit direct access to global variables like window
or document
for security and maintainability reasons. However, Aurelia recognizes that some JavaScript globals are frequently needed—like Math
, JSON
, or Array
—and therefore provides a predefined list of global objects that can be safely accessed in template expressions.
Security: Restricting direct access to browser globals reduces the risk of accidental or malicious operations on sensitive objects.
Maintainability: Encourages developers to keep logic in their view models, improving code clarity.
Performance: Minimizes the amount of unnecessary logic in templates, preventing overuse of global operations in tight rendering loops.
Despite these constraints, Aurelia acknowledges the utility of common global constructors and functions. Below is the canonical list accessible within Aurelia 2 templates without additional configuration:
Infinity
NaN
isFinite
isNaN
parseFloat
parseInt
decodeURI
decodeURIComponent
encodeURI
encodeURIComponent
Array
BigInt
Boolean
Date
Map
Number
Object
RegExp
Set
String
JSON
Math
Intl
Below are illustrative examples showing how to use these built-in globals in Aurelia templates. The syntax is identical to standard JavaScript, but you simply call them within Aurelia’s binding expressions.
JSON
Serialize an object for debugging or quick display:
Perform simple or complex calculations:
Use global numeric checks to conditionally display elements:
Construct inline regular expressions for quick validation:
Use Object methods for reflection or retrieval:
De-duplicate arrays or combine sets inline:
Leverage encodeURI / decodeURI for safe link construction:
Localize numbers, currency, or dates easily:
Filter, map, and transform arrays:
Use Sparingly
Keep business logic in your view models, not in templates. Inline calls to complex global functions (e.g., JSON.stringify on large data) can degrade performance and reduce readability.
Security
Even though Aurelia limits global access, treat any data you process via global functions (e.g., decodeURI) with caution to prevent potential XSS attacks or other vulnerabilities.
Performance
Template expressions run on each re-render. If you repeatedly perform expensive operations (like JSON.stringify on large objects), consider handling them in the view model and binding to a computed property instead.
Reactivity
Accessing global objects doesn’t magically become reactive. If you want to update the UI when data changes, store and manipulate it in the view model, ensuring Aurelia’s change detection can pick it up.
Clarity and Testing
Test heavy logic in a view model or service, not in templates. This approach keeps your code testable with unit tests and fosters a separation of concerns.
By sticking to these guidelines, you can leverage Aurelia’s built-in global access without sacrificing maintainability or performance.
Event binding in Aurelia 2 offers a streamlined approach to managing DOM events directly within your templates. By declaratively attaching event listeners in your view templates, you can effortlessly respond to user interactions like clicks, keystrokes, form submissions, and more. This guide explores the intricacies of event binding in Aurelia 2, providing detailed explanations and practical examples to deepen your understanding and effective utilization of this feature.
Aurelia 2 simplifies the connection between DOM events and your view model methods. It employs a clear and concise syntax, enabling you to specify the event type and the corresponding method to be invoked in your view model when that event occurs.
The general syntax for event binding in Aurelia 2 follows this pattern:
<element>
: The HTML element to which you are attaching the event listener.
event
: The name of the DOM event you wish to listen for (e.g., click
, input
, mouseover
).
.command
: The binding command that instructs Aurelia how to handle the event. Common commands are .trigger
and .capture
.
methodName
: The name of the method in your view model that will be executed when the event is dispatched.
argument1
, argument2
, ...: Optional arguments that you can pass to the methodName
.
.trigger
and .capture
Aurelia 2 primarily offers two commands for event binding, each controlling the event listening phase:
.trigger
: This command attaches an event listener that reacts to events during the bubbling phase. This is the most frequently used and generally recommended command for event binding as it aligns with typical event handling patterns in web applications. Events are first captured by the deepest element and then propagate upwards through the DOM tree.
.capture
: This command listens for events during the capturing phase. Capturing is the less common phase where events propagate downwards from the window to the target element. .capture
is typically used in specific scenarios, such as when you need to intercept an event before it reaches child elements, potentially preventing default behaviors or further propagation.
.trigger
To bind a click event on a button to a method named handleClick
in your view model, you would use:
When a user clicks the "Click Me" button, Aurelia will execute the handleClick
method defined in your associated view model.
Often, you need access to the event object or want to pass additional data to your event handler method. Aurelia provides a straightforward way to do this.
To pass the DOM event object itself to your handler, use the $event
special variable:
In your view model, the handleClick
method would accept the event object as a parameter:
You can also pass custom arguments along with the event:
Aurelia 2 supports binding to all standard DOM events. Here are some frequently used events in web development:
click
The click
event is triggered when a pointing device button (typically a mouse button) is both pressed and released while the pointer is inside the element. It is commonly used for buttons, links, and interactive elements.
input
The input
event fires when the value of an <input>
, <textarea>
, or <select>
element has been changed. It's useful for real-time validation or dynamic updates based on user input.
change
The change
event is fired when the value of an element has been changed and the element loses focus. This is often used for <input>
, <select>
, and <textarea>
elements when you want to react after the user has finished making changes.
mouseover
and mouseout
The mouseover
event occurs when the mouse pointer is moved onto an element, and mouseout
occurs when it is moved off of an element. These are useful for hover effects and interactive UI elements.
keydown
, keyup
, and keypress
These keyboard events are triggered when a key is pressed down, released, or pressed and released, respectively. keydown
and keyup
are generally preferred for capturing special keys like arrows, Ctrl
, Shift
, etc., while keypress
is more suited for character input.
In DOM event handling, events can "bubble" up the DOM tree (from the target element up to the document) or "capture" down (from the document to the target element). Sometimes you need to control this propagation. Within your event handler methods, you can use methods of the event object to manage propagation:
event.stopPropagation()
: Prevents the event from further bubbling up the DOM tree to parent elements.
event.preventDefault()
: Prevents the default action associated with the event (if it's cancelable), without stopping event propagation. For example, preventDefault
on a click event of a link (<a>
) would stop the browser from navigating to the link's href
.
Aurelia 2 provides capabilities beyond basic event binding, allowing for performance optimization and handling specific scenarios.
For events that fire rapidly and repeatedly, such as mousemove
, scroll
, or input
, calling an event handler function on every event can be performance-intensive. Aurelia's binding behaviors offer throttle
and debounce
to limit the rate at which your handler is invoked.
Throttling: Ensures a function is called at most once in a specified time interval.
In this example, trackMouse
will be executed at most every 50 milliseconds, even if mousemove
events are firing more frequently.
Debouncing: Delays the execution of a function until after a certain amount of time has passed since the last time the event was triggered. Useful for autocomplete or search features to avoid making API calls on every keystroke.
Here, searchQuery
will be called 300ms after the user stops typing, reducing the number of search requests.
Aurelia 2 fully supports custom events, which are essential when working with custom elements or integrating third-party libraries that dispatch their own events.
In this scenario, data-loaded
is a custom event emitted by <my-custom-element>
. handleDataLoaded
in the parent view model will be invoked when this custom event is dispatched.
To solidify your understanding, let's explore practical examples showcasing different event binding scenarios in Aurelia 2.
.self
The self
binding behavior ensures that an event handler is only triggered if the event originated directly from the element to which the listener is attached, and not from any of its child elements (due to event bubbling).
In this setup, divClicked()
will only be executed if the click originates directly on the <div>
element. Clicks on the <button>
(a child element) will trigger buttonClicked()
but will not bubble up to trigger divClicked()
due to the & self
behavior.
change
Event and Two-Way BindingCombine event binding with two-way binding for interactive form elements like checkboxes.
Here, checked.bind="isAgreed"
keeps the isAgreed
property in sync with the checkbox state (two-way binding). change.trigger="agreementChanged()"
additionally allows you to execute custom logic when the checkbox state changes.
React to specific key presses within input fields.
This example shows how to check event.key
to handle specific keys like "Enter" and "Escape".
Efficiently handle events on dynamically generated lists using event delegation. Attach a single event listener to the parent <ul>
or <div>
instead of individual listeners to each list item.
The listItemClicked
handler attached to the <ul>
will be triggered for clicks on any <li>
within it due to event bubbling. We check event.target
to ensure the click originated from an <li>
and extract the data-item-id
.
Parent components can listen for and react to custom events dispatched by child custom elements.
Custom Element (Child):
Parent Component (Parent):
When the button in <my-button>
is clicked, it dispatches a custom event button-clicked
. The parent component listens for this event using button-clicked.trigger
and executes handleButtonClick
, receiving event details in $event.detail
.
Implement autocomplete functionality with debouncing to reduce API calls during typing.
The autocomplete
method will be called 500ms after the last input
event. This delay allows users to finish typing before triggering the (simulated) autocomplete API call, improving performance.
Event modifiers provide a declarative way to apply conditions or actions to event bindings directly in your templates. Event modifiers are appended to the event name after a colon:
Aurelia provides built-in modifiers for common event handling scenarios, and you can extend them with custom mappings.
Aurelia has built-in support for modifiers related to mouse buttons and keyboard keys.
Example: ctrl
Key Modifier
Execute onCtrlClick()
only when the button is clicked and the Ctrl
key is pressed.
Example: ctrl+enter
Key Combination
Execute send()
only when the Enter
key is pressed while the Ctrl
key is also held down. Modifiers can be combined using +
.
prevent
and stop
ModifiersDeclaratively call event.preventDefault()
and event.stopPropagation()
using modifiers.
Example: prevent
and stop
Modifiers
Call validate()
when the button is clicked, and also prevent the default button behavior and stop event propagation.
left
, middle
, right
Handle clicks based on specific mouse buttons.
Example: middle
Mouse Button Modifier
Execute newTab()
only when the button is clicked with the middle mouse button.
You can use character codes as modifiers for keyboard events. For example, 75
is the char code for uppercase 'K'.
Example: Ctrl + K
using Char Code Modifier
Execute openSearchDialog()
when Ctrl + K
is pressed in the textarea.
While using char codes works, it can be less readable. You can create custom key mappings to use more descriptive modifier names. For example, map upper_k
to the key code for 'K'.
Custom Key Mapping Setup (in your main application file, e.g., main.ts
):
Now you can use :upper_k
as a modifier:
This makes your template more readable as :ctrl+upper_k
is more self-explanatory than :ctrl+75
.
Event binding in Aurelia 2 is a powerful and intuitive mechanism for creating interactive web applications. By mastering the syntax, commands, event modifiers, and advanced techniques like throttling and custom events, you can effectively handle user interactions and build dynamic, responsive user interfaces. Leverage the .trigger
command for typical scenarios and .capture
when you need to intercept events during the capturing phase. With these tools and patterns, you can craft a seamless and engaging user experience in your Aurelia 2 applications.
Learn how to build and enhance Aurelia 2 custom attributes, including advanced configuration, binding strategies, and accessing the host element.
Custom attributes in Aurelia empower you to extend and decorate standard HTML elements by embedding custom behavior and presentation logic. They allow you to wrap or integrate existing HTML plugins and libraries, or simply enhance your UI components with additional dynamic functionality. This guide provides a comprehensive overview—from basic usage to advanced techniques—to help you leverage custom attributes effectively in your Aurelia 2 projects.
Custom attributes are one of the core building blocks in Aurelia 2. Similar to components, they encapsulate behavior and style, but are applied as attributes to existing DOM elements. This makes them especially useful for:
Decorating elements with additional styling or behavior.
Wrapping third-party libraries that expect to control their own DOM structure.
Creating reusable logic that enhances multiple elements across your application.
At its simplest, a custom attribute is defined as a class that enhances an element. Consider this minimal example:
When you apply a similar pattern using CustomElement instead, you are defining a component. Custom attributes are a more primitive (yet powerful) way to extend behavior without wrapping the entire element in a component.
This custom attribute adds a fixed size and a red background to any element it is applied to:
Usage in HTML:
The <import>
tag ensures that Aurelia’s dependency injection is aware of your custom attribute. When applied, the <div>
will render with the specified styles.
To gain finer control over your attribute’s name and configuration, Aurelia provides the @customAttribute decorator. This lets you explicitly define the attribute name and even set up aliases.
By default, the class name might be used to infer the attribute name. However, you can explicitly set a custom name:
You can define one or more aliases for your custom attribute. This allows consumers of your attribute flexibility in naming:
Now the attribute can be used interchangeably using any of the registered names:
For simple cases, you might want to pass a single value to your custom attribute without explicitly declaring a bindable property. Aurelia will automatically populate the value property if a value is provided.
To further handle changes in the value over time, you can define the property as bindable:
Custom attributes often need to be configurable. Using the @bindable decorator, you can allow users to pass in parameters that change the behavior or style dynamically. In the following example, the background color is configurable:
You can extend this to support multiple bindable properties. For example, to also allow a dynamic size:
When you have more than one bindable property, you can use options binding syntax to bind multiple properties at once. Each bindable property in the view model corresponds to a dash-case attribute in the DOM. For instance:
The Aurelia binding engine converts the attribute names (e.g., color-square
) to the corresponding properties in your class.
If one of your bindable properties is expected to be used more frequently, you can mark it as the primary property. This simplifies the syntax when binding:
With a primary property defined, you can bind directly:
A key aspect of custom attributes is that they work directly on DOM elements. To manipulate these elements (e.g., updating styles or initializing plugins), you need to access the host element. Aurelia provides a safe way to do this using dependency injection with INode
.
Note: While you can also use resolve(Element)
or resolve(HTMLElement)
, using INode
is safer in environments where global DOM constructors might not be available (such as Node.js).
In complex UIs, you might have multiple custom attributes working together (for example, a dropdown with associated toggle buttons). Aurelia offers the CustomAttribute.closest
function to traverse the DOM and locate a related custom attribute. This function can search by attribute name or by constructor.
If you want to search based on the attribute’s constructor (for stronger typing), you can do so:
Custom attributes, like components, have lifecycle hooks that let you run code at different stages of their existence:
bind() / unbind()
: Initialize or clean up data bindings.
attached() / detached()
: Perform actions when the host element is attached to or removed from the DOM.
Separation of Concerns: Keep your custom attribute logic focused on enhancing the host element, and avoid heavy business logic.
Performance: Minimize DOM manipulations inside change handlers. If multiple properties change at once, consider batching style updates.
Testing: Write unit tests for your custom attributes to ensure that lifecycle hooks and bindings work as expected.
Documentation: Comment your code and document the expected behavior of your custom attributes, especially if you provide aliases or multiple bindable properties.
Often, you’ll want to incorporate functionality from third-party libraries—such as sliders, date pickers, or custom UI components—into your Aurelia applications. Custom attributes provide an excellent way to encapsulate the integration logic, ensuring that the third-party library initializes, updates, and cleans up properly within Aurelia's lifecycle.
DOM Manipulation: Many libraries require direct access to the DOM element for initialization.
Lifecycle Management: You can leverage Aurelia's lifecycle hooks (attached()
and detached()
) to manage resource allocation and cleanup.
Dynamic Updates: With bindable properties, you can pass configuration options to the library and update it reactively when those options change.
Consider a third-party slider library called AwesomeSlider
that initializes a slider on a given DOM element. Below is an example of how to wrap it in a custom attribute.
In place of our hypothetical AwesomeSlider
library, you can use any third-party library that requires DOM manipulation such as jQuery plugins, D3.js, or even custom UI components.
Template references in Aurelia 2 offer a powerful and declarative mechanism to establish direct links between elements in your HTML templates and properties in your JavaScript or TypeScript view models. Using the ref
attribute, you can easily obtain references to specific DOM elements, custom element instances, custom attribute instances, or even Aurelia controllers, enabling efficient DOM manipulation and streamlined interaction with template elements.
To create a template reference to a standard HTML element, simply add the ref
attribute to the element within your template. The value assigned to ref
will be the name of the property in your view model that will hold the reference.
In this basic example, firstNameInput
is declared as a template reference. Aurelia will automatically populate a property in your view model with the same name, making the <input>
element directly accessible.
Template references become immediately available for use within the template itself. You can directly access properties and methods of the referenced element using the reference name.
For example, to dynamically display the current value of the firstNameInput
field:
As the user types in the input field, the <p>
element will update in real-time, displaying the current value accessed through firstNameInput.value
.
To access a template reference in your view model, you need to declare a property in your view model class that matches the reference name you used in the template. For TypeScript projects, it's strongly recommended to explicitly type this property for enhanced type safety and code maintainability.
Important Notes:
Property Naming: The property name in your view model must exactly match the value of the ref
attribute in your template (firstNameInput
in the example above).
Type Safety: In TypeScript, always declare the type of your template reference property (e.g., HTMLInputElement
, HTMLDivElement
, MyCustomElement
). This improves code readability and helps catch type-related errors early.
Lifecycle Timing: Template references are not available during the view model's constructor. They become available after the view is bound to the view model, typically in lifecycle hooks like bound()
or later.
Aurelia's ref
attribute extends beyond simple DOM elements. It provides powerful options to reference component instances and controllers of custom elements and attributes.
component.ref
: Referencing Custom Element Instances (View Models)To obtain a reference to the view model instance of a custom element, use component.ref="expression"
. This was previously known as view-model.ref
in Aurelia v1.
In your view model:
component.ref
is invaluable when you need to directly interact with the logic and data encapsulated within a custom element's view model from a parent component.
custom-attribute.ref
: Referencing Custom Attribute Instances (View Models)Similarly, to reference the view model instance of a custom attribute applied to an element, use custom-attribute.ref="expression"
.
In your view model:
custom-attribute.ref
is useful when you need to interact with the behavior or state managed by a custom attribute from the surrounding view model.
controller.ref
: Referencing Aurelia Controller Instances (Advanced)For more advanced scenarios, controller.ref="expression"
allows you to access the Aurelia Controller instance of a custom element. The Controller provides access to Aurelia's internal workings and lifecycle management for the element. This is less commonly needed but can be powerful for framework-level integrations or very specific use cases.
In your view model:
controller.ref
provides access to the Aurelia Controller, which is an advanced API and typically used for framework extension or very specific control over component lifecycle and binding. For most application development, component.ref
or direct DOM element references are sufficient.
Template references significantly enhance Aurelia development by providing a clean, framework-integrated way to interact with elements and components. They offer several key advantages:
Direct DOM Manipulation: Template references provide a structured and type-safe way to obtain direct references to DOM elements, which is essential for tasks like:
Focusing input fields programmatically (elementRef.focus()
).
Imperative DOM manipulation when integrating with third-party libraries that require direct element access (e.g., initializing jQuery plugins, interacting with canvas elements, etc.).
Fine-grained control over element properties and attributes.
Component Interaction: component.ref
and custom-attribute.ref
enable seamless communication and interaction between parent components and their children (custom elements and attributes). This allows for:
Calling methods on child component view models.
Accessing data and state within child components.
Building more complex and encapsulated component structures.
Simplified DOM Access: Template references eliminate the need for manual DOM queries using document.querySelector
or similar methods within your view models. This leads to:
Cleaner and more readable view model code.
Reduced risk of brittle selectors that break if the template structure changes.
Improved maintainability and refactoring capabilities.
Integration with Third-Party Libraries: Many JavaScript libraries require direct DOM element references for initialization or interaction. Template references provide the ideal mechanism to obtain these references within an Aurelia application without resorting to less maintainable DOM query approaches.
Learn how to work with collections of data like arrays and maps. The repeat.for functionality allows you to loop over collections of data and work with their contents similar to a Javascript for loop.
Aurelia’s templating system offers a robust way to work with collections—be they arrays, sets, maps, or even ranges. The repeat.for
binding provides a declarative approach to iterating over data, creating scopes for each iteration, and optimizing DOM updates. This guide explains the intricacies of list rendering in detail.
repeat.for
BindingAt its core, repeat.for
acts like a template-based for...of
loop. It iterates over a collection and creates a new rendering context for each item. For example:
This snippet tells Aurelia to:
Loop over each element in the items
collection.
Assign the current element to a local variable named item
.
Render the element (here, displaying item.name
) for each iteration.
JavaScript Analogy:
When working with dynamic collections, it’s crucial to minimize unnecessary DOM operations. Aurelia allows you to specify a unique key for each item in the collection. This key helps the framework:
Track Changes: By comparing key values, Aurelia can identify which items have been added, removed, or reordered.
Optimize Updates: Only the modified elements are updated, preserving performance.
Maintain State: DOM elements (e.g., with user input or focus) retain their state even if their order changes.
You can declare the key using either literal or binding syntax:
Guidelines for Keys:
Uniqueness: Use a property that uniquely identifies each item (like an id
).
Stability: Avoid using array indices if the collection order can change.
Inside a repeat.for
block, Aurelia exposes several contextual properties that give you more control over the rendering logic:
$index
: The zero-based index of the current iteration.
$first
: A boolean that is true
on the first iteration.
$last
: A boolean that is true
on the final iteration.
$even
/ $odd
: Flags indicating whether the current index is even or odd, which is useful for styling alternating rows.
$length
: The total number of items in the collection.
$parent
: A reference to the parent binding context. This is especially useful in nested repeaters.
For nested repeats, you can access the outer scope with $parent
:
Arrays are the most common data source for repeats. Here’s an example component and its template:
Component (my-component.ts):
Template (my-component.html):
Note: Aurelia tracks changes in arrays when you use array methods like
push
,pop
, orsplice
. Direct assignments (e.g.,array[index] = value
) won’t trigger change detection.
repeat.for
isn’t limited to collections—it can also generate a sequence of numbers. For instance, to create a countdown:
This iterates 10 times and computes 10 - i
on each pass.
Sets are handled much like arrays. The syntax remains the same, though the underlying collection is a Set
.
Component (repeater-template.ts):
Template (repeater-template.html):
Maps offer a powerful way to iterate key-value pairs. Aurelia lets you deconstruct the map entry directly in the template.
Component (repeater-template.ts):
Template (repeater-template.html):
Here, [greeting, friend]
splits each map entry so you can access both the key (greeting) and value (friend).
Objects aren’t directly iterable in Aurelia. To iterate over an object’s properties, convert it into an iterable format using a value converter.
The keys
converter transforms the object into an array of its keys, making it iterable by repeat.for
.
Aurelia’s repeat system is extensible. If you need to iterate over non-standard collections (like HTMLCollections, NodeLists, or FileLists), you can create a custom repeat handler by implementing the IRepeatableHandler
interface.
Tip: Aurelia provides a default
ArrayLikeHandler
you can import directly:
If you need to override the default order in which Aurelia selects a repeat handler, you can implement your own IRepeatableHandlerResolver
:
This custom resolver can redefine how different collection types are handled by the repeater.
Aurelia 2’s list rendering capabilities are both powerful and flexible:
Versatile Iteration: Work with arrays, sets, maps, ranges, and even objects (via converters).
Efficient Updates: Use keyed iteration to minimize DOM changes.
Contextual Data: Access properties like $index
, $first
, $last
, $even
, $odd
, $length
, and $parent
for richer templates.
Extensibility: Create custom handlers and resolvers to support any iterable data structure.
Mastering these features enables you to build dynamic, efficient UIs that handle complex data sets with ease.
Get to know Aurelia's value converters (pipes) and how to use them to transform data in your templates.
Value converters transform data as it flows between your view and view model. They’re ideal for formatting text, dates, currencies, and more. In other frameworks, you might know them as pipes.
Converters work in two directions:
toView: Prepares model data for display.
fromView: Adjusts view data before updating the model (useful with two-way binding).
Both methods receive the primary value as the first argument, with any extra arguments used as configuration.
Use the pipe symbol (|
) to attach a converter:
A matching converter might be:
You can chain multiple converters by separating them with additional pipes:
Pass parameters using a colon (:
). Parameters can be:
Static:
Bound:
Object-based:
In both toView
and fromView
, the second parameter will be the passed configuration.
A converter is a simple class. Always reference it in camelCase within templates.
A no-op converter example:
This converter formats dates based on locale:
Import it in your view:
Usage examples:
View this in action on StackBlitz.
Formats numbers as currency strings.
Replaces keywords with emojis.
Transforms text into “1337” speak.
Flips text upside down.
Adds ordinal suffixes to numbers (1st, 2nd, 3rd, etc.).
Transforms text into Morse code.
These examples highlight the flexibility of Aurelia 2's value converters. Experiment with your own transformations to tailor data exactly as you need it.
Learn about the various methods for conditionally rendering content in Aurelia 2, with detailed explanations and examples.
Conditional rendering in Aurelia 2 is a powerful feature that lets you create dynamic interfaces that respond to your application's state. You can conditionally include or exclude parts of your view using boolean expressions. This guide will walk you through the different techniques provided by Aurelia to manage conditional content.
if.bind
The if.bind
directive allows you to conditionally add or remove elements from the DOM based on the truthiness of the bound expression.
When the bound value evaluates to false
, Aurelia removes the element and its descendants from the DOM. This process includes the destruction of custom elements, detaching of events, and cleanup of any associated resources, which is beneficial for performance and memory management.
Consider an application where you want to display a loading message while data is being fetched:
The isLoading
variable controls the presence of the div in the DOM. When it's true
, the loading message appears; when false
, the message is removed.
if.bind
with else
Aurelia enables if/else
structures in the view, similar to conditional statements in JavaScript. The else
binding must immediately follow an element with if.bind
:
This snippet displays a welcome message for authenticated users and a login prompt for others.
Be mindful that if.bind
modifies the DOM structure, which can trigger reflow and repaint processes in the browser. For applications with extensive DOM manipulation, this may become a performance bottleneck. Optimize your usage of if.bind
by minimizing the frequency and complexity of conditional rendering operations.
show.bind
The show.bind
directive offers an alternative approach to conditional rendering. Instead of adding or removing elements from the DOM, it toggles their visibility. This is akin to applying display: none;
in CSS—the element remains in the DOM but is not visible to the user.
Here, isDataLoaded
dictates the visibility of the message. When false
, the message is hidden; when true
, it is shown. All bindings and events remain intact since the element is not removed from the DOM.
switch.bind
For more complex conditional rendering cases, such as when dealing with enumerated values, switch.bind
is the ideal choice. It offers a clean, semantic way to handle multiple conditions by mimicking the switch
statement in JavaScript.
For instance, given an enumeration of order statuses:
Displaying a message based on the order status with if.bind
can become unwieldy. Instead, switch.bind
offers a concise and clear approach:
This structure allows for a straightforward mapping between the status and the corresponding message. The default-case
acts as a catch-all for any status not explicitly handled.
You can bind an array of values to a case
, thus grouping multiple conditions:
This will display "Order is being processed." for both Received
and Processing
statuses.
The switch
construct in Aurelia supports fall-through logic similar to JavaScript's switch
:
When orderStatus
is Received
, both the "Order received." and "Order is being processed." messages will be displayed because of the fall-through
attribute.
switch.bind
Aurelia's switch.bind
can accommodate various advanced use cases, making it a versatile tool for conditional rendering. Below are examples of such scenarios:
switch.bind
with Static ExpressionsYou can use switch.bind
with a static expression, while the case.bind
attributes feature more dynamic conditions:
This example iterates over numbers 0 to 99 and applies the FizzBuzz logic, displaying "Fizz", "Buzz", or "FizzBuzz" depending on whether the number is divisible by 3, 5, or both.
switch.bind
switch.bind
can be combined with au-slot
to project content into custom elements conditionally:
In this case, the custom element foo-bar
will project different messages based on the status
value.
switch.bind
switch.bind
can be nested within itself for complex conditional logic:
This example demonstrates how you can use nested switch.bind
statements to handle multiple levels of conditional rendering.
case
UsageThe case
attribute must be used within the context of a switch
and should be its direct child. The following are examples of incorrect and unsupported usages:
These examples will either throw an error or result in unexpected behavior. If you need to support a use case like this, consider reaching out to the Aurelia team.
By exploring these advanced scenarios, you can harness the full potential of switch.bind
to address complex conditional rendering needs in your Aurelia applications. Remember to adhere to the guidelines and limitations to ensure proper functionality and maintainability.
Aurelia 2 significantly simplifies the handling of Promises directly within your templates. Unlike previous versions where promise resolution typically occurred in the view model, Aurelia 2 empowers you to manage asynchronous operations directly in the view.
This is accomplished through the promise.bind
template controller. It intelligently manages the different states of a Promise: pending
, resolved
(then
), and rejected
(catch
). This approach reduces boilerplate code and makes asynchronous data handling in templates more declarative and intuitive.
The promise.bind
attribute allows you to bind a Promise to a template, rendering different content based on the Promise's current state.
In this example:
promise.bind="myPromise"
: Binds the div
to the Promise named myPromise
in your view model.
<template pending>
: Content rendered while myPromise
is in the pending state (still resolving).
<template then="data">
: Content rendered when myPromise
resolves successfully. The resolved value is available as data
within this template.
<template catch="error">
: Content rendered if myPromise
rejects. The rejection reason (typically an Error object) is available as error
.
Let's illustrate with a view model that manages different promise scenarios:
In this example, promise1
is set to resolve after 2 seconds, and promise2
is set to reject after 3 seconds. The template dynamically updates to reflect each promise's state. Notice in promise2
's then
template, we don't specify a variable, indicating we only care about the resolved state, not the resolved value itself.
You can directly bind a function call to promise.bind
. Aurelia is smart enough to re-invoke the function only when its parameters change, treating function calls in templates as pure operations.
The following example fetches a random advice slip from an API each time a button is clicked:
Key Points:
adviceIndex
: This variable, initialized with let adviceIndex.bind="0"
, acts as a parameter to fetchAdvice
. Incrementing adviceIndex
via the button click triggers Aurelia to re-evaluate fetchAdvice(adviceIndex)
.
Function Re-execution: Aurelia re-executes fetchAdvice
only when adviceIndex
changes, ensuring efficient handling of function-based promises.
Error Handling: The .catch
template gracefully handles fetch errors, providing user-friendly feedback and a "Try Again" button.
The promise.bind
template controller creates its own isolated scope. This is crucial to prevent naming conflicts and unintended modification of the parent view model or scope.
In this example:
userData
and userError
: These variables are scoped only within the promise.bind
context. They do not pollute the parent view model scope.
Component Communication: To pass data to child components (like <user-profile>
), use property binding (e.g., user-data.bind="userData"
).
Parent Scope Access (Discouraged): While you can access the parent scope using $parent
, it's generally better to manage data flow through explicit bindings and avoid relying on parent scope access for maintainability.
Aurelia 2 supports nesting promise.bind
controllers to handle scenarios where one asynchronous operation depends on the result of another.
Flow of Execution:
initialFetchPromise
: The outer promise.bind
starts with initialFetchPromise
.
Pending State: While initialFetchPromise
is pending, "Fetching initial data..." is displayed.
First then
(Response): When initialFetchPromise
resolves, the resolved value (initialResponse
) becomes available in the then
template.
Nested promise.bind
(JSON Deserialization): Inside the first then
template, a nested promise.bind
is used: promise.bind="initialResponse.json()"
. This starts a new promise based on deserializing the initialResponse
.
Nested then
(JSON Data): When initialResponse.json()
resolves, the parsed JSON data (jsonData
) is available in this then
template. "Data received and deserialized: ${jsonData.name}" is displayed.
Nested catch
(JSON Error): If initialResponse.json()
fails (e.g., invalid JSON), the nested catch
template handles the error.
Outer catch
(Fetch Error): If initialFetchPromise
initially rejects, the outer catch
template handles the initial fetch error.
repeat.for
LoopsWhen using promise.bind
within a repeat.for
loop, it's crucial to manage scope correctly, especially if you need to access data from each promise iteration. Using let
bindings within the <template promise.bind="...">
is highly recommended to create proper scoping for each iteration.
Importance of <let>
Bindings:
Scoped Context: The lines <let itemData.bind="null"></let>
and <let itemError.bind="null"></let>
inside the promise.bind
template are essential. They create itemData
and itemError
properties in the overriding context of each promise.bind
iteration.
Preventing Overwriting: Without these let
bindings, itemData
and itemError
would be created in the binding context, which is shared across all iterations of the repeat.for
loop. This would lead to data from later iterations overwriting data from earlier ones, resulting in incorrect or unpredictable behavior.
Correct Output: With let
bindings, each iteration of the repeat.for
loop gets its own isolated scope for itemData
and itemError
, ensuring correct rendering for each promise in the list.
Learn how to style elements, components and other facets of an Aurelia application using classes and CSS. Strategies for different approaches are discussed in this section.
Aurelia makes it easy to modify an element inline class list and styles. You can work with not only strings but also objects to manipulate elements.
The class binding allows you to bind one or more classes to an element and its native class
attribute.
Adding or removing a single class value from an element can be done using the .class
binding. By prefixing the .class
binding with the name of the class you want to display conditionally selected.class="myBool"
you can add a selected class to an element. The value you pass into this binding is a boolean value (either true or false), if it is true
the class will be added; otherwise, it will be removed.
Inside of your view model, you would specify isSelected
as a property and depending on the value, the class would be added or removed.
Here is a working example of a boolean value being toggled using .class
bindings.
Unlike singular class binding, you cannot use the .class
binding syntax to conditionally bind multiple CSS classes. However, there is a multitude of different ways in which this can be achieved.
class.bind="someString"
string
'col-md-4 bg-${bgColor}'
class="${someString}"
string
col-md-4 ${someString}
Once you have your CSS imported and ready to use in your components, there might be instances where you want to dynamically bind to the style attribute on an element (think setting dynamic widths or backgrounds).
You can dynamically add a CSS style value to an element using the .style
binding in Aurelia.
Inside of your view model, you would specify bg
as a string value on your class.
Here is a working example of a style binding setting the background colour to blue:
To bind to one or more CSS style properties you can either use a string containing your style values (including dynamic values) or an object containing styles.
This is what a style string looks like, notice the interpolation here? It almost resembles just a plain native style attribute, with exception of the interpolation for certain values. Notice how you can also mix normal styles with interpolation as well?
You can also bind a string from your view model to the style
property instead of inline string assignment by using style.bind="myString"
where myString
is a string of styles inside of your view model.
Styles can be passed into an element by binding to the styles property and using .bind
to pass in an object of style properties. We can rewrite the above example to use style objects.
From a styling perspective, both examples above do the same thing. However, we are passing in an object and binding it to the style
property instead of a string.
Learn how to build forms in Aurelia, bind data to various input elements, and handle submission and validation.
Handling forms and user input is a common task in most applications. Whether you are building a login form, a data-entry screen, or even a chat interface, Aurelia makes it intuitive to work with forms. By default, Aurelia’s binding system uses two-way binding for form elements (like <input>
, <textarea>
, and contenteditable
elements), which keeps your view and model in sync automatically.
Aurelia’s two-way binding updates your view model properties whenever users enter data into form elements, and likewise updates the form elements if the view model changes:
The user types in the input (e.g., John).
The native input events fire. Aurelia observes the value change.
The binding system updates the corresponding view model property.
Any references to that property automatically reflect its new value.
Because of this automatic synchronization, you generally don’t need to write custom event handlers or watchers to track form inputs.
Aurelia lets you create forms in pure HTML without any special setup. Here’s a simple login form illustrating how little code is required.
Key Points:
We created a form with two inputs: email and password.
The value.bind
syntax binds these inputs to class properties named email
and password
.
We call a handleLogin()
method on submit to process the form data.
And here is the view model (login-component.ts
):
Whenever the email or password fields change in the UI, their corresponding view model properties are updated. Then, in handleLogin()
, you can handle form submission however you wish.
Using submit.trigger
on a form prevents the default browser submission. If you want the form to submit normally, return true
from your handler or remove submit.trigger
entirely.
Binding to text inputs in Aurelia is straightforward:
You can also bind other attributes like placeholder:
Textareas work just like text inputs, with value.bind for two-way binding:
Any changes to textAreaValue
in the view model will show up in the <textarea>
, and vice versa.
Aurelia supports two-way binding for checkboxes in various configurations.
Bind a boolean property to the checked attribute:
When using checkboxes as a multi-select, bind an array to each input’s checked attribute. Provide a model for each checkbox to indicate its value:
Numbers aren’t the only value type you can store. Here’s how to manage an array of objects:
If your objects do not share reference equality (e.g., same data, different instances), define a custom matcher:
If your “selected items” array holds strings, you can rely on the standard value attribute:
Radio groups in Aurelia are similarly straightforward. Only one radio button in a group can be checked at a time.
If the selected object doesn’t share reference equality, define a custom matcher:
You can use <select>
as either a single-select or a multiple-select input:
Use value.bind
in single-select mode.
Use value.bind
to an array in multiple-select mode.
Provide <option>
elements that specify their own model (or value) attributes.
Typically, a <form>
groups related inputs. Aurelia allows you to intercept submission using submit.trigger
:
For <form>
elements without a method (or method="GET"), Aurelia automatically calls event.preventDefault()
to avoid a full page reload. If you prefer the default browser submission, return true
from your handler:
Working with file uploads in Aurelia typically involves using the standard <input type="file">
element and handling file data in your view model. While Aurelia doesn’t provide special bindings for file inputs, you can easily wire up event handlers or use standard properties to capture and upload files.
In most cases, you’ll want to listen for the change
event on a file input:
multiple
: Allows selecting more than one file.
accept="image/*"
: Restricts file selection to images (this can be changed to fit your needs).
change.trigger="handleFileSelect($event)"
: Calls a method in your view model to handle the file selection event.
You can retrieve the selected files from the event object in your view model:
Key Points:
Reading File Data: input.files
returns a FileList
; converting it to an array (Array.from
) makes it easier to iterate over.
FormData: Using FormData
to append files is a convenient way to send them to the server (via Fetch).
Error Handling: Always check response.ok
to handle server or network errors.
Disabling the Button: In the HTML, disabled.bind="!selectedFiles.length"
keeps the button disabled until at least one file is selected.
If you only need a single file, omit multiple and simplify your logic:
When handling file uploads, consider adding validation and security measures:
Server-side Validation: Even if you filter files by type on the client (accept="image/*"), always verify on the server to ensure the files are valid and safe.
File Size Limits: Check file sizes either on the client or server (or both) to prevent excessively large uploads.
Progress Indicators: For a better user experience, consider using XMLHttpRequest or the Fetch API with progress events (via third-party solutions or polyfills), so you can display an upload progress bar.
Validation is essential for robust, user-friendly forms. Aurelia provides a dedicated Validation plugin that helps you:
Validate inputs using built-in or custom rules.
Display error messages and warnings.
Integrate seamlessly with Aurelia’s binding system.
However, the crucial distinction lies in the scope of access. Binding behaviors have complete access to the binding instance throughout its entire lifecycle. This contrasts sharply with value converters, which are limited to intercepting and transforming values as they flow between the model and the view.
This broader access empowers binding behaviors to fundamentally alter the behavior of bindings, unlocking a wide array of capabilities as demonstrated in the examples below.
Aurelia provides several built-in binding behaviors to address common scenarios. The throttle
behavior is designed to limit the rate at which updates propagate. This can apply to updates from the view-model to the view (in to-view
or one-way
bindings) or from the view to the view-model (in two-way
bindings).
By default, throttle
enforces a minimum time interval of 200ms between updates. You can easily customize this interval.
Here are some practical examples:
Limiting property updates to a maximum of once every 200ms
In this example, the searchQuery
property in your view model will update at most every 200ms, even if the user types more rapidly in the input field. This is especially useful for search inputs or other scenarios where frequent updates can be inefficient or overwhelming.
You'll notice the &
symbol, which is used to introduce binding behavior expressions. The syntax for binding behaviors mirrors that of value converters:
Arguments: Binding behaviors can accept arguments, separated by colons: propertyName & behaviorName:arg1:arg2
.
Chaining: Multiple binding behaviors can be chained together: propertyName & behavior1 & behavior2:arg1
.
Combined with Value Converters: Binding expressions can include both value converters and binding behaviors: ${data | valueConverter:arg & bindingBehavior:arg2}
.
Let's see how to customize the throttling interval:
Limiting property updates to a maximum of once every 850ms
The throttle
behavior is particularly valuable when used with event bindings, especially for events that fire frequently, such as mousemove
.
Handling mousemove
events at most every 200ms
In this case, the mouseMoveHandler
method in your view model will be invoked at most every 200ms, regardless of how frequently the mousemove
event is triggered as the user moves their mouse.
In certain situations, you might need to immediately apply any pending throttled updates. Consider a form with throttled input fields. When a user tabs out of a field after typing, you might want to ensure the latest value is immediately processed, even if the throttle interval hasn't elapsed yet.
The throttle
binding behavior supports this via a "signal". You can specify a signal name as the second argument to throttle
. Then, using Aurelia's ISignaler
, you can dispatch this signal to force a flush of the throttled update.
In this example:
value.bind="formValue & throttle:200:'flushInput'"
: The formValue
binding is throttled to 200ms and associated with the signal 'flushInput'
.
blur.trigger="signaler.dispatchSignal('flushInput')"
: When the input loses focus (blur
event), signaler.dispatchSignal('flushInput')
is called. This immediately triggers any pending throttled update associated with the 'flushInput'
signal, ensuring the formValue
is updated in the view model right away.
You can also specify a list of signals:
The debounce
binding behavior is another rate-limiting tool. debounce
delays updates until a specified time interval has passed without any further changes. This is ideal for scenarios where you want to react only after a user has paused interacting.
A classic use case is a search input that triggers an autocomplete or search operation. Making an API call with every keystroke is inefficient. debounce
ensures the search logic is invoked only after the user has stopped typing for a moment.
Updating a property after typing has stopped for 200ms
Updating a property after typing has stopped for 850ms
Similar to throttle
, debounce
is highly effective with event bindings.
Calling mouseMoveHandler
after the mouse stops moving for 500ms
Like throttle
, debounce
also supports flushing pending updates using signals. This is useful in scenarios like form submission where you want to ensure the most recent debounced values are processed immediately, even if the debounce interval hasn't elapsed.
In this example, the validateInput
method (which could perform input validation or other actions) will be called when the input field loses focus, even if the 300ms debounce interval isn't fully over, ensuring timely validation.
As with throttle
, you can also provide a list of signal names to debounce
.
The updateTrigger
binding behavior allows you to customize which DOM events trigger updates from the view to the view model for input elements. By default, Aurelia uses the change
and input
events for most input types.
However, you can override this default behavior. For example, you might want to update the view model only when an input field loses focus (blur
event).
Updating the view model only on blur
You can specify multiple events that should trigger updates:
Updating the view model on blur
or paste
events
This is useful in scenarios where you need fine-grained control over when view-model updates occur based on specific user interactions with input elements.
The signal
binding behavior provides a mechanism to explicitly tell a binding to refresh itself. This is particularly useful when a binding's result depends on external factors or global state changes that Aurelia's observation system might not automatically detect.
Consider a "translate" value converter that translates keys into localized strings, e.g., ${'greeting.key' | translate}
. If your application allows users to change the language dynamically, how do you refresh all the translation bindings to reflect the new language?
Another example is a value converter that displays a "time ago" string relative to the current time, e.g., Posted ${post.date | timeAgo}
. As time progresses, this binding needs to refresh periodically to show updated relative times like "5 minutes ago," "an hour ago," etc.
signal
binding behavior solves these refresh scenarios:
Using a Signal to Refresh Bindings
In this example, signal:'time-update'
assigns the signal name 'time-update'
to this binding. Multiple bindings can share the same signal name.
To trigger a refresh of all bindings with the signal name 'time-update'
, you use the ISignaler
:
Dispatching a Signal to Refresh Bindings
Every 5 seconds, the setInterval
function updates lastUpdated
and then calls signaler.dispatchSignal('time-update')
. This tells Aurelia to re-evaluate all bindings that are configured with & signal:'time-update'
, causing them to refresh and display the updated "time ago" value.
The oneTime
binding behavior optimizes string interpolation bindings for scenarios where the bound value is not expected to change after the initial render. Applying oneTime
indicates to Aurelia that the binding should only be evaluated once.
One-time String Interpolation Binding
oneTime
bindings are the most efficient type of binding because they eliminate the overhead of property observation. Aurelia doesn't need to track changes to staticText
after the initial binding, leading to performance improvements, especially in large lists or complex views.
Aurelia also provides binding behaviors for explicitly specifying toView
and twoWay
binding modes, although these are less commonly used as binding behaviors since the binding commands (.to-view
, .two-way
, .bind
) are more direct.
toView
and twoWay
binding behaviors
Note the casing difference between binding mode commands and behaviors. Binding commands (e.g., .to-view
, .two-way
) use lowercase, dash-separated names due to HTML case-insensitivity. However, binding behaviors used in expressions (e.g., toView
, twoWay
) use camelCase as dashes are not valid in JavaScript variable names.
The self
binding behavior is used in event bindings to ensure that the event handler only responds to events dispatched directly from the element the listener is attached to, and not from any of its descendant elements due to event bubbling.
Consider a scenario with a panel component:
Scenario without self
binding behavior
Without self
, the onMouseDown
handler will be invoked not only when the user mousedown on the <header>
element itself, but also on any element inside the header, such as the "Settings" and "Close" buttons, due to event bubbling. This might not be the desired behavior if you want the panel to react only to direct interactions with the header, not its contents.
You could handle this in your event handler by checking the event.target
:
Event Handler without self
binding behavior (manual check)
However, this mixes DOM event handling logic with component-specific behavior. The self
binding behavior offers a cleaner, more declarative solution:
Using self
binding behavior
Event Handler with self
binding behavior
By adding & self
to the event binding, Aurelia ensures that onMouseDown
is only called when the mousedown
event originates directly from the <header>
element, simplifying your event handler logic and separating concerns.
You can create your own custom binding behaviors to encapsulate reusable binding modifications. Like value converters, custom binding behaviors are view resources.
Instead of toView
and fromView
methods (like value converters), custom binding behaviors implement bind(binding, scope, [...args])
and unbind(binding, scope)
methods:
bind(binding, scope, [...args])
: This method is called when the binding is created and attached to the DOM. It's where you implement the behavior modification to the binding
instance.
binding
: The binding instance whose behavior you want to alter. It's an object implementing the IBinding
interface.
scope
: The binding's scope, providing access to the view model (scope.bindingContext
) and override context (scope.overrideContext
).
[...args]
: Any arguments passed to the binding behavior in the template (e.g., & myBehavior:arg1:arg2
).
unbind(binding, scope)
: This method is called when the binding is detached from the DOM (e.g., when the view is unrendered). Here, you should clean up any changes made in the bind
method to restore the binding to its original state and prevent memory leaks.
Let's look at some practical examples of custom binding behaviors.
This behavior logs the current binding context to the browser's console every time the binding updates its target (view). This is invaluable for debugging and understanding data flow in your Aurelia application.
Usage in Template:
Now, whenever the userName
binding updates the input element, you'll see the current binding context logged to the console, helping you inspect the data available at that point.
This behavior adds a temporary tooltip to the element displaying the binding's current value whenever it updates. This offers a quick way to inspect binding values directly in the UI without resorting to console logs.
Usage in Template:
As the itemName
binding updates, the input element will temporarily display a tooltip showing the current value, providing immediate visual feedback for debugging.
This behavior visually highlights an element by briefly changing its background color whenever the binding updates the element's target property. This visual cue helps quickly identify which parts of the UI are reacting to data changes, particularly useful during development and debugging complex views.
Usage in Template:
Whenever the message
binding updates the textContent
of the div
, the div's background will briefly flash light blue for 1 second (1000ms), visually indicating the update. You can customize the highlight color and duration by passing arguments to the binding behavior in the template.
A developer guide for enabling SVG binding in the Aurelia.
Enhance your Aurelia applications by reducing boilerplate with powerful lambda expressions. This documentation provides a comprehensive guide to using lambda expressions in Aurelia templates, includin
Lambda expressions in Aurelia templates offer a concise and powerful approach to incorporating JavaScript-like functionality directly within your HTML. By leveraging a subset of JavaScript's arrow function syntax, Aurelia ensures your templates remain expressive, readable, and maintainable.
Less Boilerplate: Embed logic inline without extra view-model methods or value converters.
Enhanced Readability: Express inline operations in a way that clearly reflects the intended transformation or filtering.
Flexibility: Manipulate data on the fly right where it's rendered.
Maintainability: Localize small bits of logic with the markup, which in certain scenarios simplifies updates and debugging.
Lambda expressions are defined using JavaScript's arrow function syntax. Supported forms include:
You can integrate lambda expressions directly in your template expressions for filtering, transforming, or handling events:
Aurelia supports a limited subset of the standard JavaScript arrow function syntax. The following patterns are not supported:
Block Bodies: Only expression bodies are permitted.
Default Parameters: You cannot specify default values.
Destructuring Parameters: Both array and object destructuring are not supported.
Lambda expressions shine when working with array methods, especially inside repeat.for
bindings.
repeat.for
Instead of using value converters for filtering, you can directly use lambda expressions:
You can chain multiple array operations to transform data inline:
Note: Aurelia observes only the properties referenced in the lambda expression. In the above example, changes to
selected
andorder
will trigger re-evaluation.
Lambda expressions can simplify event handlers by keeping the logic in the template:
This inline style helps clarify what data is being passed to your handler.
Lambda functions can also be used within interpolation expressions for dynamic data calculations.
For instance, to render a comma-separated list from an array of keywords:
Compute aggregate values inline, such as totals or averages:
Here are several advanced examples showcasing the versatility of lambda expressions:
Perform filtering, slicing, sorting, and mapping in one chained expression:
Flatten nested structures and dynamically generate lists:
Pass additional context to event callbacks with inline lambdas:
Implement a dynamic filter that updates as the user types:
Calculate averages or other complex aggregates inline:
While lambda expressions keep your templates clean, there are a few points to consider for optimal performance:
Observability: Aurelia observes only the properties directly referenced in the lambda. Be sure to include all necessary properties to trigger re-evaluation.
Array Mutations: Methods that mutate the array (e.g., sort
, splice
) won't trigger the binding update. Prefer non-mutating techniques (e.g., using slice
before sorting).
Complexity: Keep lambda expressions simple where possible. For logic that becomes too complex or involves multiple operations, consider moving it to the view-model.
Debugging: Inline expressions can be harder to debug. If you encounter issues, isolating the logic in the view-model might help identify problems.
Over-Complex Expressions: When lambda logic is too complicated, it might hinder readability and may lead to performance bottlenecks. Refactor complex logic into a dedicated view-model method if necessary.
Property Dependencies: If updates to a property do not trigger a re-evaluation, confirm that the property is correctly referenced within the lambda.
Mutating Methods: Since some array methods do not automatically trigger binding updates, cloning or using non-mutating methods is recommended.
Learn how to define, use, and optimize local (inline) templates in Aurelia 2 to remove boilerplate and simplify your components.
Most of the time, when working with templated views in Aurelia, you want to create reusable components. However, there are scenarios where reusability isn’t necessary or might cause unnecessary overhead. Local (inline) templates allow you to define a template as a "one-off" custom element, usable only within the scope of its parent view. This helps reduce boilerplate and fosters clear, localized organization of your code.
By defining <template as-custom-element="person-info">
, you create a local component named person-info, which can only be used in this file (my-app.html). It accepts a bindable property person (specified via the <bindable>
tag). You can now reuse <person-info>
repeatedly in this view without creating a separate file or global custom element.
Local templates are similar to HTML-Only Custom Elements, with the major difference that local templates are scoped to the file that defines them. They are ideal for:
One-off Components: When you need a snippet repeated multiple times in a single view but have no intention of reusing it elsewhere.
Reducing Boilerplate: You don’t have to create a new .html and .ts file for every small piece of UI logic.
Maintain High Cohesion: Local templates can be optimized for a specific context without worrying about external usage. They can contain deeply nested markup or references to local data without polluting your global component space.
That said, if you find your local template would be useful across multiple views or components, consider extracting it into a shared component.
A local template must be declared with <template as-custom-element="your-element-name">
. Inside this
Every component instance has a lifecycle that you can tap into. This makes it easy for you to perform various actions at particular times.
For example, you may want to execute some code as soon as your component properties are bound but before the component is first rendered. Or, you may want to run some code to manipulate the DOM as soon as possible after your element is attached to the document.
If you register a listener or subscriber in one callback, remember to remove it in the opposite callback. For example, a native event listener registered in attached
should be removed in detached
When the framework instantiates a component, it calls your class's constructor, just like any JavaScript class. This is the best place to put your basic initialization code that is not dependent on bindable properties.
The "hydrating" hook allows you to add contextual DI registrations (to controller.container
) to influence which resources are resolved when the template is compiled. It is still considered part of "construction".
The "hydrated" hook is a good place to influence how child components are constructed and rendered contextually. It runs synchronously after the definition is compiled (which happens synchronously after hydrating
) and, like hydrating
, can still be considered part of "construction".
The "created" hook is the last hook that can be considered part of "construction". It is called (synchronously) after this component is hydrated, which includes resolving, compiling and hydrating child components. In terms of the component hierarchy, the created hooks execute bottom-up, from child to parent (whereas hydrating
and hydrated
are all top-down). This is also the last hook that runs only once per instance.
Here you can perform any last-minute work that requires having all child components hydrated, which might affect the bind
and attach
lifecycles.
If your component has a method named "binding", then the framework will invoke it after the bindable properties of your component are assigned. In terms of the component hierarchy, the binding hooks execute top-down, from parent to child, so your bindables will have their values set by the owning components, but the bindings in your view are not yet set.
You can optionally return a Promise
either making the method asynchronous or creating a promise object. If you do so, it will suspend the binding and attaching of the children until the promise is resolved. This is useful for fetching/save of data before rendering.
If your component has a method named "bound", then the framework will invoke it when the bindings between your component and its view have been set. This is the best place to do anything that requires the values from let
, from-view
or ref
bindings to be set.
If your component has a method named "attaching, " the framework will invoke it when it has attached the component's HTML element. You can queue animations and/or initialize certain 3rd party libraries.
If your component has a method named "attached", the framework will invoke it when it has attached the current component and all of its children. In terms of the component hierarchy, the attached hooks execute bottom-up.
If your component has a method named "detaching", then the framework will invoke it when removing your HTML element from the document. In terms of the component hierarchy, the detaching hooks execute bottom-up.
If your component has a method named "unbinding, " the framework will invoke it when it has fully removed your HTML element from the document. In terms of the component hierarchy, the unbinding
hooks execute bottom-up.
If your component has a method named "dispose", then the framework will invoke it when the component is to be cleared from memory completely. It may be called, for example, when a component is in a repeater, and some items are removed that are not returned to the cache.
This is an advanced hook mostly useful for cleaning up resources and references that might cause memory leaks if never explicitly dereferenced.
Components are the building blocks of Aurelia applications. This guide covers the essentials of creating, configuring, and using components, complete with practical code examples.
Custom elements are the foundation of Aurelia applications. As a developer, you'll often create custom elements that consist of:
An HTML template (view)
A class acting as the view model
An optional CSS stylesheet
Naming Components
A common best practice is to use a consistent two or three-character prefix for your components. For instance, all Aurelia-provided components start with the prefix au-
.
There are various ways to create custom components in Aurelia, from simple convention-based components to more explicit and configurable ones.
The creation process is flexible, allowing you to adopt the approach that best fits your project's needs.
Aurelia treats any exported JavaScript class as a component by default. As such, there's no difference between an Aurelia component and a vanilla JavaScript class at their core.
Here's an example of a basic Aurelia component. You might add logic and bindable properties as needed, but at its simplest, a component is just a class.
By convention, Aurelia pairs the app-loader.ts
view model with a corresponding app-loader.html
file.
Embrace Conventions
Using Aurelia's conventions offers several benefits:
Reduced boilerplate code.
Cleaner and more portable codebases.
Enhanced code readability and learnability.
Less setup and ongoing maintenance.
Smoother upgrades to new versions and different platforms.
The @customElement
decorator provides a way to define components, bypassing conventions explicitly.
The @customElement
decorator allows for a variety of customizations, such as defining a different HTML template or inline template string, specifying the element's tag name, and configuring other component properties that would otherwise be managed by Aurelia.
Here's an example of defining the template inline:
This approach is useful for simple components that don't require a separate view file.
The @customElement
decorator allows for several configuration options:
This option sets the HTML tag name for the component. For instance, specifying "app-loader" means the component can be used in views as <app-loader></app-loader>
.
If you only need to set the name, you can use a simpler syntax:
The template
option allows you to define the content of your component's template. You can specify an external template file, an inline template string, or even set it to null
for components that don't require a view:
Omitting the template
property means Aurelia won't use conventions to locate the template.
You can declare explicit dependencies within the @customElement
decorator, which can be an explicit way to manage dependencies without using the <import>
tag in your templates:
Aurelia provides an API for creating components programmatically, which is especially useful for testing.
The CustomElement.define
method allows for a syntax similar to the @customElement
decorator, including dependencies and other configurations.
$au
Beside the custom element and CustomElement.define
usages, it's also possible to to delcare a components using static $au
property, like the following example:
It's possible to create components in Aurelia using only HTML without a corresponding view model.
For instance, an HTML-only loader component might look like this:
To use this component, import and reference it:
You can create HTML components with bindable properties using the <bindable>
custom element, which serves a similar purpose to the @bindable
decorator in a view model:
Here's how you would use it:
Though less common, there are times when you might need a component with a view model but no view. Aurelia allows for this with the @customElement
decorator by omitting the template
property.
For example, a loading indicator using the nprogress library might be implemented as follows:
In this example, nprogress manages the DOM manipulation, so a template isn't necessary.
To use your custom components, you must register them either globally or within the scope of their intended use.
Register a component globally in main.ts
using the .register
method:
To use a component within a specific template, import it using the <import>
tag:
To use a component but with an alias, import it using the <import>
tag, together with the as
attribute for the new name:
To use alias for a specific resource on an import, using the <import>
tag, together with the {name}.as
attribute for the new name, with {name}
being the resource name:
Sometimes you may want to render a component without its enclosing tags, effectively making it "containerless."
Be cautious when using containerless components, as you lose the ability to reference the element's container tags, which can complicate interactions with third-party libraries or testing. Use containerless only when necessary.
Mark a component as containerless with the containerless
property:
The @containerless
decorator is an alternative way to indicate a containerless component:
When using <my-component></my-component>
, Aurelia will remove the surrounding tags, leaving only the inner content.
Declare a containerless component inside a view using the <containerless>
tag:
Binding behaviors are a powerful category of view resources in Aurelia 2, alongside value converters, custom attributes, and custom elements. They are most analogous to in that you declaratively use them within binding expressions to modify binding behavior.
After adding this registration, bindings with attributes will work as expected and the syntax is the same with the other bindings. Readmore on the basic binding syntax of Aurelia .
Furthermore, the constructor is where you handle the injection of dependencies using dependency injection. You will learn about DI in the , but here is a basic example of where the constructor is used.
The lifecycle hooks API supports all of the above lifecycle methods. Using the lifecycleHooks
decorator, you can perform actions at various points of the component lifecycle. Because the router uses lifecycle hooks, they are documented in the router section, but do not require the use of the router to use (except for router-specific hooks).
For <au-compose>
, there are extra lifecycle hooks that are activate
/deactivate
. Refers to for more details.
The component name, derived from the file name, must include a hyphen to comply with the Shadow DOM specifications (see ). This requirement is part of the W3C Web Components standard to ensure proper namespacing for custom HTML elements.
Dependencies can also be declared within the template using the <import>
tag or globally registered through .
While it's useful to know about this API, it's typically unnecessary to define custom elements within Aurelia applications. This method is more relevant for writing tests, which you can learn about .
For more on working with Aurelia's Dependency Injection and registering dependencies, see the .
Learn how to manipulate the DOM from the usage-side of a custom element using the processContent hook.
There are scenarios where we would like to transform the template provided by the usage-side. The 'processContent' hook lets us define a pre-compilation hook to make that transformation.
The signature of the hook function is as follows.
There are two important things to note here.
First is the node
argument. It is the DOM tree on the usage-side for the custom element. For example, if there is a custom element named my-element
, on which a 'processContent' hook is defined, and it is used somewhere as shown in the following markup, then when the hook is invoked, the node
argument will provide the DOM tree that represents the following markup.
Then inside the hook this DOM tree can be transformed/mutated into a different DOM tree. The mutation can be addition/removal of attributes or element nodes.
Second is the return type boolean | void
. Returning from this function is optional. Only an explicit false
return value results in skipping the compilation (and thereby enhancing) of the child nodes in the DOM tree. The implication of skipping the compilation of the child nodes is that Aurelia will not touch those DOM fragments and will be kept as it is. In other words, if the mutated node contains custom elements, custom attributes, or template controllers, those will not be hydrated.
The platform
argument is just the helper to have platform-agnostic operations as it abstracts the platform. Lastly the this
argument signifies that the hook function always gets bound to the custom element class function for which the hook is defined.
The most straight forward way to define the hook is to use the processContent
property while defining the custom-element.
Apart from this, there is also the @processContent
decorator which can used class-level or method-level.
That's the API. Now let us say consider an example. Let us say that we want to create a custom elements that behaves as a tabs control. That is this custom element shows different sets of information grouped under a set of headers, and when the header is clicked the associated content is shown. To this end, we can conceptualize the markup for this custom element as follows.
The markup has 2 slots for the header and content projection. While using the tabs
custom element we want to have the following markup.
Now note that there is no custom element named tab
. The idea is to keep the usage-markup as much dev-friendly as possible, so that it is easy to maintain, and the semantics are quite clear. Also it is easy to refactor as now we know which parts belong together. To support this usage-syntax we will use the 'processContent' hook to rearrange the DOM tree, so that the nodes are correctly projected at the end. A prototype implementation is shown below.
Example transformation function for default [au-slot]
If you have used au-slot
, you might have noticed that in order to provide a projection the usage of [au-slot]
attribute is mandatory, even if the projections are targeted to the default au-slot
. With the help of the 'processContent' hook we can workaround this minor inconvenience. The following is a sample transformation function that loops over the direct children under node
and demotes the nodes without any [au-slot]
attribute to a synthetic template[au-slot]
node.
How to create components that accept one or more bindable properties. You might know these as "props" if you are coming from other frameworks and libraries.
When creating components, sometimes you will want the ability for data to be passed into them instead of their host elements. The @bindable
decorator allows you to specify one or more bindable properties for a component.
The @bindable
attribute also can be used with custom attributes as well as custom elements. The decorator denotes bindable properties on components on the view model of a component.
This will allow our component to be passed in values. Our specified bindable property here is called loading
and can be used like this:
In the example above, we are binding the boolean literal true
to the loading
property.
Instead of literal, you can also bind another property (loadingVal
in the following example) to the loading
property.
As seen in the following example, you can also bind values without the loading.bind
part.
Aurelia treats attribute values as strings. This means when working with primitives such as booleans or numbers, they won't come through in that way and need to be coerced into their primitive type using a bindable setter or specifying the bindable type explicitly using bindable coercion.
The @bindable
decorator signals to Aurelia that a property is bindable in our custom element. Let's create a custom element where we define two bindable properties.
You can then use the component in this way,`<name-component first-name="John" last-name="Smith"></name-component>
By default, Aurelia will call a change callback (if it exists) which takes the bindable property name followed by Changed
added to the end. For example, firstNameChanged(newVal, previousVal)
would fire every time the firstName
bindable property is changed.
Due to the way the Aurelia binding system works, change callbacks will not be fired upon initial component initialization. If you worked with Aurelia 1, this behavior differs from what you might expect.
If you would like to call your change handler functions when the component is initially bound (like v1), you can achieve this the following way:
If you have multiple bindable properties like firstName
/lastName
in the above example, and want to use a single callback to react to those changes, you can use propertyChanged
callback. propertyChanged
callback will be called immediately after the targeted change callback. The parameters of this callback will be key
/newValue
/oldValue
, similar like the following example:
In the above example, even though propertyChanged
can be used for multiple properties (like firstName
and lastName
), it's only called individually for each of those properties. If you wish to act on a group of changes, like both firstName
and lastName
at once in the above example, propertiesChanged
callback can used instead, like the following example:
For the order of callback when there' multiple callbacks involved, refer the following example: If we have a component class that looks like this:
When we do
the console logs will look like the following:
Like almost everything in Aurelia, you can configure how bindable properties work.
You can specify the binding mode using the mode
property and passing in a valid BindingMode
to it; @bindable({ mode: BindingMode.twoWay})
- this determines which way changes flow in your binding. By default, this will be BindingMode.oneWay
You can change the name of the callback that is fired when a change is made @bindable({ callback: 'propChanged' })
Bindable properties support many different binding modes determining the direction the data is bound in and how it is bound.
By default, bindable properties will be one-way binding. This means values flow into your component but not back out of it (hence the name, one way).
Unlike the default, the two-way binding mode allows data to flow in both directions. If the value is changed with your component, it flows back out.
Much like most facets of binding in Aurelia, two-way binding is intuitive. Instead of .bind
you use .two-way
if you need to be explicit, but in most instances, you will specify the type of binding relationship a bindable property is using with @bindable
instead.
Explicit two-way binding looks like this:
The myVal
variable will get a new value whenever the text input is updated. Similarly, if myVal
were updated from within the view model, the input would get the updated value.
In some cases, you want to make an impact on the value that is binding. For such a scenario, you can use the possibility of new set
.
Suppose you have a carousel
component in which you want to enable navigator
feature for it.
In version two, you can easily implement such a capability with the set
feature.
Define your property like this:
For set
part, we need functionality to check the input. If the value is one of the following, we want to return true
, otherwise, we return the false
value.
''
: No input for a standalone navigator
property.
true
: When the navigator
property set to true
.
"true"
: When the navigator
property set to "true"
.
So our function will be like this
Now, we should set truthyDetector
function as follows:
Although, there is another way to write the functionality too:
You can simply use any of the above four methods to enable/disable your feature. As you can see, set
can be used to transform the values being bound into your bindable property and offer more predictable results when dealing with primitives like booleans and numbers.
By default, you'll find yourself work with binable and field most of the time, like the examples given above. But there' cases where it makes sense to have bindable as a getter, or a pair of getter/setter to do more logic when get/set.
For example, a component card nav that allow parent component to query its active status. With bindable on field, it would be written like this:
Note that because active
value needs to computed from other variables, we have to "actively" call setActive
. It's not a big deal, but sometimes not desirable.
For cases like this, we can turn active
into a getter, and decorate it with bindable, like the following:
Simpler, since the value of active
is computed, and observed based on the properties/values accessed inside the getter.
The bindable setter section shows how to adapt the value is bound to a @bindable
property. One common usage of the setter is to coerce the values that are bound from the view. Consider the following example.
Without any setter for the @bindable
num we will end up with the string '42'
as the value for num
in MyEl
. You can write a setter to coerce the value. However, it is a bit annoying to write setters for every @bindable
.
To address this issue, Aurelia 2 supports type coercion. To maintain backward compatibility, automatic type coercion is disabled by default and must be enabled explicitly.
There are two relevant configuration options.
enableCoercion
The default value is false
; that is Aurelia 2 does not coerce the types of the @bindable
by default. It can be set to true
to enable the automatic type-coercion.
coerceNullish
The default value is false
; that is Aurelia2 does not coerce the null
and undefined
values. It can be set to true
to coerce the null
and undefined
values as well. This property can be thought of as the global counterpart of the nullable
property in the bindable definition (see Coercing nullable values section).
Additionally, depending on whether you are using TypeScript or JavaScript for your app, there can be several ways to use automatic type coercion.
type
in @bindable
You need to specify the explicit type
in the @bindable
definition.
Currently, coercing four primitive types are supported out of the box. These are number
, string
, boolean
, and bigint
. The coercion functions for these types are respectively Number(value)
, String(value)
, Boolean(value)
, and BigInt(value)
.
Be mindful when dealing with bigint
as the BigInt(value)
will throw if the value
cannot be converted to bigint; for example null
, undefined
, or non-numeric string literal.
It is also possible to coerce values into instances of classes. There are two ways how that can be done.
coerce
methodYou can define a static method named coerce
in the class used as a @bindable
type. This method will be called by Aurelia2 automatically to coerce the bound value.
This is shown in the following example with the Person
class.
According to the Person#coercer
implementation, for the example above MyEl#person
will be assigned an instance of Person
that is equivalent to new Person('john', null)
.
@coercer
decoratorAurelia2 also offers a @coercer
decorator to declare a static method in the class as the coercer. The previous example can be rewritten as follows using the @coercer
decorator.
With the @coercer
decorator, you are free to name the static method as you like.
To maintain backward compatibility, Aurelia2 does not attempt to coerce null
and undefined
values. We believe that this default choice should avoid unnecessary surprises and code breaks when migrating to newer versions of Aurelia.
However, you can explicitly mark a @bindable
to be not nullable.
When nullable
is set to false
, Aurelia2 will try to coerce the null
and undefined
values.
set
and auto-coercionIt is important to note that an explicit set
(see bindable setter) function is always prioritized over the type
. In fact, the auto-coercion is the fallback for the set
function. Hence whenever set
is defined, the auto-coercion becomes non-operational.
However, this gives you an opportunity to:
Override any of the default primitive type coercing behavior, or
Disable coercion selectively for a few selective @bindable
by using a noop
function for set
.
When using TypeScript, usages of union types are not rare. However, using union types for @bindable
will deactivate the auto-coercion.
For the example above, the type metadata supplied by TypeScript will be Object
disabling the auto-coercion.
To coerce union types, you can explicitly specify a type
.
However, using a setter would be more straightforward to this end.
Spreading syntaxes are supported for simpler binding of multiple bindable properties.
Given the following component:
with template:
and its usage template:
The rendered html will be:
Here we are using ...$bindables
to express that we want to bind all properties in the object { first: 'John', last: 'Doe' }
to bindable properties on <name-tag>
component. The ...$bindables="..."
syntax will only connect properties that are matching with bindable properties on <name-tag>
, so even if an object with hundreds of properties are given to a ...$bindables
binding, it will still resulted in 2 bindings for first
and last
.
...$bindables
also work with any expression, rather than literal object, per the following examples:
Sometimes when the expression of the spread binding is simple, we can simplify the binding even further. Default templating syntax of Aurelia supports a shorter version of the above examples:
Remember that HTML is case insensitive, so ...firstName
actually will be seen as ...firstname
, for example
Bindables properties will be tried to matched as is, which means a firstName
bindable property will match an object firstName
property, but not first-name
If the expression contains space, it will result into multiple attributes and thus won't work as intended with spread syntax ...
. For example ...a + b
will be actually turned into 3 attributes: ...a
, +
and b
The order of the bindings created will be the same with the order declared in the template. For example, for the NameTag
component above, if we have a usage
Then the value of the first
property in NameTag
with id=1
will be Jane
, and the value of first
property in NameTag
with id=2
will be John
.
An exception of this order is when bindables spreading is used together with ...$attrs
, ...$attrs
will always result in bindings after ...$bindables
/$bindables.spread
/...expression
.
Bindings will be created based on the keys available in the object evaluated from the expression
of a spread binding. The following example illustrate the behavior:
For the NameTag
component above:
The rendered HTML of <name-tag>
will be
When clicking on the button with text Change last name
, the rendered html of <name-tag>
won't be changed, as the original object given to <name-tag>
doesn't contain last
, hence it wasn't observed, which ignore our new value set from the button click. If it's desirable to reset the observation, give a new object to the spread binding, like the following example:
With the above behavior of non-eager binding, applications can have the opportunity to leave some bindable properties untouched, while with the opposite behavior of always observing all properties on the given object based on the number of bindable properties, missing value (null
/undefined
) will start flowing in in an unwanted way.
There are some other behaviors of the spread binding that are worth noting:
All bindings created with $bindables.spread
or ...
syntax will have binding mode equivalent to to-view
, binding behavior cannot alter this. Though other binding behavior like throttle
/debounce
can still work.
If the same object is returned from evaluating the expression, the spread binding won't try to rebind its inner bindings. This means mutating and then reassigning won't result in new binding, instead, give the spread binding a new object.
Attribute transferring is a way to relay the binding(s) on a custom element to its child element(s).
As an application grows, the components inside it also grow. Something that starts simple, like the following component
with the template
can quickly grow out of hand with a number of needs for configuration: aria, type, min, max, pattern, tooltip, validation etc...
After a while, the FormInput
component above will become more and more like a relayer to transfer the bindings from outside, to the elements inside it. This often results in an increase in the number of @bindable
. While this is fine, you end up with components that have a lot of boilerplate.
And the usage of our component would look like this:
to be repeated like this inside:
To juggle all the relevant pieces for such a task isn't difficult, but somewhat tedious. With attribute transferring, which is roughly close to object spreading in JavaScript, the above template should be as simple as:
, which reads like this: for some bindings on <form-input>
, change the targets of those bindings to the <input>
element inside it.
To transfer attributes & bindings from a custom element, there are two steps:
Set capture
to true
on a custom element via @customElement
decorator:
Or use the capture
decorator from aurelia
package if you don't want to declare the customElement
decorator and have to specify your name and template values.
As the name suggests, this is to signal the template compiler that all the bindings & attributes, with some exceptions, should be captured for future usage.
Spread the captured attributes onto an element
Using the ellipsis syntax which you might be accustomed to from Javascript, we can spread our attributes onto an element proceeding the magic variable $attrs
Spread attributes and overriding specific ones
In case you want to spread all attributes while explicitly overriding individual ones, make sure these come after the spread operator.
It's recommended that this feature should not be overused in multi-level capturing & transferring. This is often known as prop-drilling in React and could have a bad effect on the overall & long-term maintainability of an application. It's probably healthy to limit the max level of transferring to 2.
Aurelia conventions enable the setting of capture
metadata from the template via <capture>
tag, like the following example:
Sometimes it is desirable to capture only certain attributes on a custom element. Aurelia supports this via 2nd form of the custom element capture
value: a function that takes 1 parameter, which is the attribute name, and returns a boolean to indicate whether it should be captured.
What attributes are captured
Everything except the template controller and custom element bindables are captured.
A usage example is as follows:
What is captured:
value.bind="extraComment"
class="form-control"
style="background: var(--theme-purple)"
tooltip="Hello, ${tooltip}"
What is not captured:
if.bind="needsComment"
(if
is a template controller)
label.bind="label"
(label
is a bindable property)
How will attributes be applied in ...$attrs
Attributes that are spread onto an element will be compiled as if it was declared on that element.
This means .bind
command will work as expected when it's transferred from some element onto some element that uses .two-way
for .bind
.
It also means that spreading onto a custom element will also work: if a captured attribute targets a bindable property of the applied custom element. An example:
if value
is a bindable property of my-input
, the end result will be a binding that connects the message
property of the corresponding app.html
view model with <my-input>
view model value
property. The binding mode is also preserved like normal attributes.
Styling components using CSS, CSS pre and post-processors as well as working with web components.
Aurelia 2 simplifies the process of styling components, supporting a variety of CSS flavors and encapsulation methods. Whether you prefer raw CSS, PostCSS, SASS, or Stylus, Aurelia 2 streamlines their integration into your components. Ultimately, all styles compile into standard CSS that browsers can interpret.
Aurelia 2 automatically imports styles for custom elements based on file naming conventions. For instance, if you have a custom element named my-component
, Aurelia 2 looks for a corresponding stylesheet named my-component.css
.
Consider the following file structure:
my-component.ts
: The TypeScript file defining the MyComponent
class.
my-component.css
: The stylesheet containing styles for MyComponent
.
my-component.html
: The HTML template for MyComponent
.
Aurelia 2's convention-based approach eliminates the need to explicitly import the CSS file. When you run your application, Aurelia 2 automatically detects and applies the styles.
When MyComponent
is used in the application, the associated styles are automatically applied, giving the paragraph text a blue color and a light grey background with padding and rounded corners.
The Shadow DOM API, part of the Web Components standard, provides encapsulation by hiding the component's internal DOM and styles from the rest of the application. Aurelia 2 offers several options for working with Shadow DOM:
Global Shadow DOM: By default, encapsulates all components in your application.
Configured Shadow DOM: Use the useShadowDOM
decorator to opt-in per component.
Global opt-out Shadow DOM: Enable Shadow DOM globally but disable it for specific components.
To enable Shadow DOM after the initial setup, configure it in the main.ts
file:
The StyleConfiguration
class from Aurelia allows you to specify how styles are applied, including options for Shadow DOM.
When using Webpack, ensure the following rule is included in the Webpack configuration:
This rule ensures that HTML files within your src
directory are processed correctly to work with Shadow DOM.
In the Shadow DOM, styles are scoped to their components and don't leak to the global scope. To apply global styles across all components, use the sharedStyles
property in the Shadow DOM configuration.
The sharedStyles
property accepts an array, allowing you to include multiple shared stylesheets.
useShadowDOM
The useShadowDOM
decorator, imported from Aurelia, lets you enable Shadow DOM on a per-component basis. Without configuration options, it defaults to open
mode.
You can specify the Shadow DOM mode (open
or closed
) as a configuration option. The open
mode allows JavaScript to access the component's DOM through the shadowRoot
property, while closed
mode restricts this access.
The useShadowDOM
decorator also allows disabling Shadow DOM for a specific component by passing false
.
Shadow DOM introduces special selectors that offer additional styling capabilities. These selectors are part of the CSS Scoping Module specification.
The :host
selector targets the custom element itself, not its children. You can use the :host()
function to apply styles based on the host element's classes or attributes.
In this example, all app-header
elements have a solid border, and those with the active
class have a grey background.
The :host-context
selector styles the custom element based on its context within the document.
Here, app-header
elements will have white text on a dark background inside an element with the dark-mode
class.
When Shadow DOM does not meet your needs, CSS Modules balance style encapsulation and global accessibility. CSS Modules transform class names into unique identifiers, preventing style collisions.
To use CSS Modules, include the following loader configuration in your Webpack setup:
This rule processes CSS files, enabling CSS module functionality.
Define your styles, and reference the class names in your HTML templates. Webpack will handle the conversion to unique class names.
After processing, the title
class may be transformed to a unique identifier like title_1a2b3c
.
CSS Modules support the :global
selector for styling global elements without transformation.
This style will apply globally to elements with the button-primary
class, maintaining the class name without transformation.
Shadow DOM encapsulates a component's styles, preventing them from leaking into the global scope. Aurelia 2 offers granular control over Shadow DOM usage, including global and per-component configuration.
When using Shadow DOM, you can style content passed into slots using the ::slotted()
pseudo-element.
In this example, content assigned to the tab-content
slot will receive padding and a border. At the same time, the encapsulation ensures that these styles don't affect other elements outside the tab-panel
component.
CSS variables can be used within Shadow DOM to create themeable components:
Users of the button-group
component can then define these variables at a higher level to theme the buttons consistently across the application.
Animations can be defined within Shadow DOM to ensure they are scoped to the component:
The slide-in
animation is encapsulated within the animated-banner
component, preventing it from conflicting with any other animations defined in the global scope.
CSS Modules provide a powerful way to locally scope class names, avoiding global conflicts. With Aurelia 2, you can leverage CSS Modules to create maintainable, conflict-free styles.
CSS Modules support composing classes from other modules, promoting reusability.
In this example, primaryButton
composes the baseButton
styles, adding its own background and text color. CSS Modules ensures that these class names are unique to avoid styling conflicts.
CSS Modules can also facilitate theming by exporting and importing variables.
By defining and exporting theme variables in theme.css
, they can be imported and used in other CSS Modules to ensure consistent theming across the application.
For more guidance on class and style bindings in Aurelia applications, please take a look at the [CSS classes and styling section](.. templates/class-and-style-bindings.md). This section covers strategies for dynamically working with classes and inline styles.
The CustomElement
resource is a core concept in Aurelia 2, enabling developers to create encapsulated and reusable components. Understanding how to leverage the CustomElement
API is crucial for building robust applications. In this documentation, we will delve into the usage of CustomElement
and its methods, providing detailed examples and explanations.
This method retrieves the Aurelia controller associated with a DOM node. The controller offers access to the element's view-model, lifecycle, and other properties.
node
: The DOM Node for which to retrieve the controller.
opts
: An object with optional properties to customize the behavior of the method.
Returns an instance of ICustomElementController
or null
/undefined
, depending on the options provided and whether a controller is found.
The define
method registers a class as a custom element in Aurelia.
nameOrDef
: A string representing the name or a PartialCustomElementDefinition
object with configuration options.
Type
: The class containing the logic for the custom element.
Returns a CustomElementType
representing the defined custom element.
Retrieves the CustomElementDefinition
for a custom element class.
Type
: The class of the custom element.
Returns a CustomElementDefinition
object with metadata about the custom element.
These methods are used to attach and retrieve metadata to/from a custom element class.
Type
: The custom element class to annotate or from which to retrieve annotations.
prop
: The property key for the annotation.
value
: The value for the annotation (for annotate
method).
CustomElement.annotate
does not return a value. CustomElement.getAnnotation
returns the annotation value.
Generates a unique name for a custom element, which is useful for components that do not require a specific name.
A string representing a unique name for a custom element.
Creates an InjectableToken
for dependency injection.
An instance of InjectableToken
.
Dynamically generates a CustomElementType
with a given name and prototype.
name
: The name of the custom element.
proto
: An object representing the prototype of the custom element.
A CustomElementType
that can be used to define a custom element.
The Aurelia template compiler is powerful and developer-friendly, allowing you extend its syntax with great ease.
Sometimes you will see the following template in an Aurelia application:
Aurelia understands that value.bind="message"
means value.two-way="message"
, and later creates a two way binding between view model message
property, and input value
property. How does Aurelia know this?
By default, Aurelia is taught how to interpret a bind
binding command on a property of an element via a Attribute Syntax Mapper. Application can also tap into this class to teach Aurelia some extra knowledge so that it understands more than just value.bind
on an <input/>
element.
You may sometimes come across some custom input element in a component library, some examples are:
Microsoft FAST text-field
element: https://explore.fast.design/components/fast-text-field
Ionic ion-input
element: https://ionicframework.com/docs/api/input
Polymer paper-input
element: https://www.webcomponents.org/element/@polymer/paper-input
and many more...
Regardless of the lib choice an application takes, what is needed in common is the ability to have a concise syntax to describe the two way binding intention with those custom elements. Some examples for the above custom input elements:
should be treated as:
In the next section, we will look into how to teach Aurelia such knowledge.
As mentioned earlier, the Attribute Syntax Mapper will be used to map value.bind
into value.two-way
. Every Aurelia application uses a single instance of this class. The instance can be retrieved via the injection of interface IAttrMapper
, like the following example:
After grabbing the IAttrMapper
instance, we can use the method useTwoWay(fn)
of it to extend its knowledge. Following is an example of teaching it that the bind
command on value
property of the custom input elements above should be mapped to two-way
:
Teaching Aurelia to map value.bind
to value.two-way
is the first half of the story. The second half is about how we can teach Aurelia to observe the value
property for changes on those custom input elements. We can do this via the Node Observer Locator. Every Aurelia application uses a single instance of this class, and this instance can be retrieved via the injection of interface INodeObserverLocator
like the following example:
After grabbing the INodeObserverLocator
instance, we can use the method useConfig
of it to extend its knowledge. Following is an example of teaching it that the value
property, on a <fast-text-field>
element could be observed using change
event:
Similarly, examples for <ion-input>
and <paper-input>
:
If an object is passed to the .useConfig
API of the Node Observer Locator, it will be used as a multi-registration call, as per following example, where we register <fast-text-field>
, <ion-input>
, <paper-input>
all in a single call:
Combining the examples in the two sections above into some more complete code block example, for Microsoft FAST components:
And with the above, your Aurelia application will get two way binding flow seamlessly:
Learn how to work with the @aurelia/router package to implement routing in your Aurelia applications.
Routing with Aurelia feels like a natural part of the framework. It can easily be implemented into your applications in a way that feels familiar if you have worked with other frameworks and library routers.
This section is broken up into two parts—a quick introduction to the router and router configuration.
Currently, two routers ship with Aurelia: router lite and core router. This section refers to the core router package that lives in @aurelia/router
— please see the warning note below on a caveat some developers encounter when working with the router.
Before you go any further: Please ensure you are importing from the @aurelia/router
package. Sometimes import extensions will autocomplete your imports and import from the aurelia
package, which currently exports the lite router. Eventually, the aurelia
package will export the @aurelia/router
package, but it currently does not. We have noticed, in many instances, that using the incorrect router imports is why routing is not working.
See how you can configure and implement routing in your Aurelia applications in only a few minutes. Of course, you will want to expand upon this as you build your routes. Here we only learn the bare minimum to get started.
The following getting started guide assumes you have an Aurelia application already created. If not, consult our Quick Start to get Aurelia installed in minutes.
To use the router, we have to register it with Aurelia. We do this inside of main.ts
(or main.js
if you're working with Javascript) — the router is then enabled after it is registered. You might already have code like this if you chose the routing example when generating using the Makes scaffolding tool.
Once again, it bears repeating. Please make sure your router imports are being imported from @aurelia/router in your `main.ts` file, but also in other parts of your Aurelia application as well.
Now, we create our routes. We'll do this inside my-app.ts
and use the static routes
property. Please note that there is also a @routes
decorator, which is detailed inside the Creating Routes section.
For our two routes, we import and provide their respective components. Your components are just classes and can be very simple. Here is the HomePage
component. Please note that you can use inline imports when creating routes, also detailed in the Creating Routes section.
Take note of the path
property which is empty. This tells the router that the HomePage
component is our default route. If no route is supplied, it will load this as the default component. The component
property is the component that will be loaded (self-explanatory). And the title
property is the title for our route.
And the view model for our component is equally simple:
First, let's look at the HTML. If you use the makes
tool to scaffold your Aurelia application. This might be my-app.html
load
Notice how we use a standard hyperlink <a>
tags, but they have an load
attribute instead of href
? This attribute tells the router that these are routable links. The router will translate these load
values into routes (either path or route name). By default, the router does also allow you to use href
for routes (a setting that can be turned off below configuring useHref
).
au-viewport
This tells the router where to display your components. It can go anywhere inside your HTML. It can also have a name (handy for instances where there are multiple au-viewport
elements), and you can have more than one.
The router allows you to configure how it interprets and handles routing in your Aurelia applications. The customize
method on the RouterConfiguration
object can be used to set numerous router settings besides the useUrlFragmentHash
value.
The title can be set for the overall application. By default, the title uses the following value: ${componentTitles}${appTitleSeparator}Aurelia
the component title (taken from the route or component) and the separator, followed by Aurelia.
In most instances, using the above string title is what you will want. You will want the solution below if you need to set the title or transform the title programmatically.
Using the transformTitle
method from the router customization, the default title-building logic can be overwritten. This allows you to set the title programmatically, perform translation (using Aurelia i18n or other packages) and more.
If you do not provide any configuration value, the default is hash-based routing. This means a hash will be used in the URL. If your application requires SEO-friendly links instead of hash-based routing links, you will want to use pushState.
We are performing the configuration inside of the main.ts
file, which is the default file created when using the Makes
CLI tool.
By calling the customize
method, you can supply a configuration object containing the property useUrlFragmentHash
and supplying a boolean value. If you supply true
this will enable hash mode. The default is true
.
If you are working with pushState routing, you will need a base HREF value in the head of your document. The scaffolded application from the CLI includes this in the index.html
file, but if you're starting from scratch or building within an existing application, you need to be aware of this.
PushState requires server-side support. This configuration is different depending on your server setup. For example, if you are using Webpack DevServer, you'll want to set the devServer
historyApiFallback
option to true
. If you are using ASP.NET Core, you'll want to call routes.MapSpaFallbackRoute
in your startup code. See your preferred server technology's documentation for more information on how to allow 404s to be handled on the client with push state.
The useHref
configuration setting is something all developers working with routing in Aurelia need to be aware of. By default, the router will allow you to use both href
as well as load
for specifying routes.
Where this can get you into trouble are external links, mailto links and other types of links that do not route. A simple example looks like this:
By default, this seemingly innocent and common scenario will trigger the router and cause an error in the console.
You have two options when it comes to working with external links. You can specify the link as external using the external
attribute.
Or, you can set useHref
to false
and only ever use the load
attribute for routes.
If you are using the router to render components in your application, there might be situations where a component attempts to be rendered that do not exist. This can happen while using direct routing (not configured routing)
This section is not for catch-all/404 routes. If you are using configured routing, you are looking for the section on catch-all routes here.
To add in fallback behavior, we can do this in two ways. The fallback
attribute on the <au-viewport>
element or in the router customize
method (code).
Let's create the missing-page
component (this is required, or the fallback behavior will not work). First, we'll create the view model for our missing-page
component.
For the fallback
component, an ID gets passed as a parameter which is the value from the URL. If you were to attempt to visit a non-existent route called "ROB," the missingComponent
value would be ROB.
Now, the HTML.
By using the fallback
property on the customize
method when we register the router, we can pass a component.
Sometimes the fallback
attribute can be the preferred approach to registering a fallback. Import your fallback component and pass the name to the fallback
attribute. The same result, but it doesn't require touching the router registration.
The swapStrategy
configuration value determines how contents are swapped in a viewport when transitioning. Sometimes, you might want to change this depending on the type of data you are working with or how your routes are loaded. A good example of configuring the swap order is when you're working with animations.
attach-next-detach-current
(default)
attach-detach-simultaneously
detach-current-attach-next
detach-attach-simultaneously
Still, confused or need an example? You can find an example application with routing over on GitHub here.
Learn how to work with slots to work with the concept of dynamic slot placeholders in your custom elements.
In Aurelia, we have two ways to project content into custom elements. In the case of Shadow DOM, we can use <slot>
and for situations where Shadow DOM is not desirable, we have <au-slot>
When working with Shadow DOM-enabled components, the <slot>
element is a web platform native way to allow content projection into components. In some instances, the <slot>
element will not be the right choice, and you will need to consider <au-slot>
(referenced below) instead.
The slot element will only work when Shadow DOM is enabled for your component. Attempting to use the slot element with it disabled will throw an error in the console.
In the case of a fictional but realistic example, we have a modal element. The user can provide content which is rendered inside of the element.
Assuming this is a Shadow DOM-enabled component, all is well. We have a custom element that allows for content to be used inside of it.
Because we named our component au-modal
we will then use it like this:
Notice how we use the attribute slot
on our content being passed in? This tells Aurelia to project our content into the default slot. Custom elements can have multiple slots, so how do we tell Aurelia where to project our content?
A named slot is no different to a conventional slot. The only difference is the slot has a name we can reference. A slot without a name gets the name default
by default.
Now, to use our element with a named slot, you can do this:
A slot can display default content when nothing is explicitly projected into it. Fallback content works for default and named slot elements.
<slot>
element), with the slotchange
eventThe <slot>
element comes with an event based way to listen to its changes. This can be done via listening to slotchange
even on the <slot>
element, like the following example:
@children
decoratorIn case where it's not desirable to go to listen to projection change at the targets (<slot>
elements), it's also possible to listen to projection at the source with @children
decorator. Decorating a property on a custom element class with @children
decorator will setup mutation observer to notify of any changes, like the following example:
After the initial rendering, myDetails.divs
will be an array of 1 <div>
element, and any future addition of any <div>
elements to the <my-details>
element will update the divs
property on myDetails
instance, with corresponding array.
Additionally, the @children
decorator will also call a callback as a reactive change handler. The name of the callback, if omitted in the decorator, will be derived based on the property being decorated, example: divs
-> divsChanged
@children
decorator usage@children() prop
Use default options, observe mutation, and select all elements
@children('div') prop
Observe mutation, and select only div
elements
@children({ query: 'my-child' })
Observe mutation, and select only my-child
elements, get the component instance if available and fallback to the element itself
@children({ query: 'my-child', map: (node, viewModel) => viewModel ?? node })
Observe mutation, and select only my-child
elements, get the component instance if available and fallback to the element itself
When using @children
to target projected element components, it's often desirable to get the underlying component instances rather than the host elements of those. The @children
decorator by default automatically retrieves those instances, like the following examples:
As items
property is decorated with @children('my-item')
, its values is always a list of MyItem
instances instead of <my-item>
elements. You can alter this behavior by specifying a map option, like the following example:
In the above example, we give map
option a function to decide that we want to take the host element instead of the component instance.
Aurelia provides another way of content projection with au-slot
. This is similar to the native slot
when working with content projection. However, it does not use Shadow DOM. au-slot
is useful where you want externally defined styles to penetrate the component boundary, facilitating easy styling of components.
Suppose you create your own set of custom elements solely used in your application. In that case, you might want to avoid the native slots in the custom elements, as it might be difficult to style them from your application.
However, if you still want slot-like behavior, then you can use au-slot
, as that makes styling those custom elements/components easier. Instead of using shadow DOM, the resulting view is composed purely by the Aurelia compilation pipeline.
There are other aspects of au-slot
as well which will be explored in this section with examples.
Like slot
, a "projection target"/"slot" can be defined using a <au-slot>
element, and a projection to that slot can be provided using a [au-slot]
attribute. Consider the following example.
In the example above, the my-element
custom element defines two slots: one default and one named. The slots can optionally have fallback content; i.e. when no projection is provided for the slot, the fallback content will be displayed. Projecting to a slot is, therefore, also optional. However, when a projection is provided for a slot, that overrides the fallback content of that slot.
Similar to native shadow DOM and <slot/>
/[slot]
pair, [au-slot]
attribute is not mandatory if you are targeting the default slot. All content without explicit [au-slot]
is treated as targeting the default slot. Having no [au-slot]
is also equal to having explicit au-slot
on the content:
Another important point to note is that the usage of [au-slot]
attribute is supported only on the direct children elements of a custom element. This means that the following examples do not work.
Inject the projected slot information
It is possible to inject an instance of IAuSlotsInfo
in a element component view model. This provides information related to the slots inside a custom element. The information includes only the slot names for which content has been projected. Let's consider the following example.
The followingrk would be logged to the console for the instances of my-element
.
It is also possible to use data-binding, interpolation etc., while projecting. While doing so, the scope accessing rule can be described by the following thumb rule:
When the projection is provided, the scope of the custom element providing the projection is used.
When the projection is not provided, the scope of the inner custom element is used.
The outer custom element can still access the inner scope using the $host
keyword while projecting.
To further explain how these rules apply, these rules are explained with the following examples.
Let's consider the following example with interpolation.
Although the my-element
has a message
property, but as my-app
projects to s1
slot, scope of my-app
is used to evaluate the interpolation expression. Similar behavior can also be observed when binding properties of custom elements, as shown in the following example.
Let's consider the following example with interpolation. This is the same example as before, but this time without projection.
Note that in the absence of projection, the fallback content uses the scope of my-element
. For completeness, the following example shows that it also holds while binding values to the @bindable
s in custom elements.
$host
The outer custom element can access the inner custom element's scope using the $host
keyword, as shown in the following example.
Note that using the $host.message
expression, MyApp
can access the MyElement#message
. The following example demonstrates the same behavior for binding values to custom elements.
Let's consider another example of $host
which highlights the communication between the inside and outside of a custom element that employs <au-slot>
In the example above, we replace the 'content' template of the grid, defined in my-element
, from my-app
. While doing so, we can grab the scope of the <au-slot name="content" />
and use the properties made available by the binding expose.bind="{ person, $even, $odd, $index }"
, and use those in the projection template.
Note that $host
allows us to access whatever the <au-slot/>
element exposes, and this value can be changed to enable powerful scenarios. Without the $host
it might not have been easy to provide a template for the repeater from the outside.
The $host
keyword can only be used in the context of projection. Using it in any other context is not supported and will throw errors with high probability.
It is possible to provide multiple projections to a single slot.
This is useful for many cases. One evident example would a 'tabs' custom element.
This helps keep things closer that belong together. For example, keeping the tab-header and tab-content next to each other provides better readability and understanding of the code to the developer. On other hand, it still places the projected contents in the right slot.
Having more than one <au-slot>
with the same name is also supported. This lets us project the same content to multiple slots declaratively, as can be seen in the following example.
Note that projection for the name is provided once, but it gets duplicated in 2 slots. You can also see this example in action here.
<au-slot>
changeSimilar like the standard <slot>
element allows the ability to listen to changes in the content projected, <au-slot>
also provides the capability to listen & react to changes.
@slotted
decoratorOne way to subscribe to au-slot
changes is via the @slotted
decorator, like the following example:
After rendering, the MySummaryElement
instance will have paragraphs value as an array of 2 <p>
element as seen in the app.html
.
The @slotted
decorator will invoke change handler upon initial rendering, and whenever there's a mutation after wards, while the owning custom element is still active. By default, the callback will be selected based on the name of the decorated property. For example: paragraphs
-> paragraphsChanged
, like the following example:
### Change handler callback reminders - The change handler will be called upon the initial rendering, and after every mutation afterwards while the custom element is still active {% %}
@slotted
usageThe @slotted
decorator can be used in multiple forms:
@slotted() prop
Use default options, observe projection on the default
slot, and select all elements
@slotted('div') prop
Observe projection on the default
slot, and select only div
elements
@slotted('div', 'footer') prop
Observe projection on the footer
slot and select only div
elements
@slotted('$all')
Observe projection on the default
slot, and select all nodes, including text
@slotted('*')
Observe projection on the default
slot, and select all elements
@slotted('div', '*')
Observe projection on all slots, and select only div
elements
@slotted('*', '*')
Observe projection on all slots, and select all elements
@slotted({ query: 'div' })
Observe projection on the default
slot, and select only div
elements
@slotted({ slotName: 'footer' })
Observe projection on footer
slot, and select all elements
@slotted({ callback: 'nodeChanged' })
Observe projection on default
slot, and select all elements, and call nodeChanged
method on projection change
Note: the `@slotted` decorator won't be notified if the children of a slotted node change — only if you change (e.g. add or delete) the actual nodes themselves. {% %}
slotchange
bindingThe standard <slot>
element dispatches slotchange
events for application to react to changes in the projection. This behavior is also supported with <au-slot>
. The different are with <slot>
, it's an event while for <au-slot>
, it's a callback as there's no host to dispatch an event, for <au-slot>
is a containerless
element.
The callback will be passed 2 parameters:
name
string
the name of the slot calling the change callback
nodes
Node[]
the list of the latest nodes that belongs to the slot calling the change callback
An example of using slotchange
behavior may look like the following:
slotchange
callback remindersThe callback will not be called upon the initial rendering, it's only called when there's a mutation after the initial rendering.
The callback pass to slotchange of <au-slot>
will be call with an undefined
this, so you should either give it a lambda expression, or a function like the example above.
The nodes passed to the 2nd parameter of the slotchange
callback will always be the latest list of nodes.
the slotchange
callback doesn't fire if the children of a slotted node change — only if you change (e.g. add or delete) the actual nodes themselves.
Sometimes developers want to simulate the situation they have experienced in other frameworks in Aurelia, like Angular or Vue binding syntax. Aurelia provides an API that allows you to change how it interprets templating syntax and even emulate other framework syntax with ease.
attributePattern
decorator in the form of extensibility
feature in Aurelia. With it, we can introduce our own syntax to Aurelia's binding engine.
Its parameters are as follows
pattern
You define the pattern of your new syntax in terms of a very special keyword, PART
. That's essentially the equivalent of this regex: (.+)
.
symbols
In symbols you put anything that should not be included in part extraction, anything that makes your syntax more readable but plays no role but separator e.g. in value.bind
syntax, the symbols
is .
which sits there just in terms of more readability, and does not play a role in detecting parts
of the syntax.
Consider the following example:
foo@bar
would give you the parts foo
and bar
, but if you omitted symbols, then it would give you the parts foo@
and bar
.
This attribute should be on top of a class, and that class should have methods whose name matches the pattern
property of each pattern you have passed to the attributePattern
. Consider the following example:
We have defined the Angular two-way binding pattern, [(PART)]
, the symbols are [()]
which behaves as a syntax sugar for us; the public method defined in the body of the class has the same name as the pattern defined.
This method also accepts three parameters, rawName
, rawValue
, and parts
.
rawName
Left-side of assignment.
rawValue
Right-side of assignment.
parts
The values of PARTs of your pattern without symbols.
rawName
: "[(value)]"
rawValue
: "message"
parts
: ["value"]
The ref
binding command to create a reference to a DOM element. In Angular, this is possible with #
. For instance, ref="uploadInput"
has #uploadInput
equivalent in Angular.
Given the above example and the implementation, the parameters would have values like the following:
rawName
: "#uploadInput"
rawValue
: "" , an empty string.
parts
: ["uploadInput"]
If we want to extend the syntax for ref.view-model="uploadVM"
, for example, we could just add another pattern to the existing class:
It is up to you to decide how each PART
will be taken into play.
You can register attributePattern
in the following two ways:
Globally
Go to the main.ts
or main.js
and add the following code:
Locally
You may want to use it in a specific part of your application. You can introduce it through dependencies
.
Import from somewhere else:
Define it inline:
The Aurelia template compiler is powerful and developer-friendly, allowing you extend its binding language with great ease.
The Aurelia binding language provides commands like .bind
, .one-way
, .trigger
, .for
, .class
etc. These commands are used in the view to express the intent of the binding, or in other words, to build binding instructions.
Although the out-of-box binding language is sufficient for most use cases, Aurelia also provides a way to extend the binding language so that developers can create their own incredible stuff when needed.
In this article, we will build an example to demonstrate how to introduce your own binding commands using the @bindingCommand
decorator.
Before jumping directly into the example, let's first understand what a binding command is. In a nutshell, a binding command is a piece of code used to register "keywords" in the binding language and provide a way to build binding instructions from that.
To understand it better, we start our discussion with the template compiler. The template compiler is responsible for parsing templates and, among all, creating attribute syntaxes. This is where the attribute patterns come into play. Depending on how you define your attribute patterns, the attribute syntaxes will be created with or without a binding command name, such as bind
, one-way
, trigger
, for
, class
, etc. The template compiler then instantiates binding commands for the attribute syntaxes with a binding command name. Later, binding instructions are built from these binding commands, which are "rendered" by renderers. Depending on the binding instructions, the " rendering " process can differ. For this article, the rendering process details are unimportant, so we will skip it.
To create a binding command, we use the @bindingCommand
decorator with a command name on a class that implements the following interface:
A binding command must return true
from the ignoreAttr
property. This tells the template compiler that the binding command takes over the processing of the attribute, so the template compiler will not try to check further whether it's a custom attribute, custom element bindable etc...
The more interesting part of the interface is the build
method. The template compiler calls this method to build binding instructions. The info
parameter contains information about the element, the attribute name, the bindable definition (if present), and the custom element/attribute definition (if present). The parser
parameter is used to parse the attribute value into an expression. The mapper
parameter of type IAttrMapper
is used to determine the binding mode, the target property name, etc. (for more information, refer to the documentation). In short, here comes your logic to convert the attribute information into a binding instruction.
For our example, we want to create a binding command that can trigger a handler when custom events such as bs.foo.bar
, bs.fizz.bizz
etc. is fired, and we want the following syntax:
instead of
We first create a class that implements the BindingCommandInstance
interface to do that.
Note that from the build
method, we are creating a ListenerBindingInstruction
with bs.
prefixed to the event name used in the markup. Thus, we are saying that the handler should be invoked when a bs.*
event is raised.
To register the custom binding command, it needs to be registered with the dependency injection container.
This function can then be called wherever you configure your Aurelia application, so that the compiler knows about your custom command.
In your main.ts
(or equivalent entry point) where you configure Aurelia, you can call the registerBindingCommands
function. For example:
This ensures that when Aurelia boots, it's aware of your new binding command.
ignoreAttr = true
?Setting ignoreAttr = true
tells the compiler that this binding command fully manages the attribute in the view. Without this flag, Aurelia might attempt to interpret the same attribute as a custom attribute or a normal bindable property. This can lead to conflicts or warnings if you reuse attribute names already in use by other features.
If your command doesn't behave as expected:
Make sure you've registered it before Aurelia starts (see the main.ts
snippet above).
Double-check that the command name (e.g., 'bs'
) matches in both the @bindingCommand('bs')
decorator and your view markup (foo.bar.bs="..."
).
Use browser dev tools to confirm whether your event is fired and that the method in your view model is triggered.
And that's it! We have created our own binding command and registered it. This means the following syntax will work:
This binding command can be seen in action below.
Note that the example defines a custom attribute pattern to support
foo.bar.fizz.bs="ev => handle(ev)"
syntax.
Learn about binding values to attributes of DOM elements and how to extend the attribute mapping with great ease.
When dealing with Aurelia and custom elements, we tend to use the @bindable
decorator to define bindable properties. The bindable properties are members of the underlying view model class. However, there are cases where we want to work directly with attributes of the DOM elements.
For example, we want an <input>
element with a maxlength
attribute and map a view model property to the attribute. Let us assume that we have the following view model class:
Then, intuitively, we would write the following template:
This binds the value to the maxlength
attribute of the <input>
element. Consequently, the input.maxLength
is also bound to be 10
. Note that binding the value of the maxLength
attribute also sets the value of the maxLength
property of the input element. This happens because Aurelia, in the background, does the mapping for us.
On a broad level, this is what attribute mapping is about. This article provides further information about how it works and how to extend it.
To facilitate the attribute mapping, Aurelia uses IAttrMapper
, which has information about how to map an attribute to a property. While creating property binding instructions from binding commands, it is first checked if the attribute is a bindable. If it is a bindable property, the attribute name (in kebab-case) is converted to the camelCase property name. However, the attribute mapper is queried for the target property name when it is not a bindable. If the attribute mapper returns a property name, then the property binding instruction is created with that property name. Otherwise, the standard camelCase conversion is applied.
If we want to bind a non-standard <input>
attribute, such as fizz-buzz
, we can expect the input.fizzBuzz
property to be bound. This looks as follows.
The attribute mapping can be extended by registering new mappings with the IAttrMapper
. The IAttrMapper
provides two methods for this purpose. The .useGlobalMapping
method registers mappings applicable for all elements, whereas the .useMapping
method registers mapping for individual elements.
To this end, we can grab the IAttrMapper
instance while bootstrapping the app and register the mappings (there is no restriction, however, on when or where those mappings are registered). An example might look as follows.
In the example above, we are registering a global mapping for foo-bar
attribute to FooBar
property, which will apply to all elements. We are also registering mappings for individual elements. Note that the key of the object is the nodeName
of the element; thus, for an element, it needs to be the element name in upper case. In the example above, we map the fizz-buzz
attribute differently for <input>
and <my-ce>
elements.
With this custom mapping registered, we can expect the following to work.
In addition to registering custom mappings, we can teach the attribute mapper when using two-way binding for an attribute. To this end, we can use the .useTwoWay
method of the IAttrMapper
. The .useTwoWay
method accepts a predicate function determining whether the attribute should be bound in two-way mode. The predicate function receives the attribute name and the element name as parameters. If the predicate function returns true
, then the attribute is bound in two-way mode, otherwise it is bound in to-view mode.
An example looks as follows.
In this example, we are instructing the attribute mapper to use two-way binding for fizz-buzz
attribute of <my-ce>
element. This means that the following will work.
A similar example can be seen in action below.
The template compiler is used by Aurelia under the hood to process templates and provides hooks and APIs allowing you intercept and modify how this behavior works in your applications.
There are scenarios where an application wants to control how to preprocess a template before it is compiled. There could be various reasons, such as accessibility validation, adding debugging attributes etc...
Aurelia supports this via template compiler hooks, enabled with the default template compiler. To use these features, declare and then register the desired hooks with either global (at startup) or local container (at dependencies (runtime) or <import>
with convention).
An example of declaring global hooks that will be called for every template:
compiling: this hook will be invoked before the template compiler starts compiling a template. Use this hook if there need to be any changes to a template before any compilation.
All hooks from local and global registrations will be invoked: local first, then global.
The default compiler will remove all binding expressions while compiling a template. This is to clean the rendered HTML and increase the performance of cloning compiled fragments.
Though this is not always desirable for debugging, it could be hard to figure out what element mapped to the original part of the code. To enable an easier debugging experience, the default compiler has a property debug
that when set to true
will keep all expressions intact during the compilation.
This property can be set early in an application lifecycle via AppTask
, so that all the rendered HTML will keep their original form. An example of doing this is:
List of attributes that are considered expressions:
containerless
as-element
ref
attr with binding expression (attr.command="..."
)
attr with interpolation (attr="${someExpression}"
)
custom attribute
custom element bindables
Now that we understand how the template compiler works let's create fun scenarios showcasing how you might use it in your Aurelia applications.
If your application uses feature flags to toggle features on and off, you may want to modify templates based on these flags conditionally.
Here, elements with a data-feature
attribute will be removed from the template if the corresponding feature flag is set to false
, allowing for easy management of feature rollouts.
For accessibility purposes, form fields must associate label
elements with matching for
and id
attributes. We can automate this process during template compilation.
In this use case, the hook generates a unique id
for each form field that doesn't already have one and updates the corresponding label
's for
attribute to match. This ensures that form fields are properly labelled for screen readers and other assistive technologies.
To enhance accessibility, you might want to automatically assign ARIA roles to certain elements based on their class or other attributes to make your application more accessible without manually annotating each element.
This hook assigns the role="button"
to all elements that have the .btn
class and do not already have a role defined. This helps ensure that custom-styled buttons are accessible.
If your application needs to comply with strict Content Security Policies, you should ensure that inline styles are not used within your templates. A template compiler hook can help you enforce this policy.
This hook scans for any elements with inline style
attributes and removes them, logging a warning for developers to take notice and refactor the styles into external stylesheets.
For performance optimization, you should implement lazy loading for images. The template compiler can automatically add lazy loading attributes to your image tags.
This hook finds all img
elements without a loading
attribute and sets it to lazy
, instructing the browser to defer loading the image until it is near the viewport.
If your application supports multiple themes, you can use a template compiler hook to inject the relevant theme class into the root of your templates based on user preferences.
This hook adds a theme-specific class to the root element of every template, allowing for theme-specific styles to be applied consistently across the application.
The default template compiler will turn a template, either in string or already an element, into an element before the compilation. During the compilation, these APIs on the Node
& Element
classes are accessed and invoked:
Node.prototype.nodeType
Node.prototype.nodeName
Node.prototype.childNodes
Node.prototype.childNode
Node.prototype.firstChild
Node.prototype.textContent
Node.prototype.parentNode
Node.prototype.appendChild
Node.prototype.insertBefore
Element.prototype.attributes
Element.prototype.hasAttribute
Element.prototype.getAttribute
Element.prototype.setAttribute
Element.prototype.classList.add
If it is desirable to use the default template compiler in any environment other than HTML, ensure the template compiler can hydrate the input string or object into some object with the above APIs.
Learn how to navigate the router programmatically using the router load method and the HTML load attribute for creating in-page routes.
This section details how you can use the load method on the router instance or load attribute to navigate to other parts of your application.
To use the load
method, you have first to inject the router into your component. This can be done easily by using the IRouter
decorator on your component constructor method. The following code will add a property to your component, which we can reference.
The load
method can accept a simple string value allowing you to navigate to another component without needing to supply configuration options.
You could also use the string value method to pass parameter values and do something like this where our route expects a product ID, and we pass 12:
The router instance load
method allows you to specify different properties on a per-use basis. The most common one is the title
property, which allows you to modify the title as you navigate your route.
A list of available load options can be found below:
title
— Sets the title of the component being loaded
parameters
— Specify an object to be serialized to a query string and then set to the query string of the new URL.
fragment
— Specify the hash fragment for the new URL.
These option values can be specified as follows and when needed:
The router also allows you to decorate links and buttons in your application using a load
attribute, which works the same way as the router instance load
method.
If you have routes defined on a root level (inside of my-app.ts
) you will need to add a forward slash in front of any routes you attempt to load. The following would work in the case of an application using configured routes.
The load attribute can do more than accept a string value. You can also bind to the load attribute for more explicit routing. The following example is a bit redundant as specifying route:product
would be the same as specifying load="product"
, but if you're wanting more explicit routing, it conveys the intent better.
And where things start to get interesting is when you want to pass parameters to a route. We use the params
configuration property to specify parameters.
In the above example, we provide the route (id
) value (via route: profile
). But, then also provide an object of parameters (via params.bind: { name: 'rob' }
).
These parameter values correspond to any parameters configured in your route definition. In our case, our route looks like this:
Depending on the scenario, you will want to redirect users in your application. Unlike using the load
API on the router where we manually route (for example, after logging in) redirection allows us to redirect inside router hooks.
The <au-viewport>
element is where all of the routing magic happens, the outlet. It supports a few different custom attributes, allowing you to configure how the router renders your components. It also allows you to use multiple viewports to create different layout configurations with your routing.
The router allows you to add multiple viewports to your application and render components into each viewport element by their name. The <au-viewport>
element supports a name attribute, which you'll want to use if you have more than one.
In this example, we have the main viewport for our main content, and another viewport called sidebar
for our sidebar content which is dynamically rendered. When using viewports, think of them like iframes, independent containers that can maintain their own states.
Routes will load in the default viewport element if there are one or more viewports. However, routes can be told to load into a specific viewport.
By specifying the viewport
property on a route, we can tell it to load into a specific route.
Learn all there is to know about creating routes in Aurelia.
The router takes your routing instructions and matches the URL to determine what components to render. When the URL patch matches the configured route path, the component is loaded in the case of configured routes.
To register routes, you can either use the @route
decorator or the static routes property static routes
to register one or more routes in your application.
The routing syntax used in the Aurelia router is similar to that of other routers you might have worked with before. The syntax will be very familiar if you have worked with Express.js routing.
A route is an object containing a few required properties that tell the router what component to render, what URL it should match and other route-specific configuration options.
At a minimum, a route must contain path
and component
properties, or path
and redirectTo
properties. The component
and redirectTo
properties can be used in place of one another, allowing you to create routes that point to other routes.
The path
property on a route is where you'll spend the most time configuring your routes. The path tells the router what to match in the URL, what parameters there are and if they're required.
A path can be made up of either a static string with no additional values or an array of strings. An empty path value is interpreted as the default route, and only one should be specified.
Named required parameters that are prefixed with a colon. :productId
when used in a path, a named required parameter might look like this:
This named parameter is denoted by the colon prefix and is called productId
which we will be able to access within our routed component.
Named optional parameters. Like required parameters, they are prefixed with a colon but end with a question mark.
In the above example, we have an optional parameter called variation
. We know it's optional because of the question mark at the end. This means it would still be valid if you visited this route with supplying the variation parameter.
Using optional name parameters is convenient for routes where different things can happen depending on the presence of those optional parameters.
Wildcard parameters. Unlike required and optional parameters, wildcard parameters are not prefixed with a colon, instead using an asterisk. The asterisk works as a catch-all, capturing everything provided after it.
In the above code example, we can have an endless path after which it is supplied as a value to the canLoad
and load
methods.
Besides the basics of path
and component
a route can have additional configuration options.
id
— The unique ID for this route
redirectTo
— Allows you to specify whether this route redirects to another route. If the redirectTo
path starts with /
it is considered absolute, otherwise relative to the parent path.
caseSensitive
— Determines whether the path
should be case sensitive. By default, this is false
transitionPlan
— How to behave when this component is scheduled to be loaded again in the same viewport. Valid values for transitionPlan are:
replace
— completely removes the current component and creates a new one, behaving as if the component changed.
invoke-lifecycles
— calls canUnload
, canLoad
, unload
and load
(default if only the parameters have changed)
none
— does nothing (default if nothing has changed for the viewport)
title
— Specify a title for the route. This can be a string, or it can be a function that returns a string.
viewport
— The name of the viewport this component should be loaded into.
data
— Any custom data that should be accessible to matched components or hooks. This is where you can specify data such as roles and other permissions.
routes
— The child routes that can be navigated from this route.
By specifying the redirectTo
property on our route, we can create route aliases. These allow us to redirect to other routes. We redirect our default route to the products page in the following example.
When creating routes, it is important to note that the component
property can do more than accept inline import statements. You can also import the component and specify the component class as the component property if you prefer.
If you are working with the Aurelia application generated using npx makes aurelia
you would already have a my-app.ts
file to place your routes in. It's the main component of the scaffolded Aurelia application.
As you will learn towards the end of this section, inline import statements allow you to implement lazy-loaded routes (which might be needed as your application grows in size).
If you have a lot of routes, the static property might be preferable from a cleanliness perspective.
The syntax for routes stays the same using the decorator. Just how they have defined changes slightly.
As your application grows, child routes can become a valuable way to organize your routes and keep things manageable. Instead of defining all your routes top-level, you can create routes inside your child components to keep them contained.
An example of where child routes might be useful in creating a dashboard area for authenticated users.
We add a route in our top-level my-app.ts
component where we added routes in our previous examples. Now, we will create the dashboard-page component.
You will notice we create routes the same way we learned further above. However, we are defining these inside a component we use for our dashboard section. Notice how we use the au-viewport
element inside of the dashboard-page
component.
Lastly, let's create our default dashboard component for the landing page.
Now, we can contain all dashboard-specific routes inside of our dashboard-page
component for dashboard views. Furthermore, it allows us to implement route guards to prevent unauthorized users from visiting the dashboard.
When a user attempts to visit a route that does not exist, we want to catch this route attempt using a catch-all route. We can use a wildcard *
to create a route that does this.
When using a catch-all wildcard route, ensure that it is the last route in your routes array, so it does not hijack any other valid route first.
A good use of a catch-all route might be to redirect users away to a landing page. For example, if you had an online store, you might redirect users to a products page.
You can also specify a component that gets loaded like a normal route:
Most modern bundlers like Webpack support lazy bundling and loading of Javascript code. The Aurelia router allows you to create routes that are lazily loaded only when they are evaluated. What this allows us to do is keep the initial page load bundle size down, only loading code when it is needed.
By specifying an arrow function that returns an inline import
we are telling the bundler that our route is to be lazily loaded when requested.
Inline import statements are a relatively new feature. Inside your tsconfig.json file, ensure your module property is set to esnext to support inline import statements using this syntax.
We went over creating routes with support for parameters in the creating routes section, but there is an additional property you can specify on a route called data,
, which allows you to associate metadata with a route.
This data property will be available in the routable component and can be a great place to store data associated with a route, such as roles and auth permissions. In some instances, the route parameters can be used to pass data, but for other use cases, you should use the data property.
A common scenario is styling an active router link with styling to signify that the link is active, such as making the text bold. When a route is active, by default, a CSS class name of active
will be added to the route element.
In your HTML, if you were to create some links with load
attributes and visit one of those routes, the active
class would be applied to the link for styling. In the following example, visiting the about route would put class="active"
onto our a
element.
The routing lifecycle allows you to run code at different points of the routing lifecycle such as fetching data or changing the UI.
Inside your routable components which implement the IRouteableComponent
interface, certain methods are called at different points of the routing lifecycle. These lifecycle hooks allow you to run code inside of your components, such as fetching data or changing the UI itself.
If you are working with components you are rendering, implementing IRouteableComponent
will ensure that your code editor provides you with intellisense to make working with these lifecycle hooks easier.
The canLoad
method is called upon attempting to load the component. If your route has any parameters supplied, they will be provided to the canLoad
method as an object with one or more parameters as the first argument.
The canLoad
method allows you to determine if the component should be loaded or not. If your component relies on data being present from the API or other requirements being fulfilled before being allowed to render, this is the method you would use.
When working with the canLoad
method, you can use promises to delay loading the view until a promise and/or promises have been resolved. The component would be loaded if we were to return true
from this method.
If you wanted to load data from an API, you could make the canLoad
method async, which would make it a promise-based method. You would be awaiting an actual API call of some kind in place of ....load data
Unlike other async methods, if the promise does not resolve, the component will not load. The canLoad
lifecycle method tells the router if the component is allowed to load. It's a great router method for components that rely on data loading such as product detail or user profile pages.
Not only can we allow or disallow the component to be loaded, but we can also redirect it. If you want to redirect to the root route, return a string with an /
inside it. You can return a route ID, route path match or navigation instruction from inside this callback to redirect.
Returning a boolean false, string or RoutingInstruction from within the canLoad
function will cancel the router navigation.
The loading
method is called when your component is navigated to. If your route has any parameters supplied, they will be provided to the load
method as an object with one or more parameters as the first argument.
In many ways, the loading
method is the same as canLoad
with the exception that loading
cannot prevent the component from loading. Where canLoad
can be used to redirect users away from the component, the loading
method cannot.
All of the above code examples for canLoad
can be used with loading
and will work the same, with exception of being able to return true
or false
boolean values to prevent the component being loaded (as we just mentioned).
The canUnload
method is called when a user attempts to leave a routed view. The first argument of this callback is a INavigatorInstruction
it provides information about the next route. You can return a component, boolean or string value from this method.
Like the canLoad
method, this is just the inverse. It determines if we can navigate away from the current component.
The unloading
method is called if the user is allowed to leave and is in the process of leaving. The first argument of this callback is a INavigatorInstruction
it provides information about the next route.
Like the loading
method, this is just the inverse. It is called when the component is unloaded (provided canUnload
wasn't false).
A common router scenario is you want to route to a specific component, say a component that displays product information based on the ID in the URL. You request the API to get the information and display it.
Two asynchronous lifecycles are perfect for dealing with loading data: canLoad
and load
- both supporting returning a promise (or async/await).
If the component you are loading absolutely requires the data to exist on the server and be returned, the canLoad
lifecycle method is the best place to do it. Using our example of a product page, if you couldn't load product information, the page would be useful, right?
From the inside canLoad
you can redirect the user elsewhere or return false to throw an error.
Similarly, if you still want the view to load, even if we can't get the data, you would use the loading
lifecycle callback.
When you use load
and async
the component will wait for the data to load before rendering.
If you worked with routing in Aurelia 1, you might be accustomed to a currentInstruction
property available on the router. In Aurelia 2, this property does not exist. There are, however, two properties on the router called activeNavigation
and activeComponents
which can be used to achieve a similar result. You can also reference the instruction itself from within route lifecycle functions.
The activeNavigation
property contains quite a few properties but most notably has the current URL path, query parameters and other navigation-specific values. You might want to get information about the current route.
We can get information about the current route by accessing the activeComponents
array and determining the active component. Still, it is possible that more than one component will be in this array. An easier way is to get the route instruction on the canLoad
and loading
lifecycle methods.
It might seem like a mouthful, but to get the current instruction that resulted in the viewport's current content, this is the current approach to take from within those aforementioned methods inside your components.
The parameters
object contains a Javascript object of any URL parameters. If your URL contains /?myprop=22&frag=0
then this object would contain {myprop: '22', frag: '0'}
, allowing you to get fragment values.
While you would often set the title of a route in your route configuration object using the title
property, sometimes you want the ability to specify the title property from within the routed component itself.
You can achieve this from within the canLoad
and load
methods in your component. By setting the next.title
property, you can override or transform the title.
Strategies for stateful router animation
A common scenario in a single-page application is page transitions. When a page loads or unloads, an animation or transition effect might be used to make it feel more interactive and app-like.
At first glance, this might look like a lot of code, but we do the animation inside of the attaching
and detaching
hooks. Using the Anime.js animation library, we create two animation functions for sliding our views in and out.
We use the created
lifecycle callback to access the host element (the outer element of our custom element) which we will animate. Most of the other callbacks determine the direction we are heading in.
We inject out AnimationHooks
class into our main component, but we also inject it into the sub-components we want to animate. We avoid setting our hook globally, or it would run for all components (which you might want).
As you can see, besides some router-specific lifecycle methods, animating with the router isn't router-specific and leverages Aurelia lifecycles.
A link to a demo of slide in and out animations based on routing can be seen below:
How to implement router "guards" into your applications to protect routes from direct access.
You might know router hooks as guards in other routers. Their role is to determine how components are loaded. They're pieces of code that are run in between.
If you worked with Aurelia 1, you might know these by their previous name: router pipelines.
Shared lifecycle hook logic can be defined by implementing a router lifecycle hook on a class with the @lifecycleHooks()
decorator. This hook will be invoked for each component where this class is available as a dependency. This can be either via a global registration or via one or more component-local registrations, similar to how, e.g. custom elements and value converters are registered.
In the example above, we register NoopAuthHandler
globally, which means it will be invoked for each routed component and return true
each time, effectively changing nothing.
Because lifecycle hooks are invoked for each component, it is considered best practice to ensure that you name your lifecycle hooks appropriately, especially if you're working in a team where developers might not be aware of hooks modifying global component lifecycle behaviors.
While lifecycle hooks are indeed their own thing independent of the components you are routing to, the functions are basically the same as you would use inside an ordinary component.
This is the contract for ordinary route lifecycle hooks for components:
And this is the contract for shared lifecycle hooks
The only difference is the addition of the first viewModel
parameter. This comes in handy when you need the component instance since the this
keyword won't give you access like in ordinary component methods.
When dealing with route hooks, you might only want to apply those to specific components. Imagine an authentication workflow where you would want to allow unauthenticated users to access your login or contact page.
To do this, we can specify our route hook as a dependency in the component via the static dependencies
property, which takes an array of one or more dependencies.
Whenever someone tries to route to the SettingsPage
component, they will trigger the authentication hook you created. This per-component approach allows you to target the needed components you want behind a route hook.
Shared lifecycle hooks run in parallel with (but are started before) component instance hooks, and multiple of the same kind can be applied per component. When multiple hooks are registered per component, they are invoked in the registration order.
It is also permitted to define more than one hook per shared hook class:
Understand the scope and binding context.
You might have noticed the words "Scope", "binding context", and "override context" in other places in the documentation or while working with Aurelia in general. Although you can go a long way without even understanding what these are (Aurelia is cool that way), these are some (of many) powerful concepts that are essential when dealing with the lower-level Aurelia 2 API. This section explains what these terms mean.
Here's what you'll learn...
What is Scope?
What is binding context and override context?
How to troubleshoot the rare and weird data binding issues?
How is a context selected?
When we start an Aurelia app, the compilation pipeline JIT compiles the templates (HTML/markup) associated with custom elements. The compilation process demands documentation of its own and is out of this topic's scope. Without going into much detail about that, we can think of the compilation process in terms of the following steps:
Parse the template text,
Create instructions for custom elements, custom attributes, and template controllers (if
, else
, repeat.for
etc.), and
Create a set of bindings for every instruction.
Most of the bindings also contain expressions.
In the example above, the interpolation binding has the expression firsName
, and the property binding has the expression address.pin
(quite unsurprisingly, things are a bit more involved in actuality, but this abstraction will do for now).
An expression in itself might not be that interesting, but when it is evaluated, it becomes of interest. Enter scope. To evaluate an expression, we need a scope.
The expressions themselves do not hold any state or context. This means that the expression firstName
only knows that given an object, it needs to grab the firstName
property of that object. However, the expression, in itself, does not hold that object. The scope is the container that holds the object(s) which can be supplied to the expression when it is evaluated.
These objects are known as contexts. There are typically two types of contexts: binding context and override context. An expression can be evaluated against any of these two kinds of contexts. Even though there are a few subtle differences between these two kinds of contexts (see Override context), in terms of expression evaluation, there is no difference between these two.
Let us consider the following example.
If we invoke this function like foo()
, we will get NaN
. However, binding any object to it might return a more meaningful value, depending on the bound object.
Following that analogy, the expressions are like this function, or more precisely, like the expression a ** 2
in the function. Binding contexts are like the objects used to bind the function. That is, given 2 different binding contexts, the same expression can produce different results when evaluated. Scope, as said before, wraps the binding context, almost like the scope in JavaScript. The need to have this wrapper over the binding context is explained in later sections.
Aurelia pipeline injects a $controller
property to every custom element, custom attribute, and template controller. This property can be used to access the scope and binding context.
Let us consider the following example.
Note that we haven't assigned any value explicitly to the $controller
property, and the Aurelia pipeline assigns that. We can use the $controller.scope
to access the scope and subsequently $controller.scope.bindingContext
can be used to access the binding context.
Note how the bindingContext
in the above example points to this
, that is the current instance of App
(with template controllers, this gets a little more involved; but we will leave that one out for now). However, we refer to the data source as a "context" in evaluating expressions.
The relations explored so far can be expressed as follows.
From here, let us proceed to understand what override context is.
As the name suggests, it is also a context that overrides the binding context. Aurelia gives higher precedence to the overriding context when the desired property is found there. This means that while binding if a property is found in both binding and override context, the latter will be selected to evaluate the expression.
We continue with the previous example; it renders <div>Hello World!</div>
. However, things might be a bit different if we toy with the overriding context, as shown in the following example.
The assignment to overrideContext.message
the rendered output is now <div>Hello Aurelia!</div>
, instead of <div>Hello World!</div>
. This is because of the existence of the property message
in the overriding context.
Now with this information, we also have a new diagram.
As you might know that repeat.for
template controller provides contextual properties such as $index
, $first
, $last
etc. These properties end up being in the override context.
Now imagine if those properties actually end up being in the binding context, which is often the underlying view-model instance. It would have caused a lot of other issues. First, that would have restricted you from having properties with the same name to avoid conflicts.
This, in turn, means that you need to know the template controllers you are using thoroughly to know about such restrictions, which is not a sound idea in itself. And with that, if you define a property with the same name, as used by the template controller, coupled with change observation etc., we could have found ourselves dealing with numerous bugs in the process. Override context helps us to get out of that horrific mess.
Another prominent use caseend for override context is the let
binding. When not specified otherwise, the properties bound via the let
binding ends up in the overriding context.
This can be seen in the example below.
Typically the properties for the let
-bindings are view-only properties. It makes sense to have those properties in the overriding context.
The discussion so far has explained the necessity of context. However, that still does not answer the question, 'If the expressions are evaluated based on the context, why do we even need scope?'. Apart from serving as a logical container for the contexts, a scope also optionally points to the parent scope.
Let us consider the following example to understand that.
The example above App
uses the FooBar
custom element, and both have property named message
, initialized with different values. As expected, the rendered output in this case is Hello Foo-Bar! Hello App!
.
You might have used the $parent
keyword a lot, but for completeness, it should be clarified that the parent scope can be accessed using the $parent
keyword. The example above FooBar#$controller.scope.parentScope.bindingContext
points to the instance of App
where <foo-bar>
is used. In short, every scope instance has a parentScope
property that points to the parent scope when available.
With this information, our diagram changes one last time.
Note that the parentScope
for the scope of the root component is null
.
As we are talking about scope, it needs to be noted that the term 'host scope' is used in the context of au-slot
. There is no difference between a "normal" scope and a host scope; it just acts as the special marker to instruct the scope selection process to use the scope of the host element instead of the scope of the parent element.
Now let us discuss change observation. A comprehensive discussion on change observation is a bit out of this documentation's scope. However, for this discussion, it would suffice to say that generally, whenever Aurelia binds an expression to the view, it employs one or more observers.
This is how when the value of the underlying property changes, the change is also propagated to view or other associated components. The focus of this discussion is how some interesting scenarios occur in conjunction with binding/override context and the change observation.
Let's start with a simple example.
The example above updates the message
property of the binding context every 1 second. As Aurelia is also observing the property, the interpolated output is also updated after every 1 second. Note that as the scope.bindingContext
above points to the this
, updating this.message
that way has the same effect.
As the next example, we change the property in both the binding context and the override context.
Although it has been said before that the property in override context takes precedence over binding context, the output from the example above is Hello Binding Context! #i: 1
, Hello Binding Context! #i: 2
, and so on. The reason for this behavior is that the scope.bindingContext.message
is bound to the view instead of scope.overrideContext.message
, as the latter was non-existent during the binding phase (note that the values are being changed in attached
lifecycle hook).
Therefore, the change observation is also applied for the scope.bindingContext.message
as opposed to that of override context. This explains why updating the scope.overrideContext.message
is rather 'futile' in the example above.
However, the result would have been quite different, if the message
property is introduced to override context during the binding
phase (or before that, for that matter).
Note that the example above introduces the message
property in the overriding context during the binding
phase. When the interpolation expression is evaluated in the view, it is that property from the overriding context that ends up being bound. This means that the message
property in the overriding context is also observed.
Thus, quite expectedly, every 1-second output of the above-shown example changes as Hello Override Context! #i: 1
, Hello Override Context! #i: 2
, and so on.
So far, we have seen various aspects of scope, binding and override context. One thing we have not addressed so far is how the contexts are selected for expression evaluation or assignment. In this section, we will look into that aspect.
The context selection process can be summed up (simplified) as follows.
IF $parent
keyword is used once or more than once, THEN
traverse up the scope, the required number of parents (that is, for $parent.$parent.foo
, we will go two steps/scopes up)
RETURN override context if the desired property is found there, ELSE RETURN binding context.
ELSE
LOOP till either the desired property is found in the context or the component boundary is hit. Then perform the following.
IF the desired property is found in the overriding context, return the override context.
ELSE RETURN binding context.
The first rule involving $parent
should be self-explanatory. We will focus on the second part.
Let us first see an example to demonstrate the utility of the rule #2.1.
.
As expected, the example produces the following output.
Note that both App
and FooBar
initializes their own message
properties. According to our rule #2.3.
binding context is selected, and the corresponding message
property is bound to the view. However, it is important to note that if the FooBar#message
stays uninitialized, that is the message
property exists neither in binding context nor in override context (of FooBar
's scope), the output would have been as follows.
Although it should be quite as per expectation, the point to be noted here is that the scope traversal never reaches to App
in the process. This is because of the 'component boundary' clause in rule #2.1.
. In case of this example, the expression evaluation starts with the scope of the innermost repeat.for
, and traversed upwards.
When traversal hits the scope of FooBar
, it recognize the scope as a component boundary and stops traversing any further, irrespective of whether the property is found or not. Contextually note that if you want to cross the component boundary, you need to explicitly use $parent
keyword.
The rule #2.2.
is also self-explanatory, as we have seen plenty of examples of overriding context precedence so far. Thus the last bit of this story boils down to the rule #2.3.
. This rule facilitates using an uninitialized property in binding context by default or as a fallback, as can be seen in the example below.
The example shown above produces Hello World!
as output after 2 seconds of the invocation of the attached
hook. This happens because of the fallback to binding context by the rule #2.3.
.
That's it! Congratulations! You have made it till the end. Go have that tea break now! Hope you have enjoyed this documentation as much as you will enjoy that tea. Have fun with Aurelia2!
Please see the section to learn how to implement redirection inside your components.
Parameters are supplied to canLoad
and loading
router lifecycle callbacks as the first argument. They are passed as an object with key/value pairs. Please consult the section to learn how to access them.
By leveraging , we can perform animations and transition effects in code.
The lifecycle hooks sharing API can be used to define reusable hook logic. In principle, nothing new needs to be learned: their behavior is the same as described in , with the only difference being that the view model instance is added as the first parameter.
One way to think about expression and binding context is in terms of functions and binding those functions with an execution context (Refer: ).
As the assignment is made pre-binding phase (created
hook in the example above), the context sees that the required property exists in the overriding context and selects that with higher precedence even though a property with the same name also exists in the binding context.
Now let's address the question 'Why do we need override context at all?'. The reason it exists has to do with the template controllers (mostly). While writing template controllers, many times we want a context object that is not the underlying view-model instance. One such prominent example is the template controller.
Moreover, this is a special kind of scope that is valid only in the context of au-slot
. This is already discussed in detail in the , and thus not repeated here.
While the docs do a great job explaining the intricacies of the router, sometimes you just need a code snippet and a brief explanation to do something. You will find code snippets for basic things, from creating routes to working with router hooks.
A component that is loaded as part of a route definition. The IRouteableComponent
When working with the router, sometimes you want to access the currently active route. The router provides an array of activeComponents
which can be one or more components currently active. In most instances, this array will only contain one component. However, if you are working with multiple viewports, this array will contain all components from those viewports.
By leveraging the route.match
property, we can get the currently active route. This is where you can access its data, path, name and other route configuration properties.
To get all registered routes, you can use the getRoutes
method from the rootScope
property of the router.
As outlined in the Creating Routes section, routes can be specified using the routes
decorator or the static routes
property.
A parameter is denoted by the prefixed colon :
followed by the parameter's name. In this example, our parameter is called productId
, which is required for the route to load.
You can have more than one parameter (as many as you like):
Routes support a custom data
property allowing you to decorate your routes. Some use cases might include marking a route as requiring a user to be authenticated or an icon.
Some routes might be loaded into specific viewports in applications with multiple viewports. You can use the viewport
property on routes to specify which route.
Inside components displayed by routes, the best place is to load data inside canLoad
or load
hooks. If your view depends on the data being loaded (like a product detail page), use canLoad
otherwise, use load
. The first argument is any parameters passed through the route.
Using the canLoad
lifecycle hook, we can redirect users. In the following example, we redirect a user to a /products
route. You would have this wrapped in a check to determine if the component loads or the user is redirected away.
Routing with Aurelia feels like a natural part of the framework. It can easily be implemented into your applications in a way that feels familiar if you have worked with other frameworks and library routers. Here is a basic example of routing in an Aurelia application using router-lite
.
The following getting started guide assumes you have an Aurelia application already created. If not, consult our Quick Start to get Aurelia installed in minutes.
router-lite
To use the router-lite
, we have to register it with Aurelia. We do this at the bootstrapping phase.
For this example, we have a root component which is MyApp
. And then we are going to define two routes for the root component, namely home
and about
.
Let us define these components first.
With the routable components in place, let's define the routes. To this end, we need to add the route configurations to our root component MyApp
.
There are couple of stuffs to note here. We start by looking at the configurations defined using the @route
decorator where we list out the routes under the routes
property in the configuration object in the @route
decorator. The most important things in every route configurations are the path
and the component
properties. This instructs the router to use the defined component
in the viewport when it sees the associated path
.
The viewport is specified in the view (see my-app.html
) by using the <au-viewport>
custom element. For example, the router will use this element to display the Home
component when it sees the /
(the empty path) or the /home
paths.
The nav>a
elements are added to navigate from one view to another.
See this in action:
If you have opened the demo then you can notice that the URL in the address bar or the URLs in the nav>a
elements contains a #
(example: /#home
, /#about
etc.). Depending on your project need and esthetics you may want to get rid of the #
-character. To this end, you need set the useUrlFragmentHash
to false
, which is also the default.
For the documentation of router-lite, many live examples are prepared. It is recommended to use the live examples as you read along the documentation. However, if you are feeling adventurous enough to explore the features by yourself, here is the complete collection of the live examples at your disposal.
Aurelia comes with a powerful fully-featured router without the need to install any additional dependencies. If you are new to Aurelia, we recommend visiting the Getting Started section first to familiarise you with the framework.
You are here because you want to familiarize yourself with the router, so we'll make this quick. At the end of this tutorial, you will be familiar with all the concepts and APIs you need to add routing into your Aurelia applications.
While building our recipe application, we will cover the following:
How to create and configure routes
Navigating with load
in your views as well as view-models
Styling active links
Programmatically navigating using router APIs
Working with route parameters
A working demo and code for the following tutorial can also be found here.
To do this tutorial, you'll need a working Aurelia application. We highly recommend following the Quick Start guide to scaffold an application. However, for this tutorial, we have a starter Aurelia 2 application ready to go that we recommend. It will allow you to follow along and live code.
As you will see, it's a boring application with no routing or anything exciting. All it says is Recipes (hardly a recipe application).
The viewport is where our loaded routes are dynamically loaded into. When we click on a route, the <au-viewport>
element will be populated with the requested component.
Open my-app.html
and add in the <au-viewport>
and some navigation links.
This won't do anything just yet because we haven't enabled routing or created any routes.
Let's go into main.ts
and configure Aurelia to use routing. Import the RouterConfiguration
object and pass it to Aurelia's register
method.
We also configure the router to use push-state routing instead of the hash-based router. This gives us cleaner URLs.
Now that we have a viewport and the router enabled let's create some routes. We will start by adding our routes to our root my-app.ts
component.
The important thing to note here is that we are not specifying a component to load for these routes. We will do that shortly, but the routing structure is what we are creating here.
The first route has an empty path
, which means it's a default route (the router will load this first if no other route is requested). The second route recipes
tells the router when the user visits /recipes
to load our recipes component. Lastly, the recipes/:recipeId
route has a route parameter that allows us to load specific recipes.
We now need to create three components for our routes: the homepage, recipes page, and recipe detail page.
Unlike other frameworks and libraries, Aurelia works on the premise of a view-and-view model. If you familiarize yourself with the Getting Started section, you would already be familiar with these concepts.
Let's create the homepage first:
Let's create the recipes page:
Let's create the recipe detail page:
Lastly, let's import our components in my-app.ts
for the routes to load them.
In a non-tutorial application, you would have your API and server providing this data. But, for our tutorial will use a public recipe API instead. We could use mock data, but it would be a lot of data for a recipe application.
The MealDB is a fantastic free meal API that can give us recipes. We will use the Aurelia Fetch Client to get this data, which wraps up the native Fetch API.
In recipes-page.ts
add the following to the component view model:
We've loaded the recipes. Now it's time to display them. Open recipes-page.html
and add in the following:
We use Aurelia's repeat.for
functionality to loop over the recipes. But take notice of the link with load
attribute. Aurelia Router sees this and knows this is a route. We are providing the recipe detail route with the ID of the recipe.
It's not pretty, but we now have a list of recipes.
Sometimes a user might attempt to visit a recipe or page that doesn't exist. You might want to redirect the user or display a 404 page in those situations.
Let's create another component and call it fourohfour-page
We then specify on our <au-viewport>
what our fallback is. We need to import this 404 component to use it.
When we created our routes in my-app.ts
you might recall we created a recipe detail route which had a recipeId
parameter. Now, we are going to modify our recipe-detail
component to read this value from the URL and load the content.
There is a little more to unpack here. We inject the router because we will programmatically redirect away if the user attempts to view a non-existent recipe ID. We use the canLoad
method because loading our recipe view relies on existing recipes. If the recipe can't be found using the API, we redirect to the recipes page programmatically using the router.load
method.
Inside recipe-detail.html
we'll render our recipe:
The API we use returns ingredients on a line-by-line basis, so we've omitted those from this example. Now, you should be able to run your application and click on recipes to view them. A headline, image and instructions should now be visible.
As an additional step, you could add those in yourself.
The router automatically adds a active
class to all route links when they are active. Because our routes all live in my-app
We will edit my-app.css
We don't even have to import it (Aurelia does that automatically for us).
The active
class is now bold when active. We also do some quick styling tweaks to the route links to remove the underline and make them black so we can see the bold stand out more.
You just built a recipe application. It doesn't allow you to create recipes, and it's not very pretty, but it works. To see the application in action, a working demo of the above code can be seen here (or below).
Learn about configuring routes in Router-Lite.
The router takes your routing instructions and matches the URL to one of the configured Routes to determine which components to render.
To register routes you can either use the @route
decorator or you can use the static routes
property to register one or more routes in your application.
This section describes the route configuration options in details.
The routing configuration syntax for router-lite is similar to that of other routers you might have worked with before. If you have worked with Express.js routing, then the syntax will be very familiar to you.
A route is an object containing a few required properties that tell the router what component to render, what URL it should match on and other route-specific configuration options.
The most usual case of defining a route configuration is by specifying the path
and the component
properties.
The idea is to use the path
property to define a pattern, which when seen in the URL path, the view model defined using the component
property is activated by the router-lite.
Simply put, a routing configuration is a mapping between one or more path patterns to components.
Below is the simple example (from the getting started section) of this.
For the example above, when the router-lite sees either the path /
or /home
, it loads the Home
component and if it sees the /about
path it loads the About
component.
Note that the example above uses the @route
decorator.
In case you cannot use the decorator, you can use the static properties instead.
The example shown above can be rewritten as follows.
As the re-written example shows, you can convert the properties in the options object used for the @route
decorator into static
properties in the view model class.
Apart from the static API including the @route
decorator, there is also an instance-level hook named getRouteConfig
that you can use to configure your routes.
This is shown in the example below.
See this in action below.
Note that the hook is also supplied with a parent route configuration, and the new route node. These values can be nullable; for example, for root node there is no parent route configuration.
The getRouteConfig
can also be async
.
This is shown in the example below.
See this in action below.
path
and parametersThe path defines one or more patterns, which are used by the router-lite to evaluate whether or not an URL matches a route or not. A path can be either a static string (empty string is also allowed, and is considered as the default route) without any additional dynamic parts in it, or it can contain parameters. The paths defined on every routing hierarchy (note that routing configurations can be hierarchical) must be unique.
Required parameters are prefixed with a colon.
The following example shows how to use a required parameter in the path
.
When a given URL matches one such route, the parameter value is made available in the canLoad
, and load
routing hooks.
Note that the value of the id
parameter as defined in the route configuration (:id
) is available via the params.id
.
Check out the live example to see this in action.
Optional parameters start with a colon and end with a question mark.
The following example shows how to use an optional parameter in the path
.
In the example, shown above, the Product
component is loaded when the router-lite sees paths like /product
or /product/some-id
, that is irrespective of a value for the id
parameter.
You can see the live example below.
Note that there is an additional link added to the products.html
to fetch a random product.
As the id
parameter is optional, even without a value for the id
parameter, clicking the link loads the Product
component.
Depending on whether or not there is a value present for the id
parameter, the Product
component generates a random id and loads that.
The wildcard parameters, start with an asterisk instead of a colon, act as a catch-all, capturing everything provided after it.
The following example shows how to use a wildcard parameter in the path
.
In the example, shown above, the Product
component is loaded when the router-lite sees paths like /product/some-id
or /product/some-id/foo/bar
.
You can see the live example below.
The example utilizes a wildcard parameter named rest
, and when the value of rest
is 'image'
, an image for the product is shown.
To this end, the canLoad
hook of the Product
view-model reads the rest
parameter.
Any required and optional parameters can be constrained by using a regular expression.
The following example shows how to use a wildcard parameter in the path
.
Note that the syntax to define a parameter constraint is as follows.
The example above shows that the Product
component is loaded when the router-lite sees paths like /product/123
, but not /product/abc
.
You can see the live example below.
You can configure the title for the routes while you are configuring the routes.
The title can be configured in the root level, as well as in the individual route level.
This can be seen in the following example using the @route
decorator.
If you prefer using the static routes
property, the title can be set using a static title
property in the class.
The following example has exactly the same effect as of the previous example.
With this configuration in place, the default-built title will be Home | Aurelia
when user is navigated to /
or /home
route.
That is, the titles of the child routes precedes the base title.
You can customize this default behavior by using a custom buildTitle
function when customizing the router configuration.
Note that, instead of a string, a function can also be used for title
to lazily set the title.
By specifying the redirectTo
property on our route, we can create route aliases.
These allow us to redirect to other routes.
In the following example, we redirect our default route to the home
page and the about-us
to about
page.
You can see this action below.
Note that redirection also works when there are multiple paths/aliases defined for the same component.
You can see this action below.
You can use route parameters for redirectTo
.
The following example shows that the parameters from the about-us
path is rearranged to the about
path.
You can see this action below.
We can instruct the router-lite to redirect the users to a different configured path, whenever it sees any unknown/un-configured paths.
To this end, we can use the fallback
configuration option.
Following example shows how to use this configuration option.
As the example shows, the fallback
is configured as follows.
There is a custom element, named NotFound
, which is meant to be loaded when any unknown/un-configured route is encountered.
As you can see in the above example, clicking the "Foo" link that is with un-configured href
, leads to the NotFound
view.
Another way of defining the fallback
is to use the route-id
.
The following example demonstrates this behavior, where the NotFound
view can be reached via multiple aliases, and instead of choosing one of these aliases the route-id
is used to refer the route.
The name of the custom element, meant to be displayed for any un-configured route can also be used to define fallback
.
The following example demonstrates this behavior, where not-found
, the name of custom element NotFound
, is used to refer the route.
An important point to note here is that when you are using the custom element name as fallback, you need to ensure that the custom element is registered to the DI container.
Note that in the example above, the NotFound
component is registered to the DI container in main.ts
.
A fallback
defined on parent is inherited by the children (to know more about hierarchical routing configuration, refer the documentation).
However, every child can override the fallback as needed.
The following example demonstrate this.
The root has two sibling viewports and two children components can be loaded into each of those by clicking the link.
Every child defines their own child routing configuration.
The root defines a fallback
and one of the children overrides the fallback
by defining one of its' own.
With this configuration in place, when navigation to a un-configured route ('Foo') is attempted for each children, one loads the overridden version whereas the other loads the fallback inherited from the parent (in this case the root).
A function can be used for fallback
.
The function takes the following signature.
An example can look like below, where the example redirects the user to NF1
component if an attempt to load a path /foo
is made.
Every other attempt to load an unknown path is results loading the NF2
component.
You can also see this in action below.
You can also use non-string fallbacks.
For example, you can use a class as the value for fallback
; such as fallback: NotFound
.
Or, if you are using a function, you choose to return a class instead of returning a string.
These combinations are also supported by router-lite.
Routes can be marked as case-sensitive in the configuration, allowing the navigation to the component only when the case matches exactly the configured path. See the example below where the navigation to the "about" page is only successful when the casing matches.
Thus, only an attempt to the /AbOuT
path loads the About
component; any attempt with a different casing is navigated to the fallback
.
See this in action below.
There are few other routing configuration which aren't discussed above. Our assumption is that these options are more involved and might not be used that often. Moreover, to understand the utility of these options fully, knowledge of other parts of the route would be beneficial. Therefore, this section only briefly introduces these options providing links to the sections with detailed examples.
id
— The unique ID for this route. The router-lite implicitly generates a id
for a given route, if an explicit value for this property is missing. Although this is not really an advanced property, due to the fact that a route can be uniquely identified with id
, it can be used in many interesting ways. For example, this can be used to generate the href
s in the view when using the load
custom attribute or using the Router#load
API. Using this property is also very convenient when there are multiple aliases for a single route, and we need a unique way to refer to this route.
transitionPlan
— How to behave when the currently active component is scheduled to be loaded again in the same viewport. For more details, please refer the documentation.
viewport
— The name of the viewport this component should be loaded into. This demands a full fledged documentation of its own. Refer to the viewport documentation for more details.
data
— Any custom data that should be accessible to matched components or hooks. The value of this configuration property must be an object and the object can take any shape (that is there is no pre-defined interface/class for this object). A typical use-case for the data
property is to define the permissions, required by the users, when they attempt to navigate to this route. Refer an example of this.
nav
- Set this flag to false
(default value is true
), to instruct the router not to add the route to the navigation model. This is typically useful to exclude routes from the public navigation menu.
Before finishing the section on the route configuration, we need to discuss one last topic for completeness, and that is how many different ways you can configure the component
.
Throughout various examples so far we have seen that components are configured by importing and using those in the routing configuration.
However, there are many other ways in which the components can be configured.
This section discusses those.
import()
Components can be configured using the import()
or dynamic import.
Instead of statically importing the components, those can be imported using import()
-syntax, as the example shows below.
You can see this in action below.
Components can be configured using only the custom-element name of the component.
However, when configuring the route this way, you need to register the components to the DI.
You can see this configuration in action below.
Components can be configured using a function that returns a class.
You can see this configuration in action below.
Components can be configured using custom element definition.
You can see this configuration in action below.
Components can be configured using custom element instance.
You can see this configuration in action below.
When using HTML-only custom elements, facilitated via the convention, the custom element can be directly used while configuring routes.
Using import()
function directly is also supported.
When importing the HTML-only custom elements in the HTML file using <require from="">
syntax, use the custom element name in the route configuration.
Components can be configured using a navigation strategy.
A navigation strategy is an instance of NavigationStrategy
class that takes a factory method to return a routable component.
The following example shows how to use a navigation strategy.
The factory method takes has the following signature.
The parameters can be used further to determine which component to load.
Using router-lite it is also possible to use the routed view model classes directly as routes configuration.
While doing so, if no paths have been explicitly configured for the components, the custom element name and aliases can be used as routing instructions.
The following example demonstrates that the C1
and C2
classes are used directly as the child routes for the Root
.
The example above implies that router.load('c-1')
, or router.load('c-a')
and router.load('c-2')
, router.load('c-two')
will load the C1
and C2
respectively.
The examples discussed so far demonstrate the classic use-cases of route configurations where the parents define the child routes. Another aspect of these examples are that all the route configurations are centralized on the parent component. This section provides some examples where that configuration is distributed across different components.
We start by noting that every component can define its own path. This is shown in the following example.
The example shows that both Home
and About
uses the @route
decorator to define their own paths.
This reduces the child-route configuration for MyApp
to @route({ routes: [Home, About] })
.
The example can be seen in action below.
Note that other properties of route configuration can also be used in this way.
The previous example demonstrates that the Home
and About
components define the title
for themselves.
The example can be seen in action below.
While adapting to distributes routing configuration, the parent can still override the configuration for its children.
This makes sense, because even if a component defines its own path
, title
etc. the parent may choose to reach (route) the component via a different path or display a different title when the component is loaded.
This is shown below, where the MyApp
overrides the routing configurations defined by About
.
This can be seen in action below.
You can also use the @route
decorator and the getRouteConfig
together.
This can be seen in action below.
The advantage of this kind of distributed configuration is that the routing configuration of every component is defined by every component itself, thereby encouraging encapsulation, leaving the routing configuration at the parent level to merely listing out its child components. On the other hand, highly distributed route configuration may prevent easy overview of the configured routes. That's the trade-off. Feel free to mix and match as per your need and aesthetics.
Apart from configuring routes, you may also want to read which route is currently active and obtain any query parameters. If you only defined or named a few of them as route parameters, others might appear as query parameters. Here is a short example:
You can inject ICurrentRoute
in any routed view-model. For an elaborate approach, see additional references in the navigation model and navigation docs.
Learn about configuring the Router-Lite.
The router allows you to configure how it interprets and handles routing in your Aurelia applications. The customize
method on the RouterConfiguration
object can be used to configure router settings.
useUrlFragmentHash
If you do not provide any configuration value, the default is pushState routing. If you prefer hash-based routing to be used, you can enable this like so:
By calling the customize
method, you can supply a configuration object containing the property useUrlFragmentHash
and supplying a boolean value. If you supply true
this will enable hash mode. The default is false
.
If you are working with pushState routing, you will need a <base>
element with href
attribute (for more information, refer MDN) in the head of your document. The scaffolded application from the CLI includes this in the index.html
file, but if you're starting from scratch or building within an existing application you need to be aware of this.
basePath
Configuring a base path is useful in many real-life scenarios. One such example is when you are hosting multiple smaller application under a single hosting service. In this case, you probably want the URLs to look like https://example.com/app1/view42
or https://example.com/app2/view21
. In such cases, it is useful to specify a different base#href
value for every app.
Run the following example to understand how the value defined in base#href
is affecting the URLs.
When you open the example in a new browser tab, you can note that the URL in the address bar looks the HOSTING_PREFIX/app/home
or HOSTING_PREFIX/app/about
. This is also true for the href
values in the a
tags. This happens because <base href="/app">
is used in the index.ejs
(producing the index.html). In this case, the router-lite
is picking up the baseURI
information and performing the routing accordingly.
This needs bit more work when you are supporting multi-tenancy for your app. In this case, you might want the URLs look like https://example.com/tenant-foo/app1/view42
or https://example.com/tenant-bar/app2/view21
. You cannot set the document.baseURI
every time you start the app for a different tenant, as that value is static and readonly, read from the base#href
value.
With router-lite
you can support this by setting the basePath
value differently for each tenant, while customizing the router configuration, at bootstrapping phase. Following is an example that implements the aforementioned URL convention. To better understand, open the the example in a new tab and check the URL in address bar when you switch tenants as well as the links in the a
tags.
The actual configuration takes place in the main.ts
while customizing the router configuration in the following lines of code.
There are also the following links, included in the my-app.html
, to simulate tenant switch/selection.
Note the a
tags with external
attribute. Note that when you switch to a tenant, the links in the a
tags also now includes the tenant name; for example when we switch to tenant 'foo' the 'Home' link is changed to /foo/app/home
from /app/home
.
A buildTitle
function can be used to customize the default behavior of building the title. For this example, we assume that we have the configured the routes as follows:
With this route configuration in place, when we navigate to /home
, the default-built title will be Home | Aurelia
. We can use the following buildTitle
function that will cause the title to be Aurelia - Home
when users navigate to /
or /home
route.
Check out the following live example. You might need to open the demo in a new tab to observe the title changes.
Translating the title
When localizing your app, you would also like to translate the title. Note that the router does not facilitate the translation by itself. However, there are enough hooks that can be leveraged to translate the title. To this end, we would use the data
property in the route configuration to store the i18n key.
As data
is an object of type Record<string, unknown>
, you are free to chose the property names inside the data
object. Here we are using the i18n
property to store the i18n key for individual routes.
In the next step we make use of the buildTitle
customization as well as a AppTask
hook to subscribe to the locale change event.
This customization in conjunction with the previously shown routing configuration will cause the title to be Aurelia - Startseite
when user is navigated to /
or /home
route and the current locale is de
. Here we are assuming that the i18n resource for the de
locale contains the following.
The following example demonstrate the title translation.
href
custom attribute using useHref
By default, the router will allow you to use both href
as well as load
for specifying routes. Where this can get you into trouble is external links, mailto:
links and other types of links that do not route. A simple example looks like this:
This seemingly innocent and common scenario by default will trigger the router and will cause an error.
You have two options when it comes to working with external links. You can specify the link as external using the external
attribute.
Or, you can set useHref
to false
(default is true
) and only ever use the load
attribute for routes.
Using the historyStrategy
configuration option it can be instructed, how the router-lite should interact with the browser history object. This configuration option can take the following values: push
, replace
, and none
.
push
This is the default strategy. In this mode, the router-lite will interact with Browser history to push
a new navigation state each time a new navigation is performed. This enables the end users to use the back and forward buttons of the browser to navigate back and forth in an application using the router-lite.
Check out the following example to see this in action.
The main configuration can be found in the main.ts
.
To demonstrate the push
behavior, there is a small piece of code in the my-app.ts
that listens to router events to create informative text (the history
property in the class) from the browser history object that is used in the view to display the information.
As you click the Home
and About
links in the example, you can see that the new states are being pushed to the history, and thereby increasing the length of the history.
replace
This can be used to replace the current state in the history. Check out the following example to see this in action. Note that the following example is identical with the previous example, with the difference of using the replace
-value as the history strategy.
As you interact with this example, you can see that new states are replacing old states, and therefore, unlike the previous example, you don't observe any change in the length of the history.
none
Use this if you don't want the router-lite to interact with the history at all. Check out the following example to see this in action. Note that the following example is identical with the previous example, with the difference of using the none
-value as the history strategy.
As you interact with this example, you can see that there is absolutely no change in the history information, indicating non-interaction with the history object.
You can use the navigation options to override the configured history strategy for individual routing instructions.
Using the activeClass
option you can add a class name to the router configuration. This class name is used by the load
custom attribute when the associated instruction is active. The default value for this option is null
, which also means that the load
custom attribute won't add any class proactively. Note that the router-lite does not define any CSS class out-of-the-box. If you want to use this feature, make sure that you defines the class as well in your stylesheet.
Beyond setting up routes, hash/push mode, or titles, you can optionally observe the active route and track query parameters. One way is to inject ICurrentRoute
in any of your components. Another is to watch router events:
This can help debug or log your router's runtime state. See the ICurrentRoute docs for an example usage.
Learn about how to subscribe to and handle router events.
The router emits the following events.
au:router:navigation-start
: Emitted by router before executing a routing instruction; or in other words, before performing navigation.
au:router:navigation-end
: Emitted when the navigation is completed successfully.
au:router:navigation-cancel
: Emitted when the navigation is cancelled via a non-true
return value from canLoad
or canUnload
lifecycle hooks.
au:router:navigation-error
: Emitted when the navigation is erred.
To this end, inject the IRouterEvents
and use the IRouterEvents#subscribe
.
Note that the event-data for every event has a different type. When you are using TypeScript, using IRouterEvents
correctly types the event-data to the corresponding event type and naturally provides you with intellisense. This type information won't be available if you subscribe to the events using the event aggregator.
The following example demonstrates the usage of router events, where the root component displays a spinner at the start of navigation, and removes it when the navigation ends.
This is shown in action below.
Sometimes, you only need the information about the current route. In that case, you can surely subscribe to the au:router:navigation-end
event and get the current route from the event data. For the ease of use, there is a ICurrentRoute
exposed from router-lite that does exactly the same. You can directly inject it to your class to use it and avoid the boilerplate code altogether. Below is an example of how you can use it.
Learn about the different routing hooks and how to leverage those in terms of dis/allow loading or unloading as well as performing setup and teardown of a view.
Inside your routable components which implement the IRouteViewModel
interface, there are certain methods that are called at different points of the routing lifecycle. These lifecycle hooks allow you to run code inside of your components such as fetch data or change the UI itself.
If you are working with components you are rendering, implementing IRouteViewModel
will ensure that your code editor provides you with intellisense to make working with these lifecycle hooks in the appropriate way a lot easier.
Using the canLoad
and canUnload
hooks you can determine whether to allow or disallow navigation to and from a route respectively. The loading
and unloading
hooks are meant to be used for performing setup and clean up activities respectively for a view. Note that all of these hooks can return a promise, which will be awaited by the router-lite pipeline. These hooks are discussed in details in the following section.
canLoad
The canLoad
method is called upon attempting to load the component. It allows you to determine if the component should be loaded or not. If your component relies on some precondition being fulfilled before being allowed to render, this is the method you would use.
To disallow loading the component you can return a boolean
false
. You can also return a navigation instruction to navigate the user to a different view. These are discussed in the following sections.
The following example shows that a parameterized route, such as /c1/:id?
, can only be loaded if the value of id
is an even number. Note that the value of the id
parameter can be grabbed from the the first argument (params
) to the canLoad
method.
You can also see this example in action below.
canLoad
You can also see this example in action below.
Note that you can also choose to return a sibling navigation instructions. This can be done by returning an array of navigation instructions.
You can also see the example in action below.
Apart from accessing the route parameter, the query and the fragment associated with the URL can also be accessed inside the canLoad
hook. To this end, you can use the second argument (next
) to this method.
The following example shows that id
query parameter is checked whether that is an even number or not. If that condition does not hold, then user is redirected to a different view with the query and fragment.
You can also see the example in action below.
loading
The loading
method is called when your component is navigated to. If your route has any parameters supplied, they will be provided to the loading
method as an object with one or more parameters as the first argument.
In many ways, the loading
method is the same as canLoad
with the exception that loading
cannot prevent the component from loading. Where canLoad
can be used to redirect users away from the component, the loading
method cannot.
This lifecycle hook can be utilized to perform setup; for example, fetching data from backend API etc.
All of the above code examples for canLoad
can be used with load
and will work the same with the exception of being able to return true
or false
boolean values to prevent the component being loaded.
One of the examples is refactored using loading
hook that is shown below.
Following is an additional example, that shows that you can use the next.title
property to dynamically set the route title from the loading
hook.
canUnload
The canUnload
method is called when a user attempts to leave a routed view. The first argument (next
) of this hook is a RouteNode
which provides information about the next route.
This hook is like the canLoad
method but inverse. You can return a boolean false
from this method, to disallow the router-lite to navigate away from the current component.
The following example shows that before navigating away, the user is shown a confirmation prompt. If the user agrees to navigate way, then the navigation is performed. The navigation is cancelled, if the user does not confirm.
You can see this example in action below.
unloading
The unloading
hook is called when the user navigates away from the current component. The first argument (next
) of this hook is a RouteNode
which provides information about the next route.
This hook is like the loading
method but inverse.
The following example shows that a unloading
hook logs the event of unloading the component.
This can also be seen in the live example below.
For completeness it needs to be noted that the canLoad
hook is invoked before loading
and canUnload
hook is invoked before unloading
. In the context of swapping two views/components it is as follows.
canUnload
hook (when present) of the current component is invoked.
canLoad
hook (when present) of the next component (assuming that the canUnload
returned true
) is invoked.
unloading
hook (when present) of the current component is invoked.
loading
hook (when present) of the current component is invoked.
Note that the last 2 steps may run in parallel, if the hooks are asynchronous.
In this case, the component lifecycle hooks are invoked in the following order.
component-two attached
.
component-one attached
.
app-root attached
.
This is also the same for the router-lite, except for the "application root" component. Tweaking the example above slightly, let us assume that we have the following constellation of components.
In this case, the component lifecycle hooks are invoked in the following order.
app-root attached
.
component-two attached
.
component-one attached
.
routed-view attached
.
Within lifecycle hooks like canLoad
, loading
, etc., you can also inspect the RouteNode
:
If you prefer, you can also inject ICurrentRoute
for a global view of the route, query, and title. Combine these approaches as you see fit for your canLoad
, loading
, etc.
Create a navigation menu using navigation model in Router-Lite.
The navigation model can be thought of as view-friendly version of the configured routes. It provides similar information as of the configured routes with some additional data to it. This is typically useful when you want to create navigation menu from the already registered/configured routes in the router, without necessarily duplicating the data. The information takes the following shape.
This section provides example of how to use navigation model while discussing different aspects of it.
The following example shows how to create a navigation menu using the info from the navigation model.
In this example, we are using a custom element named nav-bar
. In the custom element we inject an instance of IRouteContext
and we grab the navigation model from the routing context.
Then the information from the model is used in the view to create the navigation menu.
It additionally shows that from the NavBar#binding
, INavigationModel#resolve()
is awaited. This is recommended, when dealing with async route configuration. This allows all the promises to be resolved and thereafter building the navigation information correctly.
isActive
propertyThe isActive
property is true
when this route is currently active (loaded), and otherwise it is false
. A typical use-case for this property is to apply or remove the "active" style to the links, depending on if the link is active or not. You can see this in the following example where a new .active
class is added and that is bound to the isActive
property.
You can see this in action below.
By default, all configured routes are added to the navigation model. However, there might be routes which is desired to be excluded from the navigation model; for example: a fallback route for un-configured routes. To this end, a route can be configured with nav: false
to instruct the router not to included it in the navigation model.
You see this in action in the example below.
ICurrentRoute
with the navigation modelWhile the navigation model provides information about all configured routes, sometimes you also need the exact active route or query parameters. The ICurrentRoute
object can be injected in tandem with your navigation model:
Using both navModel
and currentRoute
, you can dynamically highlight the active route in your navigation menu, differentiate query-only changes, and so on.
You can use the lifecycle hooks ( and ) to intercept different stages of the navigation when you are working with the routed components directly. However, if you want to tap into different navigation phases from a non-routed component, such as standalone service or a simple custom element, then you need to leverage router events. This section discusses that.
au:router:location-change
: Emitted when the browser location is changed via the and events.
The events can be subscribed to using the . However, there is another type-safe alternative to that.
In case you are looking for the global/shared routing hooks, there is a separate dedicated for that.
Not only can we allow or disallow the component to be loaded, but we can also redirect. The simplest way is to return a string path from canLoad
. In the following example, we re-write the , but instead of returning false
, we return a path, where the user will be redirected.
If you prefer a more then you can also do so. Following is the same example using route-id and parameters object.
The component are invoked bottom-up. As an example, let us assume that we have the following constellation of components.
Note that the application root is attached before any other components are attached. This happens because the router-lite starts loading the first route only after the app-root, and thereby the viewport(s) it is hosting, are fully activated/attached. In order to load a route, the router needs registered viewports. The registration process of a viewport only happens during the attaching
phase of a viewport. More details on this topic, can be found in this .
Note that apart from , all other properties of the route object are same as the corresponding configured route.
If you are not creating a menu using the navigation model, you can also deactivate the navigation model by setting false
to the useNavigationModel
. Doing so, will set the IRouteContext#navigationModel
to null
and skip further processing.
How to implement router "guards" into your applications to protect routes from direct access.
Router hooks are pieces of code that can be invoked at the different stages of routing lifecycle. In that sense, these hooks are similar to the lifecycle hooks of the routed view models. The difference is that these hooks are shared among multiple routed view models. Therefore, even though the hook signatures are similar to that of the lifecycle hooks, these hooks are supplied with an additional argument that is the view model instance.
Shared lifecycle hook logic can be defined by implementing one of the router lifecycle hooks (canLoad
, loading
etc.) on a class with the @lifecycleHooks()
decorator. This hook will be invoked for each component where this class is available as a dependency.
While the router hooks are indeed independent of the components you are routing to, the functions are basically the same as you would use inside of an ordinary component.
This is the contract for ordinary route lifecycle hooks for components:
And the following is the contract for shared lifecycle hooks.
The only difference is the addition of the first viewModel
parameter. This comes in handy when you need the component instance since the this
keyword won't give you access to the component instance like it would in ordinary instance hooks.
Before starting with the involved details of the shared/global lifecycle hooks, let us first create an example lifecycle hook. To this end, we consider the typical use-case of authorization; that is restricting certain routes to users with certain permission claims.
For this example, we will create two lifecycle hooks; one for authentication and another is for authorization. However, before directly dive into that, let us briefly visit, how the routes are configured.
Note that the data
property of the route configuration option is used here to define the routes' permission claim. This is used by the auth hooks later to determine whether to allow or disallow the navigation. With that we are now ready to discuss the hooks.
The first hook will check if the current route is protected by a claim and there is a currently logged in user. When there is no logged in user, it performs a redirect to login page. This is shown below.
The second hook will check if the current user has the permission claims to access the route. Where the user does not satisfies the claims requirements the user is redirected to a forbidden page. This is shown below.
Lastly, we need to register these two hooks to the DI container to bring those into action.
Note that the authentication hook is registered before the authorization hook. This ensures that the authentication hook is invoked before than the authorization hook which is also semantically sensible.
And that's the crux of it. You can see this example in action below.
Note that even though in the example we limit the the hooks to only canLoad
method, more than one lifecycle methods/hooks can also be leveraged in a shared lifecycle hook (a class decorated by the @lifecycleHooks()
decorator).
The lifecycle hooks can be registered either globally (as it is done in the previous example or as local dependencies.
The globally registered lifecycle hooks are invoked for every components. Thus, it is recommended to use those sparsely. On the other hand, when a hook is registered as a dependency of a particular component, it is invoked only for that one component.
This is shown in the example below, where there are two globally registered hooks, which are invoked for every components.
Note that the globally registered hooks in the example above do nothing significant other than logging the invocations. This is shown below.
The log entries are then enumerated on the view. The following is one such example of log entries.
You may get a different log depending on your test run. However, it can still be clearly observed that both hook1
and hook2
are invoked for every components. Depending on your use-case, that might not be optimal.
To achieve a granular control on the lifecycle hooks, you can register the hooks as the dependencies
for individual routed view models. This ensures that the lifecycle hooks are invoked only for the components where those are registered as dependencies. This shown in the example below where there are three hooks, and one component has two hooks registered as dependencies
and another component has only hook registered.
When ChildOne
or ChildTwo
is loaded or unloaded you can observe that only Hook2
is invoked for ChildTwo
, whereas both Hook1
and Hook2
are invoked for ChildOne
. Below is an example log from one of such test runs.
You can see the example in action below.
You can of course choose to use both kind of registrations. The following example shows that Hook3
is registered globally and therefore is invoked for every components whereas Hook1
is only invoked for ChildOne
and Hook2
is only invoked for ChildTwo
.
When using multiple lifecycle hooks, if any hook returns a non-true
value (either a false
or a navigation instruction) from canLoad
or canUnload
, it preempts invocation of the other hooks in the routing pipeline.
This is shown in the following example. The example shows that there are two hooks, namely hook1
and hook2
. hook1
return false
if the path c1
is navigated with a non-number and non-even number; for example it denies navigation to c1/43
but allows c1/42
.
You can see the example in action below.
If you run the example and try clicking the links, you can observe that once hook1
returns false
, hook2
is not invoked. One such example log is shown below.
The thumb rule is that the hooks are invoked in the order they are registered. That is if some Hook1
is registered before Hook2
in DI then Hook1
will be invoked before the Hook2
. You can see this in the example of globally registered hooks.
That is also true, when registering hooks as one of the dependencies
for a custom element. You can see this in the example of hooks as dependencies.
When using both globally registered hooks as well as local dependencies, the global hooks are invoked before the locally registered hooks. You can see this in action in this example.
Lastly, the shared lifecycle hooks are invoked before the instance lifecycle hooks.
Here is a small snippet added to the canLoad
or loading
hook, demonstrating how to read query parameters. The next
argument contains the queryParams
which can be read directly:
You can also do similar reading in loading
, canUnload
, etc. This approach can be combined with injecting ICurrentRoute
if your logic is broader than a single hook.
Learn to navigate from one view to another using the Router-Lite. Also learn about routing context.
This section details the various ways that you can use to navigate from one part of your application to another part. For performing navigation the router-lite offers couple of alternatives, namely the href
and the load
custom attributes, and the IRouter#load
method.
href
custom attributeYou can use the href
custom attribute with an a
(anchor) tag, with a string value. When the users click this link, the router-lite performs the navigation. This can be seen in action in the live example below.
The example shows that there are two configured routes, home
and about
, and in markup there are two anchor tags that points to these routes. Clicking those links the users can navigate to the desired components, as it is expected.
You can also use the parameterized routes with the href
attribute, exactly the same way. To this end, you need to put the parameter value in the path itself and use the parameterized path in the href
attribute. This is shown in the example below.
The example shows two configured routes; one with an optional parameter. The markup has three anchor tags as follows:
The last href
attribute is an example of a parameterized route.
Note that the example set a route id that is different than the defined path. These route-ids are later used in the markup as the values for the href
attributes.
The following live example, demonstrates that.
The example shows the following variations.
Note that using the viewport name in the routing instruction is optional and when omitted, the router uses the first available viewport.
The navigation using href
attribute always happens in the current routing context; that is, the routing instruction will be successful if and only the route is configured in the current routing parent. This is shown in the example below.
In the example, the root component has two child-routes (c1
, c2
) and every child component in turn has 2 child-routes (gc11
, and gc12
and gc21
, and gc22
respectively) of their own. In this case, any href
pointing to any of the immediate child-routes (and thus configured in the current routing parent) works as expected. However, when an href
, like below (refer child1.ts
), is used to navigate from one child component to another child component, it does not work.
In such cases, the router-lite offers the following syntax to make such navigation possible.
That is, you can use ../
prefix to instruct the router to point to the parent routing context. The prefix can also be used multiple times to point to any ancestor routing context. Naturally, this does not go beyond the root routing context.
href
custom attributeTo disable/bypass the default handling of router-lite for any particular href
attribute, you can avail couple of different ways as per your need and convenience.
Using external
or data-external
attribute on the a
tag.
Using a non-null value for the target
, other than the current window name, or _self
.
Other than that, when clicking the link if either of the alt
, ctrl
, shift
, meta
key is pressed, the router-lite ignores the routing instruction and the default handling of clicking a link takes place.
Following example demonstrate these options.
load
custom attributeTo support structured way to constructing URL the router-lite offers another alternative namely the load
attribute. This custom attribute accepts structured routing instructions as well as string-instructions, just like the href
attribute. Before starting the discussion on the features supported exclusively by the load
attribute, let us quickly review the following example of using string-instructions with the load
attribute.
The example shows various instances of load
attribute with various string instructions.
The following sections discuss the various other ways routing instruction can be used with the load
attribute.
params
Using the bindable params
property in the load
custom attribute, you can bind the parameters for a parameterized route. The complete URL is then constructed from the given route and the parameters. Following is an example where the route-id is used with bound parameters.
The example above configures a route as follows. The route-id is then used in the markup with the bound params
, as shown in the example below.
An important thing to note here is how the URL paths are constructed for each URL. Based on the given set of parameters, a path is selected from the configured set of paths for the route, that maximizes the number of matched parameters at the same time meeting the parameter constraints.
For example, the third instance (params: {p1: 4, p3: 5}
) creates the path /c2/4/foo/?p3=5
(instance of 'c2/:p1/foo/:p2?'
path) even though there is a path with :p3
configured. This happens because the bound parameters-object is missing the p2
required parameter in the path pattern 'c2/:p1/foo/:p2/bar/:p3'
. Therefore, it constructs the path using the pattern 'c2/:p1/foo/:p2?'
instead.
In other case, the fourth instance provides a value for p2
as well as a value for p3
that results in the construction of path /c2/6/foo/7/bar/8
(instance of 'c2/:p1/foo/:p2/bar/:p3'
). This case also demonstrates the aspect of "maximization of parameter matching" while path construction.
One last point to note here is that when un-configured parameters are included in the params
object, those are converted into query string.
route
You can see this in action below.
However, this default behavior can be changed by binding the context
property of the load
custom attribute explicitly. To this end, you need to bind the instance of IRouteContext
in which you want to perform the navigation. The most straightforward way to select a parent routing context is to use the parent
property of the IRouteContext
. The current IRouteContext
can be injected using the resolve(IRouteContext)
. Then one can use context.parent
, context.parent?.parent
etc. to select an ancestor context.
Such ancestor context can then be used to bind the context
property of the load
attribute as follows.
The following live example demonstrate this behavior.
Note that even though the ChildOne
defines a route with r2
route-id, specifying the context
explicitly, instructs the router-lite to look for a route with r2
route-id in the parent routing context.
Using the IRouteContext#parent
path to select the root routing context is somewhat cumbersome when you intend to target the root routing context. For convenience, the router-lite supports binding null
to the context
property which instructs the router to perform the navigation in the root routing context.
This is shown in the following example.
When the route context selection involves only ancestor context, then the ../
prefix can be used when using string instruction. This also works when using the route-id. The following code snippets shows, how the previous example can be written using the ../
prefix.
active
statusWhen using the load
attribute, you can also leverage the bindable active
property which is true
whenever the associated route, bound to the href
is active. In the following example, when a link in clicked and thereby the route is activated, the active*
properties are bound from the views to true
and thereby applying the .active
-CSS-class on the a
tags.
This can also be seen in the live example below.
Along with the custom attributes on the markup-side, the router-lite also offers the IRouter#load
method that can be used to perform navigation, with the complete capabilities of the JavaScript at your disposal. To this end, you have to first inject the router into your component. This can be done by using the IRouter
decorator on your component constructor method as shown in the example below.
Now you are ready to use the load
method, with many supported overloads at your disposal. These are outlined below.
The easiest way to use the load
method is to use the paths directly.
With respect to that, this method supports the string instructions supported by the href
and the load
attribute. This is also shown in the example below.
An array of paths (string) can be used to load components into sibling viewports. The paths can be parameterized or not non-parameterized.
This is shown in the example below.
The load
method also support non-string routing instruction.
Using custom elements
You can use the custom element classes directly for which the routes have been configured. Multiple custom element classes can be used in an array to target sibling viewports.
This can be seen in action in the live example below.
Using custom element definitions
You can use the custom element definitions for which routes have been configured. Multiple definitions can be used in an array to target sibling viewports.
This can be seen in action in the live example below.
Using a function to return the view-model class
This can be seen in action in the live example below.
Using import()
This can be seen in action in the live example below.
Note that because invoking the import()
function returns a promise, you can also use a promise directly with the load
function.
Using a viewport instruction
Any kind of routing instruction used for the load
method is converted to a viewport instruction tree. Therefore, you can also use a (partial) viewport instruction directly with the load
method. This offers maximum flexibility in terms of configuration, such as routing parameters, viewports, children etc. Following are few examples, how the viewport instruction API can be used.
This can be seen in the example below.
Along with using the routing instructions, the load
method allows you to specify different navigation options on a per-use basis. One of those, the context
, you have already seen in the examples in the previous sections. This section describes other available options.
title
The title
property allows you to modify the title as you navigate to your route. This looks like as follows.
Note that defining the title
like this, overrides the title defined via the route configuration. This can also be seen in the action below where a random title is generated every time.
titleSeparator
This can also be seen in the action below where a random title separator is selected every time.
queryParams
This option lets you specify an object to be serialized to a query string. This can be used as follows.
This can be seen in the live example below.
fragment
Like the queryParams
, using the fragment
option, you can specify the hash fragment for the new URL. This can be used as follows.
This can be seen in the live example below.
context
As by default, the load
method performs the navigation relative to root context, when navigating to child routes, the context needs to be specified. This navigation option has also already been used in various examples previously. Various types of values can be used for the context
.
The easiest is to use the custom element view model instance. If you are reading this documentation sequentially, then you already noticed this. An example looks like as follows.
Here is one of the previous example. Take a look at the child1.ts
or child2.ts
that demonstrates this.
You can also use an instance of IRouteContext
directly. One way to grab the instance of IRouteContext
is to get it inject via constructor
using resolve(IRouteContext)
. An example looks like as follows.
You can see this in action below.
Using a custom element controller instance is also supported to be used as a value for the context
option. An example looks as follows.
You can see this in action below.
And lastly, you can use the HTML element as context. Following is live example of this.
historyStrategy
After this, if we issue the following instruction,
then performing a history.back()
should load the c1
route, as the state for c2
is replaced.
transitionPlan
This can be seen in action below.
For completeness it needs to be briefly discussed that apart from the explicit navigation instruction, there can be need to redirect the user to a different route or handle unknown routes gracefully. Other sections of the router-lite documentation discusses these topics in detail. Hence these topics aren't repeated here. Please refer to the linked documentations for more details.
Sometimes, when navigating, you'll want to compare the route you're navigating to with the current one. This can help skip redundant navigations, read query parameters, or handle special cases. You can inject ICurrentRoute
from @aurelia/router-lite
and compare it:
Learn about viewports in Router-Lite and how to configure hierarchical routing.
To understand it better, let us consider an example. We want to show a list of products, as links, and when a product link is clicked, we display the details of the product.
To this end we want a viewport on the root component which is used to host the list component. The list component itself houses anther viewport, or more accurately a child viewport, which is then used to host the product details.
The route configuration for the root component consists of a single root for the list component and it also houses a single viewport. The Products
component defines its own child route configuration and houses a child viewport. This is shown below.
The IProductService
injected to the Products
is just some service used to populate the product list and has very little relevance to the discussion related to the router. And the extra style is added to display the list and the details side by side. And that's pretty much all you need for a simple hierarchical routing configuration. You can see this in action below.
If you open the example in a new tab, you can see how the URL paths are constructed. For example, when you click a product link, the URL is /42/details
or /products/42/details
. This also means that when you try to navigate to that URL directly, the product details will be loaded from the start. It essentially creates shareable URLs.
Two viewports are called siblings if they are under one parent routing hierarchy. Let us recreate the previous example but using sibling viewports this time.
To this end, we want to viewports, housed side-by-side on the root component. We show the products' list on one viewport and the details of the selected product on the other one.
To this end, let us start with the routing configuration on the root component.
Two routes, one for the list another for the details, are configured on the route. Next we need 2 viewports to on the root component. Let us get that done.
Even though we are not yet done, you can check out our work so far in the live example below.
If you run the example, you can immediately see a "problem" that both the viewports are loading the products' list. Although it is not an error per se, with natural use-case in mind, you probably like to avoid that. Let us fix this "problem" first.
However, we need a way to prevent this duplication. To this end, we can bind null
to the default
attribute of a viewport, which instructs the router-lite that this particular viewport should be left out when it is empty (that is no component is targeted to be loaded in this viewport).
You can see in the live example below that this fixes the duplication issue.
If you open the example in a new tab, you can see how the URL paths are constructed. For example, when you click a product link, the URL is /details/42@details+products@list
.
In the following example, we have the main
viewport for our main content and then another viewport called sidebar
for our sidebar content.
The names can be used to instruct the router-lite to load a specific component to a specific named viewport. To this end the path syntax is as follows:
The live example below shows this.
Note the load
attributes in the anchor
elements.
In the example, clicking the first anchor loads the products
component in the list
viewport and the details of the product with #{id
} into the details
viewport. The second anchor facilitates loading only the the details of the product with #{id
} into the details
viewport.
In this example, we are specifying that the Products
component needs to be loaded into the list
viewport and the Product
component need to be loaded into the details
viewport. You can also see this in the live example below.
Note the anchor
s in the example that show that the viewport names can now be dropped from the routing instructions.
used-by
In this example, we are instructing the router-lite to reserve the first viewport for ce-two
custom element and the reserve the second viewport for ce-one
custom element. You can see this in the live example below, by clicking the links and observing how the components are loaded into the reserved viewports.
You can reserve a viewport for more than one component. To this end, you can use comma-separated values for the used-by
attribute.
The live example below shows this in action
Note how clicking the links load the components also in the first viewport without any value for the used-by
.
Another path can be used to override the default value of the default
attribute. The following example shows four viewports with varied values for the default
attribute. Whereas the first viewport might be the usual viewport with empty path, the other three specifies different default values. These components are loaded into the viewport, by default when the application is started.
The example below shows this in action.
A function for the value of fallback
is also supported. An example looks like as follows, where the example redirects the user to NF1
component if an attempt to load a path /foo
is made. Every other attempt to load an unknown path is results loading the NF2
component.
You can also see this in action below.
Template controllers are a unique and powerful feature in Aurelia 2, providing a way to encapsulate and reuse templating logic directly within your views. They allow you to manipulate the rendering of a template based on custom logic, offering a higher level of abstraction compared to simple value bindings or even custom attributes.
At their core, template controllers are classes that interact with Aurelia's rendering engine to dynamically manage the display of content. They operate on the element they are declared on and can conditionally render, repeat, or otherwise modify the template based on your defined logic.
Aurelia 2 offers two primary ways to define a template controller, providing flexibility based on your preference and project style:
Using the @templateController()
Decorator: This approach uses a decorator to explicitly register your class as a template controller and assign it a name for use in templates.
Using Naming Conventions: Aurelia's convention-based approach automatically recognizes classes as template controllers if their names end with TemplateController
. The name used in templates will be the class name with TemplateController
removed, in lower camel case. This method often leads to cleaner and more intention-revealing code, as the class name itself clearly signals its purpose.
In this case, PermissionTemplateController
is automatically recognized and registered as a template controller with the name permission
. This convention clearly communicates the role of the class as a template controller.
Let's create a custom template controller for handling permissions, demonstrating the convention-based approach. This example will conditionally display content based on a user's role and a required role.
Let's break down the key parts of this PermissionTemplateController
:
Convention-based Registration: By naming the class PermissionTemplateController
, Aurelia automatically registers it as a template controller with the name permission
. This is the name you will use in your HTML templates. Alternatively, you could have used the decorator @templateController('permission')
on a class named Permission
, achieving the same result.
Dependency Injection:
IViewFactory
: Injected via resolve(IViewFactory)
. This service is responsible for creating views, which are instances of templates. We use it to create both the mainView
(for the original content) and the deniedView
(for the "Access Denied" message).
IRenderLocation
: Injected via resolve(IRenderLocation)
. This represents the precise location in the DOM where the template controller's content should be rendered. Aurelia automatically provides the IRenderLocation
that corresponds to the element where the template controller is used in the HTML.
IContainer
: Injected via resolve(IContainer)
. This is Aurelia's dependency injection container itself. We use it here to create a specialized ViewFactory
specifically for the deniedDefinition
.
$controller: ICustomElementController<this>
: Aurelia automatically injects the controller instance for this template controller. This controller provides access to the template controller's lifecycle and its internal state management within Aurelia's rendering system.
mainView: ISyntheticView
and deniedView: ISyntheticView
: These properties hold instances of ISyntheticView
. ISyntheticView
represents a lightweight, reusable view instance in Aurelia.
mainView
represents the original content within the element in your HTML where you apply the permission
template controller.
deniedView
represents the "Access Denied" message template, which is rendered when the permission check fails.
@bindable() userRole: string = '';
and @bindable() requiredRole: string = '';
: These are @bindable
properties. They define inputs to your template controller that you can bind to from your HTML. In this case, userRole
will be bound to the current user's role, and requiredRole
will be set to the role required to view the content.
deniedDefinition
: This static
property holds a CustomElementDefinition
. It defines a simple, anonymous custom element named denied-message
that is used to render the "Access Denied" message. Defining it directly within the template controller encapsulates this specific view logic.
Constructor:
this.mainView = this.viewFactory.create().setLocation(this.renderLocation);
: This line creates the mainView
. this.viewFactory.create()
gets a new view instance from the IViewFactory
. .setLocation(this.renderLocation)
tells Aurelia where in the DOM this mainView
should be rendered – at the IRenderLocation
associated with the element using the permission
template controller.
The code then creates a ViewFactory
specifically for the deniedDefinition
using new ViewFactory(this.container, PermissionTemplateController.deniedDefinition)
. This specialized factory is then used to create the deniedView
, and its location is also set to this.renderLocation
.
Lifecycle Hooks: attaching()
and detaching()
:
attaching()
: This lifecycle method is called by Aurelia when the template controller is being attached to the DOM. Inside, this.updateViews()
is called to determine and render the correct view based on initial property values.
detaching()
: This lifecycle method is called when the template controller is being detached from the DOM. It's crucial for cleanup. Here, it deactivates both mainView
and deniedView
using .deactivate()
. Deactivating views ensures that resources are released and lifecycle methods of any child view resources are correctly invoked.
userRoleChanged()
and requiredRoleChanged()
: These are property change handler methods. Aurelia automatically calls these methods whenever the value of the @bindable
properties userRole
or requiredRole
changes. Each method calls this.updateViews()
to re-evaluate the permission and update the view accordingly.
updateViews()
: This private method contains the core logic of the template controller:
if (!this.$controller.isActive) { return; }
: This is a safety check. It ensures that updateViews
only proceeds if the template controller is currently active in the view.
if (this.userRole === this.requiredRole)
: This is the permission check. It compares the userRole
and requiredRole
.
If the roles match (permission granted): this.deniedView.deactivate(...)
is called to hide the "Access Denied" message, and this.mainView.activate(...)
is called to display the original content. activate()
and deactivate()
are methods on ISyntheticView
that control the view's lifecycle (attaching/detaching from DOM, invoking lifecycle hooks).
If the roles do not match (permission denied): this.mainView.deactivate(...)
hides the original content, and this.deniedView.activate(...)
shows the "Access Denied" message.
Once you have created the PermissionTemplateController
, you can use it in your HTML templates. Because we used the convention or the decorator @templateController('permission')
, we use permission
as the attribute name in our HTML:
In this example:
permission="user-role.bind: user.role; required-role.bind: 'admin'"
: This is how you apply the permission
template controller to a div
element.
user-role.bind: user.role
: This binds the userRole
bindable property of the PermissionTemplateController
to the role
property of the user
object in your view model. Whenever user.role
changes in your view model, the userRoleChanged()
method in the template controller will be automatically invoked.
required-role.bind: 'admin'
: This binds the requiredRole
bindable property to the string literal 'admin'
. The template controller will use 'admin'
as the role required to view this section. Changes to 'admin'
(which is a constant here) won't trigger updates, but if you bound this to a view model property, changes would trigger requiredRoleChanged()
.
When Aurelia processes this HTML:
It encounters the permission
attribute on the div
element. Aurelia recognizes permission
as a registered template controller.
Aurelia creates an instance of PermissionTemplateController
and associates it with this div
.
The attaching()
lifecycle hook of PermissionTemplateController
is called. Inside attaching()
, this.updateViews()
is initially invoked.
updateViews()
checks the bound values of user.role
(from your view model) and the required-role: 'admin'
.
Based on the comparison, updateViews()
will either:
Activate the mainView
, rendering the <h2>VIP Area</h2>
and <p>Welcome to the VIP area...</p>
content within the div
, and deactivate the deniedView
.
Or, deactivate the mainView
and activate the deniedView
, rendering the "Access Denied" message instead of the original content.
The last div
in the example, which does not have the permission
attribute, is rendered normally without any template controller logic applied.
Template controllers are highly versatile and can be used to implement various templating behaviors, including:
Conditional Rendering: The built-in if.bind
and else.bind
template controllers are prime examples of conditional rendering. You can create custom template controllers for more complex conditional logic, like the permission
example we just built.
List Rendering/Repeating: The repeat.for
template controller is used for efficiently rendering lists of items. You could create custom template controllers for specialized list rendering scenarios, such as virtual scrolling or infinite scrolling.
Contextual Scoping: Template controllers like with.bind
can create new binding scopes, useful for working with nested data structures or isolating parts of your template. You could build template controllers to manage specific data contexts or apply transformations to the scope.
Lazy Loading: A template controller could be created to lazily load and render content only when it becomes visible in the viewport, improving initial load times and performance for content-heavy pages.
Custom Templating Logic: Any scenario where you need to programmatically control the rendering of a template based on custom logic is a good candidate for a template controller. This could involve complex data transformations, dynamic template selection, or integration with external services.
Reusability: Template controllers encapsulate templating logic into reusable components. Once created, you can apply a template controller across your application to enforce consistent behavior in templates.
Encapsulation: They keep templating logic contained within a dedicated class, separating it from your view model and making your templates cleaner and more declarative.
Improved Template Readability: By abstracting complex templating logic into template controllers, your HTML templates become more concise and easier to understand.
Enhanced Expressiveness: Template controllers extend the expressiveness of Aurelia's templating system, allowing you to create custom templating constructs tailored to your application's needs.
While template controllers, custom attributes, and custom elements are all view resources in Aurelia, they serve distinct purposes:
Template Controllers: Control the rendering of a template. They don't add new HTML elements or modify element attributes directly. Their primary focus is managing whether and how template content is rendered and its lifecycle.
Custom Attributes: Primarily modify the behavior or appearance of existing HTML elements. They are applied as attributes and typically interact with element properties, attributes, or styles. They do not control the rendering of the element's content in the way template controllers do.
Custom Elements: Define completely new HTML elements, encapsulating both template structure and behavior. They are used to create reusable UI components with their own encapsulated logic and markup.
Template controllers are specifically designed for manipulating the template rendering process itself, making them the ideal choice when you need to create reusable templating patterns and implement conditional or dynamic content display logic.
Aurelia allows you to configure the application startup in a couple of different ways. A quick setup where some defaults are assumed and a verbose setup where you can configure some of the framework-specific behaviors.
The quick startup approach is what most developers will choose.
To start an Aurelia application, create a new Aurelia()
object with a target host
and a root component
and call start()
.
In most instances, you will not use the verbose approach to starting your Aurelia applications. The verbose approach is more aimed at developers integrating Aurelia into existing web applications and views.
To make a custom element globally available to your application, pass the custom element constructor to the .register()
method on your Aurelia app.
If you have a package that exports all your custom elements, you can pass the entire package to the .register()
method on your Aurelia app.
src/components/index.ts:
src/main.ts:
Aurelia v2 employs a synchronous binding system, which immediately notifies changes as they occur. This approach provides great control and predictability over state changes. However, managing multiple state updates that must be processed together requires careful handling to ensure consistency.
Synchronous binding systems notify changes immediately, providing instant feedback and control. In contrast, asynchronous binding systems queue changes and notify them later, typically in the next microtask or tick, which can help avoid issues like state tearing but introduces other complexities like race conditions (if you worked with Aurelia 1, then you might be familiar with the need to use queueMicroTask
to work around this in Aurelia 1).
State tearing occurs when multiple state updates that should be processed together result in premature change notifications and recomputations. This can lead to inconsistent states and application errors. Aurelia v2’s synchronous binding system is particularly prone to this issue.
Consider the following example:
In this example, updating firstName
and lastName
simultaneously causes an error. This happens because the synchronous change propagation causes the computed property fullName
to be evaluated before both firstName
and lastName
have been updated.
Aurelia provides the batch
function to handle multiple state updates efficiently. The batch function groups state changes and defer change notifications until all updates within the batch are complete. This ensures that related states are updated together, maintaining consistency.
Here’s how to use the batch function to manage state updates:
By wrapping the state updates in a batch
function, change notifications for firstName
and lastName
are deferred until both updates are complete. This ensures that the fullName
computed property is evaluated with the latest values of firstName
and lastName
.
Consistency: Ensures that all related state changes are processed together, avoiding premature evaluations.
Predictability: Maintains the predictable nature of the synchronous binding system by controlling when notifications are sent.
Performance: Reduces unnecessary recomputations by grouping state changes.
Aurelia’s synchronous binding system provides immediate change notifications, offering great control over state updates. Using the batch
function, developers can efficiently manage multiple state updates, ensuring consistency and predictability in their applications. Proper use of batch enhances the robustness of Aurelia applications, making state management more reliable and efficient.
Learn how Router-Lite handles the re-entrance of the same component and how to override the default behavior.
The transition plan in router-lite is meant for deciding how to process a navigation instruction that intends to load the same component that is currently loaded/active, but with different parameters. As the router-lite uses a sensible default based on the user-voice, probably you never need to touch this area. However, it is still good to know how to change those defaults, whenever you are in need to do that (and we all know that such needs will arise from time to time).
replace
: This instructs the router to completely remove the current component and create a new one, behaving as if the component is changed. This is the default behavior if the parameters are changed.
invoke-lifecycles
: This instructs the router to call the lifecycle hooks (canUnload
, canLoad
, unloading
and loading
) of the component.
none
: Does nothing. This is the default behavior, when nothing is changed.
The child routes inherits the transitionPlan
from the parent.
Transition plans defined on the root are inherited by the children. The example below shows that the transitionPlan
on the root is configured to replace
and this transition plan is inherited by the child route configuration. This means that every time the link is clicked, the component is created new and the view reflects that as well.
See this example in action below.
You can use a function to dynamically select transition plan based on route nodes. The following example shows that, where for every components, apart from the root component, invoke-lifecycles
transition plan is selected.
The behavior can be validated by alternatively clicking the links multiple times and observing that the CeOne#id2
increases, whereas CeOne#id1
remains constant. This shows that every attempt to load the CeOne
only invokes the lifecycle hooks without re-instantiating the component every time. You can try out this example below.
The example above selects invoke-lifecycles
for the CeTwo
and replace
for everything else. When you alternatively click the links multiple times, you can see that CeOne
is re-instantiated every time whereas for CeTwo
only the lifecycles hooks are invoked and the instance is reused. You can see the example in action below.
Aurelia's dynamic composition enables you to render a view/view model pair—or just a view—dynamically at runtime. With <au-compose>
, you can:
Render any custom element by binding its definition to the component
property.
Render plain HTML templates.
Dynamically swap components based on user state or configuration.
Access lifecycle hooks (including an extra activate
method) on the composed view model.
To render a custom element using its component definition, bind the element to <au-compose>
:
In your view model, define the component with a custom element definition:
Info: When a custom element is used, all standard lifecycle events (including
activate
) will be invoked.
If you pass a string to the component
property, Aurelia treats it as the name of a custom element and performs a lookup. For example:
If my-input
is registered globally, the lookup will succeed; otherwise, an error is thrown if no matching element definition is found.
Dynamic composition isn’t limited to custom elements. You can compose a view only or combine a simple view model (plain object) with a view.
Render a simple HTML template:
Here, Aurelia processes the HTML string with its template compiler and renders it in place.
You can supply both a view (template) and a literal object as the component:
Note: When composing without a custom element as the view model, the composed component will by default use the parent scope—unless you override it using
scope-behavior
.
By default, composing a custom element creates a host element based on the element’s name (e.g. <my-input>
). For non-custom element compositions, Aurelia inserts a comment boundary:
Renders as:
To wrap the composed content in a real HTML element, use the tag
bindable property:
Renders as:
Bindings declared on <au-compose>
will be transferred to the new host element.
model.bind
The model
bindable lets you pass data into the composed view model. When the model
changes, the activate
method of the composed view model is called with the new value.
Or, pass an inline object:
In the composed component:
Dynamic composition is best suited for scenarios where standard custom element usage may be too rigid. Consider using <au-compose>
when:
Sometimes you need a reference to the view model of the composed component. Use the component.ref
binding to capture it:
This works similarly to view-model.ref
in Aurelia 1, giving you programmatic access to the instance.
Bindings declared on <au-compose>
are automatically passed to the composed view model (if it is a custom element). For example:
Assuming the following component definition:
This works as if you had written in app.html
:
<compose>
Dynamic composition in Aurelia 2 differs from Aurelia 1. Key changes include:
Aurelia 1: view.bind
and view-model.bind
Aurelia 2: Use template.bind
and component.bind
Passing a string now only works for custom element names. A string passed to template
is interpreted as literal HTML.
By default, when composing a view only or a plain object, the parent scope is inherited. To disable this, set the scope-behavior
attribute:
All bindings on <au-compose>
are now transferred to the composed custom element’s view model. Thus, component.ref
now returns the view model, not the composer itself.
If you need to load a module dynamically for the view, use a value converter:
This value converter fetches the template from a URL and returns its text.
Learn how to use Aurelia with existing HTML (inside of other frameworks and libraries), hydrating server-generated HTML or running multiple instances of Aurelia in your application.
Enhancement in Aurelia allows for integrating Aurelia’s capabilities with existing DOM elements or dynamically inserted HTML content. This feature is particularly useful in scenarios where the application is not entirely built with Aurelia, such as when integrating with server-rendered pages or dynamically generated content.
The basic usage of enhance
is straightforward:
Key Points to Understand:
Anonymous Custom Element Hydration: The enhancement treats the target node as an anonymous custom element, allowing Aurelia to apply its behavior to the existing DOM structure.
Component Flexibility: The component parameter in enhance can be a custom element class, a class instance, or an object literal. If a class is provided, it's instantiated by Aurelia's dependency injection container, which can be either provided or automatically created.
Host Element: The host is typically an existing DOM node that is not yet under Aurelia's control. It's crucial to note that enhance ' neither detaches nor attaches the
hostto the DOM. Existing event handlers on the
host` or its descendants remain unaffected.
Controller Deactivation: an enhance
call results in an application root that requires manual deactivation or integration into an existing controller hierarchy for automatic framework management.
Example of deactivating an application root:
Enhance can be particularly useful during application startup, especially when integrating Aurelia into an existing web page or alongside other frameworks.
Let's create an index.html
file containing some HTML markup you would encounter if you generated an Aurelia application using npx makes aurelia
or other means.
Pay attention to the contents of the <body>
as there is a container called app
as well as some HTML tags <my-app>
Now, let's enhance our application by using the enhance
API inside of main.ts
Or if your component has one of its lifecycle return a promise:
We first wrap our async code in an anonymous async function. This allows us to catch errors and throw them to the console if enhancement fails, but take note of the enhance
call itself. We supply the host element (in our case, it's a DIV with an ID of app
as the host).
Above our enhance
call, we register our main component, MyApp
, the initial component our application will render.
Pay attention to what you are enhancing. Please make sure you are enhancing the container and not the component itself. It's an easy mistake, and we have seen some developers get caught on.
This approach will work for existing applications as well. Say you have a WordPress website and want to create an Aurelia application on a specific page. You could create a container and pass the element to the host
on the enhance
call.
While using the enhance
during registration, the enhance
API is convenient for situations where you want to control how an Aurelia application is enhanced. There are times when you want to enhance HTML programmatically from within components. This may be HTML loaded from the server or elements created on the fly.
In the following example, we query our markup for an item-list
element and insert some Aurelia-specific markup into it (a repeater).
As you can see, we are dynamically injecting some HTML into an existing element. Because this is being done after Aurelia has already compiled the view, it would not work. This is why we must call enhance
to tell Aurelia to parse our inserted HTML.
You can use this approach to enhance HTML inserted into the page after initial compilation, which is perfect for server-generated code.
While configuring routes, an can be set explicitly. This id
can also be used with the href
attribute. This is shown in the example below.
Note that using the route-id of a parameterized route with the href
attribute might be limiting or in some cases non-operational as with href
attribute there is no way to specify the parameters for the route separately. This case is handled by the .
You can target and/or viewports. To this end, you can use the following syntax.
Contextually, note that the also demonstrates the behavior of navigating in the current context. In that example, the root component uses r1
, and r2
as route identifiers, which are the same identifiers used in the children to identify their respective child-routes. The route-ids are used in the markup with the href
attributes. Despite being the same route-ids, the navigation works because unless specified otherwise, the routing instructions are constructed under the current routing context.
By default the router-lite enables usage of the href
custom attribute, as that ensures that the router-lite handles the routing instructions by default. There might be cases, where you want to avoid that. If you want to globally deactivate the usage of href
, then you can customize the router configuration by setting false
to the .
Although the usage of href
is the most natural choice, it has some limitations. Firstly, it allows navigating in the . However, a bigger limitation might be that the href
allows usage of only string values. This might be bit sub-optimal when the routes have parameters, as in that case you need to know the order of the parameterized and static segments etc. to correctly compose the string path. In case the order of those segments are changed, it may cause undesired or unexpected results if your application.
The bindable route
property in the load
attribute supports binding a class instead of route-id. The following example demonstrates using the classes (child1
, child2
) directly, instead of using the route-id.
Just like the href
attribute, the load
attribute also supports navigating in the by default. The following example shows this where the root component has two child-routes with r1
and r2
route-ids and the child-components in turn defines their own child-routes using the same route-ids. The load
attributes also use the route-ids as routing instruction. The routing works in this case, because the routes are searched in the same routing context.
Note that the also offers a .
The active
bindable can be used for other purposes, other than adding CSS classes to the element. However, if that's what you need mostly the active
property for, you may choose to configure the in the router configuration. When configured, the load
custom attribute will add that configured class to the element when the associated routing instruction is active.
There is a major important difference regarding the context selection in the IRouter#load
method and the href
and load
custom attributes. By default, the custom attributes performs the navigation in the current routing context (refer the and documentation). However, the load
method always use the root routing context to perform the navigation. This can be observed in the ChildOne
and ChildTwo
components where the load
method is used the following way to navigate from ChildOne
to ChildTwo
and vice versa. As the load
API uses the the root routing context by default, such routing instructions works. In comparison, note that with href
we needed to use the ..
prefix or with load
method we needed to set the context to null.
However, on the other hand, you need to specify the routing context, when you want to navigate inside the current routing context. The most obvious use case is when you issue routing instruction for the child-routes inside a parent component. This can also be observed in ChildOne
and ChildTwo
components where a specific context is used as part of the to navigate to the child routes.
Similar to , for load
you can use a function that returns a class as routing instruction. This looks like as follows.
Similar to , for load
you can use an import()
statement to import a module. This looks like as follows.
As the name suggests, this provides a configuration option to customize the separator for the . By default router-lite uses |
(pipe) as separator. For example if the root component defines a title 'Aurelia'
and has a route /home
with title Home
, then the resulting title would be Home | Aurelia
when navigating to the route /home
. Using this option, you can customize the separator.
Using this navigation option, you can override the . Let us consider the example where three routes c1
, c2
, and c3
are configured with the push
history strategy. Let us also assume that the following navigation instructions have already taken place.
Using this navigation option, you can override the per routing instruction basis. The following example demonstrates that even though the routes are configured with a specific transition plans, using the router API, the transition plans can be overridden.
Fallback using the
Fallback using the
The <au-viewport>
element, or commonly referred to as viewport (not to confuse with ), is the "outlet", where the router-lite attaches/loads the components. For a basic example of viewport, please refer the . Most of the examples in the preceding sections show the usage of only a single viewport. However, you can use multiple viewports with the sibling viewports and hierarchical routing. These are useful to create different routing layouts. In the subsequent sections, we first discuss about that. Later in this part of the documentation, we focus on the different configuration options available for viewports.
As seen in the , a component can define a set of children routes (using ). The child routes can also in turn define children routes of there own. Such route configuration are commonly known as hierarchical route configuration.
This is happening due to the default value of the of the <au-viewport>
that is set to ''
(empty string). This default value enables loading the component associated with the empty path without any additional configuration. This default behavior makes sense as the usage of a single viewport at every routing hierarchy might be prevalent.
We still need a way to load the product details on the second viewport. Note that till now, the two viewports cannot be referentially differentiated from one another; that is if you want to load a component specifically on the first or on the second viewport, there is no way to do this for now. To this end, we need to .
Although we name the viewports semantically, it is not necessary, and you are free to choose viewport names, as you like. Lastly, we need to use the in the Products
component to construct the URL, or more accurately the routing instruction correctly, such that the details of the product is loaded on the details
viewport.
Using the load
attribute we are instructing the router-lite to load the Product
(using the route-id details
) component, with the id
parameter of the route set to the id
of the current item
in the repeater, in the details
viewport. With the context.bind:null
, we are instructing the router-lite to perform this routing instruction on the root routing context (refer for the load
attribute for more details). Now, when someone clicks a product link the associated details are loaded in the details
viewport. You can see this in action below.
As seen in , viewports can be named. It is particularly useful when there are multiple present. Note that specifying a value for the name
attribute of viewport is optional, and the default value is simply 'default'
.
For more details about navigating and instructions for router-lite, please refer the .
By default, the routes/components are loaded into the first available viewport, when there is no viewport instruction is present. However, the routes can also be configured, such that a configured route is allowed to be loaded only in a certain viewport. This is useful when you know that a certain component needs to be loaded in a certain viewport, because in that case you can use the simple {path}
instruction instead of the more verbose alternative, the {path}@{viewport-name}
instruction. To this end, use the viewport
option of the .
The used-by
attribute on the au-viewport
component can be thought of as (almost) the parallel of the on route configuration. Using this property on a viewport, you can "reserve" a viewport for particular component(s). In other words, you are instructing the router that no other components apart from those specified can be loaded into a viewport with used-by
set.
Although the used-by
attribute feels like a markup alternative of the on route configuration, there is a subtle difference. Having the used-by
property on a particular viewport set to X
component, does not prevent a preceding viewport without any value for the used-by
property to load the X
component. This is shown in action in the example below.
When no route is loaded into a viewport, a 'default' route is loaded into the viewport. For every viewport, such defaults can be configured using the default
attribute. It is optional to specify a value for this attribute and the empty string (''
) is used as the default value for this property. This explains why the route with empty path (when exists) is loaded into a viewport without the default
attribute set, as seen in the .
Note that default
attribute can also be bound to null
, to instruct the router-lite not to load any component into ths viewport when no component is scheduled (either by explicit instruction of implicit availability check) to be loaded into the viewport. This is useful when you have more than one viewports and you want to load the empty path (assuming it is configured) in a particular viewport. In that case, you can bind null
to the default
attribute of the other viewport. To see examples of this, please refer to the section.
If a route cannot be recognized, a fallback route is looked for and loaded (when configured) into the viewport. Such fallback can be configured using the fallback
property of the . au-viewport
also offers a similar fallback
attribute, using which a fallback component can be configured for a particular viewport. The fallback
attribute is similar to its route configuration counterpart, with only one difference. The fallback
attribute in the au-viewport
, when configured, always takes precedence over the fallback
route configuration option. This is shown in the live example below.
Think of template controllers as building blocks for creating reusable templating patterns. They are distinct from in that they don't define new HTML elements. Instead, they control the rendering behavior of existing elements and their content. Common examples of built-in template controllers in Aurelia include if.bind
, repeat.for
, and with.bind
.
Transition plan can be configured using the transitionPlan
property in the . The allowed values are replace
, invoke-lifecycles
, none
or a function that returns one of these values.
When the transitionPlan
property in the is not configured, router-lite uses replace
when the parameters are changed and none
otherwise.
This can be interesting when dealing with , as you can select different transition plan for different siblings.
The showed how to start Aurelia for an empty root node. While that's the most frequent use case, there might be other scenarios where we would like to work with an existing DOM tree with Aurelia.
Scenario
Why Use Dynamic Composition
Dynamic Content Based on User Input or State
To render different views or components on the fly (e.g., dashboards, conditional widgets).
Rendering Unknown Components at Runtime
In plugin-based architectures, where components are not known at compile time.
Conditional Rendering of Multiple Views
Instead of complex if.bind
or switch statements, use a single dynamic composition point.
Complex Reusable Layouts
To inject varying content into consistent layouts, promoting reusability and DRY code.
Simplifying Component Interfaces
To encapsulate multiple parameters in a model object, avoiding overly complex interfaces.
Decoupling Component Logic
To separate the decision of what to render from the rendering logic itself.
Lazy Loading to Reduce Initial Load Time
To load components only when needed, improving performance in large, single-page applications.
Learn how to work with Aurelia's observable decorator to create reactive properties inside your component view models that have change callbacks.
Unlike the @watch decorator, the @observable
decorator allows you to decorate properties in a component and optionally call a change callback when the value changes. It works quite similarly to the @bindable
property.
By convention, the change handler is a method whose name is composed of the property_name and the literal value 'Changed'. For example, if you decorate the property color
with @observable
, you have to define a method named colorChanged()
to be the change handler.
This is what a basic observable would look like using conventions:
When the color
value is changed, the colorChanged
callback will be fired. The new changed value will be the first argument, and the existing one will be the second one.
If you prefer, you can also put the @observable
decorator on classes:
If you do not want to use the convention, you can define the callback name for the change handler by setting the callback
property of the @observable
decorator:
HTML elements are special objects that often require different observation strategies, and most of the time, listening to some specific event is the preferred way. For this reason, Aurelia encourages using events to observe HTML elements.
As an example, the value
property of an <input />
element should be observed by listening to the <input />
change events such as input
or change
on the element. Another example is the value
property of a <select />
element should be observed by listening to the change
event on it.
By default, the observation of HTML elements is done using a default node observer locator implementation. This default locator has a basic set of APIs that allows users to teach Aurelia how to observe HTML element observation effectively.
The following is the trimmed interface of the node observer locator, highlighting its capability to learn how to observe HTML elements:
useConfig
and useConfigGlobal
are two methods that can be used to teach the default node observer locator what events can be used to observe a property of a specific element or any element.
Using the nodeObserverLocator
API, we can tell Aurelia how to observe properties of HTML elements for changes. Under the hood, Aurelia already observes properties like values on form inputs, but it is good to understand how this functionality works, especially for custom elements and web components.
value
property of a <textarea />
element:In this example, the eventsConfig
argument has the value { events: ['input', 'change']}
.
length
of an <input />
element:In this example, eventsConfig
argument has the value { events: ['input']}
.
scrollTop
of all elements:In this example, eventsConfig
argument has the value { events: ['scroll']}
.
It should be the same as observing custom (HTML) elements and normal HTML elements. It is common for Web Components to have well-defined events associated with their custom properties, so observing them often means adding a few configuration lines.
An example of how to teach Aurelia to observe the value
property of a <my-input />
element, and <my-input />
dispatches valueChanged
event when its value has been changed:
The Observer Locator API allows you to watch properties in your components for changes without the need for using the @observable
decorator. In most cases, manual observation will not be required using this API, but it is there if you want it.
By default, an observer locator is used to create observers and subscribe to them for change notification.
A basic observer has the following interface:
The subscribe
method of an observer can be used to subscribe to the changes that it observes. This method takes a subscriber as its argument.
A basic subscriber has the following interface:
An observer of an object property can be retrieved using an observer locator.
An example of this is:
And to subscribe to changes emitted by this observer:
It's not always sufficient to observe a single property on an object, and it's sometimes more desirable to return a computed value from the source so that subscribers of an observer don't have to perform any logic dealing with the updated values. An example of this is the follow observation of firstName
and lastName
to notify full name:
Doing it the way above is cumber some as we need to setup 2 observers and 2 subscribers, also there's a typo risk. We can also use a getter to express a computed value, and then observe that getter to avoid having to do heavy setup work:
This is not always feasible since the obj could be from a 3rd party library, or some json data from server, and the risk of having a typo fullName
is still there.
Aurelia provides another API of creating observer to deal with this scenario, where it's more desirable to use a function/lambda expression to express the dependencies and computed value to notify the subscriber. To use this API, replace the 2nd parameter of getObserver
with a getter function to express the value:
Aurelia provides a higher-level API for simplifying some common tasks to handle a common reactivity intent in any application: run a function again when any of its dependencies have been changed.
This function is called an effect, and the dependencies are typically tracked when they are accessed (read) inside this effect function. The builtin @observable
decorator from Aurelia enables this track-on-read capability by default.
Aurelia provides a few ways to declare a dependency for an effect function. The most common one is the track "on read" of a reactive property.
In the following example:
The property coord
of a MouseTracker
instance will be turned into a reactive property and is also aware of effect function dependency tracking.
The effect APIs are provided via the default implementation of the interface IObservation
, which can be retrieved like one of the following examples:
Getting from a container directly:
Getting through injection:
Or
After getting the observation object, there are two APIs that can be used to created effects as described in the following sections:
Run effects describe a function to be called repeatedly whenever any dependency tracked inside it changes.
After getting an IObservation
instance, a run effect can be created via the method run
of it:
Note that the effect function will be run immediately.
By default, a effect is independent of any application lifecycle, which means it does not stop when the application that owns the observation
instance has stopped. To stop/destroy an effect, call the method stop()
on the effect object:
Sometimes it's desirable to cleanup an effect, i.e we only want to send a request to track the last mouse position within 100ms, not all the intermediate movements. Setting a timeout in effect body and removing the timeout in the cleanup function is a simple way to achieve this, like the following example:
Watch is a way to describe a getter based observation of an object. The below example demos how to create a watch effect:
Note that the effect function will be run immediately. If you do not want to run the callback immediately, pass an option immediate: false
as the 4th parameter:
By default, a watch effect is independent of any application lifecycle, which means it does not stop when the application that owns the observation
instance has stopped. To stop an effect, call the method stop()
on the effect object.
Instead of having getter as a watch expression, a string can also be used, like the following example:
The following section gives some examples of what it looks like when combining @observable
and run effect.
Now whenever the user moves the mouse around, a log will be added to the console with the coordinate of the mouse.
An element in two places at once.
There are situations that some elements of a custom element should be rendered at a different location within the document, usually at the bottom of a document body or even inside of another element entirely. Aurelia supports this intuitively with the portal
custom attribute.
While the location of the rendered element changes, it retains its current binding context. A great use for the portal
attribute is when you want to ensure an element is displayed in the proper stacking order without needing to use CSS hacks like z-index:9999
Using the portal
attribute without any configuration options will portal the element to beneath the document body (before the closing body tag).
If you want to choose where a portalled element is moved to, you can supply a CSS selector where it should be moved.
Target an element with an ID of somewhere:
Target an element by class:
Target an element by tagName:
The portal attribute can also reference other elements with a ref
attribute on them.
You can also target elements not using the ref
attribute too. A good example is a custom element. Maybe you want to portal a section of a child element to the parent element.
We can do things with the injected element instance, like access the parentElement or other non-standard scenarios you might encounter.
You could also do this with query calls such as querySelector
and so forth as well aliased to class properties.
By default, the portal
attribute will portal your elements before the closing tag of your target. By default using portal
without any configuration values will portal it just before the closing </body>
tag.
We can override this behavior using the position
property and the following values:
beforebegin
afterbegin
beforeend
(the default value)
afterend
In this example, our element will move to just after the opening body tag <body>
the other values are self-explanatory.
Observe changes in your applications.
Aurelia provides a multitude of different wants to observe properties in your components and call a callback function when they change.
The following sections in the observation documentation will help you decide which observation strategy is appropriate for your applications, from the most commonly used to more advanced observation strategies.
The easiest way to watch for changes to specific view model properties is using the @observable
decorator which provides an easy way to watch for changes to properties and react accordingly.
While still using the @observable
API, the effect observation approach has more boilerplate and is convenient for instances where you want to observe one or more effects. Examples include when the user moves their mouse or other changes you might want to watch, independent of the component lifecycle.
Unlike other forms of observation, HTML observation is when you want to watch for changes to specific properties on elements, especially for web component properties.
The observer locator API allows you to observe properties for changes manually. In many instances, you will want to use @observer
or @watch
however, the observer locator can be useful in situations where you want to watch the properties of objects.
The Event Aggregator provides a lightweight pub/sub mechanism for communication between components in your Aurelia applications. This documentation covers the basics of using the Event Aggregator, along with several advanced use cases for cross-component communication.
To use the Event Aggregator in Aurelia, inject the IEventAggregator
interface into your component. One common pattern is to resolve it using the resolve
function.
To publish an event with an optional payload, simply call the publish
method. Any component subscribed to the event will receive the data.
It is best practice to dispose of your subscriptions when a component is no longer active—typically in the unbinding
lifecycle hook—to prevent memory leaks.
The Event Aggregator can be used in several advanced scenarios where components need to communicate in a decoupled manner.
In this use case, a child component publishes an event (e.g., a form submission), and a parent component subscribes to that event.
Sometimes a plugin needs to communicate internally without affecting global event subscriptions. You can create a new instance of the Event Aggregator to scope these events locally.
A plugin can require both local (plugin-scoped) event handling as well as listening to global events. In this scenario, inject two separate instances of the Event Aggregator.
You may wish to extend the functionality of the Event Aggregator by wrapping or subclassing it. For example, the following implementation tracks every event for which a subscription was created.
Always dispose of your subscriptions when they are no longer needed to prevent memory leaks.
Watching data for changes, including support for expressions where you want to watch for changes to one or more dependencies and react accordingly.
The @watch
decorator lets you respond to changes in your view model properties or computed expressions. It is intended for use on custom element and attribute view models. Once a watcher is created, it binds after the binding
lifecycle and unbinds before unbinding
—meaning mutations during binding
or after unbinding
will not trigger the watcher.
There are two primary ways to use @watch
:
Class-level Decoration: Attach the decorator to a class with an expression and a callback.
Method-level Decoration: Attach the decorator to a method; the method itself acts as the callback when the watched value changes.
Syntax:
expressionOrPropertyAccessFn
string
or IPropertyAccessFn
Specifies the value to watch. When a string is provided, it is used as an expression (similar to Aurelia templating). When a function is provided, it acts as a computed getter that returns the value to observe.
changeHandlerOrCallback
string
or IWatcherCallback
Optional. The callback invoked when the watched value changes. If a string is provided, it is used to resolve a method name (resolved only once, so subsequent changes to the method are not tracked). If a function is provided, it is called with three parameters: new value, old value, and the instance.
The simplest use case is to watch a single property. For example, to react whenever the name
property changes:
You can also observe expressions on arrays. For instance, watching the length of an array:
Sometimes you need to monitor changes in multiple properties. In these cases, you can provide a computed getter function to the @watch
decorator. The function should return the value you want to observe and can also register dependencies manually if needed.
Example – Watching Array Length with a Computed Getter:
In this example, the callback receives the new and old computed values every time the dependency (packages.length
) changes. The view model (post
) is also passed as a parameter so you can access other properties if needed.
Below are several examples illustrating different ways to use the @watch
decorator.
Warning: The method is resolved only once. Changes to the method after instance creation are not detected.
Watchers created via the @watch
decorator activate and deactivate in sync with component lifecycles:
During binding
: Watchers are not active. Mutations here won’t trigger callbacks.
No log output during binding
.
During bound
: Watchers are active. Changes will trigger the callback.
Logs: packages changes: 0 -> 1
.
During detaching
: Watchers are still active and will respond to changes.
Logs: packages changes: 0 -> 1
.
During unbinding
: Watchers have been deactivated; changes are ignored.
No log output during unbinding
.
Info: Lifecycles between
binding
andunbinding
(such asattaching
,attached
, anddetaching
) behave normally with respect to watchers.
When you apply @watch()
, a watcher is created to monitor the specified expression:
String or Symbol Expressions: Interpreted like Aurelia template expressions.
Function Expressions (Computed Getters): The function is called to obtain a value and register its dependencies. Two mechanisms exist:
With Native Proxy Support: Proxies intercept property reads, including collection method calls (e.g., .map()
), to automatically track dependencies.
Without Native Proxy Support: You receive a second parameter—the watcher instance—to manually register dependencies.
In environments without native proxies, the computed getter receives a watcher with the following interface:
Example:
Automatic Array Observation:
Note: In computed getters, common array mutation methods (
push
,pop
,shift
,unshift
,splice
,reverse
) are not observed automatically because they don’t expose clear dependency signals.
Do not alter properties or collections when returning a computed value:
Due to proxy wrapping, a raw object and its proxied version may not be strictly equal. Always access the dependency from the first parameter to maintain proper identity checks.
The dependency tracking is synchronous. Returning a promise or using an async function will break the reactivity.
Not to be confused with the task queue in Aurelia 1, the TaskQueue in Aurelia is an advanced scheduler designed to handle synchronous and asynchronous tasks efficiently. It provides a robust solution to common issues like timing problems, memory leaks, and race conditions often arising from traditional JavaScript timing functions like setTimeout
, setInterval
, and unmanaged promises.
Improved Performance: By managing tasks more efficiently, the TaskQueue enhances the performance of applications.
Synchronous and Asynchronous Support: It supports both synchronous and asynchronous tasks, providing greater flexibility in handling tasks.
Deterministic Task Execution: Facilitates testing by providing deterministic ways to wait for task completion, reducing test flakiness.
Avoids Common Pitfalls: Helps avoid common issues associated with setTimeout
and setInterval
, such as memory leaks and race conditions.
setTimeout
(Synchronous)Instead of `setTimeout, ' the TaskQueue offers a more reliable way to queue tasks without delay.
If you were to use a native setTimout
, it would look like this:
setTimeout
*Testability: You can await PLATFORM.taskQueue.yield()
or use PLATFORM.taskQueue.flush()
in tests for predictable task execution.
Improved Reliability: Reduces the chances of intermittent and hard-to-debug failures.
For asynchronous operations, the TaskQueue can handle tasks without the issues of floating promises.
The TaskQueue can mimic setInterval
functionality, offering more control and reliability.
For tasks that need to synchronize with the browser's repaint, domWriteQueue
is a safer alternative to requestAnimationFrame
.
For continuous animations, the TaskQueue can be used to create a loop, similar to requestAnimationFrame
.
Aurelia's testing library enhances the developer experience by offering a Fluent API for creating test fixtures. This API provides a more readable, flexible, and chainable way to set up component tests. With the Fluent API, you can incrementally build your test fixture, making the configuration of your tests more intuitive and maintainable.
The Fluent API for createFixture
comprises a series of chainable methods that allow you to configure each aspect of your test fixture in a step-by-step manner. This methodical approach to building test fixtures is particularly beneficial when dealing with complex setups or when you need to express the configuration in a more descriptive way.
Previously, creating a test fixture required passing all configuration parameters to the createFixture
function in a single call, which could become unwieldy as the number of configurations grew:
With the introduction of the Fluent API, you can now configure your test fixture using several self-explanatory methods, each responsible for a specific part of the setup:
The Fluent API provides the following methods, which can be chained together to configure your test fixture:
.component(component: any)
: Specifies the root component class for the test fixture.
.deps(...dependencies: any[])
: Registers additional dependencies required by the test or the components under test.
.html(template: string | HTMLTemplateElement)
: Sets the HTML template for the test. This can be provided as a string literal, a tagged template literal, or an HTMLTemplateElement
.
.build()
: Finalizes the configuration and builds the test fixture.
.start()
: Initializes the test fixture and returns a promise that resolves when the component is bound and attached.
Consider you have a MyCustomElement
that relies on Dependency1
and Dependency2
. The following example demonstrates how to use the Fluent API to create a test fixture for this component:
In this example, the Fluent API clearly outlines each step of the test fixture setup. It begins by defining the component under test, registers any dependencies, and sets the HTML template. Finally, the fixture is built and started, and the test awaits the startPromise
before performing assertions.
The Fluent API offers several advantages over the traditional approach:
Readability: The step-by-step configuration makes the test setup easier to read and understand.
Maintainability: It's easier to update and maintain tests as configurations can be changed independently without affecting the entire setup.
Flexibility: The API allows for dynamic adjustments to the test setup, accommodating various testing scenarios.
By employing the Fluent API, developers can write more coherent and expressive tests, enhancing the overall testing experience in Aurelia 2 applications.
Dependency Injection (DI) is a design pattern that enables classes to receive their dependencies from an external source rather than instantiating them directly. This inversion of control simplifies wiring up your application, promotes loose coupling, and enables advanced patterns such as singleton, transient, and scoped lifetimes. In Aurelia, DI is a core feature that not only manages the creation and resolution of dependencies but also provides powerful strategies—called resolvers—to control how dependencies are delivered.
Dependency Injection is a design pattern that decouples object creation from business logic. Instead of a class instantiating its own dependencies, those dependencies are provided by an external DI container. This approach:
Improves testability: You can inject mocks or stubs for unit testing.
Promotes loose coupling: Classes depend on abstractions rather than concrete implementations.
Manages lifetimes: The DI container controls the lifetime of objects (singleton, transient, scoped, etc.).
Facilitates configuration: Changing implementations or registration strategies is centralized.
In Aurelia 2, the DI container not only instantiates classes but also resolves dependencies based on metadata declared via constructor parameters, static properties, or decorators.
Aurelia supports several approaches for declaring dependencies:
Define dependencies by setting a static inject
property that lists the dependencies in the same order as the constructor parameters.
Warning: The order in the
inject
array must match the constructor parameters.
Leverage the @inject
decorator for a more declarative style.
A typical Aurelia application has a single root-level DI container:
Register services with the container using the register
API. This associates a key with a value (or class) and controls its lifetime.
You can also remove a service from the container when needed:
Although constructor injection is the norm, you can manually resolve services from the container:
Single instance:
Multiple implementations:
Since TypeScript interfaces don’t exist at runtime, use symbols or tokens for injection.
DI.createInterface()
Create a strongly typed injection token that can also provide a default implementation:
Then register the implementation with the container:
For cases like inheritance where constructor injection may not suffice, use property injection via the resolve
function:
You can also use resolve
in factory functions:
Note:
resolve
must be used within an active DI container context.
Resolvers in Aurelia 2 provide strategies for how dependencies are resolved. They give you granular control over instance creation and lifetime management.
The table below summarizes the built-in resolvers available in Aurelia:
Resolver
Purpose
Usage
lazy
Delays creation of a service until it is needed.
@inject(lazy(MyService))
or static inject = [lazy(MyService)]
all
Injects an array of all instances registered under a particular key.
@inject(all(MyService))
or static inject = [all(MyService)]
optional
Injects the service if available, otherwise undefined
.
@inject(optional(MyService))
or static inject = [optional(MyService)]
factory
Provides a function to create instances, offering control over instantiation.
@inject(factory(MyService))
or static inject = [factory(MyService)]
newInstanceForScope
Provides a unique instance within a particular scope (e.g., component or sub-container).
@inject(newInstanceForScope(MyService))
newInstanceOf
Always creates a fresh instance, regardless of existing registrations.
@inject(newInstanceOf(MyService))
last
Injects the most recently registered instance among multiple registrations.
@inject(last(MyService))
Each resolver can be used with both the @inject
decorator and the static inject
property. Below are detailed examples for a few of them.
Last Resolver Example with Multiple Registrations
If no instances are registered, the last resolver returns undefined
:
You can create custom resolvers by implementing the IResolver
interface to handle complex resolution logic.
Building maintainable applications in Aurelia often involves creating services that encapsulate shared functionality (business logic, data access, etc.). There are several approaches to define injectable services.
DI.createInterface()
to Create Injectable ServicesThis method creates an injection token that doubles as a type and, optionally, provides a default implementation.
Then register the service with the container:
For services that do not require an abstraction, simply export the class.
Register if needed:
Use decorators like @singleton()
for auto-registration.
This approach reduces redundancy by using the class as its own interface.
Then use it for injection:
Aurelia’s DI system offers various registration types to control how services are instantiated:
Registration Type
Description
Example
singleton
One instance per container.
Registration.singleton(MyService, MyService)
transient
A new instance is created every time.
Registration.transient(MyService, MyService)
instance
Registers a pre-created instance.
Registration.instance(MyService, myServiceInstance)
Decorators can also be used to register classes:
You can further customize injection by using additional decorators and helper functions:
For extending built-in objects, you might define interfaces that augment native types:
Many DI concepts remain consistent between Aurelia 1 and Aurelia 2. However, it is recommended to use DI.createInterface()
to create injection tokens for better forward compatibility and improved type safety. When injecting interfaces, you can use decorators or resolve functions directly:
By following these guidelines and examples, you can leverage Aurelia 2’s powerful Dependency Injection system to create well-architected, maintainable applications with clear separation of concerns and flexible service management.
Testing components in Aurelia 2 is a straightforward process thanks to the framework's design and the utilities provided by the @aurelia/testing
package. This guide will walk you through the steps to test your components effectively, ensuring they work as expected within the context of a view.
In Aurelia, a component typically consists of a view (HTML) and a view model (JavaScript or TypeScript). To ensure the quality and correctness of your components, you should write tests that cover both aspects. Testing components involves checking that the view renders correctly with given data and that the view model behaves as intended when interacting with the view.
When testing components, we will focus on integration tests that involve both the view and view model. This approach allows us to verify the component as a whole, as it would function within an Aurelia application.
For demonstration purposes, we will use a simple PersonDetail
component with bindable properties name
and age
.
We aim to test that the PersonDetail
component renders the expected text when provided with name
and age
properties.
Before writing the test, ensure your environment is correctly set up for testing. Refer to the Overview section for details on how to initialize the Aurelia testing platform.
Create a test file for your component, such as person-detail.spec.ts
, and implement your tests using the syntax of your chosen test runner. The following example uses Jest:
In this example, createFixture
is used to instantiate the component with a test context, binding name
and age
to specified values. We then assert that the component's text content includes the correct information. After the test completes, tearDown
cleans up the component instance to avoid memory leaks and ensure test isolation.
If your component has dependencies, such as services or other custom elements, you'll need to register these within the Aurelia testing container.
Assume PersonDetail
depends on a PersonFormatter
service:
To test this component, you can create a mock PersonFormatter
and register it with the Aurelia container:
In the test above, we use Jest's jest.fn()
to create a mock implementation of PersonFormatter
. We then verify that the mock's format
method is called with the correct arguments and that the component's text content includes the formatted details.
Testing Aurelia components involves setting up a test environment, creating fixtures, and writing assertions based on your expectations. By following these steps and best practices, you can ensure that your components are reliable and maintainable. Remember to clean up after your tests to maintain a clean test environment and to avoid any side effects between tests.
Testing is integral to modern software development, ensuring that your code behaves as expected in various scenarios. Aurelia 2 facilitates testing by providing helper methods and utilities to instantiate the framework in a test environment. While Aurelia supports different test runners, such as Jest and Mocha, the core testing principles remain consistent across these tools.
Aurelia's dedicated testing library, @aurelia/testing
, offers helpful functions for testing, including fixture creation methods that instantiate components with ease and handle both setup and teardown processes.
In Aurelia, testing often involves integration tests where you interact with the DOM and observe changes to content, rather than pure unit tests, which focus solely on isolated logic. It's important to test the behavior of code within the context of the view, but unit testing individual pieces of logic is also highly recommended for a comprehensive test suite.
Setting up a consistent test environment is crucial to ensure tests run correctly in different environments. This setup involves initializing the Aurelia platform using the setPlatform
method and configuring the Aurelia application's environment to operate within the test runner.
Place the following initialization code in a shared file to be loaded by all your tests, or include it in each individual test suite:
By creating the bootstrapTestEnvironment
function, you can easily initialize the test environment at the beginning of each test suite. This approach ensures consistency and reduces code duplication:
With your test environment configured, you can now focus on writing effective tests for your Aurelia components, ensuring that they perform as intended under various conditions.
Value converters in Aurelia 2 are an essential feature that allows you to create custom logic to transform data for display in your views. When it comes to testing value converters, you should aim for a mix of unit and integration tests to ensure that they function correctly both in isolation and when integrated within a view.
Let's start with a simple value converter that transforms a string to uppercase:
This value converter checks if the input is a string and, if so, transforms it to uppercase. If the input is null or undefined, it simply returns the input without modification.
When testing value converters, we will create unit tests to validate the converter logic and integration tests to ensure the converter works as expected within an Aurelia view.
Before writing tests, make sure to set up the test environment as described in the Overview Section.
Create a test file for your value converter, such as to-uppercase.spec.ts
. Here we will write tests for both unit and integration scenarios.
In the unit tests, we instantiate the ToUpper
value converter and directly call its toView
method with different inputs to verify the output. We test with null, a valid string, and a string with numbers to cover various scenarios.
The integration test uses the createFixture
function to test the value converter within an Aurelia view. We define a mock component with a text
property bound to the view and apply the toUpper
value converter. We then assert that the rendered text content is transformed as expected.
This method of testing can be applied to any class-based code in Aurelia 2. While the fixture bootstrap functionality is excellent for testing component output and behavior, it's not always necessary for unit testing pure code logic.
Testing value converters is an essential step in ensuring the reliability and robustness of your Aurelia 2 applications. By writing both unit and integration tests, you can confidently verify that your value converters perform correctly in isolation and within the context of an Aurelia view.
A developer guide that details numerous strategies for implementing animation into Aurelia applications.
Learn numerous techniques for implementing animations into your Aurelia applications.
In instances where you don't need to implement router-based transition animations (entering and leaving), we can lean on traditional CSS-based animations to add animation to our Aurelia applications.
Let's animate the disabled state of a button by making it wiggle when we click on it:
Some animations are reactive based on user input or other application actions. An example might be a mousemove
event changing the background colour of an element.
In this example, when the user moves their mouse over the DIV, we get the clientX
value and feed it to a reactive style string that uses the x
value to complete the HSL color value. We use lower percentages for the other values to keep the background dark for our white text.
Not to be confused with state animations, a reactive animation is where we respond to changes in our view models instead of views and animate accordingly. You might use an animation library or custom animation code in these instances.
In the following example, we will use the animation engine Anime.js to animate numeric values when a slider input is changed. Using the change
event on our range slider, we'll animate the number up and down depending on the dragged value.
Testing custom attributes in Aurelia is analogous to testing components, with the primary difference being that custom attributes do not have a view template. They are, however, responsible for modifying the behavior or appearance of existing DOM elements. By leveraging the same testing techniques used for components, we can effectively validate the functionality of our custom attributes.
Let's consider a ColorSquareCustomAttribute
that we previously created. This attribute applies a color border and sets the size of an element, resulting in a square of uniform dimensions with a colored background.
We will now write tests to ensure that the ColorSquareCustomAttribute
behaves as expected when applied to an element.
Create a test file for your custom attribute, such as color-square.spec.ts
, and use the following example as a guide:
In the first test, we verify that the default size and color are applied to an element when the custom attribute is used without any bindings. In the second test, we bind the color and size properties and then change the color to ensure the colorChanged
method updates the element's style as expected.
As with components, we use createFixture
to set up our test environment. The first argument is the HTML view where we use our custom attribute. The second argument is the view model, which can define values to bind in our view model if needed. The third argument specifies any dependencies required by our tests, such as custom elements, value converters, or attributes.
Testing custom attributes in Aurelia 2 is essential to ensure they correctly manipulate DOM elements as intended. By setting up a proper testing environment, creating fixtures, and writing assertions, we can confidently verify that our custom attributes perform their duties correctly. Always remember to clean up your tests to maintain a pristine testing state.
Testing in Aurelia often involves testing components that have dependencies injected into them. Using dependency injection (DI) simplifies the process of replacing these dependencies with mocks, stubs, or spies during testing. This can be particularly useful when you need to isolate the component under test from external concerns like API calls or complex logic.
Mocks are objects that replace real implementations with fake methods and properties that you define. They are useful for simulating complex behavior without relying on the actual implementation.
Stubs are like mocks but typically focus on replacing specific methods or properties rather than entire objects. They are useful when you want to control the behavior of a dependency for a particular test case.
Spies allow you to wrap existing methods so that you can record information about their calls, such as the number of times they were called or the arguments they received.
Sinon is a popular library for creating mocks, stubs, and spies in JavaScript tests. It provides a rich API for controlling your test environment and can significantly simplify the process of testing components with dependencies.
To make use of Sinon in your Aurelia project, you need to install it along with its type definitions for TypeScript support:
If you are not using TypeScript, you can omit the @types/sinon
.
After installing Sinon, import it in your test files to access its functionality. Let's look at how to apply Sinon to mock, stub, and spy on dependencies in Aurelia components.
In this example, the MyComponent
class has a dependency on IRouter
and a method navigate
that delegates to the router's load
method.
To stub the load
method of the router, use Sinon's stub
method:
When you need to replace the entire dependency, create a mock object and register it in place of the real one:
By using Registration.instance
, we can ensure that any part of the application being tested will receive our mock implementation when asking for the IRouter
dependency.
To observe and assert the behavior of methods, use Sinon's spies:
To test that the save
method is called correctly, wrap it with a spy:
Unit tests may require you to instantiate classes manually rather than using Aurelia's createFixture
. In such cases, you can mock dependencies directly in the constructor:
In this test, we directly provide a mock router object when creating an instance of MyComponent
. This technique is useful for more traditional unit testing where you want to test methods in isolation.
Mocking, stubbing, and spying are powerful techniques that can help you write more effective and isolated tests for your Aurelia components. By leveraging tools like Sinon and Aurelia's dependency injection system, you can create test environments that are both flexible and easy to control. Whether you're writing unit tests or integration tests, these methods will enable you to test your components' behavior accurately and with confidence.
Aurelia provides a powerful logging API that allows you to display debug and error messages in your applications in a controlled manner.
Aurelia comes with a flexible and powerful logging system that allows you to display debug and error messages in your applications in a slightly better way than using native console.log
statements.
Reasons to use logging inside of your apps and plugins include helpful debug messages for other developers (living comments) or displaying helpful information to the end-user in the console when something goes wrong.
The logger is injected using dependency injection into your components:
In this example, we scope our logger to our component. But scoping is optional, and the logger can be used without using scopeTo
however, we highly recommend using the scoping feature to group your messages in the console.
Just like console.log
the Aurelia logger supports the following methods:
debug
info
warn
trace
These methods are called on the logger instance you injected into your component.
Just like console.log
you can also pass in values such as strings, booleans, arrays and objects.
To create a custom logger for your applications and plugins, you can create a more reusable wrapper around the logging APIs to use in your applications.
It might look like a lot of code, but this logger implementation will create a scoped logger wrapper you can import into your applications and use in the following way:
The basics of the web-component plugin for Aurelia.
Web Components are part of an ever-evolving web specification that aims to allow developers to create native self-contained components without the need for additional libraries or transpilation steps. This guide will teach you how to use Aurelia in Web Components.
The web components package needs to be installed:
To use the plugin, import the interface IWcElementRegistry
interface from @aurelia/web-components
module and start defining web-component custom elements by calling method define
on the instance of IWcElementRegistry
.
WC custom elements can be defined anytime, either at the application start or later. Applications are responsible for ensuring names are unique.
Extending built-in elements is supported via the 3rd parameter of the define
call, like the define
call on the global window.customElements.define
call.
Each of WC custom element will be backed by a view model, like a normal Aurelia element component.
For each define
call, a corresponding native custom element class will be created and defined.
Each bindable property on the backing Aurelia view model will be converted to a reactive attribute (via observedAttributes
) and reactive property (on the prototype of the extended HTML Element class created).
Slot: [au-slot]
is not supported when upgrading an existing element. slot
can be used as a normal WC custom element.
Notes:
WC custom element works independently with the Aurelia component. This means the same class can be both a WC custom element and an Aurelia component. Though this should be avoided as it could result in double rendering.
containerless
mode is not supported. Use extend-built-in instead if you want to avoid wrappers.
the defined WC custom elements will continue working even after the owning Aurelia application has stopped.
template
info will be retrieved & compiled only once per define
call. Changing it after this call won't have any effects.
bindables
info will be retrieved & compiled only once per define
call. Changing it after this call won't have any effects.
Defining a tick-clock
element
Defining a tick-clock
element using shadow DOM with open
mode
Injecting the host element into the view model
Defining a tick-clock
element with format
bindable property for formatting
Defining a tick-clock
element extending built-in div
element:
Aurelia makes it easy to create your plugins. Learn how to create individual plugins, register them, and work with tasks to run code during certain parts of the lifecycle process.
Aurelia plugins allow you to encapsulate functionality that can be reused across multiple applications. They can include custom elements, value converters, and other resources. The goal is to create packaged, easily shared, ready-to-use functionalities that integrate seamlessly with Aurelia applications.
At its core, a plugin in Aurelia is an object with a register
method that configures dependencies and sets up your component or functionality for use in an Aurelia application.
You often want to add custom components to make your plugin more useful.
Use Clear Naming Conventions: Prefix your plugin resources with your plugin name to avoid naming collisions.
Provide Sensible Defaults: Always have sensible default values when adding configuration options.
Document Plugin Usage: Write clear documentation on how to use your plugin and its configurations.
Once you’ve mastered the basics of creating plugins, you may find that your needs evolve to require more complex functionality. This section covers advanced features in plugin development, such as advanced configuration management, lifecycle tasks, and mono-repository structures.
When your plugin settings are more intricate, consider adding more comprehensive configuration handling:
Plugins can adjust settings dynamically based on user input or environment conditions.
Aurelia provides lifecycle hooks that you can hook into within your plugins to perform operations at specific times during the app’s startup process.
This approach is beneficial when your plugin needs to load data, configure services, or perform actions that require awareness of the application’s runtime state.
Maintaining individual repositories may become cumbersome for larger projects with multiple interrelated plugins. A mono-repository can simplify this by organizing multiple packages in a single repository.
Directory Structure
Top-level package.json
Setup
Link Dependencies Use tools like lerna
or native npm
workspaces (npm install -g lerna
).
Initialize the repository:
This setup helps maintain consistency, allows for easier inter-package dependencies, and simplifies testing across multiple plugins.
App tasks provide injection points to run code at certain points in the compiler lifecycle, allowing you to interface with different parts of the framework and execute code.
Falling between component lifecycles and lifecycle hooks, app tasks offer injection points into Aurelia applications that occur at certain points of the compiler lifecycle. Think of them as higher-level framework hooks.
App tasks run at key moments in the Aurelia lifecycle. The table below summarizes each phase:
Aurelia’s app task API provides methods that correspond to each lifecycle phase. The common app task methods include:
AppTask.creating(...)
AppTask.hydrating(...)
AppTask.hydrated(...)
AppTask.activating(...)
AppTask.activated(...)
AppTask.deactivating(...)
AppTask.deactivated(...)
Each of these methods accepts a callback—and optionally a key—to perform operations during that phase. App tasks can be registered with the DI container during application instantiation or within plugins.
Register app tasks with your DI container (typically in main.ts
) or from within a plugin.
Within a plugin, you would export a registration function that receives the container:
App tasks can also be asynchronous. This is useful for scenarios where you need to perform asynchronous operations (such as dynamic imports) before the application fully starts.
In this example, the hydrating task waits for an asynchronous import and registers the result with the DI container before the application proceeds.
This example demonstrates using an app task to initialize and attach the Google Analytics SDK during the activating
phase.
Register the task in main.ts
:
The Google Analytics SDK is initialized and attached during the activating phase of the application lifecycle.
This app task dynamically loads features based on the current user’s roles, ideal for role-based access control.
Set up a global error handler during the creating
phase to catch any uncaught errors.
Initialize and start a telemetry session after the application is hydrated.
By using app tasks, you can inject custom behavior at critical points in your Aurelia application’s lifecycle. Whether you’re setting up global error handling, dynamically loading features, or initializing third-party integrations, app tasks provide a powerful, flexible mechanism to customize the startup (and shutdown) of your application.
Install via npm
Load the plugin
Simply bind an array to virtual-repeat
like you would with the standard repeat
. The repeated rows are expected to have equal height throughout the list, and one item per row.
With a surrounding fixed height container with overflow scroll. Note that overflow: scroll
styling is inlined on the elemenet. It can also be applied from CSS. An error will be thrown if no ancestor element with style overflow: scroll
is found.
<template/>
is not supported as root element of a virtual repeat template. This is due to the difficulty of technique employed: item height needs to be calculatable. With <tempate/>
, there is no easy and performant way to acquire this value.
Similar to (1), other template controllers cannot be used in conjunction with virtual-repeat
, unlike repeat
. I.e: built-in template controllers: with
, if
, etc... cannot be used with virtual-repeat
. This can be workaround'd by nesting other template controllers inside the repeated element, with <template/>
element, for example:
Beware of CSS selector :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:
Similar to (3), virtualization requires appropriate removing and inserting visible items, so not all views will have their lifecycle invoked repeatedly. Rather, their binding contexts will be updated accordingly when the virtual repeat reuses the view and view model. To work around this, you can have your components work in a reactive way, which is natural in an Aurelia application. An example is to handle changes in change handler callback.
Encountered an error and looking for answers? You've come to the right place.
This section is a work in progress and not yet complete. If you would like to help us document errors in Aurelia, we welcome all contributions.
Coded error in Aurelia comes with format: AURxxxx:yyyy
where:
AUR
is the prefix to indicate it's an error from Aurelia
xxxx
is the code
:
is the delimiter between the prefix, code and the dynamic information associated with the error
yyyy
is the extra information, or parameters related to the error
When using the production build of the core Aurelia packages, you'll get some error message that looks like this AUR0015:abcxyz
, which may not help much during development. If you wish to have better information with regards to an issue you are facing, you can use the development build. To configure your bundler/dev server to pick development build:
Our @aurelia/vite-plugin
will automatically pick the development build when process.env.NODE_ENV
is not production
. It can also be overriden using useDev
property option, like the following example:
Add alias to the resolve.alias
in your webpack config in webpack.config.js
, like the scaffolding template at https://github.com/aurelia/new/blob/06f06862bab5f7b13107237a69cf59de1385d126/webpack/webpack.config.js#L117-L123
The dist folder of an Aurelia core package looks like this:
Whenever there's a request to retrieve dist/esm/index.mjs
, you can redirect it to dist/esm/index.dev.mjs
.
The section below will list errors by their prefix, and code and give a corresponding explanation, and a way to fix them.
Router-Lite logs various events. Majority of those events are traces. The non-warn, non-error events are not logged in non-dev build, and are only available for troubleshooting in the dev-build. This section only lists the error codes.
Before writing tests, ensure that your test environment is properly configured. The setup for testing custom attributes is the same as for components, so refer to the section.
Dependency Injection errors can be found .
Phase
When It Runs
Use Cases
creating
Just before DI creates the root component.
Last chance to register dependencies that must be injected into the root component.
hydrating
After instantiating the root view, but before compiling the root and its child elements.
Ideal for plugins (e.g., routers) to perform initial work before child elements are processed.
hydrated
After self-hydration of the root controller, but before hydrating child elements.
Allows pre-hydration tasks to complete before further initialization.
activating
Right before the root component is activated; at this point, the scope hierarchy is formed and bindings are being bound.
Prepare the application for activation (e.g., feature toggling, initial data loading).
activated
Immediately after the root component is activated.
The app is fully running; additional startup logic may be executed here.
deactivating
Right before the root component is deactivated; scope hierarchy is unlinked and bindings are getting unbound.
Useful for cleanup or saving state before the application stops.
deactivated
Immediately after the root component is deactivated.
Final cleanup tasks and post-deactivation processing.
AUR0701
This happens when a template has a single template element in your template, and it has as-local-element
attribute on it
AUR0702
This happens when a template has one or more attributes that are supposed to be unique on its surrogate elements
AUR0703
This happens when a template controller attribute is used on a surrogate element of a template
AUR0704
This happens when an attribute on a <let/>
element is used without .bind
or .to-view
command
AUR0705
This happens when enhancing a template with one or more element in it already have a class au
on it
AUR0706
This happens when [au-slot]
attribute is used on an element that is not an immediate child of a custom element
AUR0707
This happens when the template compiler encounters binding to a non-bindable property of a custom attribute
AUR0708
This happens when the template of a custom element has nothing beside template elements with as-local-element
AUR0709
This happens when an as-local-element
template is not defined as an immediate child of the root of a custom element template
AUR0710
This happens when an as-local-element
template has a <bindable>
element inside its template, that is not not an immediate child of its fragment
AUR0711
This happens when a <bindable>
inside an as-local-element
template does not have a valid property
attribute on it
AUR0712
This happens when an as-local-element
template has 2 or more <bindable>
elements with non-unique attribute
or property
attributes
AUR0713
This happens when an unknown binding command is encountered in a custom element template
AUR0714
This happens when a custom element or attribute definition has more than 1 primary bindable property
AUR0715
This happens when an as-local-template
template has the value of as-local-template
as an empty string
AUR0716
This happens when a custom element has 2 or more local elements with the same name
AUR0750
This happens when there is a binding that looks like this view.ref="..."
. This likely comes from a v1 template migration.
AUR0751
This happens when there is a ref
binding in the template that does not have matching target. Most likely a custom attribute reference
AUR0752
This happens when a controller renders a custom element instruction that it doesn't have a registration. Normally happens in hand-crafted definition
AUR0753
This happens when a controller renders a custom attribute instruction that it doesn't have a registration. Normally happens in hand-crafted definition
AUR0754
This happens when a controller renders a template controller instruction that it doesn't have a registration. Normally happens in hand-crafted definition
AUR0755
This happens when a view factory provider tries to resolve but does not have a view factory associated
AUR0756
This happens when a view factory provider tries to resolve but the view factory associated does not have a valid name
AUR0757
This happens when IRendering.render
is called with different number of targets and instructions
AUR0758
This happens when BindingCommand.getDefinition
is called on a class/object without any binding command metadata associated
AUR0759
This happens when CustomAttribute.getDefinition
is called on a class/object without any custom attribute metadata associated
AUR0760
This happens when CustomElement.getDefinition
is called on a class/object without any custom element metadata associated
AUR0761
This happens when CustomElementDefinition.create
is called with a string as first parameter
AUR0762
This happens when CustomElement.for
is called on an element that does not have any custom element with a given name, without searching in ancestor elements
AUR0763
This happens when CustomElement.for
is called and Aurelia isn't able to find any custom element with the given name in the given element, or its ancestors
AUR0764
This happens when CustomElement.for
is called on an element with a given name, and Aurelia is unable to find any custom element in the given the element, or its ancestors
AUR0765
This happens when CustomElement.for
is called on an element without a given name, and Aurelia is unable to find any custom element in the given element, or its ancestors
AUR0766
This happens when @processContent
is called with a string as its first parameter, and Aurelia couldn't find the method on the decorated class
AUR0767
This happens when root
property on an Aurelia
instance is access before at least one application has been started with this Aurelia
instance
AUR0768
This happens when a new Aurelia
is created with a predefined container that already has IAurelia
registration in it, or its ancestors
AUR0769
This happens when an Aurelia
application is started with a document fragment before it's adopted by a document
AUR0770
This happens when Aurelia.prototype.start
is called with a null
/undefined
value as the first parameter
AUR0771
This happens when Aurelia.prototype.dispose
is called before the instance is stopped
AUR0772
This happens when the @watch
decorator is used without a valid first parameter
AUR0773
This happens when the @watch
decorator is used and Aurelia is not able to resolve the first parameter to a function
AUR0774
This happens when the @watch
decorator is used on a class property instead of a method
AUR0651
This happens when the binding created .attr
binding command is forced into two way mode against any attribute other than class
/style
AUR0652
This happens when the default NodeObserverLocator.getObserver
is called with an object and property combo that it doesn't know how to observe, and dirty checking is disabled
AUR0653
This happens when NodeObserverLocator
property->observation events mapping is getting overridden
AUR0654
This happens when a <select>
element is specified multiple
, but the binding value is not an array
AUR0500
This happens when Controller.getCachedOrThrow
throws
AUR0501
This happens when a custom element is specified containerless
and has <slot>
element in its template
AUR0502
This happens when a disposed controller is being activated
AUR0503
This happens when the internal state of a controller is corrputed during activation
AUR0504
This happens when a synthetic view is activated without a proper scope
AUR0505
This happens when the internal state of a controller is coruppted during deactivation
AUR0506
This happens when Aurelia fails to resolve a function from the first parameter of a @watch
decorator
AUR0801
This happens when & self
binding behavior is used on non-event binding
AUR0802
This happens when & updateTrigger
binding behavior is used without any arguments
AUR0803
This happens when & updateTrigger
binding behavior is used on binding without view -> view model observation
AUR0804
This happens when & updateTrigger
binding behavior is used on binding that does not target a DOM element
AUR0805
This happens when <au-compose>
scopeBehavior
property is assigned a value that is not either auto
or scoped
AUR0806
This happens when <au-compose>
component
binding is used with a custom element with containerless = true
AUR0807
This happens when there's a corrupted internal state of <au-compose>
and activation is called twice
AUR0808
This happens when there's a corrupted internal state of <au-compose>
and deactivation is called twice
AUR0809
This happens when <au-render>
component
binding is given a string value, and there's no custom element with matching name
AUR0810
This happens when else
attribute does not follow an if
attribute
AUR0811
This happens when portal
attribute is a given an empty string as CSS selector fortarget
, and strict
mode is on
AUR0812
This happens when portal
attribute couldn't find the target element to portal to, and strict
mode is on
AUR0813
This happens when then
/catch
/pending
attributes is used outside of a promise
attribute
AUR0814
This happens when the internal of the repeat
attribute get into a race condition and is corrupted
AUR0815
This happens when case
/default-case
attributes is used outside of a switch
attribute
AUR0816
This happens when there are multiple default-case
attributes inside a switch
attribute
AUR0817
This happens when & signal
binding behavior is used on binding that does not have handleChange
method
AUR0818
This happens when & signal
binding behavior is used without a valid name (non empty)
AUR0901
Dialog
This happens when an application is closed with some dialogs still open
AUR0903
Dialog
This happens when IDialogService.open
is called without both component
and template
property
AUR0904
Dialog
This happens when the default configuration of the dialog plugin is used, as there's no registration associated for key interfaces
AUR0101
This happens when Aurelia couldn't find a binding behavior specified in an expression
AUR0102
This happens when there are two binding behaviors with the same name in an expression
AUR0103
This happens when a value converter for a given name couldn't be found during the evaluation of an expression
AUR0104
This happens when a value converter for a given name couldn't be found during the assignment of an expression
AUR0105
This happens when the special $host
contextual property is accessed but no thing is found in the scope tree
AUR0106
This happens when an expression looks like this $host = ...
, as $host
is a readonly property
AUR0107
This happens when a call expression is evaluated but the object evaluated by the expression isn't a function
AUR0108
This happens when a binary expression is evaluated with an unknown operator
AUR0109
This happens when an unary expression is evaluated with an unknown operator
AUR0110
This happens when a tagged template (function call) is but the function specified isn't a function
AUR0111
This happens when a function call AST is evaluated but no function is found
AUR0112
This happens when a non-object or non-array value is assigned for destructured declaration for a repeat.for
statement
AUR0113
This happens when an increment operator is used outside of an event handler
AUR0151
An expression has an invalid character at the start
AUR0152
An expression has ..
or ...
AUR0153
The parser encounters an unexpected identifier in an expression
AUR0154
The parser encounters an invalid AccessMember
expression
AUR0155
The parers encounters an unexpected end in an expression
AUR0156
The parser encounters an unconsumable token in an expression
AUR0158
The expression has an invalid assignment
AUR0159
An expression has no valid identifier after the value converter ` | ` symbol
AUR0160
An expression has no valid identifier after the binding behavior &
symbol
AUR0161
The parser encounters an invalid of
keyword
AUR0162
The parser encounters an unconsumed token
AUR0163
The parser encounters an invalid binding identifier at left hand side of an of
keyword
AUR0164
The parser encounters a literal object with a property declaration that it doesn't understand
AUR0165
An expression has an opening string quote '
or "
, but no matching ending quote
AUR0166
An expression has an opening template string quote ```, but has no matching end
AUR0167
The parser encounters an unexpected token
AUR0168
The parser encounters an unexpected character
AUR0169
The parser encounters an unexpected character while parsing destructuring assignment expression
AUR0201
BindingBehavior.getDefinition
is called on a class/object without any binding behavior metadata associated
AUR0202
ValueConverter.getDefinition
is called on a class/object without any value converter metadata associated
AUR0203
BindingContext.get
is called with null
/undefined
as the first parameter
AUR0204
Scope.fromOverride
is called with null
/undefined
as the first parameter
AUR0205
Scope.fromParent
is called with null
/undefined
as the first parameter
AUR0206
ConnectableSwitcher.enter
is called with null
/undefined
as the first parameter
AUR0207
ConnectableSwitcher.enter
is called with the currently active connectable
AUR0208
ConnectableSwitcher.exit
is called with null
/undefined
as the first parameter
AUR0209
ConnectableSwitcher.exit
is called with an inactive connectable
AUR0210
getCollectionObserver
is called with an not-supported collection type
AUR0211
a binding subscried to an observer, but does not implement method handleChange
AUR0212
a binding subscribed to a collection observer, but does not implement method handleCollectionChange
AUR0220
a Set
/Map
size observer .setValue
method is called
AUR0221
the setValue
method on a computed property without a setter
AUR0222
Aurelia doesn't know how to observe a property on an object, and dirty checking is disabled
AUR0224
Encounters an invalid usage of @observable
AUR0225
An effect is attempted to run again, after it has stopped
AUR0226
An effect has reach its limit of recursive update
AUR3155
The route context cannot be resolved from the given DOM node. This happens if the given node is not a custom element or not a child-node of a custom element.
AUR3166
This happens when an attempt to eagerly (without involving the route recognizer) recognize a routing instruction failed. If you are getting this error, please report it.
AUR3167
This happens when the application root is not yet set, but the router is trying to set a routing root. If you are getting this error, please report it.
AUR3168
This happens when a route context already exist for the application root; for example if it attempted to set the routing root more than once.
AUR3169
This happens when no controller exists for the application root. If you are getting this error, please report it.
AUR3170
A route context cannot be resolved for the given input.
AUR3171
This happens when the route node of the route context is not set. If you are getting this error, please report it.
AUR3172
This happens when the viewport agent of the route context is not set. If you are getting this error, please report it.
AUR3173
This happens the import()
function is used as component
, while configuring a route, but no path
has been specified. This is not supported.
AUR3174
No viewport agent can be resolved for a given request.
AUR3175
This happens the import()
function is used as component
, while configuring a route, but the module does not export any aurelia custom element.
AUR3270
A routing transition failed.
AUR3271
The routing context of the router is not set. If you are getting this error, please report it.
AUR3350
Activation of component from a viewport failed due to incorrect state. If you are getting this error, please report it.
AUR3351
Deactivation of component from a viewport failed due to incorrect state. If you are getting this error, please report it.
AUR3352
The state of the viewport agent is not as expected. If you are getting this error, please report it.
AUR3353
The transition was either erred or cancelled via one of the can*
hooks, but the router attempts to continue with the current instruction instead of cancelling it. If you are getting this error, please report it.
AUR3400
A navigation instruction cannot be created.
AUR3401
Neither the given routing instruction can be recognized, nor a fallback
is configured.
AUR3401
The redirect route cannot be recognized.
AUR3403
toUrlComponent
is invoked on a navigation instruction with incompatible type. This happens when the type of the instruction is a promise or a view-model.
AUR3450
Thrown by the navigation model when the endpoint for a path is not found.
AUR3500
Thrown by the route expression parser upon encountering an unexpected segment.
AUR3501
Thrown by the route expression parser when all of the given input string cannot be consumed.
AUR3502
Thrown if an unexpected segment is encountered during migrating parameters for redirect route.
AUR3550
Thrown when a re-attempt is made to call the getRouteConfig
hook for the same component. If you are getting this error, please report it.
AUR3551
A custom element definition could not be resolved from the given string name, as no route context was provided. If you are getting this error, please report it.
AUR3552
A custom element definition could not be resolved from the given string name, as it is potentially not a custom element.
AUR3553
A custom element definition could not be resolved from theimport()
function, as no route context was provided to resolve it.
AUR3554
The validation of a route config failed due to unexpected type of property.
AUR3555
The validation of a route config failed, as the config is either undefined
or null
.
AUR3556
The validation of a route config failed due to unexpected property.
AUR3556
The validation of a redirect route config failed due to unexpected property.
yyyy not registered, did you forget to add @singleton()?
yyyy
not registered, did you forget to add @singleton()?
Name of the key being resolved
A DI container is trying to resolve a key, but there's not a known strategy for it.
Try adding a strategy for your resolved key. You can do this using @singleton
or other forms of DI resolution
Please also note that this error could be caused by a plugin and not your application. After ruling out that the error is not being caused by your code, try removing any registered plugins one at a time to see if the error resolves itself.
Attempted to jitRegister an intrinsic type: yyyy. Did you forget to add @inject(Key)
Attempted to jitRegister an intrinsic type: yyyy
. Did you forget to add @inject(Key)
Interface name
A DI container is trying to resolve an instance of an interface, but there is no registration for it. This means the instance you are trying to load has not been registered with Dependency Injection.
Ensure that you are registering your interface with Aurelia. This can be done inside of the register
method on the Aurelia instance or through the DI methods themselves.
Please also note that this error could be caused by a plugin and not your application. After ruling out that the error is not being caused by your code, try removing any registered plugins one at a time to see if the error resolves itself.
The router emits several events via the Event Aggregator, which allows you to listen to router events as they occur. In some situations, you might opt for a router hook, but in other cases, an event might be what you are after.
A good example of where using events might be more appropriate is showing and hiding loaders and other parts of your applications that relate to routing.
The events fired are:
au:router:router-start
au:router:router-stop
au:router:navigation-start
au:router:navigation-end
au:router:navigation-cancel
au:router:navigation-error
To listen to these events, you subscribe to them using the event aggregator like this:
As you might expect, these events are named in an intuitive way depending on the action taking place inside of the router.