Thursday 10 March 2016

Lightning Components - It's Inheritance, Jim, But Not As We Know It

Lightning Components - It's Inheritance, Jim, But Not As We Know It

Override

Introduction

Continuing the saga of developing Lightning Components for our BrightMedia product, I’ve been trying to make use of inheritance in order to be able to change the layout and styling of a bunch of components solving similar problems through a single super-component. The Lightning Components Developer’s Guide states that:

"A sub component's helper inherits the methods from the helper of its super component. A sub component can override a super component's helper method by defining a method with the same name as an inherited method." 

which all sounded very straightforward, but actually wasn’t.

The components in question were pretty complex, but to investigate what was actually happening I created some very simple components, initially just a super and sub component. And I was supposed to be on leave for the day.

Is it a bird? is it a plane? No its Super Component

The super component defined the markup and had the extensible attribute set to true to allow inheritance: 

<aura:component extensible="true">
    <aura:attribute type="Integer" name="clicks" default="0"/>
    
    <p>Hi, I'm supercomponent. Click the button below to execute a
    helper method</p>
    <button onclick="{!c.updateClicks}">Click Me</button>
    <p>This button has been clicked {!v.clicks} times.</p>
</aura:component>

The super component controller has a single method - updateClicks - that is called when the button is pressed: 

({
    updateClicks : function(component, event, helper) {
        helper.updateClicks(component, event);
    }
})

and the helper has a single method - updateClicks - which increments the 'clicks' attribute

({
    updateClicks : function(cmp, ev) {
        var clicks=cmp.get('v.clicks');
        clicks++;
        cmp.set('v.clicks', clicks);
    }
})

Using this component inside an application works as expected and every click of the button updates the number of clicks messages:

Screen Shot 2016 03 10 at 15 26 56

Not very exciting, but stick with me as it will get more interesting.

On a Sub Mission

I then create a sub component, which extends the super component by specifying the extends attribute: 

<aura:component extends="c:SuperComponent">
</aura:component>

This component doesn't have a controller, just a helper that overrides the updateClicks method, to add 10 to the clicks count

({
    updateClicks : function(cmp, ev) {
        var clicks=cmp.get('v.clicks');
        clicks+=10;
        cmp.set('v.clicks', clicks);
    }
})

and as far as I was concerned, job done. Updating my application and using the sub component rather than the super component, I clicked the button and the behaviour was exactly the same as before - the counter incremented by 1.

(In fact what actually happened was that I forgot to update my application, but that spoils the image of infallibility that I carefully cultivate in these posts!).

After reconfirming that my syntax, attributes etc were correct, I set about trying to understand what was happening. 

You’re Not My Real Helper, You Can’t Tell Me What To Do!

After a few blind alleys, I added the following line to my controller method:

console.log('Component = ' + JSON.stringify(component));
console.log('Helper method =' + helper.updateClicks);

and got the following output:

Component = "{\"descriptor\":\"markup://bblightning:SuperComponent\",\"globalId\” …}" 

Helper method =function (cmp, ev) {
        var clicks=cmp.get('v.clicks');
        clicks++;
        cmp.set('v.clicks', clicks);
    }

Which showed that the component being used by the framework wasn’t the subcomponent at all, but the super component! This sort of makes sense when you consider that there is only one controller, and that lives in the super component.It isn’t the way that inheritance works in an OO language such as Java, where the controller would be available as part of the sub component, but it is the way that Lightning Components work so I had to accept that and move on.

O Helper, Helper! Wherefore Art Thou, Helper?

Digging through the Lightning JavaScript reference documentation, I came across the following method on the component object:

getConcreteComponent ()

Gets the concrete implementation of a component. If the component is concrete, the method returns the component itself. For example, call this method to get the concrete component of a super component. 

So it appeared that it was up to me to sort out the correct component in the controller, which could be handled by a one line change:

var cc=component.getConcreteComponent();

and this worked exactly as expected - logging the value of cc to the JavaScript console showed that the concrete component was indeed an instance of SubComponent. All I needed now was to get the concrete component’s helper, and I could invoke the appropriate subcomponent helper method. Once again, it all sounded simple.

Sadly, reading through the documentation showed no getHelper() method on the component object, nor anything that obviously would allow me to derive the helper. The reference documentation is okay if you know what you are looking for, but searching for method names doesn’t return any results, and I wasn’t looking forward to scouring every page. Then I remembered that aura is an open source framework and the source is available on github, which has pretty good repository search tools. 

Executing a search for getHelper, not only showed that there was a method available, it also returned a couple of results that were perfect examples of how to use it:

helper = component.getConcreteComponent().getDef().getHelper();

the getDef() returns the ComponentDef object for the component, which exposes a getHelper() method, which gives me access to the helper. My controller method could now determine the correct helper and execute the method:

updateClicks : function(component, event, helper) {
    console.log('Component = ' + JSON.stringify(component));
    console.log('Helper method =' + helper.updateClicks);
        
    var cc=component.getConcreteComponent();
    var cchelper=cc.getDef().getHelper();
    cchelper.updateClicks(component, event);
}

back to my application, and things now worked as expected, adding 10 to the clicks count every time I clicked the link. Finally some good news!

Subbing the Sub

Something I needed in my real-world application was the ability to have a SubSub component, that extended a Sub component that in turn extended a Super component. The Lightning Components Developer’s Guide was a little ambivalent about this, saying that a component can extend one extensible component. This can be read that:

  • multiple inheritance is not supported
  • once a single level of extension had taken place, no further extension is possible

After my inheritance fun and games, I wasn’t taking any chances, so enter SubSub Component, which was much the same as SubComponent, but added 100 to the clicks count. Updating my application to use the SubSub Component and clicking the button added 100, so the good news continued, which is most unusual in my experience.

Ever Decreasing Circles

The other scenario I was keen to understand, not because I needed to but because I was curious, was what happened if I called a super component method from the sub component, and that then called a method that was present in the super component but overridden in the sub component. So in my super component I have:

super : function(cmp, ev) {
    this.delegate(cmp, ev);
},
delegate : function(cmp, ev) {
    alert('In SuperComponent Delegate');
}

and in my sub component I have:

doStuff : function(cmp, ev) {
    this.super(cmp, ev);
},
delegate : function(cmp, ev) {
    alert('In SubComponent Delegate');
}

When I execute doStuff() in my sub helper, it will call the super() method in the super helper. What I wasn’t clear about was which delegate() method would then be called - the one from the super helper or the overridden one in the sub helper. What I hoped would happen is that the overridden method in the sub helper would be invoked, as that is how I expect inheritance to work - I shouldn’t end up in an instance of the super helper just because I execute a method that I’ve inherited. The good news is that’s exactly how it works! More good news - this was turning into a great day.

In Conclusion

Simply put, inheritance works as expected as long as you start your processing from the sub component.

Where its not intuitive is when you start the processing from the super component. The controller only knows about its own helper, rather than that of any others that may have inherited from it, so it can only execute methods on its own helper. If you want use inheritance techniques from the super component controller, you need to help it out and find the correct helper.

Simply put (again), if you have any helper methods that can be overridden, and you execute any of these from the super controller, you need to make sure that your super controller finds the correct helper and uses that.

Just One More Thing

Don’t use controller inheritance, tempting though it is. It works now, but may not in the future, and it will take you ages to realise that and figure out why your funky inheritance model no longer works. From the Developer’s Guide:

We don't recommend using inheritance of client-side controllers as this feature may be deprecated in the future to preserve better component encapsulation. 

(Just one more one more thing : the title of this post is a play on the “It’s life, Jim, but not as we know it” that Mr Spock said to Captain Kirk in Star Trek, which never happened!)

Related Posts

 

6 comments:

  1. Awesome article. Perfectly what i was looking for. Thanks. :)

    ReplyDelete
  2. Hi Bob,

    Could you please update this article with a link to your Stack Exchange question? As you discovered, this technique doesn't work any more :(

    https://salesforce.stackexchange.com/questions/118270/lightning-lockerservice-and-component-inheritance

    ReplyDelete
  3. For some strange reason `getDef` is no longer working, is there any workaround?

    ReplyDelete
  4. Hello Bob,

    I tried to use the same approach and did not work. Perhaps, they've changed the way they expose the methods and now every time I try to call getDef I get undefined.

    Have you tried this solution using newer APIs? I didn't bother trying to change the API (currently using 42) because I had to use some of the latest Lightning components.

    I've ended up using a Lightning event registering it in the super component and handling it in the sub-component.

    Do you have any better idea to work around this?

    Furthermore, I've found a trailhead where they state that we should prefer using Composition instead of traditional inheritance due to performance advantages. Even though I do not have any conclusions on the subject of Composition, I think that sometimes inheritance is truly necessary, or am I wrong?

    Here's the trailhead: https://trailhead.salesforce.com/modules/lex_dev_lc_vf_tips/units/lex_dev_lc_vf_tips_apex

    Do you have any insight on this matter?

    Kind regards,
    Zuinglio Jr.

    ReplyDelete
  5. Great post Keir!

    This might be a stupid question but wondering if it's even possible.

    I'm trying to initialize or call a function in the sub component from the super component. The reason I'm trying to do this is to run a few sub component specific methods because I have multiple sub components extending the same super component. Is this even possible?

    ReplyDelete
  6. You Nailed it with Perfect Narration. Thanks a lot

    ReplyDelete