A select-like menu with searching and filtering capabilities
To get started, install this addon, ember-cli-sass, and include the ember-searchable-select styles in your app.scss.
ember install ember-searchable-select
ember install ember-cli-sass
@import "ember-searchable-select/style";
Searchable select is data-down, actions up by default. This means there is no automatic two-way binding and you will need to update the selected value elsewhere in your app by using the on-change
action. The selected object gets passed to your on-change action as an argument.
{{searchable-select
content=TEDevents
sortBy="title"
on-change=(action "update")}}
<p>Set by Searchable: {{setBySearchable.title}}</p>
export default Ember.Component.extend({
classNames: 'Example',
setBySearchable: null,
actions: {
update(selection) {
this.set('setBySearchable', selection);
}
}
});
Set by Searchable:
You can set an initial selection by passing in one of the objects from the content array. This value will *not* be two-way bound and therefore, not mutated by the select component. It's up to you to sync up data outside the select by responding to the on-change action.
{{searchable-select
content=TEDevents
sortBy="title"
selected=initialSelection
on-change=(action "update")}}
<p>Initial selection: {{initialSelection.title}}</p>
<p>Set by Searchable: {{setBySearchable.title}}</p>
You can sort the list by a single property (as seen in the examples above) or multiple comma-separated properties as shown here. You can also use the :desc
qualifier to reverse sort order on a property, similar to Ember.computed.sort.
{{searchable-select
content=TEDspeakers
sortBy="firstName,lastName:desc"
optionLabelKey="fullName"
on-change=(action "update")}}
<p>Set by Searchable: {{setBySearchable.fullName}}</p>
Set by Searchable:
Just like a real select menu, you can disable menu items. Pass in a item key to use as a flag; when true the item will not be selectable. If you don't have an existing model key that evaluates to true/false, you can set up a computed property on the content model that returns a boolean value.
{{searchable-select
content=TEDevents
sortBy="title"
optionDisabledKey="isTEDxEvent"
on-change=(action "update")}}
<p>Set by Searchable: {{setBySearchable.title}}</p>
Set by Searchable:
{{searchable-select
content=TEDevents
sortBy="title"
on-change=(action "update")
limitSearchToWordBoundary=true}}
<p>Set by Searchable: {{setBySearchable.title}}</p>
Set by Searchable:
Setting multiple=true
will put the menu into multi-select mode. Selected options are displayed as pills that can be individually removed. When in multi-select mode, 'on-change' sends an array of objects as an argument, rather than a single object.
When combined with new item creation (below), searchable-select can be used as a tagging UI component.
{{searchable-select
content=TEDevents
sortBy="title"
multiple=true
prompt="Choose events"
on-change=(action "update")}}
<p>Set by Searchable:</p>
<ul>
{{#each setBySearchable as |selection|}}
<li>{{selection.title}}</li>
{{/each}}
</ul>
You can allow users to add a new item when no match is found by specifying an action for on-add
. The typed text gets sent with the action as a parameter. Works with both single and multi select. You will need to handle updating the content array and the current selection on your own.
The "Add " text can be customized with the addLabel
option.
{{searchable-select
content=talkTags
sortBy="name"
optionLabelKey="tag"
multiple=true
selected=selectedTags
prompt="Select tags"
on-add=(action "addNew")}}
export default Ember.Component.extend({
classNames: 'Example',
newItemName: null,
talkTags: null,
selectedTags: [],
numTags: Ember.computed.alias('talkTags.length'),
actions: {
addNew(text) {
this.set('newItemName', text);
let newTag = {
id: this.get('numTags'),
tag: text
};
this.get('talkTags').addObject(newTag);
this.get('selectedTags').addObject(newTag);
}
}
});
You can capture the search text outside the select by setting an on-search
action. This allows you to do things like populate the content array via AJAX, or use a custom filter on your data set (eg. that searched on additional properties). Note that passing in an action implies you're handling filtering on your own and will disable the addon's internal filter.
eg. Try typing 'NYC' in the search below.
<p>Query text: {{queryText}}</p>
{{searchable-select
content=filteredTEDevents
sortBy="title"
on-change=(action "updateSelection")
on-search=(action "updateSearch")
on-close=(action "clearResultsList")}}
<p>Set by Searchable: {{setBySearchable.title}}</p>
export default Ember.Component.extend({
classNames: 'Example',
TEDevents: null,
filteredTEDevents: null,
setBySearchable: null,
queryText: null,
actions: {
updateSelection(selection) {
this.set('setBySearchable', selection);
},
updateSearch(text) {
// this example filters a local data set,
// you could also AJAX update your content here
this.set('queryText', text);
let regex = this.get('queryText') ?
new RegExp(this.get('queryText'), 'i') :
new RegExp('/S', 'i');
let matches = this.get('TEDevents').filter(item => {
return regex.test(item.title) || regex.test(item.keywords);
});
this.set('filteredTEDevents', Ember.A(matches));
},
clearResultsList() {
this.set('filteredTEDevents', null);
}
}
});
You may want to trigger a loading state on the menu when using your own on-search
action with an AJAX fetch. To do this, pass in your own boolean `isLoading` property. When set to true, a loader animation will display. It will disappear when false. You can customize the loading message with the loadingMessage
property.
Note that with a slow AJAX search you might want to debounce your requests to fire less than every on-search
event.
<p>Query text: {{queryText}}</p>
{{searchable-select
content=filteredTEDevents
sortBy="title"
on-change=(action "updateSelection")
on-search=(action "updateSearch")
isLoading=isLoadingEvents}}
<p>Set by Searchable: {{setBySearchable.title}}</p>
export default Ember.Component.extend({
classNames: 'Example',
TEDevents: null,
filteredTEDevents: null,
setBySearchable: null,
queryText: null,
isLoadingEvents: false,
actions: {
updateSelection(selection) {
this.set('setBySearchable', selection);
},
updateSearch(text) {
this.set('queryText', text);
this.set('filteredTEDevents', null);
if (text) {
this.send('searchForEvents');
}
},
searchForEvents() {
// If you have a slow AJAX response, you can pass
// in an `isLoading` flag to display a loader.
// Set to true while you're fetching results...
this.set('isLoadingEvents', true);
let regex = this.get('queryText') ?
new RegExp(this.get('queryText'), 'i') :
new RegExp('/S', 'i');
let matches = this.get('TEDevents').filter(item => {
return regex.test(item.title) || regex.test(item.keywords);
});
// ...then set back to false once the AJAX call resolves.
// Here, we pretend have a slow response using .setTimeout().
// With a real AJAX fetch this would happen in the callback or
// promise resolution.
window.setTimeout(() => {
this.set('filteredTEDevents', Ember.A(matches));
this.set('isLoadingEvents', false);
}, 1000);
}
}
});
Although data-down, actions-up is the recommended usage, you can force a two-way binding with Ember's mut
helper. Instead of passing in an action name to on-change
, pass in something like (mut boundValue)
.
{{searchable-select
content=TEDevents
sortBy="title"
selected=selectedOption
on-change=(action (mut selectedOption))}}
<p>Selected option (bound): {{selectedOption.title}}</p>
Selected option (bound): TED2015
Option | Description | Type | Default value |
---|---|---|---|
content | An array of objects used to populate the list. | Ember.A() | null |
selected | Pass in an initial selection or update the selection when outside data has changed. Must be an object from the content array for single select or an array of options for multi-select. | Object | null |
on-change | Specify your own named action to trigger when the selection changes. eg. (action "update") For single selection (default behaviour), the selected object is sent as an argument. For multiple selections, an array of options is sent. |
Ember action | null |
optionLabelKey | The item property to use as the visible label in the menu. | string | 'title' |
optionDisabledKey | Provide a boolean item property to use a flag for disabling menu items. [optional] | string, null | null |
sortBy | The item property to use for a sort key. Accepts a single property or a comma separated list. Also allows the use of the :desc qualifier for reversing sort order. Sorting is disabled when set to null. eg. sortBy:title , sortBy="firstName,lastName" , sortBy="year:desc" |
string, null | null |
limitSearchToWordBoundary | When true, search will only match the beginning of words | boolean | true |
multiple | Set to true to enable multiple selection. You must use an array for selected in mutli-select mode. |
boolean | false |
closeOnSelection | Set to false to disabled automatically closing the menu once a selection is made. | boolean | true |
prompt | Prompt text for the main input. | string | 'Select an option' |
searchPrompt | Prompt text for the search input. | string | 'Type to search' |
noResultsMessage | Message to display if a filter produces no results | string | 'No matching results' |
isClearable | When true, a clear button is available to clear the input and remove any selection that has been made. | boolean | true |
clearLabel | Text to display beside the clear button when isClearable is true. |
string | 'Clear selection' |
on-add | Allow unfound items to be added to the content array by specifying your own named action. eg. (action "addNew") The new item name is sent as an argument. You must handle adding the item to the content array and selecting the new item outside the component. |
Ember action | null |
addLabel | Text to show before the new text to be added when an on-add action is provided. |
string | 'Add' |
on-search | Specify your own named action to trigger when the search text changes. eg. (action "search") The search text is sent as an argument. Useful for custom filtering or AJAX search. The component's internal filter is automatically disabled when an on-search action is provided. |
Ember action | null |
isLoading | Pass in your own boolean flag to toggle visibility of a loading animation. | boolean | false |
loadingMessage | Text to displayed beside the loading animation when isLoading is true. |
string | 'Searching...' |
on-open | Specify your own named action to trigger when the menu opens. Useful if you opt not to auto close the menu when a selection is made and you'd like to hold off on propagating changes until the menu closes. | Ember action | null |
on-close | Specify your own named action to trigger when the menu closes. Useful hook for clearing out content that was previously passed in with AJAX. | Ember action | null |