16

In Lightning, I've got into the habit of using $A.util.isEmpty() instead of testing for null, undefined, and empty Array. That seemed to be a good thing, but I've come across one situation where it doesn't work as expected...

When using component.getElement(), the result is either null (we're not rendered yet), or a proxy of the element (we are rendered and locker service has wrapped the element). In both cases, $A.util.isEmpty() returns true, which seems odd.

Here's an app I built to investigate:

App:

<aura:application description="IsEmptyTest">

    <aura:handler name="init"   value="{!this}" action="{!c.init}" />
    <aura:handler name="render" value="{!this}" action="{!c.render}" />

    <div aura:id="theDiv">
        IsEmptyTest
    </div>

</aura:application>

Controller:

({
    init : function(component, event, helper) {
       console.log('init');
       helper.logDivElement(component);
    },

    render : function(component, event, helper) {
       console.log('render');
       helper.logDivElement(component);
    }
})

Helper:

({
    logDivElement : function(component) {
       var theDivElement = component.find('theDiv').getElement();
       console.log('theDivElement:', theDivElement);
       console.log('$A.util.isEmpty(theDivElement):', $A.util.isEmpty(theDivElement));
       console.log('theDivElement == null', theDivElement == null);
    }
})

The output on the console is:

  • init
  • theDivElement: null
  • $A.util.isEmpty(theDivElement): true
  • theDivElement == null true
  • render
  • theDivElement: Proxy {}
  • $A.util.isEmpty(theDivElement): true
  • theDivElement == null false

I would have expected the second $A.util.isEmpty(theDivElement) to return false, but it does not. So, I had a dig at the source code:

https://github.com/forcedotcom/aura/blob/5eef4646c0db6e55b45c63af8c52a2b24db9716d/aura-impl/src/main/resources/aura/util/Util.js

Aura.Utils.Util.prototype.isEmpty = function(obj){
    if (obj === undefined || obj === null || obj === '') {
        return true;
    }
    if (Array.isArray(obj)) {
        return obj.length === 0;
    } else if (typeof obj === 'object' && Object.prototype.toString.call(obj) === '[object Object]') {
        return Object.keys(obj).length === 0;
    }
    return false;
};

And this shows where it falls down, on the proxy element Object.keys(theDivElement).length returns 0.

So, is this a mistake in the proxy? Am I just missing something?

I've been working on the assumption that if(myVar == null) is a bad smell in a Lightning component and if(!$A.util.isEmpty(myVar)) is a good smell. It certainly seems that, today, the smells are more complicated than that.

Any insights?

Some extra code/info

I've been trying to get somewhere with SF support on this. In an attempt to explain it to them I wrote this, which might be informative:

var myProxyHandler = {
  ownKeys: function(target) {
    return []
  },
  get: function(target, name) {
    return target[name];
  }
}

var obj = {a: 1};

var pObj = new Proxy(obj, myProxyHandler);

console.log('obj.a', obj.a);
console.log('Object.keys(obj).length', Object.keys(obj).length);
console.log('pObj.a', pObj.a);
console.log('Object.keys(pObj).length', Object.keys(pObj).length);

This is an implementation of a JS Proxy object that behaves a bit like the Locker Service on is doing in this instance. It has a misleading implementation of ownKeys that makes the underlying object look empty to a test such as the one in $A.util.isEmpty(). So, the question is: Is Locker Service doing this by design? Is it a bug? Am I just confused?

  • In many instances you don't need to test for empty, undefined etc in a Proxy because you are protected from any null property errors. However, so far I haven't been able to find out a definitive way of testing if a Proxy object truly isEmpty - this is partly because Proxies are implemented in different ways. Good luck!! – Caspar Harmer Nov 28 '17 at 16:43
9
+50

The root cause of this issue is $A.util.isEmpty() not being able to handle the Proxy wrapping the DOMElement. We might fix it at the LockerService layer by unwrapping the proxy around the DOMElement and passing the raw element to $A.util.isEmpty(). I will confirm the fix and timeframe once we discuss internally with the team.

tl;dr
LockerService wraps the DOMElement with a proxy to secure the DOMElement and its prototype.

$A.util.isEmpty() does not seem to be able to handle a proxy. It is calling toString() on the object but is using the primitive toString i.e Object.prototype.toString.call. That check(typeof obj === "object" && Object.prototype.toString.call(obj) === "[object Object]") passes for a proxy but fails for a raw DOMElement. This is why your scenario works without LockerService but fails with LockerService on.

Let me explain it with an example showing how $A.util.isEmpty() behaves with a Proxy v/s a raw DOMElement.

    // In native browser environment(no locker)
    var x = new Proxy({}, {});
    console.log(Object.prototype.toString.call(x)); // [object Object]
    var div = document.createElement("div");
    console.log(Object.prototype.toString.call(div)); // [object HTMLDivElement]

If $A.util.isEmpty() were to call the toString on the wrapped element itself, then we would be good to go. But I assume they are using the primitive for a specific reason.

    // In locker environment(locker on)
    var div = document.createElement("div");
    console.log(div.toString()); // SecureElement: [object HTMLDivElement]{ key: {"namespace":"c"} }
  • @Aidan I have filed a bug for our team(LockerService) W-4514560 is the bug number and I have alerted Salesforce support to associate it with your case – Ravi Jayaramappa Nov 28 '17 at 20:14
  • @Aidan W-4514560 is fixed in Spring 18 release. That release is currently scheduled to rollout to production soon. – Ravi Jayaramappa Dec 18 '17 at 18:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.