LogoLogo
HomeDiscourseBlogDiscord
  • Introduction
  • Introduction
    • Quick start
    • Aurelia for new developers
    • Hello world
      • Creating your first app
      • Your first component - part 1: the view model
      • Your first component - part 2: the view
      • Running our app
      • Next steps
  • Templates
    • Template Syntax
      • Attribute binding
      • Event binding
      • Text interpolation
      • Template promises
      • Template references
      • Template variables
      • Globals
    • Custom attributes
    • Value converters (pipes)
    • Binding behaviors
    • Form Inputs
    • CSS classes and styling
    • Conditional Rendering
    • List Rendering
    • Lambda Expressions
    • Local templates (inline templates)
    • SVG
  • Components
    • Component basics
    • Component lifecycles
    • Bindable properties
    • Styling components
    • Slotted content
    • Scope and context
    • CustomElement API
    • Template compilation
      • processContent
      • Extending templating syntax
      • Modifying template parsing with AttributePattern
      • Extending binding language
      • Using the template compiler
      • Attribute mapping
  • Getting to know Aurelia
    • Routing
      • @aurelia/router
        • Getting Started
        • Creating Routes
        • Routing Lifecycle
        • Viewports
        • Navigating
        • Route hooks
        • Router animation
        • Route Events
        • Router Tutorial
        • Router Recipes
      • @aurelia/router-lite
        • Getting started
        • Router configuration
        • Configuring routes
        • Viewports
        • Navigating
        • Lifecycle hooks
        • Router hooks
        • Router events
        • Navigation model
        • Current route
        • Transition plan
    • App configuration and startup
    • Enhance
    • Template controllers
    • Understanding synchronous binding
    • Dynamic composition
    • Portalling elements
    • Observation
      • Observing property changes with @observable
      • Effect observation
      • HTML observation
      • Using observerLocator
    • Watching data
    • Dependency injection (DI)
    • App Tasks
    • Task Queue
    • Event Aggregator
  • Developer Guides
    • Animation
    • Testing
      • Overview
      • Testing attributes
      • Testing components
      • Testing value converters
      • Working with the fluent API
      • Stubs, mocks & spies
    • Logging
    • Building plugins
    • Web Components
    • UI virtualization
    • Errors
      • Kernel Errors
      • Template Compiler Errors
      • Dialog Errors
      • Runtime HTML Errors
    • Bundlers
    • Recipes
      • Apollo GraphQL integration
      • Auth0 integration
      • Containerizing Aurelia apps with Docker
      • Cordova/Phonegap integration
      • CSS-in-JS with Emotion
      • DOM style injection
      • Firebase integration
      • Markdown integration
      • Multi root
      • Progress Web Apps (PWA's)
      • Securing an app
      • SignalR integration
      • Strongly-typed templates
      • TailwindCSS integration
      • WebSockets Integration
      • Web Workers Integration
    • Playground
      • Binding & Templating
      • Custom Attributes
        • Binding to Element Size
      • Integration
        • Microsoft FAST
        • Ionic
    • Migrating to Aurelia 2
      • For plugin authors
      • Side-by-side comparison
    • Cheat Sheet
  • Aurelia Packages
    • Validation
      • Validation Tutorial
      • Plugin Configuration
      • Defining & Customizing Rules
      • Architecture
      • Tagging Rules
      • Model Based Validation
      • Validation Controller
      • Validate Binding Behavior
      • Displaying Errors
      • I18n Internationalization
      • Migration Guide & Breaking Changes
    • i18n Internationalization
    • Fetch Client
      • Overview
      • Setup and Configuration
      • Response types
      • Working with forms
      • Intercepting responses & requests
      • Advanced
    • Event Aggregator
    • State
    • Store
      • Configuration and Setup
      • Middleware
    • Dialog
  • Tutorials
    • Building a ChatGPT inspired app
    • Building a realtime cryptocurrency price tracker
    • Building a todo application
    • Building a weather application
    • Building a widget-based dashboard
    • React inside Aurelia
    • Svelte inside Aurelia
    • Synthetic view
    • Vue inside Aurelia
  • Community Contribution
    • Joining the community
    • Code of conduct
    • Contributor guide
    • Building and testing aurelia
    • Writing documentation
    • Translating documentation
Powered by GitBook
On this page
  • Binding command
  • Creating a custom binding command
  • Registering the custom binding command
  • Live example

Was this helpful?

Export as PDF
  1. Components
  2. Template compilation

Extending binding language

The Aurelia template compiler is powerful and developer-friendly, allowing you extend its binding language with great ease.

PreviousModifying template parsing with AttributePatternNextUsing the template compiler

Last updated 3 months ago

Was this helpful?

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.

Binding command

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 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.

Creating a custom binding command

To create a binding command, we use the @bindingCommand decorator with a command name on a class that implements the following interface:

interface BindingCommandInstance {
  type: CommandType;
  build(info: ICommandBuildInfo, parser: IExpressionParser, mapper: IAttrMapper): IInstruction;
}

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 is used to determine the binding mode, the target property name, etc. (for more information, refer to the ). 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:

<div foo.bar.bs="ev => handleCustomEvent(ev)"></div>

instead of

<div bs.foo.bar.trigger="ev => handleCustomEvent(ev)"></div>

We first create a class that implements the BindingCommandInstance interface to do that.

import { IExpressionParser } from '@aurelia/runtime';
import {
  BindingCommandInstance,
  ICommandBuildInfo,
  IInstruction,
  ListenerBindingInstruction,
  bindingCommand,
} from '@aurelia/runtime-html';

@bindingCommand('bs')
export class BsBindingCommand implements BindingCommandInstance {
  public ignoreAttr = true;

  public build(
    info: ICommandBuildInfo,
    exprParser: IExpressionParser
  ): IInstruction {
    return new ListenerBindingInstruction(
      /* from           */ exprParser.parse(info.attr.rawValue, 'IsFunction'),
      /* to             */ `bs.${info.attr.target}`,
      /* preventDefault */ true,
      /* capture        */ false
    );
  }
}

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.

Registering the custom binding command

import { IContainer } from 'aurelia';
import { BsBindingCommand } from './bs-binding-command';

export function registerBindingCommands(container: IContainer) {
  // Register our custom command with Aurelia's DI container
  container.register(BsBindingCommand);
}

This function can then be called wherever you configure your Aurelia application, so that the compiler knows about your custom command.

How to load the custom command

In your main.ts (or equivalent entry point) where you configure Aurelia, you can call the registerBindingCommands function. For example:

import Aurelia from 'aurelia';
import { registerBindingCommands } from './register';
import { MyRoot } from './my-root';

Aurelia
  .app(MyRoot)
  .register(registerBindingCommands)
  .start();

This ensures that when Aurelia boots, it's aware of your new binding command.

Why 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.

Debugging custom binding commands

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:

<div foo.bar.bs="ev => handleCustomEvent(ev)"></div>
<!--         ^^
             |_________ custom binding command
-->

Live example

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.

attribute patterns
type IAttrMapper
documentation