0
votes

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

2 Answers

1
votes

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.

0
votes

selected property must be an element included in options provided to Ember Power Select. In your scenario you are not using options property but setting the options through search action but that doesn't make a big difference.

Your search action return an array of objects (e.g. [{"name":"New Zealand","alpha2Code":"NZ"}]). nationalityChangeAction sets the selected value to the value of alpha2Code. Therefore selected is not included in options:

[{"name":"New Zealand","alpha2Code":"NZ"}].includes('NZ') // false

So the state your Power Selects ends in is similar to this one:

<PowerSelect
  @options={{array
    (hash foo="bar")
  }}
  @selected="bar"
/>

A simplified version of what you are doing look like this:

<PowerSelect
  @options={{array
    (hash foo="bar")
  }}
  @selected={{selected}}
  @onchange={{action (mut selected) value="foo"}}
/>

Please have a look in Ember Power Select documentation regarding the difference between using options and search:

When that's the case you can provide a search action instead of options (it's the only situation where the options are not mandatory) that will be invoked with the search term whenever the user types on the search box.

[...]

There is only three things to know about this action: - You should return a collection or a promise that resolves to a collection from this action. - You can provide both options and a search action. Those options will be the initial set of options, but as soon as the user performs a search, the results of that search will be displayed instead.

Therefore it doesn't make a difference for your issue if you are using options or returning a collection from search action. It all comes down to having a selected value that is not part of the collection bound to options or returned by search action.

This is actually the reason why your UI is working as expected if using onchange=(action (mut item.nationality)). In that case item.nationality is that to the selected object in collection returned by search (e.g. {"name":"New Zealand","alpha2Code":"NZ"}) and not to the value of it's alpha2Code property.

I'm using angle bracket component invocation syntax in my answer. Hope that fine. It makes it easier to read it in my opinion.