[SalesForce] Data binding for different attribute types in parent & child components under locker service

With a lack of documentation I'm trying to understand how data binding works under locker service, when using different types in <aura:attribute type="..."> to pass values between parent & child components.

Before locker everything worked beautifully, I was able to pass complex type into child component, manipulate it there an still have the same instance in the parent object, which for me enabled nice capabilities of code structuring and decomposition.

Now I know that "Secure Object" proxy is introduced. I've seen numerous questions asked here related to this, but I'd really appreciate if someone can answer what is a bug, and what will be working in target solution.

Below I tried to describe 4 scenarios and my observations. What I did are two simple components – parent and child. Parent has one attribute, passed to child. Child component has input field that allow to modify this attribute. This is how it looks:

enter image description here

Test 1 – Strings

For Strings, everything seems to work fine. Console.log displays proper value for both parent & child components. Binding seems to be done live. When you type in value in child component inputText, it's displayed live in the parent component output: Parent: {!v.testAtr}

Parent Component

<aura:component>
   <aura:attribute name="testAtr" type="String" access="global"/>

   Parent: {!v.testAtr} <a onclick="{!c.logTestAtr}">LOG PARENT</a>
   <br></br><br></br>
   <c:childCmp testAtr="{!v.testAtr}"/>

</aura:component>

Parent Component Controller:

logTestAtr : function(component) {
    console.log(component.get("v.testAtr"));
}

Child Component

<aura:component>
   <aura:attribute name="testAtr" type="String" access="global"/>

   child: <ui:inputText value="{!v.testAtr}"/>  
   <a onclick="{!c.logTestAtr}">LOG CHILD</a>

</aura:component>

Child Component Controller:

logTestAtr : function(component) {
    console.log(component.get("v.testAtr"));
}

Test 2 – Maps (with default value)

For Maps, when you specify default value for data attribute – almost all works exactly like for strings, but the data binding in the parent component is not actualized when you change value in child component. So in child inputText you have 'test', when you change it to 'test2' console.log from parent will show you 'test2', but output: Parent: {!v.testAtr} will still show old 'test' value.

Parent Component

<aura:component>
   <aura:attribute name="testAtr" type="Map" default="'data': 'test'" access="global"/>

   Parent: {!v.testAtr.data} <a onclick="{!c.logTestAtr}">LOG PARENT</a>
   <br></br><br></br>
   <c:childCmp testAtr="{!v.testAtr}"/>

</aura:component>

Parent Component Controller:

logTestAtr : function(component) {
    console.log(component.get("v.testAtr").data);
}

Child Component

<aura:component>
   <aura:attribute name="testAtr" type="Map" access="global"/>

   child: <ui:inputText value="{!v.testAtr.data}"/>  
   <a onclick="{!c.logTestAtr}">LOG CHILD</a>

</aura:component>

Child Component Controller:

logTestAtr : function(component) {
    console.log(component.get("v.testAtr").data);
}

Test 3 – Maps (without setting default value)

Now, what happens if you don't specify default="'data': 'test'" in the parent attribute, just default="{}" to avoid null pointer? Console.log from both parent & child are showing undefined, even after putting the value into inputText.

Parent Component

<aura:component>
   <aura:attribute name="testAtr" type="Map" default="{}" access="global"/>

   Parent: {!v.testAtr.data} <a onclick="{!c.logTestAtr}">LOG PARENT</a>
   <br></br><br></br>
   <c:childCmp testAtr="{!v.testAtr}"/>

</aura:component>

Parent Component Controller:

logTestAtr : function(component) {
    console.log(component.get("v.testAtr").data);
}

Child Component

<aura:component>
   <aura:attribute name="testAtr" type="Map" access="global"/>

   child: <ui:inputText value="{!v.testAtr.data}"/>  
   <a onclick="{!c.logTestAtr}">LOG CHILD</a>

</aura:component>

Child Component Controller:

logTestAtr : function(component) {
    console.log(component.get("v.testAtr").data);
}

Test 4 – Objects

Now – the same case as Test 2 – but we use type="Object" now. I would expect simillar behavior as Map, but here – I get undefined all the times in console.log

Parent Component

<aura:component>
   <aura:attribute name="testAtr" type="Object" default="'data': 'test'" access="global"/>

   Parent: {!v.testAtr.data} <a onclick="{!c.logTestAtr}">LOG PARENT</a>
   <br></br><br></br>
   <c:childCmp testAtr="{!v.testAtr}"/>

</aura:component>

Parent Component Controller:

logTestAtr : function(component) {
    console.log(component.get("v.testAtr").data);
}

Child Component

<aura:component>
   <aura:attribute name="testAtr" type="Object" access="global"/>

   child: <ui:inputText value="{!v.testAtr.data}"/>  
   <a onclick="{!c.logTestAtr}">LOG CHILD</a>

</aura:component>

Child Component Controller:

logTestAtr : function(component) {
    console.log(component.get("v.testAtr").data);
}

I'd appreciate if somebody can comment on this, or at least direct me to some further reading.

Thank you.

Best Answer

I think that after spending few hours I found the answer for the above. Still not sure if it's a bug or a feature of the framework, but definately it has something to do with how secure objects are constructed.

You need to make sure all possible properties for the object are defined (even if null) when you call component.set("v.obj", obj); for the first time.

Here is example component & controller, with one simple attribute (initialized on init), and two buttons calling log & set functions of controller:

<aura:component >
    <aura:attribute name="testAtr" type="Object" access="global"/>
    <aura:handler name="init" value="{!this}" action="{!c.initialize}"/>
    <p><a onclick="{!c.logAtr}">Console Log Parent</a></p>
    <p><a onclick="{!c.setSth}">Set Something</a></p>
</aura:component>

({
    initialize : function(component, event, helper) {
        var obj = {};
        obj.data = 'test';
        // obj.someNewProperty = null;
        component.set("v.testAtr", obj);
    },
    logAtr : function(component, event, helper) {
        console.log(component.get("v.testAtr.someNewProperty"));
    },
    setSth : function(component, event, helper) {
        var testAtr = component.get("v.testAtr");
        testAtr.someNewProperty = 'New Property';
        component.set("v.testAtr", testAtr);
    },
})

If the line obj.someNewProperty = null; in initialize method is commented out, console.log returns undefined even if you call setSth method that actually sets value of obj.someNewProperty.

If the line obj.someNewProperty = null; in initialize method is present, console.log returns null before calling setSth method and 'New Property' after calling it.

EDIT: But this has some drawbacks. Let's imagine we have a simple form for sObject. You need to initialize object with all the possible properties. If you don't do this - no values will be kept. If these lines: acc.Name = null; acc.Phone = null; acc.Account_Status__c = null; are commented as in below example - value put in the input text is not bind to testAcc attribute.

<aura:component >
    <aura:attribute name="testAcc" type="Account" access="global"/>
    <aura:handler name="init" value="{!this}" action="{!c.initialize}"/>

    <p>Name: <ui:inputText value="{!v.testAcc.Name}"/></p>
    <p>Phone: <ui:inputText value="{!v.testAcc.Phone}"/></p>
    <p>Status: <ui:inputText value="{!v.testAcc.Account_Status__c}"/></p>
    <p><a onclick="{!c.logAcc}">Console Log Acc</a></p>
</aura:component>

({
    initialize : function(component, event, helper) {
        var acc = {};
        // acc.Name = null;
        // acc.Phone = null;
        // acc.Account_Status__c = null;
        component.set("v.testAcc", acc);
    },

    logAcc : function(component) {
        console.log(component.get("v.testAcc"));
    }
})
Related Topic