diff --git a/README.md b/README.md index d558742..5361fb4 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ ember install ember-select-light }} /> ``` -`value` and `label` will be the default object keys used unless `@valueKey="...` and/or `@displayKey="...` are used respectively, like so... +The text`value` and `label` will be the default object keys used for the HTML `option` and innerText respectively unless `@valueKey="...` and/or `@displayKey="...` are used respectively, like so... ```handlebars - {{get optionValue this.displayKey}} - - {{/each}} - {{else}} - {{#each @options as | optionValue |}} - - {{/each}} - {{/if}} + {{#each @options as | optionValue |}} + + {{/each}} {{/if}} diff --git a/addon/components/select-light.js b/addon/components/select-light.js index 0dbc609..058abd1 100644 --- a/addon/components/select-light.js +++ b/addon/components/select-light.js @@ -1,16 +1,19 @@ import Component from '@glimmer/component'; import { isNone } from '@ember/utils'; import { deprecate } from '@ember/debug'; +import { action } from '@ember/object'; const noop = () => {}; export default class extends Component { + childComponents = new Set(); + constructor() { super(...arguments); + this.changeCallback = this.args.onChange ?? this.args.change ?? noop; this.valueKey = this.args.valueKey ?? 'value'; this.displayKey = this.args.displayKey ?? 'label'; - this.change = this.args.onChange ?? this.args.change ?? noop; deprecate(`Triggering @change on is deprecated in favor of @onChange due to ember-template-lint's no-passed-in-event-handlers rule`, !this.args.change, { id: 'ember-select-light.no-passed-in-event-handlers', @@ -22,6 +25,27 @@ export default class extends Component { }); } + registerChild(option) { + this.childComponents.add(option); + } + + unregisterChild(option) { + this.childComponents.delete(option); + } + + getValue(valueStr) { + return Array.from(this.childComponents).reduce((selectedValue, childComponent) => { + return childComponent.value == valueStr ? childComponent.objValue : selectedValue + }, valueStr); + } + + @action + change(ev) { + let value = this.getValue(ev.target.value); + + return this.changeCallback(value, ev); + } + get hasDetailedOptions() { return ![ // Returns a boolean if all data is available for a { label: foo, value: bar } style list of options this.args.options?.[0][this.valueKey], diff --git a/addon/components/select-light/option.hbs b/addon/components/select-light/option.hbs new file mode 100644 index 0000000..531f7ac --- /dev/null +++ b/addon/components/select-light/option.hbs @@ -0,0 +1,7 @@ + diff --git a/addon/components/select-light/option.js b/addon/components/select-light/option.js new file mode 100644 index 0000000..4cd7d23 --- /dev/null +++ b/addon/components/select-light/option.js @@ -0,0 +1,43 @@ +import Component from '@glimmer/component'; + + +export default class SelectLightOption extends Component { + constructor() { + super(...arguments); + + this.valueKey = this.args.valueKey ?? 'value'; + this.displayKey = this.args.displayKey ?? 'label'; + + if (this.args.parent) { + this.args.parent.registerChild(this); + } + } + + willDestroy() { + super.willDestroy(...arguments); + + if (this.args.parent) { + this.args.parent.unregisterChild(this); + } + } + + get objValue() { + return this.args.value; + } + + get value() { + if (typeof this.args.value === 'string' || !this.args.value) { + return this.args.value; + } + + return this.args.value?.[this.valueKey] ?? this.args.value; + } + + get label() { + return this.args.value?.[this.displayKey]; + } + + get selected() { + return this.args.selectedValue == this.args.value || this.args.selectedValue == this.value; + } +} diff --git a/app/components/select-light/option.js b/app/components/select-light/option.js new file mode 100644 index 0000000..33392bc --- /dev/null +++ b/app/components/select-light/option.js @@ -0,0 +1 @@ +export { default } from 'ember-select-light/components/select-light/option'; diff --git a/tests/integration/components/select-light-test.js b/tests/integration/components/select-light-test.js index 5e38539..58fcf25 100644 --- a/tests/integration/components/select-light-test.js +++ b/tests/integration/components/select-light-test.js @@ -4,16 +4,16 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; module('Integration | Component | select-light', function(hooks) { - setupRenderingTest(hooks); + setupRenderingTest(hooks); - test('should be a element', async function(assert) { + await render(hbs``); assert.dom('select').exists(); - }); + }); - test('should allow classes, ids and names to be added to ', async function(assert) { + await render(hbs` `); + await render(hbs``); assert.dom('select').doesNotHaveAttribute('disabled'); - this.set('disabled', true); + this.set('disabled', true); assert.dom('select').hasAttribute('disabled'); - }); + }); - test('should support tabindex', async function(assert) { - this.set('tabindex', null); + test('should support tabindex', async function(assert) { + this.set('tabindex', null); - await render(hbs``); + await render(hbs``); - assert.dom('select').doesNotHaveAttribute('tabindex', '0'); + assert.dom('select').doesNotHaveAttribute('tabindex', '0'); - this.set('tabindex', 0); - assert.dom('select').hasAttribute('tabindex', '0'); - }); + this.set('tabindex', 0); + assert.dom('select').hasAttribute('tabindex', '0'); + }); - test('should have no options if none are specified', async function(assert) { - await render(hbs``); + test('should have no options if none are specified', async function(assert) { + await render(hbs``); assert.dom('select option').doesNotExist(); - }); + }); - test('should have a (disabled) placeholder option if specified', async function(assert) { - await render(hbs``); + test('should have a (disabled) placeholder option if specified', async function(assert) { + await render(hbs``); assert.dom('select option').includesText('Walrus'); - assert.dom('option').hasAttribute('disabled'); - }); + assert.dom('option').hasAttribute('disabled'); + }); - test('should be able to yield to passed options', async function(assert) { - await render(hbs` - - - - `); + test('should be able to yield to passed options', async function(assert) { + await render(hbs` + + + + `); - assert.dom('select option').includesText('Platypus'); - assert.dom('select option').hasValue('plat'); - }); + assert.dom('select option').includesText('Platypus'); + assert.dom('select option').hasValue('plat'); + }); - test('should render options from passed flat array', async function(assert) { - let options = ['squid', 'octopus']; - this.setProperties({options}); + test('should be able to yield to passed options with option component', async function(assert) { + await render(hbs` + + Platypus + + `); - await render(hbs``); + assert.dom('select option').includesText('Platypus'); + assert.dom('select option').hasValue('plat'); + }); + + test('should render options from passed flat array', async function(assert) { + let options = ['squid', 'octopus']; + this.setProperties({options}); + + await render(hbs``); assert.dom('select option').exists({ count: options.length }); - }); + }); - test('should select option that matches value', async function(assert) { - let options = ['squid', 'octopus']; - let value = options[1]; - this.setProperties({ - options, - value, - }); + test('should select option that matches value', async function(assert) { + let options = ['squid', 'octopus']; + let value = options[1]; + this.setProperties({ + options, + value, + }); - await render(hbs` + await render(hbs` `); - assert.dom('select').hasValue(value); - }); + assert.dom('select').hasValue(value); + }); - test('should change select value when changing data down value', async function(assert) { - let options = ['shortfin', 'mako']; - let value = options[1]; - this.setProperties({ - options, - value, - }); + test('should change select value when changing data down value', async function(assert) { + let options = ['shortfin', 'mako']; + let value = options[1]; + this.setProperties({ + options, + value, + }); - await render(hbs` + await render(hbs` `); - this.set('value', options[0]); - assert.dom('select').hasValue(options[0]); - }); - - test('should render options correctly when passed array of objects', async function(assert) { - let options = [ - { value: 'shortfin', label: 'Shortfin Shark' }, - { value: 'mako', label: 'Mako Shark' }, - ]; - let value = options[1].value; - this.setProperties({ - options, - value, - }); - - await render(hbs` + this.set('value', options[0]); + assert.dom('select').hasValue(options[0]); + }); + + test('should render options correctly when passed array of objects', async function(assert) { + let options = [ + { value: 'shortfin', label: 'Shortfin Shark' }, + { value: 'mako', label: 'Mako Shark' }, + ]; + let value = options[1].value; + this.setProperties({ + options, + value, + }); + + await render(hbs` `); assert.dom('select option').exists({ count: options.length }); - assert.dom('select option').hasAttribute('value', options[0].value); - assert.dom('select option').includesText(options[0].label); - assert.dom('select').hasValue(value); - }); - - test('should render options with customized value and display keys when passed array of objects', async function(assert) { - let options = [ - { val: 'shortfin', description: 'Shortfin Shark' }, - { val: 'mako', description: 'Mako Shark' }, - ]; - let value = options[1].value; - this.setProperties({ - options, - value, - }); - - await render(hbs` + assert.dom('select option').hasAttribute('value', options[0].value); + assert.dom('select option').includesText(options[0].label); + assert.dom('select').hasValue(value); + }); + + test('should render options with customized value and display keys when passed array of objects', async function(assert) { + let options = [ + { val: 'shortfin', description: 'Shortfin Shark' }, + { val: 'mako', description: 'Mako Shark' }, + ]; + let value = options[1].value; + this.setProperties({ + options, + value, + }); + + await render(hbs` `); - assert.dom('select option').hasAttribute('value', options[0].val); - assert.dom('select option').includesText(options[0].description); - }); - - test('should render options correctly when value is an empty string', async function(assert) { - let options = [ - { value: '', label: 'None' }, - { value: 'mako', label: 'Mako Shark' }, - ]; - let value = options[1].value; - this.setProperties({ - options, - value, - }); - - await render(hbs` + assert.dom('select option').hasAttribute('value', options[0].val); + assert.dom('select option').includesText(options[0].description); + }); + + test('should render options correctly when value is an empty string', async function(assert) { + let options = [ + { value: '', label: 'None' }, + { value: 'mako', label: 'Mako Shark' }, + ]; + let value = options[1].value; + this.setProperties({ + options, + value, + }); + + await render(hbs` `); + @value={{this.value}} />`); assert.dom('select option').exists({ count: options.length }); - assert.dom('select option').hasAttribute('value', options[0].value); - assert.dom('select option').includesText(options[0].label); - assert.dom('select').hasValue(value); - }); + assert.dom('select option').hasAttribute('value', options[0].value); + assert.dom('select option').includesText(options[0].label); + assert.dom('select').hasValue(value); + }); - test('should fire onChange despite the deprecation warning if using @change', async function(assert) { - this.set('myValue', null); + test('should fire onChange despite the deprecation warning if using @change', async function(assert) { + this.set('myValue', null); - await render(hbs` - + await render(hbs` + `); - await fillIn('select', 'turtle'); - await triggerEvent('select', 'change'); + await fillIn('select', 'turtle'); + await triggerEvent('select', 'change'); - assert.dom('select').hasValue('turtle'); - assert.equal(this.myValue, 'turtle'); - }); + assert.dom('select').hasValue('turtle'); + assert.equal(this.myValue, 'turtle'); + }); - test('should fire onChange when user chooses option, mut with yield', async function(assert) { - this.set('myValue', null); + test('should fire onChange when user chooses option, mut with yield and native option', async function(assert) { + this.set('myValue', null); - await render(hbs` - + await render(hbs` + `); - await fillIn('select', 'turtle'); - await triggerEvent('select', 'change'); + await fillIn('select', 'turtle'); + await triggerEvent('select', 'change'); + + assert.dom('select').hasValue('turtle'); + assert.equal(this.myValue, 'turtle'); + }); + + test('should fire onChange when user chooses option, mut with yielded option component', async function(assert) { + this.set('myValue', null); + + await render(hbs` + + Turtle + + `); + + await fillIn('select', 'turtle'); + await triggerEvent('select', 'change'); + + assert.dom('select').hasValue('turtle'); + assert.equal(this.myValue, 'turtle'); + }); + + test('should fire onChange when user chooses option, mut with flat array', async function(assert) { + let options = ['clam', 'starfish']; + this.setProperties({ + options, + myValue: options[1], + value: options[1], + }); + + await render(hbs` + + `); + + await fillIn('select', options[0]); + await triggerEvent('select', 'change'); - assert.dom('select').hasValue('turtle'); - assert.equal(this.myValue, 'turtle'); - }); + assert.dom('select').hasValue(options[0]); + assert.equal(this.myValue, options[0]); + }); - test('should fire onChange when user chooses option, mut with flat array', async function(assert) { - let options = ['clam', 'starfish']; - this.setProperties({ - options, - myValue: options[1], - value: options[1], - }); + test('should fire onChange when user chooses option, custom action with flat array', async function(assert) { + let options = ['clam', 'starfish']; + this.setProperties({ + options, + value: options[1], + customAction: (value) => { + assert.step('handled action'); + assert.equal(value, options[0]); + }, + }); - await render(hbs` + await render(hbs` + @onChange={{action this.customAction}} /> `); + await fillIn('select', options[0]); + + assert.verifySteps(['handled action']); + }); - await fillIn('select', options[0]); - await triggerEvent('select', 'change'); + test('should allow objects to be selected onChange', async function(assert) { + const shortfin = { value: 'shortfin', label: 'Shortfin Shark' }; + const mako = { value: 'mako', label: 'Mako Shark' }; - assert.dom('select').hasValue(options[0]); - assert.equal(this.myValue, options[0]); - }); + let options = [ + shortfin, + mako, + ]; - test('should fire onChange when user chooses option, custom action with flat array', async function(assert) { - let options = ['clam', 'starfish']; - this.setProperties({ - options, - value: options[1], - customAction: ({ target: { value } }) => { + this.setProperties({ + options, + value: mako, + customAction: (value) => { assert.step('handled action'); - assert.equal(value, options[0]); - }, - }); + assert.equal(value, options[0]); + }, + }); - await render(hbs` + await render(hbs` `); - await fillIn('select', options[0]); + + await fillIn('select', shortfin.value); + + assert.verifySteps(['handled action']); + }); + + test('should allow objects to be selected onChange with yielded components', async function(assert) { + const shortfin = { value: 'shortfin', label: 'Shortfin Shark' }; + const mako = { value: 'mako', label: 'Mako Shark' }; + + let options = [ + shortfin, + mako, + ]; + + this.setProperties({ + options, + value: mako, + customAction: (value) => { + assert.step('handled action'); + assert.equal(value, options[0]); + }, + }); + + await render(hbs` + + + {{#each this.options as |option|}} + {{option.label}} + {{/each}} + + `); + + assert.dom('select').hasValue(mako.value); + + await fillIn('select', shortfin.value); + + assert.verifySteps(['handled action']); + }); + + test('should allow objects to be selected onChange with yielded components and non-string values', async function(assert) { + const shortfin = { value: 1, label: 'Shortfin Shark' }; + const mako = { value: 2, label: 'Mako Shark' }; + + let options = [ + shortfin, + mako, + ]; + + this.setProperties({ + options, + value: mako, + customAction: (value) => { + assert.step('handled action'); + assert.equal(value, options[0]); + }, + }); + + await render(hbs` + + + {{#each this.options as |option|}} + {{option.label}} + {{/each}} + + `); + + assert.dom('select').hasValue(`${mako.value}`); + + await fillIn('select', shortfin.value); assert.verifySteps(['handled action']); - }); + }); });