在Ember中使用查询参数实现JSONAPI过滤器的推荐模式?

时间:2018-05-31 09:37:26

标签: ember.js json-api

昨天我花了一大块时间尝试在Ember应用程序的部分查询参数中包含过滤器(反映JSONAPI spec)。使用Ember Data很容易将过滤器数组传递给端点,我遇到的问题是在特定路径的查询参数中反映过滤器数组。注意:其他非数组查询参数工作正常。

TL; DR我尝试过各种各样的选项而没有成功,并且有一个真正令人不满意的解决方案,而且根本没有DRY。我认为许多其他人必须解决这个问题,并且肯定找到了更好的解决方案。请继续阅读我迄今为止尝试过的详细信息。

我从这样的事情开始(我最初假设它可以阅读Ember docs on query params):

控制器:

import Controller from '@ember/controller';

export default Controller.extend({
  queryParams: ['sort', 'filter'],
  sort: 'id',

  init() {
    this._super(...arguments);
    this.set('filter', []);
  },
});

路线:

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

export default Route.extend({
  queryParams: {
    filter: {
      refreshModel: true
    },
    sort: {
      refreshModel: true
    }
  },

  model(params) {
    console.log(JSON.stringify(params)); // filter is always []
    return this.get('store').query('contact', params);
  }
});

验收测试(在我开始处理更复杂的东西之前,这只是概念测试的证明):

  test('visiting /contacts with query params', async function(assert) {
    assert.expect(1);
    let done = assert.async();

    server.createList('contact', 10);

    server.get('/contacts', (schema, request) => {
      let params = request.queryParams;

      assert.deepEqual(
        params,
        {
          sort: '-id',
          "filter[firstname]": 'wibble'
        },
        'Query parameters are passed in as expected'
      );
      done();
      return schema.contacts.all();
    });

    await visit('/contacts?filter[firstname]=wibble&sort=-id');
  });

无论我如何调整上面的代码,params.filter在Route模型函数中总是[]。

我一直在寻找最佳做法,看似是一个常见的用例,但最近没有找到任何东西。 sarus' solution here from Nov 2015有效,但意味着每个可能的过滤器密钥都必须在控制器和路由中进行硬编码,这对我来说似乎不太理想。想象一下,为20个可能的过滤键做到这一点!使用sarus的解决方案,这里的代码适用于上述验收测试,但正如我所说想象必须硬编码20多个潜在的过滤器密钥:

控制器:

import Controller from '@ember/controller';

export default Controller.extend({
  queryParams: ['sort',
    { firstnameFilter: 'filter[firstname]' }
  ],
  sort: 'id',
  firstnameFilter: null,

  init() {
    this._super(...arguments);
  }
});

路线:

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

export default Route.extend({
  queryParams: {
    firstnameFilter: {
      refreshModel: true
    },
    sort: {
      refreshModel: true
    }
  },

  model(params) {
    if (params.firstnameFilter) {
      params.filter = {};
      params.filter['firstname'] = params.firstnameFilter;
      delete params.firstnameFilter;
    }
    return this.get('store').query('contact', params);
  }
});

我希望有更好的方法!

2 个答案:

答案 0 :(得分:1)

没有理由在应用程序URL中表示过滤器值的方式与后端调用为JSON API投诉的方式相同。因此,我不会将该格式用于应用程序URL。

如果您不需要支持动态过滤器字段,我会对所有这些字段进行硬编码,以获得像/contacts?firstname=wibble&sort=-id这样的精美网址。

如果您想支持firstnamelastname的过滤,您的代码将如下所示:

// Controller
import Controller from '@ember/controller';

export default Controller.extend({
  queryParams: ['sort', 'page', 'firstname', 'lastname'],
  sort: 'id',
});

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

export default Route.extend({
  queryParams: {
    firstname: {
      refreshModel: true
    },
    lastname: {
      refreshModel: true
    }
    sort: {
      refreshModel: true
    }
  },

  model({ firstname, lastname, sort, page }) {
    console.log(JSON.stringify(params)); // filter is always []
    return this.get('store').query('contact', {
      filter: {
        firstname,
        lastname
      },
      sort,
      page
    });
  }
});

如果您必须支持动态过滤器字段,我会在应用程序URL中表示过滤器对象。对于序列化,您可以同时使用JSON.stringify()encodeURIComponent()。该网址看起来像/contacts?filter=%7B%22firstname%22%3A%22wibble%22%7D&sort=-id

答案 1 :(得分:1)

如果您不需要支持动态过滤字段,@ jelhan已经为这个问题提供了一个非常好的答案。

如果您确实需要支持读取动态过滤器字段。

首先,应该通过提及使用JSON.stringify()和encodeURIComponent()一起序列化应用程序URL的可能性,@ jelhan将我放在正确的轨道上。

以下是这个有效的示例代码......

控制器:

import Controller from '@ember/controller';

export default Controller.extend({
  queryParams: ['sort', {
    filter: {
      type: 'array'
    }
  }],

  sort: 'id',

  init() {
    this._super(...arguments);
    this.set('filter', []);
  },
});

路线(无需更改):

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

export default Route.extend({
  queryParams: {
    filter: {
      refreshModel: true
    },
    sort: {
      refreshModel: true
    }
  },

  model(params) {
    return this.get('store').query('contact', params);
  }
});

验收测试:

  test('visiting /contacts with query params', async function(assert) {
    assert.expect(1);
    let done = assert.async();

    server.createList('contact', 10);

    server.get('/contacts', (schema, request) => {
      let params = request.queryParams;

      assert.deepEqual(
        params,
        {
          sort: '-id',
          "filter[firstname]": 'wibble',
          "filter[lastname]": 'wobble'
        },
        'Query parameters are passed in as expected'
      );
      done();
      return schema.contacts.all();
    });

    // The filter is represented by a Javascript object
    let filter = {"firstname":"wibble", "lastname":"wobble"};

    // The object is converted to a JSON string and then URI encoded and added to the application URL 
    await visit('/contacts?sort=-id&filter=' + encodeURIComponent(JSON.stringify(filter)));
  });

大!这个测试通过。应用程序URL中定义的过滤器将传递到Route。 Route的模型钩子正确定义了过滤器的JSONAPI请求。耶!

正如你所看到的,那里没什么聪明的。我们需要做的就是在应用程序URL中以正确的格式设置过滤器,标准的Ember Query Params设置只适用于动态过滤器字段。

但是如何通过操作或链接更新过滤器查询参数,并查看应用程序URL中反映的内容,并通过Route模型钩子发出正确的JSONAPI请求。事实证明这也很容易:

示例动作(在控制器中):

changeFilter() {
  let filter = {
    firstname: 'Robert',
    lastname: 'Jones',
    category: 'gnome'
  };

  // Simply update the query param `filter`. 
  // Note: although filter is defined as an array, it needs to be set
  //  as a Javascript object to work
  // this.set('filter', filter); - this seems to work but I guess I should use transitionToRoute
  this.transitionToRoute('contacts', {queryParams: {filter: filter}});
}

对于一个链接(比如你想应用一个特殊的过滤器),你需要一个控制器属性来保存过滤器,我们称之为otherFilter,然后可以在link-to

示例Controller属性(在init中定义):

init() {
    this._super(...arguments);
    this.set('filter', []);    
    this.set('otherFilter', {occupation:'Baker', category: 'elf'});
}

示例链接到:

{{#link-to 'contacts' (query-params filter=otherFilter)}}Change the filters{{/link-to}}

你有它!