What is Ember's magic concerning CheckBox indeterminate

The <input type="checkbox"> has a neat indeterminate state which is documented on MDN (emphasis mine):

In addition to the checked and unchecked states, there is a third state a checkbox can be in: indeterminate . This is a state in which it’s impossible to say whether the item is toggled on or off. This is set using the HTMLInputElement object’s indeterminate property via JavaScript (it cannot be set using an HTML attribute)

I noticed today that the following template works in Ember despite documentation to the contrary:

<input type="checkbox" indeterminate={{this.isIndeterminate}}>

How is this working? What magic is allowing this property to work outside of JavaScript?

For comparison I wrote a JS only Ember component to handle this situation but it seems it is not needed?

import Component from '@ember/component';
import { computed } from '@ember/object';

export const STATES = Object.freeze({
  NONE: 'none',
  SOME: 'some',
  ALL: 'all'
});

export default Component.extend({
  tagName: 'input',
  attributeBindings: ['type'],
  type: 'checkbox',
  
  isChecked: computed('checked', function() {
    return this.checked !== STATES.NONE ? !!this.checked : false;
  }),
  
  isIndeterminate: computed('checked', function() {
    return this.checked === STATES.SOME;
  }),
  
  updateElementState() {
    this.element.checked = this.isChecked;
    this.element.indeterminate = this.isIndeterminate;
  },
  
  // didReceiveAttrs is called before didInsertElement
  // and does not have a this.element yet.

  didInsertElement() {
    this._super(...arguments);
    this.updateElementState();
  },
  
  didUpdateAttrs() {
    this._super(...arguments);
    this.updateElementState();
  },
  
  click() {
    if (this.onClick) this.onClick();
  }
});

This is a long-running bit of complexity that we haven’t been able to eliminate without breaking a lot of existing apps. Ember (really Glimmer down inside Ember) has a heuristic for deciding which things to set as attributes and which as properties. The decision is implemented here:

If you find that code in your app and put a breakpoint in it, you can see that when we get an <input> element and the “indeterminate” slot, we notice that the Element has a property named indeterminate and decide it should be handled as a prop. Then we return out to here and actually do the setup for attribute vs property:

The in-progress Strict Mode RFC mentions the best current plan for eventually simplifying this behavior:

Today, Glimmer uses a complicated set of huristics to decide if a bound HTML “attribute” syntax should indeed be set using setAttribute or set as a JavaScript property using element[...] = ...; . This does not always work well in practice, and it causes a lot of confusion and complexity.

We intend to move to an “attributes syntax always mean attributes” (and use modifiers for the rare cases of setting properties). We briefly considered grouping that change into the strict mode opt-in, but ultimately decided it would be too confusing for strict mode to include such a change. It’s better to deprecate the feature and make this an app-wide setting.

By “use modifiers”, they mean it would look something like this:

<!--- speculative, this does not work today! --->
<input {{setProperty indeterminate=this.isIndeterminate }} >
2 Likes

Wow! That answer was impressive. Thank you so much. The world feels more calm now.