14

I'm seeing some unexpected behaviour when passing an SObject attribute through multiple descendant components, in that when I update a field on the SObject the change event is fired for each descendant component, but not the ancestors.

I've dwindled this down to the following component nesting:

Level 1 -> Level 2 -> Level 3

I have two attributes - one an account SObject and one a regular String. I set the default value for these in my Level 1 component, and pass them as attributes to Level 2 and thence to Level 3.

Here are the three components and their controllers - they are pretty simple in terms of what they do and all provide much the same functionality:

Level 1 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>

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

Level 1 Controller:

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

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>

    <bblightning:Level3 acc="{!v.acc}" str="{!v.str}" />
</aura:component>

Level 2 Controller:

({
    changeName : function(component, event, helper) {
        component.set('v.acc.Name', 'Name from 2!');
        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 :

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

})

Accessing Level 1 shows the following output, which the default attributes are being passed correctly through at setup:

enter image description here

Pressing the level 1 button correctly updates the attributes and cascades down through the descendants:

enter image description here

and the console.log output shows the change events firing as expected:

enter image description here

However, reloading the page and pressing the Level 3 button only updates the String attribute in the ancestors, not the Account:

enter image description here

and the console.log output confirms that the change event for the Account object is not being received by the ancestors:

enter image description here

My initial thought was that the issue here was that I'm changing a field on a complex object which doesn't fire the change event as that is tied to the object reference itself, but that doesn't match up with the fact that it does fire to descendants through multiple levels.

I'm currently working around this by passing the individual fields through as attributes, but it means parent component(s) has to know more about the workings of the child component(s) than I'd like.

  • Did you see this very recent SF Dev Blog Post titled Understanding System Events in Lightning Components? I've only glanced at it not studied or read it in depth, but you might find something useful to help or guide you to a solution. – crmprogdev Jun 16 '15 at 19:12
  • I did look at that post - it suggests the change event should be fired up and down the components, but the examples weren't sobjects. – Bob Buzzard Jun 16 '15 at 19:19
  • In the Summary it also said 1. With multiple components, system events are triggered in phases. 2. The order in which events are sent across components differs a lot. Some start from top-level component first and some from innermost child component. 3. If you are listening to “change” event or “doneRendering” event and doing lot of work in those handlers, then you might hit performance issues. The 1st two are what I thought most relevant. – crmprogdev Jun 16 '15 at 19:37
  • The issue is that all events are triggered for 'primitives', but that isn't the case for objects. I can see the different phases for the primitive, which basically means that the order changes and you may receive multiples. – Bob Buzzard Jun 17 '15 at 6:45
15

Thanks Bob.

I can confirm the issue you are reporting: change handlers are not fired up the chain when members of attribute type "object" are modified. Here is a workaround:

Change:

component.set('v.acc.Name', 'Name from 3!');

To:

var acc = component.get('v.acc');
acc.Name = 'Name from 3!';
component.set('v.acc', acc);

And everything will work.

Here is why you are seeing this behavior, and why the workaround is working:

  • The rule to remember: all changes are propagated down, but only propagated up when they affect the root of the attribute.

  • About the workaround: because component.get('v.acc') is returning a pointer to the object (this is JavaScript after all), we are not not copying the data, and it is not less efficient than calling component.get('v.acc.Name'). Sure it is more verbose, but not less efficient!

  • 1
    Thanks! I'm fine with a couple more lines of code if it means I can pass objects through my component graph. Just to clarify my understanding, essentially this replaces the 'acc' attribute reference with itself, which the framework then propagates up because the 'acc' attribute has changed, rather than a field of that object. – Bob Buzzard Jun 17 '15 at 8:08
  • You are correct. – JF Paradis Jun 29 '16 at 21:39
  • Stating gthe obvious: this also affects direct data binding with input fields where value="{!v.parentObjet.attribute}". Currently changes to such input fields will not trigger change events up the chain. – Marco Kerwitz Jun 11 '17 at 13:41
  • Does data binding work when you pass an attribute to dynamically created component? – Tyler Zika Jul 19 '18 at 3:44

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.