Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<SelectLight
Expand Down
33 changes: 17 additions & 16 deletions addon/components/select-light.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,23 @@
{{/if}}

{{#if hasBlock}}
{{yield}}
{{yield (hash
option=(
component "select-light/option"
parent=this
valueKey=this.valueKey
displayKey=this.displayKey
selectedValue=@value
)
)}}
{{else}}
{{#if this.hasDetailedOptions}}
{{#each @options as | optionValue |}}
<option
value={{get optionValue this.valueKey}}
selected={{is-equal (get optionValue this.valueKey) @value}}>
{{get optionValue this.displayKey}}
</option>
{{/each}}
{{else}}
{{#each @options as | optionValue |}}
<option value={{optionValue}} selected={{is-equal optionValue @value}}>
{{optionValue}}
</option>
{{/each}}
{{/if}}
{{#each @options as | optionValue |}}
<SelectLight::Option
@parent={{this}}
@valueKey={{this.valueKey}}
@displayKey={{this.displayKey}}
@value={{optionValue}}
@selectedValue={{@value}} />
{{/each}}
{{/if}}
</select>
26 changes: 25 additions & 1 deletion addon/components/select-light.js
Original file line number Diff line number Diff line change
@@ -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 <SelectLight /> 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',
Expand All @@ -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],
Expand Down
7 changes: 7 additions & 0 deletions addon/components/select-light/option.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<option value={{this.value}} selected={{this.selected}} ...attributes>
{{#if hasBlock}}
{{yield}}
{{else}}
{{this.label}}
{{/if}}
</option>
43 changes: 43 additions & 0 deletions addon/components/select-light/option.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Component from '@glimmer/component';


export default class SelectLightOption extends Component {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love seeing this logic extracted to its own sub component, well done!

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;
}
}
1 change: 1 addition & 0 deletions app/components/select-light/option.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from 'ember-select-light/components/select-light/option';
142 changes: 137 additions & 5 deletions tests/integration/components/select-light-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ module('Integration | Component | select-light', function(hooks) {
assert.dom('select option').hasValue('plat');
});

test('should be able to yield to passed options with option component', async function(assert) {
await render(hbs`
<SelectLight as |sl|>
<sl.option value="plat">Platypus</sl.option>
</SelectLight>
`);

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});
Expand Down Expand Up @@ -187,7 +198,7 @@ module('Integration | Component | select-light', function(hooks) {
this.set('myValue', null);

await render(hbs`
<SelectLight @change={{action (mut myValue) value="target.value"}}>
<SelectLight @change={{action (mut myValue)}}>
<option value="turtle">Turtle</option>
</SelectLight>
`);
Expand All @@ -199,11 +210,11 @@ module('Integration | Component | select-light', function(hooks) {
assert.equal(this.myValue, 'turtle');
});

test('should fire onChange when user chooses option, mut with yield', async function(assert) {
test('should fire onChange when user chooses option, mut with yield and native option', async function(assert) {
this.set('myValue', null);

await render(hbs`
<SelectLight @onChange={{action (mut myValue) value="target.value"}}>
<SelectLight @onChange={{action (mut myValue)}}>
<option value="turtle">Turtle</option>
</SelectLight>
`);
Expand All @@ -215,6 +226,22 @@ module('Integration | Component | select-light', function(hooks) {
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`
<SelectLight @onChange={{action (mut myValue)}} as |sl|>
<sl.option value="turtle">Turtle</sl.option>
</SelectLight>
`);

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({
Expand All @@ -227,7 +254,7 @@ module('Integration | Component | select-light', function(hooks) {
<SelectLight
@options={{this.options}}
@value={{this.value}}
@onChange={{action (mut this.myValue) value="target.value"}} />
@onChange={{action (mut this.myValue)}} />
`);

await fillIn('select', options[0]);
Expand All @@ -242,7 +269,7 @@ module('Integration | Component | select-light', function(hooks) {
this.setProperties({
options,
value: options[1],
customAction: ({ target: { value } }) => {
customAction: (value) => {
assert.step('handled action');
assert.equal(value, options[0]);
},
Expand All @@ -258,4 +285,109 @@ module('Integration | Component | select-light', function(hooks) {

assert.verifySteps(['handled action']);
});

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' };

let options = [
shortfin,
mako,
];

this.setProperties({
options,
value: mako,
customAction: (value) => {
assert.step('handled action');
console.log(value)
assert.equal(value, options[0]);
},
});

await render(hbs`
<SelectLight
@options={{this.options}}
@value={{this.value}}
@onChange={{action this.customAction}} />
`);

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');
console.log(value)
assert.equal(value, options[0]);
},
});

await render(hbs`
<SelectLight
@value={{this.value}}
@onChange={{action this.customAction}} as |sl|>
<option value="">Choose</option>
{{#each this.options as |option|}}
<sl.option @value={{option}}>{{option.label}}</sl.option>
{{/each}}
</SelectLight>
`);

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');
console.log(value)
assert.equal(value, options[0]);
},
});

await render(hbs`
<SelectLight
@value={{this.value}}
@onChange={{action this.customAction}} as |sl|>
<option value="">Choose</option>
{{#each this.options as |option|}}
<sl.option @value={{option}}>{{option.label}}</sl.option>
{{/each}}
</SelectLight>
`);

assert.dom('select').hasValue(`${mako.value}`);

await fillIn('select', shortfin.value);

assert.verifySteps(['handled action']);
});
});