1

I'm using Ember v2.5.1 and Ember data v2.6.2 and I have a series of nested components to render a hierarchical tree of categories. There is a closure action in the lowest level component categories-tree-node, which calls the toggleAddCategory function in the actions of the categories-select component and passes up the relevant category object.

It works as expected in Safari, but for some reason the checked state is passed through inverted in Chrome and FF. The strange thing is that the checked state binding of the value itself renders correctly elsewhere in the template when the checkboxes are changed.

I have created a demo here, if you try checking/unchecking the boxes in Chrome/FF vs Safari you should see the issue. Is there a prefered way of handling this type of action binding with checkbox checked states? I have read that using Observers is considered an anti-pattern in Ember 2 and above, also when I tried this it did not work for the child categories.

Ross Anthony
  • 521
  • 4
  • 14
  • Quick side note, not a solution. There's no need to pass closure actions like `(action this.attrs.toggle)`, the `toggle` part is already available in the component scope so you can just say `(action toggle)` – nem035 Nov 02 '16 at 15:02

1 Answers1

0

Ok so your twiddle shows that the state is unchanged in Chrome but changed in Safari.

This is most likely a bug with jQuery in Safari because Ember.CheckBox sets checked using the jQuery prop method:

change() {
  set(this, 'checked', this.$().prop('checked'));
}

A workaround solution would be to use the checkbox as an angle-bracket component (which have one-way bindings by default) to ensure the checked binding is not changed by the input component and instead use the passed in closure action to manually toggle the checked state yourself.

This also goes along with the Ember recommended best practice for setting parent properties from components using the Data Down - Actions Up pattern.

Note: You might want to use the onchange event rather than onclick.

Child components/categories-tree-node.hbs

<input type="checkbox"
       checked={{category.checked}} <!-- checked is just passed from above, not set within the component -->
       onchange={{action toggle category}} <!-- onchange calls the passed in action which toggles checked -->
       id={{concat elementId '-' category.slug}}
>

...

Parent components/categories-select.js

export default Ember.Component.extend({
  // ...

  actions: {

    toggleAddCategory(category) {
      category.toggleProperty('checked'); // toggle the property yourself

      // ...  
    }

  }
});

If your parent action only does the toggle and nothing else, you probably don't need it and can do the toggling right inside the components/categories-tree-node.hbs using the mut helper:

<input type="checkbox"
       checked={{category.checked}} <!-- checked is just passed from above, not set within the component -->
       onchange={{action (mut category.checked) value="target.checked"}} <!-- onchange automatically sets category.checked -->
       id={{concat elementId '-' category.slug}}
>
Community
  • 1
  • 1
nem035
  • 34,790
  • 6
  • 87
  • 99
  • Excellent, thanks for the quick response. I have removed the binding with the checked state in favour of toggling this in the toggleAddCategory action instead. It's now working in Chrome, FF and Safari! See updated twiddle here: https://ember-twiddle.com/1084e7750f45b034b917ecc9cd3c5cea?openFiles=components.categories-select.js%2Ctemplates.components.categories-select.hbs – Ross Anthony Nov 02 '16 at 15:41
  • @RossAnthony glad to help out mate :) – nem035 Nov 02 '16 at 15:42
  • I just had a thought, if I take off the binding for the checked state then how can the initial state be set? E.g. when editing a record which already has some categories selected. – Ross Anthony Nov 02 '16 at 16:08
  • 1
    Sorry I missed that. Think I've found another solution. If I switch the input to a regular rather than {{input}} then add checked={{category.checked}} and make the action happen onchange rather than click it all seems to work fine, see: https://ember-twiddle.com/1084e7750f45b034b917ecc9cd3c5cea?openFiles=templates.components.categories-tree-node.hbs%2Ctemplates.components.categories-select.hbs – Ross Anthony Nov 02 '16 at 16:44
  • When using rather than {{input checked=category.checked}}, is the main difference that the input helper will bind the checked value both ways vs. a one-way binding? Is this essentially what you meant by "use a property within components/categories-tree-node that you set after the component renders to whatever the value of category.checked"? – Ross Anthony Nov 02 '16 at 16:49
  • What I meant was to use the `didReceiveAttrs` (or perhaps one of the other) component lifecycle hook and inside of it do something like `this.set('isChecked', this.get('category.checked'))`, and in your template you can bind the `isChecked` property – nem035 Nov 02 '16 at 17:02
  • Yes, angle-bracket components introduce one-way data flow by default, and provide an opt-in for two-way data flow. That's actually a better solution, I'll edit my answer accordingly. – nem035 Nov 02 '16 at 17:07
  • Thanks for the clarification and also for providing such a detailed answer, I will have a more thorough read through those Ember related resources you included. You can probably tell that I'm fairly new to the world of ember (coming over from using angular in the past) and was battling for sometime with this particular issue, I really appreciate your help! – Ross Anthony Nov 02 '16 at 19:23
  • No problem and welcome to Ember, it's an awesome framework :). Also I added another possible solution at the end of my answer. – nem035 Nov 02 '16 at 19:30