I've been working on a heavily customised Lightning Community where a lot of the UI effects are achieved using Javascript supplied by a design agency. These are included in the Lightning Community by adding them as static resources, then initialising them after the component has rendered.
One component that I've been having difficulty with is the navigation menu. As per the documentation (https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/aura_compref_forceCommunity_navigationMenuBase.htm), I extended the forceCommunity:navigationMenuBase component.
With that component, I need to initialise some JS after the menu has been rendered, so I used a custom renderer (the supplied JS doesn't work with v40, so I can't use the render event). The problem is that the menu items are not ready when the component are first rendered. I can only assume that forceCommunity:navigationMenuBase is doing an Apex call to get the menu.
It seems that when the menuItems are loaded by forceCommunity:navigationMenuBase, that does not trigger a rerender event in component.
To investigate, I built some small test components. A GetHelloWorld component which just fetches a string via Apex:
<aura:component controller="GetHelloWorldController" extensible="true">
<aura:attribute name="helloWorld" type="String" />
<aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
{!v.body}
</aura:component>
({
doInit : function(component, event, helper) {
var getHelloWorldAction = component.get('c.getHelloWorld');
getHelloWorldAction.setCallback(this, function(response){
var state = response.getState();
if (state === "SUCCESS") {
console.log('Setting helloWorld');
component.set('v.helloWorld', response.getReturnValue());
}
});
$A.enqueueAction(getHelloWorldAction);
}
})
({
rerender : function(cmp, helper){
console.log('GetHelloWorld.rerender');
this.superRerender();
},
render : function(cmp, helper) {
console.log('GetHelloWorld.render');
var ret = this.superRender();
return ret;
},
afterRender: function (component, helper) {
console.log('GetHelloWorld.afterRender');
this.superAfterRender();
}
})
And then two components two display the result of that. First, DisplayHelloWorldExtension:
<aura:component >
<aura:attribute name="helloWorld" type="String" />
{!v.helloWorld}
</aura:component>
({
rerender : function(cmp, helper){
console.log('DisplayHelloWorldComposition.rerender');
this.superRerender();
},
render : function(cmp, helper) {
console.log('DisplayHelloWorldComposition.render');
var ret = this.superRender();
return ret;
},
afterRender: function (component, helper) {
console.log('DisplayHelloWorldComposition.afterRender');
this.superAfterRender();
}
})
Second, DisplayHelloWorldComposition:
<aura:component extends="c:GetHelloWorld">
{!v.helloWorld}
</aura:component>
({
rerender : function(cmp, helper){
console.log('DisplayHelloWorldExtension.rerender');
this.superRerender();
},
render : function(cmp, helper) {
console.log('DisplayHelloWorldExtension.render');
var ret = this.superRender();
return ret;
},
afterRender: function (component, helper) {
console.log('DisplayHelloWorldExtension.afterRender');
this.superAfterRender();
}
})
And then a host app:
<aura:application >
<aura:attribute name="helloWorld" type="String" />
<c:DisplayHelloWorldExtension />
<c:GetHelloWorld helloWorld="{!v.helloWorld}" />
<c:DisplayHelloWorldComposition helloWorld="{!v.helloWorld}" />
</aura:application>
When I run this app, the log messages are:
DisplayHelloWorldExtension.js:18 DisplayHelloWorldExtension.render
GetHelloWorld.js:29 GetHelloWorld.render
GetHelloWorld.js:29 GetHelloWorld.render
DisplayHelloWorldComposition.js:18 DisplayHelloWorldComposition.render
DisplayHelloWorldExtension.js:23 DisplayHelloWorldExtension.afterRender
GetHelloWorld.js:34 GetHelloWorld.afterRender
GetHelloWorld.js:34 GetHelloWorld.afterRender
DisplayHelloWorldComposition.js:23 DisplayHelloWorldComposition.afterRender
GetHelloWorld.js:15 Setting helloWorld
GetHelloWorld.js:15 Setting helloWorld
GetHelloWorld.js:25 GetHelloWorld.rerender
DisplayHelloWorldComposition.js:14 DisplayHelloWorldComposition.rerender
So, we can see that only the composition version is being fully rerendered. I guess Lightning is doing a partial rerender on the extension version - just rerending the part that has changed. This makes a kind of sense. Although it could reasonably be argued that the whole of DisplayHelloWorldExtension should be rerendered, Lightning doesn't do that.
The question (after all this, there had to be one), is:
Is it wrong to use the forceCommunity:navigationMenuBase by composition?
I've written my navigation component to directly create an instance. It then shares the menuItems by variable-binding to another component which loads the external libraries. The library component then calls back to my custom navigation component when the dependencies are satisfied, and on rerender (you'll just have to trust that one, too much code to post here). Since menuItems are passed into the library component as an attribute, a change in them rerenders the library component. Since they are also an attribute of custom navigation component, a change in them rerenders the whole component:
<aura:attribute name="menuItems" type="Object[]" required="false"/>
<c:ExternalLibs initJsComponent="{!this}"
hasOtherDependency="true"
otherDependencyLoaded="{!and(v.recordTypeMetadatas.length > 0, v.menuItems.length > 0)}"
)}"/>
<forceCommunity:navigationMenuBase aura:id="menuBase" menuItems="{!v.menuItems}" />
({
onClick : function(component, event, helper) {
var id = event.target.dataset.menuItemId;
if (id) {
component.find('menuBase').navigate(id);
}
},
})
What happens here is that the initialisation is called once when menuItems is first populated - but it fails because the DOM elements for the menuItems are not yet available. Then, it gets called again for the rerender event happens for the navigation component and its children - at this point the initialisation succeeds.
As seems to happen often with Lightning, it works, but I'm not sure whether or not I can rely on it continuing to work.
--edit for the sake of proving that this is actually a problem--
John Au claims below that this whole scenario isn't a problem, and rerender is called after the menuItems are loaded in a custom navigation component. So here's a complete executable demonstration. Start with navigation menu extension as in the developer docs:
<aura:component extends="forceCommunity:navigationMenuBase" implements="forceCommunity:availableForAllPageTypes" access="global">
<ul onclick="{!c.onClick}">
<aura:iteration items="{!v.menuItems}" var="item" >
<aura:if isTrue="{!item.subMenu}">
<li class="nav-menu-item">{!item.label}</li>
<ul>
<aura:iteration items="{!item.subMenu}" var="subItem">
<li><a data-menu-item-id="{!subItem.id}" href="">{!subItem.label}</a></li>
</aura:iteration>
</ul>
<aura:set attribute="else">
<li class="nav-menu-item"><a data-menu-item-id="{!item.id}" href="">{!item.label}</a></li>
</aura:set>
</aura:if>
</aura:iteration>
</ul>
</aura:component>
Add a custom renderer:
({
rerender : function(component, helper){
var menuItems = component.get('v.menuItems');
if (menuItems.length > 0) {
console.log("[menuItems.length > 0]document.getElementsByClassName('nav-menu-item').length");
console.log(document.getElementsByClassName('nav-menu-item').length);
}
this.superRerender();
console.log('NavMenuTestRenderer.rerender after superRerender()');
console.log("document.getElementsByClassName('nav-menu-item').length");
console.log(document.getElementsByClassName('nav-menu-item').length);
},
render : function(component, helper) {
var ret = this.superRender();
console.log('NavMenuTestRenderer.render after superRender()');
return ret;
},
afterRender: function (component, helper) {
this.superAfterRender();
console.log('NavMenuTestRenderer.afterRender after superAfterRender()');
console.log("document.getElementsByClassName('nav-menu-item').length");
console.log(document.getElementsByClassName('nav-menu-item').length);
}
})
Load the community and observe the console log statements from the menu component:
components/c/NavMenuTest.js:25 NavMenuTestRenderer.render after superRender()
components/c/NavMenuTest.js:30 NavMenuTestRenderer.afterRender after superAfterRender()
components/c/NavMenuTest.js:31 document.getElementsByClassName('nav-menu-item').length
components/c/NavMenuTest.js:32 0
Judging by the log, rerender is not called and the menuItem elements are not available to be modified in the DOM.