As a developer coming to Lightning, one of the immediate questions is:
How can I share code between my components?
Well, there’s a nice summary of two code-sharing options on the Salesforce Developers Blog: Modularizing Code in Lightning Components. But,neither of those quite address the use-case that I want to address:
- Javascript libraries – this is pretty much opting out of the Lightning framework. Lighting is a component oriented framework with some Object Oriented characteristics, so I’d like to use them! I could use Javascript libraries to do anything that anyone else does with them (modules and so on), but it means having blobs of code in static resources and that doesn’t seem very Salesforce.
- A service component – i.e. a custom Lightning component which doesn’t need a UI part, but exists just to wrap up some code. The code is exposed using <aura:method> and results are retrieved via a callback. That’s fine, but it requires the component using my shared code to have a Javascript controller. I want to separate concerns and keep Javascript out of components that don’t need it.
A “Provider” Component
My solution is to use something that I’ve termed a Provider Component. Naming-wise, I’d be interested to know if others are using the same thing under a different name. Or even if I have clashed names with someone calling something else a Provider. The idea is a little similar to the service component, above, but it provides data via variable binding instead of by method calls.
A provider fetches a record or collection of records via Apex. It shares its data via variable binding. It may do some processing on that data before sharing it with whoever is consuming the data. The properties that I want from a Provider Component are:
- Sharing by composition – in Object Oriented design, we tend to favour composition over inheritance. There are multiple reasons for this, but the important one here is that when I bring shared code into my component by composition, it doesn’t stop me from doing more composition to bring in more shared code. If I use inheritance, I only get to choose one component to extend from.
- Efficiency in server calls – if multiple components on the screen need the same information, then it should result in a single server call.
- Simplicity of use – if I use the shared code inside a component with no Javascript controller, I don’t want to add a controller, just to get access to the shared code
- No central coordinator – one way to achieve the sharing of information retrieved via Apex would be to have a container component fetch the records and pass them down to the children. This means the container needs to know too much about the internal requirements of its children, and that the child components cannot be easily re-used in other contexts.
A Provider In Action
Let’s take a concrete example. We have an very simple app where we will display articles of various types. Each article type has its own styling that should be driven by Custom Metadata so that the types can be expanded/modified later without changing the components.
App container:
<aura:application extends="force:slds"> <div> <div class="slds-text-heading_large">Provider Component Demo</div> <c:Article title="Running is simple" summary="All you need is shoes" category="Running" /> <c:Article title="Cycling gets you places" summary="You can go for miles" category="Cycling" /> <c:Article title="Kayaking is exciting" summary="Sometimes you see dolphins" category="Kayaking" /> </div> </aura:application>
The App just contains three instances of the Article component. Each one receives some text and a category name. Note that there is no controller required.
Article component:
<aura:component > <aura:attribute name="title" type="String" /> <aura:attribute name="summary" type="String" /> <aura:attribute name="category" type="String" /> <aura:attribute name="categoryMetadata" type="Category_Metadata__mdt" /> <ltng:require styles="{!$Resource.provider_demo_css}" /> <div class="{!'slds-box slds-p-around_medium background-' + v.categoryMetadata.HTML_Colour__c}"> <c:CategoryMetadataProvider categoryName="{!v.category}" categoryMetadata="{!v.categoryMetadata}" /> <div class="{!'slds-text-title ' + (v.categoryMetadata.Invert_Text_Colour__c ? 'slds-text-color_inverse' : '')}">{!v.title}</div> <div class="{!v.categoryMetadata.Invert_Text_Colour__c ? 'slds-text-color_inverse' : ''}">{!v.summary}</div> </div> </aura:component>
The Article component displays a card-like item for an Article. It has no controller, but it does have an instance of the CategoryMetadataProvider. We use two attributes on the provider:
categoryName
is an input parameter (Lightning doesn’t make the distinction, but it’s useful to think of it that way) where we pass in the category name that we require information about. categoryMetadata
is an output parameter (again, only output in the way we think about it) where we will receive the metadata about the given category. By the magic of attribute binding, the categoryMetadata
attribute in Article will be updated when theCategoryMetadataProvider updates its categoryMetadata
attribute (note that the names don’t have to match, it just tends to make sense to use the same name).
The colours are driven by CSS from a static resource:
/* From http://www.colourlovers.com/palette/4509719/Hazul_5 */ .background-old-newspaper { background:
#C7B300
; } .background-wheres-the-key { background:
#B8BB86
; } .background-devils-trapezoid { background:
#003447
; } .background-pastel-skyblue { background:
#CAE4F0
; }
And the metadata looks like this:
[ { "attributes":{ "type":"Category_Metadata__mdt", "url":"/services/data/v40.0/sobjects/Category_Metadata__mdt/m00580000005538AAA" }, "Id":"m00580000005538AAA", "DeveloperName":"Cycling", "HTML_Colour__c":"old-newspaper", "Invert_Text_Colour__c":false }, { "attributes":{ "type":"Category_Metadata__mdt", "url":"/services/data/v40.0/sobjects/Category_Metadata__mdt/m0058000000553IAAQ" }, "Id":"m0058000000553IAAQ", "DeveloperName":"Kayaking", "HTML_Colour__c":"devils-trapezoid", "Invert_Text_Colour__c":true }, { "attributes":{ "type":"Category_Metadata__mdt", "url":"/services/data/v40.0/sobjects/Category_Metadata__mdt/m0058000000553DAAQ" }, "Id":"m0058000000553DAAQ", "DeveloperName":"Running", "HTML_Colour__c":"wheres-the-key", "Invert_Text_Colour__c":false } ]
So, if we want to add a new article type (or modify an existing one), we can do that by making changes to the CSS and/or the Custom Metadata with no need to change the Lightning Components.
Finally, what’s in the CategoryMetadataProvider component?
<aura:component controller="CategoryMetadataProviderController"> <aura:attribute name="categoryMetadataByName" type="Object" /> <aura:attribute name="categoryName" type="String" /> <aura:attribute name="categoryMetadata" type="Category_Metadata__mdt" /> <aura:handler name="init" value="{!this}" action="{!c.doInit}"/> <aura:handler name="change" value="{!v.categoryName}" action="{!c.updateCategoryMetadata}"/> <aura:handler name="change" value="{!v.categoryMetadataByName}" action="{!c.updateCategoryMetadata}"/> </aura:component>
We can see that there is no visible part of the component, just attributes and event handlers.
({ doInit : function(component, event, helper) { var getCategoryMetadataAction = component.get('c.getCategoryMetadata'); getCategoryMetadataAction.setStorable(); getCategoryMetadataAction.setCallback(this, function(response){ var state = response.getState(); if (state === "SUCCESS") { var categoryMetadataList = response.getReturnValue(); var categoryMetadataByName = {}; categoryMetadataList.forEach(function(thisCategoryMetadata) { categoryMetadataByName[thisCategoryMetadata.DeveloperName] = thisCategoryMetadata; }); component.set('v.categoryMetadataByName', categoryMetadataByName); } }); $A.enqueueAction(getCategoryMetadataAction); }, updateCategoryMetadata : function(component, event, helper) { var categoryMetadataByName = component.get('v.categoryMetadataByName'); if(categoryMetadataByName) { var categoryName = component.get('v.categoryName'); if(categoryName) { component.set('v.categoryMetadata', categoryMetadataByName[categoryName]); } } } })
In the
doInit
function, we use the Apex controller to load the custom metadata (ensuring to set that as a storeable action, so that Lightning will cache the results and avoid calling the server multiple times for the same data). We then store the results in an object, so that we can access them like a Map using DeveloperName as a key.
Since the updateCategoryMetadata
function is bound to changes on both categoryMetadataByName
and categoryName
, we don’t need to call it after doInit
. It gets called automatically when categoryMetadataByName
is set. All that updateCategoryMetadata
does is check that we have the categoryMetadataByName
object and categoryName
initialised, then set the correct value for categoryMetadata
.
It doesn’t matter which value comes in first: categoryMetadataByName
or categoryName
. The right value is settled for categoryName
in the end.
As expected, the Apex part is trivial:
public with sharing class CategoryMetadataProviderController { @AuraEnabled public static List<Category_Metadata__mdt> getCategoryMetadata() { return [SELECT Id, DeveloperName, HTML_Colour__c, Invert_Text_Colour__c FROM Category_Metadata__mdt]; } }
By using a Provider pattern, we have wrapped up the logic of accessing an Apex controller to retrieve Category Metadata records. Not only does this mean that we have reusable code, we have also separated concerns and gained run-time efficiency.
If we had to write an ArticleHeader component to go with our Article component, then ArticleHeader would also need to know about Category Metadata. It could get that information by just including an instance of CategoryMetadataProvider. No need to write more Javascript. And, if Article and ArticleHeader are used on the same page, then the fact that we are use storeable actions means that Lightning will cache the values and, thus, only make the server call once.
Isn’t this just Lightning Data Service?
Well, yes, Lightning Data Service is an instance of this pattern (and, in some ways, more). But Data Service is not GA. Safe Harbour, and all that: write your own Provider until Salesforce make Data Service GA. And when Data Service is released, but only supports single records? Write your own Provider. Have more complex requirements that Data Service doesn’t support? Write your own Provider.
The fact that Salesforce are doing much the same thing as me here makes me think that it’s either a good idea, or obvious. But, most good ideas are obvious in retrospect. And, if I’m being obvious, then that’s fine!