17

Since the event registration/subscription feels like a long roundtrip for simple things, I found a "hack" for implementing a simple onchange handler in my own Lightning Components in form of an Aura.Action unsing $A.enqueueAction() in the child compmonent as discussed here:

Calling Aura.Action attribute from Lightning Controller/Helper

The basic pattern is

c:child

markup :
    <aura:component>
        <aura:attribute name="onchange" type="Aura.Action" />
    </aura:component>

controller :
    onSomethingHasChanged : function(cmp, evt, hlp) { 
        $A.enqueueAction( cmp.get('v.onchange') );
    }, 

c:parent

markup :
    <aura:component>
        <c:child onchange="c.onChildChange" />
    </aura:component>

controller :
    onChildChange : function(cmp, evt, hlp) { 
        console.log('it works!')
    }, 

We are discouraged to do so

enter image description here

btw. haunted houses always made me curious, even at night... ;-)

Questions

Now it works, it's short and most importantly it's super readable code.

Can you point me to the limits:

  • what EXACTLY does not work?
  • What exactly is so dangerous to use this pattern?
  • What I can not do? What will fail?
  • If it is so bad, why it is there at all?
  • How did Salesforce wire-up their components which are providing onchange attributes such as lightning:input?

Alternatives?

  • are there any alternatives which can provide such a nice notation for the child-compo like <c:child onchange="c.onChildChange"/> without the event-pattern verbosity? Quick and clean?
  • I maybe wrong but i think it goes again component encapsulation principles and it may get blocked by locker service in the future.Documentation says we should avoid doing it but doesn't say exactly why? – RedDevil Jun 28 '18 at 12:14
  • @RedDevil thats what I'm guessing so far. I'm playing with that on a v39-compo without LockerService. Works like a charm! And it seems that Salesforce is using this or a similar pattern on their own components such as lightning:input - I want to know if they are doing this differently on their own but still getting such a nice syntax for using the <lightning:input onchange="">. I just want the usage of the parent component looking like that without the bloated event verbosity. For the child-compo some kind of verbosity would be acceptable. But for the parent not. – Uwe Heim Jun 28 '18 at 12:22
  • 1
    I have found this pattern extremely difficult to test. – David Reed Jun 28 '18 at 12:40
  • Could be worth cross-posting this into one of the developer communities, to maybe get someone who worked on the docs eyes on this. Seems unlikely though. – battery.cord Jun 28 '18 at 13:43
  • I see the charme of this elegant solution, but is the overhead really that big of a hassle? If you use a generic event class (like guiEvent) over several places the only extra line would be <aura:registerEvent> and instead of enqueueAction you have cmp.getEvent(„change“).fire(). No need for a handler attribute in the parent. But knowing about the considerations not to use this is good too – Christian Menzinger Jun 28 '18 at 15:29
10

what EXACTLY does not work?

You may find that Locker Service will filter out some or all of the data you're trying to work with, even within your own namespace. For this reason, the two related methods for using Aura.Action, $A.run and Action.run, have been deprecated in favor of event handling.

Previously, these methods would allow you to pass in parameters, but Locker Service ends up converting those parameters to Proxy objects, and may ultimately cause you to lose access to the data in the parent component, even within the same namespace. With the run functions having been deprecated, the only other alternative is $A.enqueueAction, which does work, but...

You have no event to work with. No parameters. While it works in your super-simple example, in the Real World, we like having parameters in our events so we can tell the parent about what's going on (e.g. aura:valueChange tells the handler what the old and new values are). Not having this capability is frustrating for most non-trivial events. Also, note that evt in your example is undefined. Developers assuming that evt will have a value are in for a rude awakening.

What exactly is so dangerous to use this pattern?

As above, Locker Service can make data randomly inaccessible. There's no way to predict ahead of time what scenarios will trigger it, and it might even be a moving target as they continue to refine Locker Service and Aura in general. That's also part of the Haunted House effect they mentioned. Your code can appear to be haunted, randomly causing crashes for no good reason.

What I can not do? What will fail?

I think this is pretty well covered, but you can't use the run methods (the only way to get parameters to your method), $A.enqueueAction won't have an event or parameters for you to work with, and you can't reset the Aura.Action attribute later with a new handler for any reason, which can come back later to haunt you if you're using a lot of dynamic component generation. In fact, I don't think you even get access to the correct component with $A

If it is so bad, why it is there at all?

Backwards compatibility, for the moment. This method was pre-Locker-Service. Before LS could be properly implemented, they knew they were going to have to get rid of Action.run and $A.run because of security access problems. If you want a secure component and proper access to your data, you won't use this method.

How did Salesforce wire-up their components which are providing onchange attributes such as lightning:input?

It's currently a mix of SecureEvent/aura:registerEvent and a system-only method Action.runDeprecated. I expect that they'll eventually convert the rest over, remove runDeprecated, and never look back. Aura.Action itself might eventually be deprecated, since it serves no purpose in a world where we have to do everything via SecureEvent instead.


are there any alternatives which can provide such a nice notation for the child-compo like <c:child onchange="c.onChildChange"/> without the event-pattern verbosity? Quick and clean?

Honestly, except for the fact that you need an event (which you could just use one or a handful of generic events with generic data types), it is already just as clean.

Event Way

<aura:registerEvent ... />

...

component.getEvent("eventName").setParams({...}).fire();

Aura.Action Way

<aura:attribute type="Aura.Action" ... />

...

// Cannot set any parameters, sorry...
$A.enqueueAction(component.get("v.methodName"));

Note that in both of the above examples, the parent doesn't care about the underlying complexity, as, except for the missing event parameters, it looks identical in markup.

  • You nailed it - it's definitely the params that are unreadable. I noticed it and was forced to change a while back. Not that I use events, I just pass in callbacks using bind. – Caspar Harmer Jun 28 '18 at 17:14
  • 1
    Great question, great answer !! – Kiran Machhewar Jun 29 '18 at 5:19
6

I'll tackle the Alternatives question here:

I've got two ways to address this, because I felt the exact same pain points as you (it's so ridiculously redundant to create so many events).

First method is a generic key:value eventing system where you switch keys and handle value optionally. I made this pattern here.

You'll still need a generic handler/registration, but it's basically:

EventFireComponentController.js

helper.eventService(component)
  .fireCompEvent("ACCOUNT_ID_SELECTED", selectedOptionValue);

EventListenComponent.cmp

<aura:handler name="genericCompEvent" event="c:genericCompEvent" action="{! c.handleComponentEvent }" />

EventListenComponentController.js

handleComponentEvent : function(component, event, helper) {
  event.stopPropagation();
  let params = event.getParams();

  switch(params.compEventKey) {
    case "ACCOUNT_ID_SELECTED":
      console.log(params.compEventValue)
      break;
  }
},

The other method (thanks @sfdcfox) is to leverage a custom event of your specification to just pass payloads inside the event itself without explicit registration by the listener.

EventFireComponent.cmp

<aura:component>
  <aura:registerEvent name="onload" type="c:onload" />
  <aura:handler name="change" value="{! v.myPayLoad }" action="{! c.onload }"/>
</aura:component>

<!-- FYI, the generic event -->
<aura:event type="COMPONENT">
  <aura:attribute name="value" type="Object" />
</aura:event>

EventFireComponentController.js

onload : function(component, event, helper) {
  component.getEvent("onload")
    .setParams({ value: event.getParam("value") })
    .fire();
}

EventListenComponent.cmp

<c:EventFireComponent onload="{! c.handleLoaded }"/>

EventListenComponentController.js

handleLoaded : function(component, event, helper) {
  component.set("v.myPayloadFromChild", event.getParam("value"));
},

P.S. Bonus points, switch out component event for application event and now you can use a flexipage for inter-component communication without worrying about hierarchy. If you're using Lightning Console, there's a trick to it (scope your application event to a record), otherwise it's working quite well for me in production.

2

There is an alternative way to handle an event. Consider you want following notation -

 <c:child onchange="{!c.onChildChange}"/>

Create an event. Say, 'C:ChildChange'. Register this event in the child component

<aura:registerEvent name="onchange" type="C:ChildChange"/>

Please note that, name of the event should match the attribute name i.e. onchange.

Now, fire it from the child component on the desired action.

var compEvent = cmp.getEvent("onchange");
compEvent.fire();

And you are done!

You don't need to write the code to handle the event from the parent component. And it will call onChildChange method of parent component.

<c:child onchange="{!c.onChildChange}"/>
  • this looks super cool !!! folloup questions: a) is there any documentation on this? b) do i still need to define the onchange attribute, or is this not necessary? c) are parameters supported with this pattern? – Uwe Heim Jul 4 '18 at 22:16
  • a) There is no documentation for this. b)You don't need to define the onchange attribute. c) Yes, you can pass parameters just like you pass while firing an event. And you can access those params in the parent component just like you get the params for event. – Bhushan Patil Jul 5 '18 at 8:06
  • cool, upvoted your post. keeping this still open for other ideas to collect – Uwe Heim Jul 5 '18 at 12:11
  • I believe that Salesforce would be handling the actions by same way. In case of press event of ui:button. Salesforce would have already created ui:press events and it would be present out of the box. – Bhushan Patil Jul 5 '18 at 12:38
1

This answer is one minor difference to the OP question in that I am passing a function and not and actual system event...

One thing I dislike about lightning is all of the files you have to create to do simple things. I also know that you do not get to pass any parameters this way but if al you need is an indication that something happened and you want to handle it differently in different component and not cause al event listeners to handle it this to me was an easy way to do it.

Now I would be interested to hear why this should not be done, however, when I simply need a child to call a method on a parent when the child is done doing something I simply pass the function to the child as an attribute and enque it from the child. No extra files needed.

If the general consensus is this should not be done then I will remove this answer, otherwise maybe it will help.

I tried to use the attribute type="function" as outlined here but the compiler said the type was invalid so I went this route.

Parent

<aura:component description="theParent">

   /**Can either be created dynamically**/
   <div aura:id="insertHere"></div>

   /** or placed in the component**/
   <c:theChild callback="{!c.onChildComplete}"/>

</aura:component>

Parent Controller

({
   //Just an example, it is not actually executed in this example
   createComponent: function(componentsvent,helper){
        $A.createComponent(
            "c:theChild",
            {
                "calllback" : component.getReference("onChildComplete")
            },
            function(cmp, status, errorMessage){
                //Add the new button to the body array
                if (status === "SUCCESS") {
                    var placeHolderDiv = component.find("insertHere");
                    var body = placeHolderDiv.get("v.body");
                    body.push(cmp);
                    placeHolderDiv.set("v.body", body);

                }
                //Error handling here
            }
        );

   },

   onChildComplete: function(component,event,helper){
        //do something
   }
})

Child

<aura:component description="theChild">

    <aura:attribute name="callback" type="object" description="The method to execute on completion of work"/>

    <button type="button" onclick="{!fireDone}">Click Me</button>
</aura:component>

Child Controller

({
    fireDone: function(component,event,helper){
        var showSchedule = component.get("callback");

        if(!$A.util.isUndefinedOrNull(callback)){
            $A.enqueueAction(callback);
        }
    }
})

I am no expert by any means but this seemed like a good way to allow for the parent to specify specifically what needs to be done when a utility component is done with its purpose....

  • Hi Eric, please don't remove the answer, because I'm looking for a wide spectrum of possibilities. Also +1 for Lightning is making simple things difficult. I think at this point if one tries to code in the fashion Salesforce expects us to do, one ends up in super long, verbose, hard-to-read and hard-to-maintain code. Also there are so many moving parts and unclear documentation that I think it's worth to collect different approaches which might help all of us to learn and understand. – Uwe Heim Jul 5 '18 at 5:32

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.