Deep Dive on Expressions in the Aura Framework

In our previous post Explaining Expressions in the Aura Framework: Part 1, we gave an overview of the different types of expressions and their various flavors of by value or by reference. In this post, we’ll be giving you an in-depth look at some of the features of these expressions and what you can do with them.

After this article, you’ll be able to avoid the key pitfalls when using Calculated Expressions, use deep expressions to control rendering performance, and learn all about the wonders of Aura expression functions.

Calculated Expressions features

As a reminder, Calculated Expressions are expressions created in markup files that perform operations in the expression.

Calculated Expressions cannot be modified

Something to be aware of when using Calculated Expressions is that when passing one into an attribute, that attribute can no longer be updated.

Let’s go back to our first original example of a button and a label, but change the value we pass in to label to be a Calculated Expression.

// button.cmp
<aura:component>
    <aura:attribute name="buttonText" type="String" default="Button"/>
    <c:label labelText="{!v.buttonText + 's'}"/> <button>{!v.buttonText}</button>
</aura:component>

// label.cmp
<aura:component >
    <aura:attribute type="String" name="labelText"/>
    <aura:handler name="init" value="{!this}" action="{!c.onInit}"/>
    {!v.labelText} 
</aura:component>

// labelController.js
onInit: function(cmp) {
    cmp.set("v.labelText", cmp.get("v.labelText") + " Label")
}

// Result
console.log(buttonCmp.get("v.buttonText")); // "Button";
console.log(labelCmp.get("v.labelText")); // "Buttons"

Result:


As you can see, the line cmp.set(“v.labelText”, cmp.get(“v.labelText”) + ” Label”) did not work. If you enter Lightning Component Debug Mode, you will see a warning.

Why is that? If a value has a Calculated Expression in it, you can not set it a new value without first clearing it.

And how do you clear it? You would use the clearReference function on the component.

onInit: function(cmp) {
    var text = cmp.get("v.labelText");
    cmp.clearReference("v.labelText");
    cmp.set("v.labelText", "Label: " + text);
    console.log(cmp.get("v.labelText")); // "Label: Buttons"
}

We get the value, clear the reference so we no longer have any association to v.buttonText, and then reset the value to the new computed value.

One-way data binding

One-way data binding dictates for data to only flow in one direction. This allows for your data to be in one centralized location and when you update the data source, the updates cascade down through the application. This is not the standard for Aura, which is a two-way data bound framework. Your data would live in the components that own it and is synced with property reference expressions to other components data.

If you didn’t notice, Calculated Expressions allow you the ability to do one-way data binding.

As we talked about, the setting of attributes with Calculated Expressions in them are ignored. This also means updates to an attribute are ignored, but it will still maintain a property reference to any attributes it references.

When taking this approach, you lose the automatic updating of the framework. So if you need updates, you are responsible for firing events to notify any components consuming your component of the changes.

Utilizing Calculated Expressions in this way is not a designed feature, but it does provide this ability.

To give one last example of what one-way data binding might look like, we are going to walk through an example application one file at a time.

My Numbers application

My Numbers application is going to keep track of all of my favorite numbers.

In this application, we are using myItems as the source of all data for the application. We want to always maintain the true list of data in this attribute. All sub components should communicate to the application to update the list of numbers.

// application.cmp
<aura:application >
    <aura:attribute type="List" name="myItems" default="[38, 3.14, 8]"/>
    <aura:handler name="init" value="{!this}" action="{!c.onInit}"/>
    <aura:handler name="updateData" event="c:updateData" action="{!c.updateData}"/>
    <div>
        <c:collection 
            aura:id="collectionInstance" 
            collectionItems="{!v.myItems || null}"/>
    </div>
</aura:application>

As you can see we pass v.myItems to our collection component, but we are using a Calculated Expression so that when the collection component tries to update its attributes with new information. Those changes will get ignored by my application.

We also register an event (updateData) that allows containing components to update the data.

Next, let’s show what happens if we try to change different values within the application.

// applicationController.js
({
    onInit : function(cmp, event, helper) {
        // Success: cmp.get("v.myItems") == [10, 20, 30]
        cmp.set("v.myItems", [10, 20, 30]);
        
        // Fail: cmp.get("v.myItems") == [10, 20, 30]
        cmp
            .find("collectionInstance")
            .set("v.collectionItems", [40, 50, 60]);
    },
    updateData: function(cmp, event) {
        cmp.set("v.myItems", event.getParam("myItems"));
    }
});

The second set failed because we tried to update the attribute on the collection component. Since the attribute was a Calculated Expression and not a Property Reference or raw value, it could not be updated.

Now let’s show how the collection component would need to change using this pattern.

// collection.cmp
<aura:component> 
    <aura:attribute name="collectionItems" type="List" default="[]"/>
    <aura:handler name="init" value="{!this}" action="{!c.onInit}"/>
    <aura:registerEvent name="updateData" type="c:updateData"/>
 
    <ul>
        <aura:iteration aura:id="myIteration" items="{!v.collectionItems}" var="item">
            <li>{!item}</li>
        </aura:iteration>
    </ul>
</aura:component>

For the markup file, nothing changes. It references its attributes and displays data just as usual. All the changes are in the controller.

// collectionController.js
({
    // Try to update our data on load.
    onInit : function(cmp, event, helper) {        
        // Fail: cmp.get("v.collectionItems") === [1, 2, 3, 4]
        cmp.set("v.collectionItems", ['a', 'b', 'c']);
        
        // Fail: cmp.get("v.collectionItems") === [1, 2, 3, 4]
        cmp.find("myIteration")
            .set("v.items", ['d', 'e', 'f']);

        // Success: cmp.get("v.collectionItems") === [99, 100, 101]
        cmp.getEvent("updateData").fire({ myItems: [99, 100, 101]});
    }
})

Note: In this example you would need to remove the cmp.set() calls in the applicationController.js.

Earlier I passed collectionItems as a computed value from the application and because of that, it becomes immutable. When the controller code for collection.cmp assumes it can use cmp.set() and mutate its own collectionItems attribute, those calls fail and are silently rejected.

If I wanted to modify the v.collectionItems from inside the collection.cmp component, I would need to fire an event telling the parent component the changes I would like to make. It would then handle the event, and make those changes to the v.myItems collection on the application. That change would then cascade down into the collection.cmp component.

The raw value is still mutable

It is still possible to retrieve the raw value and change it. The immutable rules are only enforced through the framework expressions.

// collectionController.js
({
    onInit : function(cmp, event, helper) {        
        // Fail: cmp.get("v.collectionItems") === [1, 2, 3, 4]
        cmp.set("v.collectionItems", ['a', 'b', 'c']);
        
        // Success: cmp.get("v.collectionItems") === [1, 2, 3, 4, "possible"]
        cmp.get("v.collectionItems").push("possible");   
    }
})

Lastly, when in Lightning Debug Mode, you’ll get a console warning line when you try to set a value that is immutable.

Computed Expressions are stored as code

Computed Expressions and Property Reference Expressions are stored differently and it impacts the usability of Computed Expressions. For Property Reference Expressions, you can generate a reference to an attribute using component.getReference(“v.propertyName”). This will return an object that you can pass to another attribute to create a two-way binding link between the two attributes.

For Computed Expressions, they are stored as functions that do the computation.

When you create one via your markup file, the framework will encode it into JavaScript for execution later.

// Stored and executed as a function
{! v.label + '...'}

It would result in a function that looks like this:

function(cmp, fn) { return fn.add(cmp.get("v.label"), "..."); }

This makes it fast to execute as we won’t have to do any parsing of the expression on the client.

Note: Because Computed Expressions are converted to functions, for security reasons, we do not allow you to create these expressions via any methods on the client.

Expression Functions

When building your calculated expression, there are the basic operations you would expect to be able to use.

  • + for concatenation or addition
  • – for subtraction
  • == for equality
  • != for not equal

Then there are also some helper functions to allow advanced functionality.

// Is Empty?
{! empty(v.list) ? "Empty" : "Not Empty" }

// Format function
{! format('Hello, {0} of {1}', v.name, v.country); } // Hello, Brienne of Tarth

// Join function
{! join(',', v.type1, 'String1', globalId ) } // MyType,String1,1:0

What do these methods do?

empty(value)
For Arrays, it is empty if the length is 0. For Objects, it is empty if it has no keys. null, undefined, empty string all equate to being empty.

format(formatString, value1, value2, …)
Nothing special here — the {index} string gets replaced with the value at that position after the format string.

join(delimiter, value1, value2, …)
Like array.join(delimiter), this method converts all the values in the function after the delimiter to an array, then runs .join() on it with the delimiter specified as the first parameter.

There are several more functions you can use. A full reference all of the available methods can be found in this documentation.

Expression syntax

Deep Expressions

A lot of people are not aware that you can chain expression references. Let’s go back to our application list example.

<aura:application >
    <aura:attribute type="List" name="myItems" default="[1, 2, 3, 4]"/>
    <aura:handler name="init" value="{!this}" action="{!c.onInit}"/>
    <div>
        <c:collection 
            aura:id="collectionInstance" 
            collectionItems="{!v.myItems || null}"/>
    </div>
</aura:application>

To change the second item in the list, you can directly change the value using a deep expression.

// Deep Expression
cmp.set("v.myItems.2", 'Third Item')

// Alternative, not as good.
var myItems = cmp.get("v.myItems");
myItems[2] = 'Third Item';
cmp.set("v.items", myItems);

This can also have a MASSIVE performance boost. Simply changing v.myItems.2 means the framework can know only the second item needs updating. We don’t have to update everything referencing v.myItems, which can be very expensive when dealing with large data sets.

This also works with get calls and has no limit to how many levels deep you can go.
For example:

// Very deep
cmp.get("v.myObjects.0.a.b.c.d");

Bracket notation

There are two ways to build the expression string itself:

// Dot notation
v.attribute.key

// Bracket notation
v.attribute[key]

You might assume that this is like JavaScript and that when using bracket notation, you can use key as a dynamic reference to a value but in this case, we’ll do things a bit differently.

When we go to evaluate both expressions, we run them through a normalizer function that converts any bracket notation expression pieces to dot notation.

That means to the framework, v.attribute[key] and v.attribute.key are exactly the same.

In JavaScript, you can have properties that would break the dot syntax.

var obj = { "salesforce.com": "Salesforce" };
obj.salesforce.com !== "Salesforce"

In Aura, you cannot specify an attribute name with a special character that would cause this issue. There should never be a time you’ll absolutely need bracket notation vs dot notation.

Dynamic Expressions?

It is often desired to be able to do the following, where v.key resolves to the value to find on the map.

{! v.map[v.key] }

This is currently not possible in markup, though in script, you can do this using component.getReference() which is the method that creates Property References. Here is an example showing how to generate a reference in script.

// By Reference
// The alternative to not being able to do {! v.map[v.key] }
component.getReference("v.map." + component.get("v.key"));

By Value is possible as well, but it is a simple get call.

// By Value
// Alternative to not being able to do {# v.map[v.key] }
component.get("v.map." + component.get("v.key"));

Conclusion

As you can see, expressions are amazingly powerful. You can create them in code, make them immutable, and use complex functions. It really opens up a wealth of possible implementation strategies in Aura.

If I did my job right, you’ll know the following things to be true.

Function Call Values cannot be modified
Passing a Function Call Value to an attribute prevents updates to that attribute. This is both an opportunity for one-way data flow, and a source of bugs when you assume attributes should be getting updated.

Function Call Values support a lot of different types of functions
Using functions like empty(), join() and format() can be very powerful for your application.

You can create Property Reference Values in code
With component.getReference() and its partner component.clearReference() you can create property reference values and then break reference bindings.

I hope this gives you some great detail on all the abilities of expressions. I’d love to hear what else you might be curious about. Feel free to message me on Twitter at @GrayJustise.

Leave your comments...

Deep Dive on Expressions in the Aura Framework