[SalesForce] Lightning Locker Service and object fields at different levels

I'm seeing some unexpected behaviour with the locker service enabled when updating two fields on an object, one of which is nested inside a sub-object.

I have a lightning application that initialises a JavaScript object and passes references to fields on the object, and the object itself, as attributes to a contained component:

App Markup

<aura:application >
    <aura:attribute name="obj" type="object" />
    <aura:handler name="init" value="{!this}" action="{!c.init}" />

    Top level value: <ui:outputText value="{!v.obj.value1}" /> <br/>
    Nested value: <ui:outputText value="{!v.obj.sub.value2}" /> <br/>

    <c:MultiFieldTest obj="{!v.obj}" value1="{!v.obj.value1}" value2="{!v.obj.sub.value2}" />

    <br/>

    <button onclick="{!c.showObj}">Show Object</button>
</aura:application>

app controller

({
    init : function(component, event, helper) {
        helper.init(component);
    },
    showObj : function(component, event, helper) {
        helper.showObj(component);
    }
})

app helper:

({
    init : function(cmp) {
        var obj={value1:'', sub:{value2:''}};
        cmp.set('v.obj', obj);
    },
    showObj : function(cmp) {
        alert('Object = ' + JSON.stringify(cmp.get('v.obj')));
    }
})

So the concept is there is an object with the structure:

{value1:'', 
 sub:{
     value2:''}
}

and I want to pass value1 and sub.value2 as attributes to a component that will update them. However, to ensure that the changes are propagated up and down the food chain (as per : Lightning Components – SObject attribute change event only sent to descendants) I am also passing in the object itself. Essentially I want the contained component to update a couple of fields and propagate the changes without having to know which fields it is updating.

The containing component markup:

<aura:component>
    <aura:attribute name="value1" type="String" />
    <aura:attribute name="value2" type="String" />
    <aura:attribute name="obj" type="object"/>

    <button onclick="{!c.setValues}">Set Values</button>

</aura:component>

controller:

({
    setValues : function(component, event, helper) {
        helper.setValues(component);
    }
})

helper:

({
    setValues : function(cmp) {
        var obj=cmp.get('v.obj');
        console.log('Obj before = ' + JSON.stringify(obj));

        cmp.set('v.value1', 'Value 1');
        cmp.set('v.value2', 'Value 2'); 

        obj=cmp.get('v.obj');
        console.log('Obj after = ' + JSON.stringify(obj));

        cmp.set('v.obj', obj);
    }
})

So when the user clicks the button, strings are set into the value1/value2 attributes and the underlying object 'reset' into the page. I then click on the 'Show Object' button, and the results are as expected with the locker service deactivated.

However, when I activate the locker service, only the second update 'sticks' and obj.value1 is empty, although both ui:outputText elements show the value1/value2 details as expected. What appears to be happening is that setting the first attribute updates the page as expected, and the object, but setting the second attribute updates the page and an earlier version of the object, minus the value1 details.

However, if I don't get the obj attribute at the beginning of the setValues method, it works:

setValues : function(cmp) {
    // var obj=cmp.get('v.obj'); <---- removed this

    cmp.set('v.value1', 'Value 1');
    cmp.set('v.value2', 'Value 2'); 

    var obj=cmp.get('v.obj');
    console.log('Obj after = ' + JSON.stringify(obj));

    cmp.set('v.obj', obj);
}

So pretty easy to work around, but I'm curious as to why this would be necessary with the locker service enabled – there's no secure versions of anything in play and the attributes are all accessible to the component. Its almost like the act of looking at the object fixes a version somewhere, and all updates retrieve that version before applying their update, rather than retrieving the latest version.

Best Answer

There are secure proxies involved here for certain - currently arrays and raw objects are being sent through filtering/wrapping/unwrapping. We just fixed (deploys in the next patch Tuesday) a similar issue in that logic specific to arrays where the initial value was getting "pinned". This looks very similar but I do not think the changes I have queued up for the array issue will address this nested object problem. I'm opening a bug to track this now.

I'd also like to second crmprogdev's sentiment and I'm certain that could be arranged if you are interested in joining the Salesforce team :-) I can tell you it always looks easier from the outside though.

The security and supportability challenge we are going after is massive and has never been solved afaik by any platform. The amount that just works and is secure (and fast) is substantial and the surface area we are covering is huge - the testing matrix includes dozens of browser versions, hundreds of libraries, etc and all of the wild and wacky JavaScript and BOM/DOM fun that has accreted in the world over decades. It's tricky. We've also rolled out access= enforcement simultaneously which has been the actual issue in many of the cases we've investigated. There have also been the usual helping of bugs in any release where thousands of developers have been working on a product for the release.

Related Topic