Ember

Searchable Select

A select-like menu with searching and filtering capabilities


Installation

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


app.scss

@import "ember-searchable-select/style";


Examples

Standard usage

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:

Pass in an inital selection

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>

Initial selection: TED2015

Set by Searchable:

Sorting the list

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:

Disabled options

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:

Limit search to word boundary

  {{searchable-select
    content=TEDevents
    sortBy="title"
    on-change=(action "update")
    limitSearchToWordBoundary=true}}

  <p>Set by Searchable: {{setBySearchable.title}}</p>

Set by Searchable:

Multiple select

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>

Set by Searchable:

Allowing new item creation

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

Custom filters or AJAX populating content

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

Query text:

Set by Searchable:

Loading state

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-searchevent.


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

Query text:

Set by Searchable:

Two-way bound

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

Configurable options

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