How can I send actions INTO a component?


#1

I’m trying to make a small component – a dropdown menu with content provided by the calling template, but that has some common functionality (hence putting it in a component).

For example, I have a page template HBS:

{{#mydropdown-menu}}
    // custom content for menu
{{/mydropdown-menu}}

with a mydropdown-menu template HBS:

<div class="dropdown" id={{id}}>
    <button class="dropdown-toggle" {{action 'closeDropdown'}}>{{buttonText}}</button> 
    <ul class="dropdown-menu">
        <li>
            {{yield}}
        </li>
    </ul>
</div>

What I want to be able to do is send an action from something inside the {{yield}} block back into the component, so it can handle it. For example, the page template might be something like this:

{{#mydropdown-menu}}
    // custom content for menu
    // button to close dropdown
{{/mydropdown-menu}}

Where pressing the button would call the ‘closeDropdown’ action inside the component. However, once I’m in the block in the main page template, I have no idea how to refer back to the component controller or actions. How can I do this, or is this what I even want to do?

Disclaimer: I’m still new to Ember, so it’s likely that I’m doing this the wrong way. If someone can let me know either a) how to do what I’m trying to, or b) why I shouldn’t be doing it that way, it would be appreciated!


#2

The easier way to handle this is to bind a variable (or attribute) to the component and then inside have a computed property that observes what that bound variable is doing.

So for example imagine you have a parameter defined outside your component

dropdownClosed: true

then a component like this

{{my-component menu dropdownClosed=dropdownClosed}}

Then inside the component you can have something like this

handleDropdownChange: Ember.computed('dropdownClosed', function() {
    var dropdownClosed = this.get('dropdownClosed');
    
    if (dropdownClosed) {
      // do something when true, like call another function or set something else
      // you could even call your closeDropdown drop function
    } else {
      // do something when false, perhaps do nothing or perhaps close the menu and clean up state 
    }
}),

#3

Aah, OK, I think I understand – so instead of trying to send something INTO the component from the calling block, I set up the component to watch a variable outside (in the page controller, say), and take action based on that.

Thanks for those pointers. I’ll play with that tomorrow, hopefully, and let you know how I go!


#4

Yeah, there is a general principle encouraged by Ember (and React) called “data down, actions up”. Meaning that you should one way bind data variables to flow data down into your component hierarchy and use actions to send events back up the chain to a more centralized location for managing state such as routes, services, and controllers.

If you get stuck try posting a JSBin that demonstrates the problem. Sometimes easier to work through a focused code demo.

Good luck!


#5

http://emberjs.jsbin.com/mabovazare/edit?html,js,output this is how I might approach this


#6

Well, I guess it depends? If you have, say, 8 different components, and you want to “send something into that component”, binding the component property to some external property might become quite unwieldy.

Of course, in the context of the original question, that’s probably the best solution.

But another approach that I have used is having my component send some action with itself as the first parameter, e.g.

<!-- component.hbs -->
{{input type="text" value=name focus-out="validate"}}

and then

// component.js
actions: {
  validate: function() {
    this.sendAction('validate', this);
  }
}

and then the controller does:

validate: function(component) {
  // perform some validation
  // if error:
  component.set('error', validationError);
}

#7

Interesting approach. How well does this work with the new closure actions?


#8

EDIT: Disregard this code, it was a POC and it doesn’t seem to match how closure actions actually work ^^

tbh I haven’t looked into that yet, but it seems that this would remove the need for my solution altogether.

If actions have return values, in my component I can simply do:

validate: function() {
  var isValid = this.sendAction('validate');
  if (!isValid) {
    this.set('error', 'some error');
  }
}

which would be much cleaner anyway, as the whole component logic now is only in the component.


#9

With the release of Ember 1.13, you can yield functions that can be passed down to child components. Something I just discovered. :smile:

The key here is to use the action helper to bind the component layout’s scope (the component itself) as the handler’s context and yield that.

// parent-component.js
Ember.Component.extend({
  handler() {
  }
});

// parent-component.hbs
{{yield (action handler)}}

// child-component.js
Ember.Component.extend({
  attrs: {
    handler() {}
  },

  method() {
    this.attrs.handler();
  }
});

// application.hbs

{{#parent-component as |handler|}}
  {{child-component handler=handler}}
{{/parent-component}}