How to combine meta with live search?


#1

I am trying to do a seemingly simple thing with a JSON API server in the backend: show a list of users with an input field that filters them by name (server-side filtering), and the total number of users in the database, returned in the meta field of the server’s JSON API response.

The official guide for handling metadata suggests returning the entire model as a promise, upon whose resolution we return a differently structured model containing separate keys for the data we were expecting (i.e. users) and meta. So the code below works as expected. (Note: I used .query() instead of .findAll() suggested by the guide on purpose).


// app/models/user.js

import DS from 'ember-data';

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


{{!-- template.hbs --}}

<p>Total users in database: {{model.meta.total_users}}</p>
<ul>
  {{#each model.users as |user|}}
    <li>{{user.name}}</li>
  {{/each}}
</ul>


// route.js

import Route from '@ember/routing/route';

export default Route.extend({
  model(params) {
    return this.get('store').query('user', {}).then((users) => {
      return {
        users: users,
        meta: users.get('meta')
      };
    });
  }
});


// controller.js

import Controller from '@ember/controller';

export default Controller.extend({
	
});

Now, I want to introduce a search box, which should send requests to the server and update the list as we type, and as I want to make it linkable, I want to bind the value of the search field to a query parameter, so the route files now look like this:


{{!-- template.hbs --}}

{{input type="text" value=searchText placeholder="Filter..."}}
<p>Total gtins in database: {{model.meta.total_users}}</p>
<ul>
  {{#each model.users as |user|}}
    <li>{{user.name}}</li>
  {{/each}}
</ul>


// route.js

import Route from '@ember/routing/route';

export default Route.extend({
  queryParams: {
    searchText: {
      refreshModel: true,
      replace: true
    }
  },

  model(params) {
    return this.get('store').query('user', {
      filter: {
        search_text: params.searchText
      }
    }).then((users) => {
      return {
        users: users,
        meta: users.get('users')
      };
    });
  }
});


// controller.js

import Controller from '@ember/controller';

export default Controller.extend({
  queryParams: ['searchText'],
  searchText: ''
});

And this works, but not exactly how it should. Namely, as every change in searchText triggers model reload, the template gets re-rendered, causing the input field to lose focus. That requires the user to click/focus the input field literally after every letter and there is an impression of “page reload”, which shouldn’t be happening.

Now, if I modify the route.js so that it looks like this:


// route.js

import Route from '@ember/routing/route';

export default Route.extend({
  queryParams: {
    searchText: {
      refreshModel: true,
      replace: true
    }
  },

  model(params) {
    return {
      users: this.get('store').query('user', {
        filter: {
          search_text: params.searchText
        }
      })
    };
  }
});

Everything regarding search behaves as it should (only the list gets re-rendered with updating the model, the search box stays in focus and you can type continuously), but… I cannot put the metadata on the model as I don’t know how to extract it.

So, is it possible to have the list update dynamically without refreshing the whole route because of the model AND be able to access the metadata returned from JSON API server?