1

Locker Service has again broken some key functionality of our Lightning Components.

In our Javascript controller we are dynamically creating some DOM elements, for example:

// Dynamically create a button
var button = document.createElement('div');


// Add a click handler to our new button
button.addEventListener('click', function (e) {
       // do something useful                    
});


// Output our new button
// v.html is output using <aura:unescapedHtml> component
component.set('v.html', button.innerHTML)

Our button markup in this case is output nicely, but the click handler is never fired. How can we dynamically create markup that does NOT exist in the .cmp, and attach click handlers so that we can dynamically interact? This is critical for us.

The Locker API documentation says that addEventListener is a supported method for SecureElement. (http://documentation.auraframework.org/lockerApiTest/secureElement.app?aura.mode=DEV)

We've been using this approach for a long time with success. Numerous iterations and additions to Locker Service did not break this until the most recent update in Summer '17.

2
  • This example could easily be replicated using a supported technique (ie creating a ui:button or a lightning:button and then assigning an event handler using component.getReference() - is there another use case that involves something a bit trickier? – Caspar Harmer Jul 10 '17 at 18:57
  • Yes it's a trickier use case. We're fetching a rendering from the server (raw HTML), and processing it (attaching handlers etc) before dumping it out into the component. This approach worked before API 40, but all of a sudden all the event handlers are never hit. – Jesse McGeachie Jul 10 '17 at 22:27
3

This isn't related to LockerService: it's due to the difference between DOM (object) and HTML (string).

There seems to be 2 errors in the code:

component.set('v.html', button.innerHTML)
  1. Using innerHTML inserts the content, not the <div> itself on which the handler is attached.

  2. Using innerHTML converts your DOM elements to an HTML string and JavaScript handlers are lost.

Solution: if you create objects (DOM nodes + JS function), you can't convert them back to HTML.

This example app and controller does work:

<aura:application >
    <aura:attribute name="html" type="Object"/>
    <ui:button label="start" press="{!c.press}"/>
    <div aura:id="container"/>
</aura:application>


({
    press : function(component, event, helper) {

        var button = document.createElement('div');
        button.appendChild(document.createTextNode("test"));
        button.addEventListener('click', function (e) {
            alert(window+'');                 
        });
        component.find("container").getElement().appendChild(button);
    }
})

With LockerService "on" (v40+), you get an alert showing "SecureWindow" as expected.

There are many other ways to insert DOM in components. You can look at afterRender() if you want to do it on init.

2
  • I can't understand how this could ever have worked – Caspar Harmer Jul 10 '17 at 20:32
  • I used a much simpler example than what I'm actually doing in practise, but this cleared up a few things for me and will work I think. But this doesn't explain how using innerHTML with event handlers would have worked before API 40.0. – Jesse McGeachie Jul 10 '17 at 22:25
0

what you can do is create a seprate component lets say a

<aura:attribute name="onClick" type="Aura.action"/>
<div onclick={!v.onClick}>
</div>

In onclick Attribute you can dynamically pass js controller method. then in your main component

<aura:attribute name="buttonDiv" type="Aura.component[]"/>
{!v.buttonDiv}

Now you have created dynamically create namespace:dynamicButton. To do so you have to write js as

$A.createComponent(
            "namespace:dynamicButton",
            {
                "onclick": cmp.getReference("c.handlePress")
            },
            function(newButton, status, errorMessage){
                //Add the new button to the body array
                if (status === "SUCCESS") {

                    cmp.set("v.buttonDiv", newButton);
                }
                else if (status === "INCOMPLETE") {
                    console.log("No response from server or client is offline.")
                    // Show offline error
                }
                else if (status === "ERROR") {
                    console.log("Error: " + errorMessage);
                    // Show error message
                }
            }
        );

For reference :- https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/js_cb_dynamic_cmp_async.htm https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/ref_attr_types_aura.htm

0

Just to elaborate a little bit:

  1. Defining event handlers dynamically in html is going to be blocked by CSP. If you want a js event handler, define it in javascript using addEventListener as JF Paradis recommended. This is also a good practice as inline event handlers in html is mixing markup with code and becomes difficult to maintain.

  2. Even if it wasn't blocked by CSP, rendering unescapedHTML is filtered with DOMPurify which will strip the event handlers. UnescapedHTML is meant to render sanitized html but not active content. Keep your html separate from your javascript.

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.