Search code examples
javascriptember.jsember-power-select

ember-power-select Custom Search Action and 'selected' with external data


Overview

I'm using ember-power-select in a Ember.js 3.8 project - and it half works and half doesn't !

To make the question more readable I've put all the code at the bottom of the question.

Situation

The select is configured to fetch data from an API endpoint and provide the user with a set of possible options to select from.

The route (routes/guest/new-using-ember-power-select.js) involved does a createRecord of the model (models/guest.js) and then, ideally, changes made to both of the form elements (templates/guests/new-using-ember-power-select.js and templates/components/guest-form-ember-power-select.hbs) are reflected back into that record in the data store.

Issue

This works fine for the text input but I can't make it work for the ember-power-select.

In the current configuration (shown below) the user may :

  • search for options to select;
  • select an option and;
  • have that selection reflected back into the guest instance in the data store. However the choice made is not reflected in the user interface - there appears to have been no selection made.

I would really appreciate someone pointing out what I'm doing wrong here. I feel like it might be quite a small thing but it did occur to me that I have to manage the state of the select via properties in the component and only when the form is submitted update the underlying data-store .... I would prefer not to do that but I would be interested to know if that was thought to be the best idea.

Thanks


EDIT 1: I forgot to say that I have attempted to alter the onchange property of the ember-power-select so that instead of looking like this

onchange=(action "nationalityChangeAction")

... it looks like this ...

onchange=(action (mut item.nationality))

That has the effect that :

  • the value selected is visible in the form (as you would normally expect but unlike my current effort) but
  • the value placed into the underlying data store record is not a two character country code but instead an instance of the array returned the API call, an object with two properties {"name":"New Zealand","alpha2Code":"NZ"}.

Model

//app/models/guest.js
import DS from 'ember-data';
import { validator, buildValidations } from 'ember-cp-validations';

const Validations = buildValidations({
  name: [
    validator('presence', true),
  ],
  nationality: [
    validator('presence', true),
  ],
});



export default DS.Model.extend( Validations, {
  name: DS.attr('string'),
  nationality: DS.attr('string')
});

Route

//app/routes/guest/new-using-ember-power-select.js
import Route from '@ember/routing/route';

export default Route.extend({
  model() {
    return this.store.createRecord('guest', {
      name: "",
      nationality: ""
    });
  },
  actions: {
    updateNationality(slctnValue) {
      this.controller.model.set('nationality' , slctnValue);
    },
  }
});

Template

//app/templates/guests/new-using-ember-power-select.js
<h2>Guest: Add New</h2>
<div class="well well-sm">
  Demonstration of 'ember-power-select'
</div>
{{guest-form-ember-power-select
  item=model
  changeNationalityHandler="updateNationality"
  updateRecordHandler="updateRecord"
  cancelHandler="cancelAndExit"
}}
{{outlet}}

Component Template

//app/templates/components/guest-form-ember-power-select.hbs
<div class="form-vertical">
  {{!-- Guest Name --}}
  <div class="form-group">
    <label class="control-label">Name</label>
    <div class="">
      {{  input type="text"
          value=item.name
          class="form-control"
          placeholder="The name of the Guest"
          focus-out=(action (mut this.errMsgDspCntrl.nameError) true)
      }}
    </div>
    {{#if this.errMsgDspCntrl.nameError}}
      <div class="text-danger">
        {{v-get item 'name' 'message'}}
      </div>
    {{/if}}
  </div>
  <div class="form-group">
    <label class="control-label">Countries (using power-select)</label>
    <div class="">
      {{#power-select
        searchPlaceholder="Text to provide user info about what they can search on"
        search=(action "searchCountries")
        selected=item.nationality
        onchange=(action (mut item.nationality))
        as |countries|
      }}
        {{countries.name}}
      {{/power-select}}
    </div>
    {{#if this.errMsgDspCntrl.nationalityError}}
      <div class="text-danger">
        {{v-get item 'nationality' 'message'}}
      </div>
    {{/if}}
  </div>
  {{!-- ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--}}
  {{!-- Buttons --}}
  <div class="form-group">
    <div class="">
      <button type="submit" class="btn btn-default" {{action "buttonSaveClicked" item}}>{{buttonLabel}}</button>
      <button type="button" class="btn btn-default" {{action "buttonCancelClicked" item}} >Cancel</button>
    </div>
  </div>
</div>
{{yield}}

Component

//app/components/guest-form-ember-power-select.js
import Component from '@ember/component';

export default Component.extend({
    actions:{
        searchCountries(term) {
          //Response to :
          //
          //https://restcountries.eu/rest/v2/name/z?fields=name;alpha2Code
          //
          //
          //looks like this
          //  [
          //    ...
          //    {"name":"New Zealand","alpha2Code":"NZ"}
          //    ...
          //  ]
          //

          let url = `https://restcountries.eu/rest/v2/name/${term}?fields=name;alpha2Code`
          let dbg = fetch(url)
            .then(function(response) {
              return response.json();
            });
          return dbg;
        },
        nationalityChangeAction(slctn){
            this.sendAction('changeNationalityHandler', slctn.alpha2Code);
        },
    }
});

Solution

  • I'm going to answer showing some diffs with the changes required to make the select work in your repo: https://github.com/shearichard/emberjs-select-addon-comparison

    The key thing to understand is that ember-power-select receives a block, in your case

    as |country|}}
      {{country.name}}
    {{/power-select}}
    

    That block will be called to render each of the options, but also the selected option. In this case, the options are country objects with this shape: {"name":"American Samoa","alpha2Code":"AS"}. That is why you call {{country.name}} to render it. However, with your approach, the selected value that you are passing in is not an object with a name property. In fact is not even an object, but the string "AS" in the case of American Samoa, so you can output the name property of a string.

    In your situation, the information you store (the country code) is not enough to display a nice "American Samoa" in the trigger of the select, and since you don't know the countries before hand until you make a search you can't look the country with that country code.

    If you don't have an edit form, my suggestion is to store the entire selected country in a property which is the one you pass to selected.

    diff --git a/app/components/guest-form-ember-power-select.js b/app/components/guest-form-ember-power-select.js
    index edf9390..2467d85 100644
    --- a/app/components/guest-form-ember-power-select.js
    +++ b/app/components/guest-form-ember-power-select.js
    @@ -25,6 +25,8 @@ export default Component.extend({
       //messages
       nameOfErrMsgDspCntrl : 'errMsgDspCntrl',
    
    +  nationality: undefined,
    +
       actions:{
    
         searchCountries(term) {
    @@ -73,7 +75,7 @@ export default Component.extend({
         },
    
         nationalityChangeAction(slctn){
    -      //this.set(this.myValue, slctn);
    +      this.set('nationality', slctn);
           this.sendAction('changeNationalityHandler', slctn.alpha2Code);
         },
    
    diff --git a/app/templates/components/guest-form-ember-power-select.hbs b/app/templates/components/guest-form-ember-power-select.hbs
    index 56f007d..5c69834 100644
    --- a/app/templates/components/guest-form-ember-power-select.hbs
    +++ b/app/templates/components/guest-form-ember-power-select.hbs
    @@ -24,7 +24,7 @@
           {{#power-select
             searchPlaceholder="Text to provide user info about what they can search on"
             search=(action "searchCountries")
    -        selected=item.nationality
    +        selected=nationality
             onchange=(action "nationalityChangeAction")
             as |countries|
           }}
    @@ -36,14 +36,14 @@
    

    This works as long as you don't want to edit the nationality of a user you created before, perhaps even weeks ago. You won't have a reference to the country in that case, only the country code. In that situation I'd recommend making selected a computed property that returns a promise the resolves to the country object with the user's country code, if your API allows that. And seems that it does, so the BEST solution would be

    diff --git a/app/components/guest-form-ember-power-select.js b/app/components/guest-form-ember-power-select.js
    index edf9390..f889734 100644
    --- a/app/components/guest-form-ember-power-select.js
    +++ b/app/components/guest-form-ember-power-select.js
    @@ -1,4 +1,5 @@
     import Component from '@ember/component';
    +import { computed } from '@ember/object';
    
     export default Component.extend({
       buttonLabel: 'Save',
    @@ -25,6 +26,16 @@ export default Component.extend({
       //messages
       nameOfErrMsgDspCntrl : 'errMsgDspCntrl',
    
    +  nationality: computed('item.nationality', function() {
    +    let countryCode = this.get('item.nationality');
    +    if (countryCode) {
    +      return fetch(`https://restcountries.eu/rest/v2/alpha/${countryCode}?fields=name;alpha2Code`)
    +        .then(function (response) {
    +          return response.json();
    +        });
    +    }
    +  }),
    +
    

    This last one will fetch the information for the country you know the code of.