In this blog post I’m going to show you how to increase the usability of your Lightning Components in your application by decoupling responsibilities and enforcing separation of concerns.
Separation of concerns
Throughout this blog post I’m going to be referring to an example of we need to create a map Lightning Component which needs to display the location of a matching postal code and house number is and if it finds the address places a pin to help the user focus on it. Kind of like how Google Maps displays results. We’re not focussing on the map component itself, but more how do we do separate out the map functionality to how it locates a particular address and drops these pins onto the map.
So let’s take our starting point with this code below. It will search for coordinates for a given postal code and house number (probably using Apex), render a map and then drop a pin where the matching coordinates are found.
<c:Map postalCode="{!v.postalCode}" houseNumber="{!v.houseNumber}" />
The problem with this map component is that the displaying and searching for an address for its coordinates is coupled together. The searching for the coordinates from an address is intertwined with the displaying of the map. If in the future we need to reuse this component to display multiple coordinates and drop multiple pins then we do not have separation of concerns and increases the complexity of maintainability.
How can we improve this?
First, we need to identify the different aspects of how our component is composed. We have two main aspects:
- A map component which renders the map and drops the pins.
- Something which searches for an address and provides the coordinates.
We’re going to cover how we can create a reusable map component which does not care where its coordinates come from and we’re going to cover how to create a component which provides those components.
Defining how we will use our components
Below is the new and improved version of the map Lightning Component.
<c:Map> <aura:set attribute="coordinates"> <c:CoordinatesProvider postalCode="{!v.postalCode}" houseNumber="{!v.houseNumber}" /> </aura:set> </c:Map>
You should notice that we’re not directly passing in the postal code and house number into the map component, but instead a new component called “CoordinatesProvider”. This is the component which is responsible only for searching and retrieving coordinates.
Next, we set a new attribute on the map component called “coordinates” with the body being the CoordinatesProvider. What we’re doing here is injecting the component into the map component and little later on we’ll go over how our new and improved map component will receive the coordinates.
So already you should be able to spot that our map is decoupled from how it receives results (coordinates) and we can also reuse the CoordinatesProvider for other components as well.
So how does it work?
When the map component loads it is going to first find the CoordinatesProvider instance which was passed into it via it’s attribute “coordinates”, ask it to provide data and then will add an event handler to the provider to handle the received data.
Huh?! Let me explain a little…
The provider is going to be used using component events. We’re going to have two component events registered in our provider:
- provide
- dataChanged
The provide event is going to be fired from within the map component and it’s responsible for asking the provider for data. When the data is loaded or if it has changed then the dataChanged event will be fired from within the provider. Our map component is going to listen for this event and then finally update to display the correct location using the coordinates found within the events attributes.
The component events
Our first component event will be the provide event. We’ll create ours with the name “CoordinatesProvideEvent”.
<aura:event type="COMPONENT" description="Event which should be fired to request data" />
The second component we need to create is the data changed event. This is the event which will be fired when the coordinates for a given address has been found. We’ll call this event “CoordinatesDataChangedEvent”.
<aura:event type="COMPONENT" description="Event which is fired when data has changed"> <aura:attribute name="longitude" type="Integer" /> <aura:attribute name="latitude" type="Integer" /> </aura:event>
It’s inside this component event which will carry the data from the provider and expose this to the map component.
Setting up the coordinates provider
This is the most trickiest part of the whole design, but not too tricky!
<aura:component> <!-- Exposed attributes --> <aura:attribute name="postalCode" type="String" access="public" /> <aura:attribute name="houseNumber" type="String" access="public" /> <!-- The registered events we're exposing --> <aura:registerEvent name="provide" type="c:CoordinatesProvideEvent" /> <aura:registerEvent name="dataChanged" type="c:CoordinatesDataChangedEvent" /> <!-- It's going to handle it's own thrown provide event --> <aura:handler name="provide" type="c:c:CoordinatesProvideEvent" action="{!c.handleProvide}" /> </aura:component>
As you can see above the component registers two events which can be thrown from within the component and one handler. It’s going to handle it’s own provide event.
The handler is redirecting the provide event to a controller function called “handleProvide”. Let’s define that method in the providers controller:
({ handleProvide: function(component){ // You could load data from the server here // For simplicity, we're firing that the data is ready // immediately with some dummy values var postalCode = component.get('v.postalCode'); var houseNumber = component.get('v.houseNumber'); // Run search here... var dataChanged = component.getEvent('dataChanged'); dataChanged.setParams({ longitude: 1234, latitude: 5432 }); dataChanged.fire(); } })
The handleProvide method is responsible for locating the data and then firing another component, dataChanged. In the example code above we’re not searching for coordinates for simplicity and are immediately firing the dataChanged event with dummy data.
Setting up the map component
Now we can set up our new map component to work with providers. The first part we need to do is when the component initializes is to find the provider within the coordinates attribute. So first we’ll set up the attribute and add a handler for the init event to call a function in our controller.
<aura:attribute name="coordinates" type="Aura.Component[]" access="public" /> <aura:handler name="init" value="{!this}" action="{!c.doInit}" />
In our map controller we need to create the doInit function which will find the provider and attach an event handler to it, then we’ll ask the provider to provide the data.
({ doInit: function(component){ var provider = component.get('v.coordinates')[0]; // Attach an event handler for data changes provider.addHandler('dataChanged', component, 'c.handleDataChanged'); // Now ask for the provider to provide the data provider.getEvent('provide').fire(); }, handleDataChanged: function(component, event){ // We'll define this in a moment... } })
On line 6 above is where we add the event handler to our found provider and it will redirect the dataChanged event back into the handleDataChanged function in the map component. Let’s define that function:
({ ... handleDataChanged: function(component, event, helper){ var params = event.getParams(); console.log('longitude', params.longitude); console.log('latitude', params.latitude); // helper.moveMapTo(params.longitude, params.latitude); } })
And there we go! We’re done!
We’ve now separated out the logic for finding the coordinates for a given address from the displaying a map within pins displaying the coordinates it receives.
Maximising reuse
So far we’ve covered a great deal, but I want to show you one last thing to help with maximising reuse across your components. All of the examples above show how to use the design pattern with one map and one coordinates provider. Now I’m going to show you how you can use the same source of the data (coordinates) and reuse them across multiple map instances.
The first thing we will do is set up a new attribute and populate it with a value immediately, which will be an instance of the CoordinatesProvider.
<aura:attribute name="coordinates" type="Aura.Component[]"> <c:CoordinatesProvider postalCode="{!v.postalCode}" houseNumber="{!v.houseNumber}" /> </aura:attribute>
We can now pass the attribute we’ve defined into multiple map instances.
<c:Map coordinates="{!v.coordinates}" /> <c:Map coordinates="{!v.coordinates}" /> <c:Map coordinates="{!v.coordinates}" />
In a few lines of code we’re now reusing the same data set across three map instances.
It’s a map?! How am I going to integrate this?
Just in case you’re wondering why should I use this approach in your application, then here are some other use cases which might help you:
- Generic datagrid component.
- Generic charts component.
- Custom record detail component.
Above I’ve defined three components which should offer maximum reusability. A datagrid component shouldn’t depend on how to load data, it should only worry about displaying data. The generic charts component is the same, displaying of nice charts. Custom record detail component could be a component which displays a record in a custom layout, perhaps for a community.
Where can you see yourself using this approach in your application?
Leave a Reply