4

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.

1
  • Sounds like a bug to me .Did you open a case ? Jul 4, 2016 at 16:02

2 Answers 2

3

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"));
    }
})
3
  • Thanks for the extensive investigation. There is a bug for requiring all possible properties to be defined at object initialization. If you open a case you can link to internal work item W-3221032. For scenarios where you can't define all properties up front, you can try this temporary hack to stringify/parse your object after adding the new property to force it through the LockerService filtering logic: component.set("v.testAtr", JSON.parse(JSON.stringify(testAtr))) Jul 5, 2016 at 22:18
  • Thanks @Trevor, I opened a case for that. Do you by any chance know when it may be resolved? Jul 5, 2016 at 22:27
  • This is scheduled to be fixed in the Summer '17 release. Thanks for your patience. Mar 21, 2017 at 17:14
1

Interesting things happen around the type attribute; I used to think it had to hold the object API name -like type="Project__c[]"- but this is causes some interesting behavior people might want to know about. I've experimented with this variable and found this:

  1. type="FooBar[]" > FAIL
  2. type="object[]" / type="sObject[]" > OK
  3. type="Project__c[]" > This is interesting; I found that when you use the API name as type with an admin profile, it's perfectly fine but when you access the page with a profile that doesn't have access to the object used ('Project__c') this wil FAIL... And unfortunately you can't capture that failure to use it as a validation method.
1
  • This was very helpful, I was struggling with defining a default object, didn't work. But with Object[] it worked, thanks!
    – itsmebasti
    Sep 20, 2017 at 7:44

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .