Call an action on a component from a controller


#1

Not sure if this is possible or the best way to do this, but I have a component that takes and input and upon sliding a button over it will fire an action that using sendAction() to bubble that up where the item controller that has the component rendered in it’s template will catch it and call an ajax function.

What I want is when that ajax function resolves with a success, for the button that the user slid over to animate back so they can enter a new input and do it again if they wanted to. I know I can put this animation logic in the success callback of the ajax function but I was wondering if there was a way to keep all that animation logic in the component and out of the controller and just have the controller “defer” the animating to the component by telling the component to animate the button back to where it started.


#2

There’s no law that says a component can’t make an Ajax call though I’ve seen posts that seem to frown on it.

If you keep the ajax call in the controller. You could have the controller set an animateState property on the component and then observe it. Perhaps something like:

app/templates/parent.hbs

{{my-button animateState='ready'}}

app/components/my-button

animateState: 'ready',
oldState: 'ready',
observer: Ember.observer('animateState', function() {
  const currState = this.get('animateState');
  const oldState = this.get('oldState');
  if(currState !== oldState) {
    if(currState == 'ready') { animateReady(); }
    else { animateError(); }
    this.set('oldState', currState);
  }
})

#3

FWIW, we make AJAX calls from components all the time. In fact, we practice the routable components pattern, so we don’t even have controllers.

But either way, trying to bundle several pieces of state, send it through an action (or chained actions) just to arrive in the controller/route so he can make an AJAX call makes absolutely no sense to me. If the data resides in a service, I could see putting a common function there that makes the AJAX call. Take it from me, when your app gets more complicated, all the “unecessary” action sending makes things more messy than component ajax calls ever will.


#4

This is what closure actions solves. Actions upstream can return something, in your case a Promise, that the component’s sendAction method will return.

export default Ember.Controller.extend({
  actions: {
    bar() {
      // your ajax method here..
      return new Ember.RSVP.Promise((resolve) => {
         setTimeout(resolve, 1000);
      });
    }
  }
});
{{x-foo action=(action 'bar')}}
// x-foo
export default Ember.Component.extend({
  sliderDisabled: false,
  actions: {
    triggerFoo() {
      this.set('sliderDisabled', true);
      
      // sendAction here returns the Promise from the action bar
      // the Ember.RSVP.cast is just here to handle the case where
      // a bound closure action doesn't return a promise.. or anything
      Ember.RSVP.cast(this.attrs.action()).finally(() => {
         this.set('sliderDisabled', false);
      });
    }
  }
});

#5

set a property, for example loading, in your controller which have boolean value, set it default to false. Then pass that attribute to the component, on the component make it watch to that property via observer. When action ajax fire, toggle that property on your controller, and when ajax call success, toggle it again. That way you can animate that button on your component.

That my though.


#6

In the console I’m getting

Uncaught TypeError: Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.

Is there a way I can call a local component action that is different with each use of the component?

For instance I have the component in an item route that I want to call an action called placeBid, but I also have this component in a cart route that I want to call an action called placePayment

When I have one action I know I can just call the action by name and the local action name in the component file will be called, but how can I put both of those actions in the component file and just call those actions based on whcih one I want to call? Is there a certain way I can do it so that the definition of the component (in handlebars) defines the name of the action and that actions gets called locally versus bubbling up to the route/controller?


#7

new Ember.RSVP.Promise


#8

I would opt for inheritance in this case and in the case {{x-foo-cart}} {{x-foo-item}}. Without knowing much detail on what you’re building, it sounds like the the “local” action should be an outside concern.

cart route/controller: {{x-foo action=(action 'cartAction')}}

item route/controller: {{x-foo action=(action 'itemAction')}}

This pattern usually allows for a more reusable component versus a monolithic component with business logic to handle many implementations.


#9

So, I’ve implemented the code you outlined here with Ember.RSVP.Promise and it appears to be working however it’s not actually waiting for the resolve before if calls the code in the .finally() block.

callAjaxAction() {
      this.setProperties({working:true});
      Ember.RSVP.cast(this.sendAction()).finally(() => {
        Ember.$('.draggable').animate({
          left: this.get('startingPosition')
        });
        this.setProperties({working:false});
      });
    }

In my controller I have this action

placeDonation() {
  		return new Ember.RSVP.Promise((resolve) => {
         setTimeout(()=>{
      		this.get('notification').notify({message: "Your donation has been placed, thanks!",success: true});
         	resolve;
         }, 5000);
      });
    }

But when the button is pressed, the working property doesn’t get set (meaning it would be set back so quickly ember doesn’t even attempt to change it) and the UI goes back to normal, then 5 seconds later the notification message pops up, which is when the UI should reset, not before like it’s currently doing.


#10

this.attrs.action() instead of this.sendAction() also invoke resolve in your placeDonations method, you just have resolve instead of resolve()


#11

what’s difference between this.attrs.action( ) and this.get(‘action’)( );