Given a Lightning Component with a custom renderer, is it possible to arbitrarily attach another Lightning Component (for example, a lightning:button
component) to the DOM generated by the parent component's custom renderer?
I know that it is possible to attach dynamically created components to other components defined in markup by setting the v.body
property, but when using a custom renderer there does not appear to be a v.body
property for elements created using the document.createElement
function. Theoretically, I could use the child component's getElement
function, except that I need to force the child component to render before I can access the getElement
function.
Following is a super-simplified example. Note that the example does not justify the dynamic button creation or the custom renderer – that's not the question. The question is, given the following, is it possible to somehow force the button to render, or otherwise attach it to the DOM created by the parent component's custom renderer?
Component Markup
<aura:component implements="force:appHostable,flexipage:availableForAllPageTypes,forceCommunity:availableForAllPageTypes,force:hasRecordId" access="global">
<aura:attribute name="dynamicButtons" type="Aura.Component[]"/>
<aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
</aura:component>
Component Controller
({
'doInit' : function(cmp) {
$A.createComponents([
["lightning:button", {
"aura:id": "dynamicButton",
"label": "Press Me",
"onclick": cmp.getReference("c.handlePress")
}],
],
function(newButtons, status, errorMessage){
if (status === "SUCCESS") {
//Add the new button to the body array
cmp.set('v.dynamicButtons', newButtons[0]);
}
}
);
},
'handlePress' : function(cmp) {
console.log("button pressed");
}
})
Component Renderer
({
'render' : function(component) {
this.superRender();
var mainElement = document.createElement('div'),
spanElement = document.createElement('span');
spanElement.innerText = 'Hello World.'
mainElement.appendChild( spanElement );
// Note that this is a super-simplified custom renderer.
// In reality, there are many third-party libraries that
// generate the DOM which cannot be easily converted to
// generate lightning components.
component.mainElement = mainElement;
return mainElement;
},
'afterRender' : function(component){
var dynamicButton = component.get('v.dynamicButtons')[0];
// How do I render the dynamic button within my div, after the span?
}
})
Out of curiosity, I decided to look at how aura was handling low-level rendering for its own components.
-
Aura documentation hints at the existence of a
renderingService
(see the "js://renderer" tab), but it does not appear to be available in Lightning, (at least in Locker Service). -
The
renderingService
has arender()
function which, given an array of components, will call therender()
function on each component it's given and, if supplied, will append the low-level DOM elements directly to a parent DOM element. Again, though, the renderingService appears to be a low-level service that is not available to custom Lightning Component developers. -
It looks like maybe the
renderingService.renderFacet()
is the real key, here, as it both handles the actual rendering of the low-level DOM for a child component and associates the child component with the parent component so that unrendering behaves correctly. Maybe. If I'm reading the code right.
Best Answer
It's Possible!
In a previous version of this answer, I stated that this was not possible. After working with some Lightning Developers at Salesforce (many thanks, JF & Jaswinder), we were able to work out how to get Lightning components to work with a component's custom renderer.
Note that this is not exactly canon, and we're working on the edge (maybe slightly beyond) of what's supported, so if you go this route, you are accepting the responsibility to maintain it in the future if some low-level change in Lightning suddenly breaks everything. Then again... isn't that the nature of software development? 😉
The example below was built with API version 41.
Container Component
The container component is the component that has my manually manipulated DOM elements. It's where I want to display other Lightning Components.
Component XML
Helper
Renderer
Wrapper Component
The documentation is pretty clear that you cannot access the
getElement()
function of a component from another namespace, but I wondered if I might be able to wrap a Lightning Component within a wrapper component that's in my namespace. This would give me access to the element and might allow me to move things around. I could render the desired component within the "renderBox" div and then move it into my custom DOM.Component XML
Controller
Renderer
Result
It works! Of course, there are still some issues here that need to be addressed. For example, you are now responsible for handling the dynamically generated component's lifecycle. If you decide to remove it or it's parent from the DOM, then you need to explicitly call destroy on the dynamic component. How exactly you handle that is going to depend on your architecture and isn't covered by the example above.
Here's a screenshot of my decidedly un-lovely page: