Building applications in a component oriented manner yields many benefits. It allows developers to think about only one piece of the user interface at a time, improves testability, allows for greater reusability without additional effort, and more. Web-Components are the Micro-Services of front-end web development.
The natural result of building an application in this style is a large number of small components that need to communicate and work together to deliver the end-user experience. In order to make these components work together cleanly, it is important to determine a pattern (or collection of patterns) for how inter-component coordination should be structured.
In order to do this, I find it helpful to ask the following questions:
- Who own’s a particular piece of data?
- Who’s responsibility is it to persist changes in data to the back end?
- Who needs to know when a piece of data changes?
Answering these questions helps determine how the communication should be structured, and which tools and techniques should be used. Aurelia provides many tools and techniques to allow web components to communicate, and as with anything in software development, there is no one correct answer. The best choice depends on the complexity of the problem you are trying to solve, which is determined in large part by the finding answers to the above questions.
I’ll summarise some of the tools and techniques that Aurelia provides, and explain in what might indicate that a particular option should be selected.
Aurelia’s Data-Binding system allows for data to be passed from a view-model to a view (in the case of a one way data-binding), and back again (in the case of a two-way data-binding). Data-binding really shines in simple situations such as:
View-Model to View
Set a property on the view-model to be read by the view.
Read the property in the view with Aurelia’s string interpolation and render it into the h1 tag.
A less obvious way of using Aurelia’s data-binding system is to pass data down from a parent-component to a child component as shown in the following diagram:
We can do this by turning our hello view + view-model into a greeter CustomElement (the primary option for creating web-components with Aurelia):
We declare a property on the root view-model called parentGreeting, again setting it in the constructor. We then bind this property to the property greeting on the child view-model:
For this simple example, the one-way data-binding from the parent down to the child worked perfectly. It was simple to set up, low concept count, and low lines of code. This pattern can be utilised to pass data down through the view heir-achy from parent to child. Any changes made in the parent will filter down to the children. There are two key area’s where this falls short though if we look to extend the application:
- What if we don’t always want the value from the parent to directly filter down to the children? For example, we only want children to receive the update on a certain event such as hitting the save button.
- How do we notify the parent component that a value has changed in the child?
- If a value is updated in the child, should it save the value to the data-store/web-api itself? How does the parent find out that it needs to re-fetch the state from the web-api and re-render itself?
These questions indicate that we’ve hit the limitations of what data-binding can offer, and we need to reach for a more advanced tool in order to fulfil these requirements.
Enter the EventAggregator.
An Event Aggregator is a simple element of indirection. In its simplest form, you have it register with all the source objects you are interested in and have all target objects register with the Event Aggregator. The Event Aggregator responds to any event from a source object by propagating that event to the target objects.
In Aurelia, this effectively means that the parent component registers for messages that the child-components publish and if needed child components listen to messages from parents.
This looks something like the diagram below:
In a more complex application, you would have many components each subscribing to key messages published from other components.
For example in a messaging application you might have a messageComposer, messageList and a messageBell component:
When new message composition is complete, a new-message event should be published to the EventAggregator. The messageBell and messageList components can then subscribe to this event and respond appropriately:
Using the EventAggregator provides more flexibility in the way that data is passed between components. It makes the message passing explicit, rather than implicit. This in turns makes it clear exactly who owns a particular state change. Components can make the decision what to do when an event occurs, rather than having the parent component decide it on their behalf.
So if the messageComposer is responsible for creating a new message, does it also have the responsibility of storing that message? What if something else needs to be done with the message before it is sent down to the back-end? At this point the natural extension of the above pattern is to move to an architecture called Data-Down, Actions Up which originated in the Ember.js community, which in turn was influenced by the Flux/Redux Uni-Directional Data-Flow pattern from React.
Data-Down, Actions Up is an a simple rule of thumb for how to structure communication in complex component based applications. Data is passed down from a parent component to the child components (similar to what we saw in the first data-binding example). Actions (EventAggregator Events in the Aurelia world) are then passed back up to the parent so that a decision can be made:
Extending our messaging application example from above, we can follow this rule of thumb by creating a new root level component called messages. This component is responsible for fetching the message list from the web-api, and persisting any changes back:
This component would pass the data down to its child components using one-way data-binding, and the child components would pass actions such as new-message up so that any required state changes could be persisted. Taking it one step further, the communication network could be simplified by subscribing to all actions/events from message related child components. That way the messaging component becomes a coordinator. All child messaging components talk to it via the EventAggregator but don’t talk to each other.
The next logical step would be implementing true uni-directional data-flow with something like Redux. It’s all about the complexity threshold, keep things simple until you find hit a wall, then look for a more advanced tool to solve tool to move forward.