Skip to content

Instantly share code, notes, and snippets.

@sukima
Last active March 13, 2019 04:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sukima/269d91b22c77d5fa7c42d72dfa9ddab3 to your computer and use it in GitHub Desktop.
Save sukima/269d91b22c77d5fa7c42d72dfa9ddab3 to your computer and use it in GitHub Desktop.
Provider Component: Options Selector
import Component from '@ember/component';
import { computed } from '@ember/object';
import { STATES } from './options-selector';
// FYI: doing ARIA role="checkbox" by hand is a PITA! Go native!
export default Component.extend({
tagName: '',
isChecked: computed('checked', function() {
return this.checked !== STATES.NONE ? !!this.checked : false;
}),
isMixed: computed('checked', function() {
return this.checked === STATES.SOME;
}),
ariaChecked: computed('{isChecked,isMixed}', function() {
if (this.isMixed) return 'mixed';
return this.isChecked ? 'true' : 'false';
}),
actions: {
handleClick(event) {
event.preventDefault();
if (this.onClick) this.onClick();
}
}
});
import Component from '@ember/component';
import { computed } from '@ember/object';
import { STATES } from './options-selector';
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();
}
});
import Component from '@ember/component';
import { computed } from '@ember/object';
export default Component.extend({
tagName: '',
isSelected: computed('{option,selected.[]}', function() {
return this.selected.includes(this.option);
})
});
import Component from '@ember/component';
import { computed } from '@ember/object';
import { equal, or } from '@ember/object/computed';
export const STATES = Object.freeze({
NONE: 'none',
SOME: 'some',
ALL: 'all'
});
export default Component.extend({
STATES,
tagName: '',
isAllOrNone: or('{isNoneSelected,isAllSelected}'),
isNoneSelected: equal('state', STATES.NONE),
isSomeSelected: equal('state', STATES.SOME),
isAllSelected: equal('state', STATES.ALL),
state: computed('{options,selected}.length', function() {
if (this.options.length === this.selected.length) {
return STATES.ALL;
} else if (this.selected.length === 0) {
return STATES.NONE;
} else {
return STATES.SOME;
}
}),
init() {
this._super(...arguments);
this.set('selected', []);
},
add(option) {
this.selected.pushObject(option);
},
remove(option) {
this.selected.removeObject(option);
},
setAll() {
this.selected.setObjects(this.options);
},
clear() {
this.selected.clear();
},
actions: {
addOption(option) {
if (!this.selected.includes(option)) {
this.add(option);
}
},
removeOption(option) {
this.remove(option);
},
toggleOption(option) {
if (this.selected.includes(option)) {
this.remove(option);
} else {
this.add(option);
}
},
addAll() {
this.setAll();
},
removeAll() {
this.clear();
},
toggleAllOrNone() {
if (this.isAllSelected) {
this.clear();
} else {
this.setAll();
}
}
}
});
import Controller from '@ember/controller';
import { reads } from '@ember/object/computed';
export default Controller.extend({
samples: reads('model')
});
import Route from '@ember/routing/route';
export default Route.extend({
model() {
return ['A', 'B', 'C'];
}
});
body {
margin: 12px 16px;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 16pt;
}
button {
background-color: lightgray;
border: thin solid black;
font-size: 16pt;
padding: 2px 8px;
}
button:hover {
background-color: white;
}
.choice-btn.selected {
background-color: gray;
}
ul.inline {
display: inline;
list-style: none;
margin: 0;
padding: 0;
}
ul.inline li {
display: inline;
margin: 0;
padding: 0;
}
ul.inline.comma li:after {
content: ',';
}
ul.inline.pipe li:before {
content: '| ';
}
ul.inline li:first-child:before,
ul.inline li:last-child:after {
content: '';
}
<OptionsSelector @options={{this.samples}} as |selector|>
<p>
<button {{action selector.toggleAll}}>
{{#if selector.isAllSelected}}
Select None
{{else}}
Select All
{{/if}}
</button>
</p>
<p>
Options:
<ul class="inline pipe">
{{#each this.samples as |sample|}}
<li>
<selector.check @option={{sample}} as |isSelected|>
<button
class="choice-btn {{if isSelected "selected"}}"
{{action selector.toggle sample}}
>
{{sample}}
</button>
</selector.check>
</li>
{{/each}}
</ul>
</p>
<p>
Selected:
<ul class="inline comma">
{{#each selector.selected as |sample|}}
<li>{{sample}}</li>
{{else}}
<li>None</li>
{{/each}}
</ul>
{{#if selector.isSomeSelected}}(partial){{/if}}
{{#if selector.isAllSelected}}(all){{/if}}
</p>
<p>
As native checkbox:
<MixedCheckBox
@checked={{selector.state}}
@onClick={{action selector.toggleAll}}
/>
</p>
<p>
As ARIA role checkbox:
<AriaCheckBox
@checked={{selector.state}}
@onClick={{action selector.toggleAll}}
/>
</p>
</OptionsSelector>
<hr>
<AboutTwiddle/>
<p>
This twiddle demonstrates two concepts:
<ol>
<li>the use of a <term>provider component</term></li>
<li>the use of a native checkbox with a partial (mixed) state</li>
</ol>
</p>
<p>The ARIA <code>role="checkbox"</code> example was just for testing. I found the native version (<code>mixed-check-box</code>) to be far superior in ease of use, keyboard interaction, and screen reader compatibility.</p>
<span
role="checkbox"
tabindex="0"
style="cursor: pointer"
aria-checked={{this.ariaChecked}}
onclick={{action "handleClick"}}
...attributes
>
{{#if this.isMixed}}
Mixed
{{else if this.isChecked}}
Checked
{{else}}
Unchecked
{{/if}}
</span>
{{yield (hash
state=this.state
isAllSelected=this.isAllSelected
isNoneSelected=this.isNoneSelected
isSomeSelected=this.isSomeSelected
selected=this.selected
check=(component "options-selector/check" selected=this.selected)
select=(action "addOption")
unselect=(action "removeOption")
toggle=(action "toggleOption")
selectAll=(action "addAll")
unselectAll=(action "removeAll")
toggleAll=(action "toggleAllOrNone")
)}}
{
"version": "0.15.1",
"EmberENV": {
"FEATURES": {}
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.js",
"ember": "3.4.3",
"ember-template-compiler": "3.4.3",
"ember-testing": "3.4.3"
},
"addons": {
"ember-data": "3.4.2"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment