Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
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.
If you are unfamiliar with the au-slot
then visit the documentation. 'processContent' can be very potent with au-slot
.
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.
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 'IgnoreAttr'
from the type
property. This tells the template compiler that the binding command takes over the processing of the attribute.
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.
And that's it! We have created our own binding command. This means that 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.
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.
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.
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 , 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.
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 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.
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.
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: