6

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 a render() function which, given an array of components, will call the render() 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.

  • Why are you doing this is in a renderer? The same behavior can be achieved if you moved your code to the init function – Sebastian Kessel Sep 19 '17 at 16:05
  • I believe I used the phrase "super simplified" at least twice. I am, however, open to any answers demonstrating how I can dynamically create HTML elements using document.createElement which can have lightning components as children. – JDB Sep 19 '17 at 16:08
  • Why are you doing this is in a renderer? The same behavior can be achieved if you moved your code to the init function – Sebastian Kessel Sep 19 '17 at 16:09
  • 1
    I think this is the recommended practice. All direct dom manipulation should be done a renderer. developer.salesforce.com/docs/atlas.en-us.lightning.meta/… – Caspar Harmer Sep 19 '17 at 17:47
  • If you wanna do nested components, I'd recommend doing it via recursive components and then supply the right data so that the framework does the it's magic. – dzh Sep 23 '17 at 9:40
4

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

<aura:component implements="flexipage:availableForAllPageTypes" access="global" >

    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>

    <div aura:id="root"></div>

    <div aura:id="renderBox" style="display: none;"></div>

</aura:component>

Helper

({
    createComponent: function(component, componentType, componentAttributes, targetElement){

        $A.getCallback(function(){
            $A.createComponent(
                "c:wrapper",
                {
                    componentType: componentType,
                    componentAttributes: componentAttributes,
                    targetElement: targetElement,
                },
                function(wrapperComponent, status, errorMessage){
                    if (status === "INCOMPLETE") {
                        return;
                    }
                    else if (status === "ERROR") {
                        return;
                    }

                    var renderBox = component.find("renderBox");
                    var body = renderBox.get("v.body") || [];
                    body.push(wrapperComponent);
                    renderBox.set("v.body", body);
                }
            );
        })();

    },
})

Renderer

({
    'afterRender' : function(component, helper) {
        var mainElement = component.mainElement = document.createElement('div'),
            span1Element = document.createElement('span'),
            span2Element = document.createElement('span');

        mainElement.style.border = 'solid red 8px';
        mainElement.style.margin = '50px 20px 120px 90px';
        mainElement.style.padding = '20px 130px 90px 10px';
        mainElement.style.backgroundColor = 'aqua';

        span1Element.innerText = 'I am not a ';
        span2Element.innerText = 'Lightning Component';
        span2Element.style.fontWeight = 'bold';

        mainElement.appendChild( span1Element );
        mainElement.appendChild( span2Element );

        component.find('root').getElement().appendChild( mainElement );

        // ************************************************************
        // This is the key line of code. I've created DOM elements
        // above, and now I'm going to render a Lightning component
        // and attach it to those DOM elements:
        helper.createComponent(
            // A reference to the parent component:
            component,
            // The type of Lightning component to render:
            'forceChatter:publisher',
            // Attributes to send to the Lightning component to render:
            { context: "GLOBAL", type: "News" }, 
            // The DOM element where the Lightning component should
            // ultimately live:
            mainElement
        );
        // ************************************************************
    }
})

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

<aura:component >

    <!-- Describes the component that we want to render -->
    <aura:attribute name="componentType" type="String"/>
    <aura:attribute name="componentAttributes" type="Object"/>
    <!-- The target element where the component should be moved -->
    <aura:attribute name="targetElement" type="Object"/>

    <!-- This is where our component is initially rendered, and it's where the component "lives" in Lightning's DOM -->
    <div aura:id="holdingPen"></div>

    <aura:handler name="init" value="{!this}" action="{!c.init}"/>

</aura:component>

Controller

({
    init: function(component) {
        $A.createComponent(
            component.type = component.get("v.componentType"),
            component.attributes = component.get("v.componentAttributes"), 
            function(cmp){
                component.find("holdingPen").set("v.body", [cmp]);
            }
        );
    }
})

Renderer

({
    afterRender: function(cmp) {
        if (cmp.moveComplete) return;

        var holdingPen = cmp.getElement(),
            target = cmp.get("v.targetElement");

        if (target && target.appendChild){
            target.appendChild(holdingPen);
            cmp.moveComplete = true;
        }
    }
})

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:

A lightning chatter component rendered within a blue box with a thick red border

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.