[SalesForce] Lightning component order of execution in inheritance problem

My problem is that sometimes – when a user enters and loads the lightning page for the first time, I get an error that is caused by inheritance order of execution:

enter image description here

I have one base component that has extensible="true" in it:

VisualField markup:

<aura:component extensible="true"
                controller="VisualField_CTRL"
                implements="force:hasRecordId,force:hasSObjectName"
                description="Interface for a visual component">

    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
    <aura:attribute name="sObjectName" type="String" description="sObject api name to control the field. Taken from force:hasSObjectName" />
    <aura:attribute name="recordId" type="String" description="Id of the current record. Taken from force:hasRecordId" />
    <aura:attribute name="fieldName" type="String" description="Visual field api name to control." />

VisualField controller:

({
    doInit: function(component, event, helper) {

        helper.getSelectedValue(component);
    }
})

VisualField helper:

({
    getSelectedValue: function(component){

        // get the object and record data from the record home
        var sObjectName = component.get("v.sObjectName");
        var recordId = component.get("v.recordId");
        var fieldName = component.get("v.fieldName");

        // get the server side function and add params to it
        var action = component.get("c.getVisualField");
        action.setParams({
            sObjectApiName  : sObjectName,
            recordId        : recordId,
            fieldName       : fieldName
        });

        action.setCallback(this, function(response){
            var state = response.getState();
            if (component.isValid() && state === "SUCCESS") {
                var record = response.getReturnValue();
                if (record) {

                    // set the new value
                    component.set("v.fieldValue", record.fieldValue);

                    // show the selected value - call inherited method
                    component.getConcreteComponent().showSelected(record.fieldValue);
                }
                else {
                    alert("VisualField Failed to get visualField record.");
                }
            }
            else {
                alert(response.getError());
            }
        });
        $A.enqueueAction(action);
    }
})

I have also a second component that inherits the base component (extends="c:VisualField") and uses jQuery Raty – A Star Rating Plugin:

Rating Markup:

<aura:component extends="c:VisualField"
                implements="flexipage:availableForAllPageTypes"
                description="Controls a field with visual stars">

    <ltng:require styles="{!$Resource.Rating + '/css/jquery.raty.css'}"
                  scripts="{!join(',',
                          $Resource.Rating + '/js/jquery.js',
                          $Resource.Rating + '/js/jquery.raty.js')}"
                  afterScriptsLoaded="{!c.loadRaty}"/>

    <!--this method should be called from the super-component-->
    <aura:method name="showSelected" action="{!c.showSelected}">
        <aura:attribute name="selectedValue" type="String" />
    </aura:method>

    <aura:attribute type="String" name="currentValue"/>
    <aura:attribute type="String" name="newValue" default="0"/>

        <div>
            <div aura:id="starRating" />
        </div>

</aura:component>

Rating Controller:

({

    // load raty rating jquery plugin.
    loadRaty : function(component, event, helper) {
        helper.initRatingElement(component);
    },

    // comes from aura:method name="showSelected"
    showSelected: function(component, event, helper) {

        if (!event)
            return;

        // get the component and the selected value from the event
        var params = event.getParam('arguments');
        if (params) {
            // show selected stars
            var newValue = params.selectedValue;
            helper.setStars(component, newValue);
        }
     },


})      

Rating Helper:

({
    initRatingElement: function(component){

        var ratingElement = component.find("starRating").getElement();
        var helper = this;

        $( ratingElement ).raty({
            number  : 5,
            starOff : '/resource/Rating/images/star_off_darkgray--small.png',
            starOn  : '/resource/Rating/images/star_on--small.png',
            single  : false,
            // load the click event and function
            click   : function(score, evt) {
                if(score == null )
                    score = 0;
                var newValue = score.toString();
                if(component.get("v.currentValue") != newValue ){
                    component.set("v.newValue", newValue);
                }
            }
        });
    },

    setStars: function(component, selectedValue) {

        // save value as current
        component.set("v.currentValue", selectedValue);

        // show on the component
        var ratingElement = component.find("starRating").getElement();
        // check if raty has been loaded, if not - load it first:
        if (!(typeof ($(ratingElement).raty) === "function")) {
            // load raty first:
            this.initRatingElement(component);
        }
        $(ratingElement).raty('score', selectedValue);
    }
})        
  1. Rating component loads jquery and raty plugins and after they load, it initiates the raty plugin.
  2. base VisualField component on it's init gets values from server side to see and set the selected value.
  3. when the server side function in the base component finishes to load, it calls the showSelected method in the inherited component Rating.
  4. the showSelected method in the inherited component Rating executes the setStars helper method that uses the jQuery and Raty plugins $(ratingElement).raty('score', selectedValue);
  5. this call fails sometimes when the page first loads with the error message:

    Uncaught Action failed: c:Rating$controller$showSelected [$ is not defined]
    Callback failed: apex://VisualField_CTRL/ACTION$getVisualField
    throws at https://mycompany.lightning.force.com/auraFW/javascript/YKnqxnHX5EzqqKoVZYsoZQ/aura_prod.js:2:15
    Object.setStars()@components/c/Rating.js:62:9
    showSelected()@components/c/Rating.js:22:20

I believe this happens because the function is called from the base component and the jQuery and Raty plugins are not loaded from there and still did not load from the inherited Rating component.

Can anyone help me fix this very annoying problem?

thanks

Itai

Best Answer

How about moving doInit() in VisualField to the helper. Then, instead of calling it from the init event of VisualField, call it from the Rating component when you're ready to do that.

Here's a generic example. Parent component:

<aura:component extensible="true">
    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
    <aura:attribute name="autoInit" default="true" type="Boolean" />

</aura:component>

Parent controller:

({
    doInit : function(component, event, helper) {
        if(component.get('v.autoInit')) {
            helper.doInit(component);
        }
    }
})

Parent helper:

({
    doInit : function(component) {
        console.log('Parent init');
    }
})

Child component:

<aura:component extends="c:Parent">
    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
    <aura:set attribute="autoInit" value="false"/>
</aura:component>

Child controller:

({
    doInit : function(component, event, helper) {
        console.log('Child init');
        helper.doInit(component);
    }
})
Related Topic