[SalesForce] Lightning – Rerender child components in aura:iterable after parent attribute changed

I have a struggle to understand something with Lightning here. Let me explain what I'm trying to do.

I have a component (let's call it ParentComponent) which has one main attribute -> object with nested objects within – attributes. I'm using this list of attributes in aura:iteration, where inside of it I'm using child lightning component (ChildComponent) – I just want to separate piece of functionality into smaller bricks.

<aura:component controller="PrdConfigCls">
  <aura:attribute name="paramProductId" type="String"/>
  <aura:attribute name="product" type="PrdConfigCls.Product"/>
  <aura:handler name="change" value="{!v.paramProductId}" action="{!c.paramProductIdChangeEventHandler}"/>

  <aura:iteration items="{!v.product.attributes}" var="attr">
      <c:ChildComponent attributeParam="{!attr}"/>
  </aura:iteration>

</aura:component>

Now everything works fine when loading the component for the first time. ChildComponent content properly renders attr objects passed to it, for all elements in the Product.attributes list.

But ParentComponent have productId which may change. If this happens I'm using calling APEX class to reload second attribute -> Product via standard mechanism:

ParentComponent

<aura:handler name="change" value="{!v.paramProductId}" action="{!c.paramProductIdChangeEventHandler}"/>

ParentComponentController

({
    paramProductIdChangeEventHandler : function(component, event, helper) {
        var paramProductId = component.get("v.paramProductId");
        if (paramProductId != null && paramProductId != '') {
            helper.loadProductConfiguration(component);
        }
    }
})

ParentComponentHelper

({
    loadProductConfiguration : function(component) {

        var action = component.get("c.getProjectProductDefinition");
        action.setParams({  "projectProductId": component.get("v.paramProductId") });
        var self = this;

        action.setCallback(this, function(a) {
            var productResp = a.getReturnValue();
            var product = JSON.parse(productResp);
            component.set("v.product", product);
        });
        $A.enqueueAction(action);

    }
})

Now, because this happens Product attribute in ParentController is changing, new Product have different list of Attributes so I would want content of <aura:iterable> to change – but unfortunately it doesn't happen. Instead I'm being left with a blank screen.

I tried to debug it and here is what happens (copied from chrome console)

1st change – loading the product for the first time:

-- Object {productDefinition: Object, product: Object, createComponentId: null, components: Array[2], attributes: Array[11]}
-- "ParentComponent": Done Rendering
-- 11x "ChildComponent": Done Rendering

2nd change – Done Rendering events are not happening – I only have output of console.log(product);

-- Object {productDefinition: Object, product: Object, createComponentId: null, components: Array[0], attributes: Array[21]}

Can you help me understand why it's not working?
Maybe I'm just missing some rerender of child components?

I think I had this problem in the past when I discovered that attribute propagation to children works fine if you use Strings instead of complex types. It quite surprising to me, as in Visualforce Components it was totally opposite. I'm not very deep into how this works, but I think that in Visualforce Components attributes for complex types were passed through reference.

It would be great help if you can help me to understand this prior adding String serialization / deserialization magic.

Best Answer

I actually found a solution (or a workaround - still don't know). I will put it here in case somebody will come across same issue.

I had to dynamically recreate child component - therefore I used ChildComponentWrapper:

How it looks right now:

ParentComponent

<aura:component controller="PrdConfigCls">
  <aura:attribute name="paramProductId" type="String"/>
  <aura:attribute name="product" type="PrdConfigCls.Product"/>
  <aura:attribute name="attributes" type="PrdConfigCls.Attribute[]"/>
  <aura:handler name="change" value="{!v.paramProductId}" action="{!c.paramProductIdChangeEventHandler}"/>

 <div aura:id="container">     
 // in this place ChildComponentWrapper will instantiate after change event (helper)
 </div>

</aura:component>

ParentComponentHelper

({
    loadProductConfiguration : function(component) {

        var action = component.get("c.getProjectProductDefinition");
        action.setParams({  "projectProductId": component.get("v.paramProductId") });
        var self = this;

        action.setCallback(this, function(a) {

            var productResp = a.getReturnValue();
            var product = JSON.parse(productResp);
            component.set("v.product", product);
            component.set("v.attributes", product.attributes);

            //console.log(component.get("v.attributes"));

            var container = component.find("container");
            $A.createComponent("c:ChildComponentWrapper",
                           {attributeParam: product.attributes},
                           function(cmp) {
                               container.set("v.body", [cmp]);
                           });

        });
        $A.enqueueAction(action);

    }
})

ChildComponentWrapper

<aura:component controller="PrdConfigCls">
  <aura:attribute name="attributeParam" type="PrdConfigCls.Attribute[]"/>

  <aura:iteration items="{!v.attributes}" var="attr">
     <c:ChildComponent attributeParam="{!attr}"/>
  </aura:iteration>
</aura:component/>