[SalesForce] Dynamically Created Lightning Components and Attributes

I have a top level lightning component that manages an sobject, which uses nested components to provide additional functionality. The nested components are replaced with dynamically constructed components as required. However, when I pass the sobject as an attribute to the dynamically constructed component, it becomes disconnected from the "version" in the main component, and changes aren't reflected in the ancestor top level component. After some investigation it appears that this also happens with "primitive" attributes, as I've reproduced it with a String.

Here's the top level component:

<aura:component implements="force:appHostable">
<aura:attribute name="acc" type="Account"
                    default="{ 'sobjectType': 'Account',
                            'Name': 'Name'
                             }" />
<aura:handler name="change" value="{!v.acc.Name}" action="{!c.accChanged}"/>    
<aura:handler name="change" value="{!v.str}" action="{!c.strChanged}"/>    

<aura:attribute name="str" type="String" default="Str"/>
<aura:handler name="change" value="{!v.items}" action="{!c.itemsChange}"/>
<div>Level 1 name : {!v.acc.Name}</div>
<div>Level 1 Str : {!v.str}</div>
<div><ui:button press="{!c.changeName}" label="Press to Change"/></div>

<div aura:id="body">

    <bblightning:Level2 acc="{!v.acc}" str="{!v.str}" />

    <div><ui:button press="{!c.swapInThree}" label="Swap in Level 3"/></div>
</div>

This initially displays the Level2 component, and pressing the bottom button swaps in the Level3 component.

Level 1 controller – note the swapInThree function that creates the components and passes the attributes retrieved from the component:

({
changeName : function(component, event, helper) {
    var acc=component.get('v.acc');
    acc.Name='Name from 1!';
    component.set('v.acc', acc);

    component.set('v.str', 'Str from 1!');
},
swapInThree : function(component, event, helper) {
    var acc=component.get("v.acc");
    var str=component.get("v.str");

    $A.createComponent(
        "c:Level3",
        {
            "acc": acc,
            "str": str
        },
        function(newComp) {
            var content = component.find("body");
            content.set("v.body", newComp);
        }
    );
},
accChanged : function (){
    console.log('Level 1 : Account changed');
},
strChanged : function (){
    console.log('Level 1 : String changed');
}
})

The level 2 component:

<aura:component >
    <aura:attribute name="acc" type="Account"/>
    <aura:attribute name="str" type="String"/>

    <aura:handler name="change" value="{!v.acc.Name}" action="{!c.accChanged}"/>    
    <aura:handler name="change" value="{!v.str}" action="{!c.strChanged}"/>    

    <div>Level 2 name : {!v.acc.Name}</div>
    <div>Level 2 Str : {!v.str}</div>
    <div><ui:button press="{!c.changeName}" label="Press to Change"/></div>
</aura:component>

Level 2 Controller – note the mechanism for updating the value on the sobject, as detailed in Lightning Components – SObject attribute change event only sent to descendants:

({
    changeName : function(component, event, helper) {
        var acc=component.get('v.acc');
        acc.Name='Name from 2!';
        component.set('v.acc', acc);
        component.set('v.str', 'Str from 2!');
    },
    accChanged : function (){
        console.log('Level 2 : Account changed');
    },
    strChanged : function (){
        console.log('Level 2 : String changed');
    }

})

Level 3 Component:

<aura:component >
    <aura:attribute name="acc" type="Account"/>
    <aura:attribute name="str" type="String"/>

    <aura:handler name="change" value="{!v.acc.Name}" action="{!c.accChanged}"/>    
    <aura:handler name="change" value="{!v.str}" action="{!c.strChanged}"/>    

    <div>Level 3 name : {!v.acc.Name}</div>
    <div>Level 3 Str : {!v.str}</div>
    <div><ui:button press="{!c.changeName}" label="Press to Change"/></div>
</aura:component>

Level 3 controller, again using the update technique referred to above to get the changes to be sent to ancestors:

({
    changeName : function(component, event, helper) {
        var acc=component.get('v.acc');
        acc.Name='Name from 3!';
        component.set('v.acc', acc);
        component.set('v.str', 'Str from 3!');
    },
    accChanged : function (){
        console.log('Level 3 : Account changed');
    },
    strChanged : function (){
        console.log('Level 3 : String changed');
    }

})

Accessing the page through Salesforce1 displays as expected:

enter image description here

and clicking the 'Press to Change' button on Level2 updates as expected:

enter image description here

with the appropriate console log messages:

enter image description here

Pressing the button to swap in Level3 brings in the correct attribute values:

enter image description here

But if I then press the update button in Level3, the changes don't get pushed to the ancestor Level1 component – the are only applied at Level3 for both the sobject and string:

enter image description here

and the console log shows that no changes were fired to the top level component:

enter image description here

Is there anything I can do to wire the dynamic component attributes to their counterparts on the parent component? At the moment I am working around by firing component events from Level3, but that feels like the sort of thing the framework should be handling for me.

Best Answer

Bob you've probably addressed this already but facing a similar problem, I have had success using the component.getReference() method rather than component.get() to set attribute values on dynamically created components. So instead of:

swapInThree : function(component, event, helper) {
    var acc=component.get("v.acc");
    var str=component.get("v.str");

you could try:

swapInThree : function(component, event, helper) {
    var acc=component.getReference("v.acc");
    var str=component.getReference("v.str");
Related Topic