构建复杂组件

优质
小牛编辑
128浏览
2023-12-01

当用户搜索租赁时,他可能还想将搜索范围缩小到特定城市。虽然我们的初始租赁列表组件仅显示租赁信息,但此新的过滤器组件还将允许用户以过滤条件的形式提供输入。
首先,让我们生成新的组件list-filter。我们的需求是希望组件根据用户输入过滤租赁列表。

$ ember g component list-filter
installing component
  create app/components/list-filter.js
  create app/templates/components/list-filter.hbs
installing component-test
  create tests/integration/components/list-filter-test.js

生成了一个组件模板、一个JavaScript文件和一个组件集成测试文件。

为组件提供标记

app/templates/rentals.hbs模板文件中,我们将添加对新list-filter组件的引用。
请注意,在下面的模板中,我们用list-filter的开始和结束标记“包裹”了之前的租赁列表标记(each)。这是一个组件的块形式示例,这允许在组件内部渲染handlebars模板。

在下面的代码中,我们将过滤器数据作为变量rentals传入到内部模板中。 app/templates/rentals.hbs:

<div class="jumbo">
  <div class="right tomster"></div>
  <h2>Welcome!</h2>
  <p>
    We hope you find exactly what you're looking for in a place to stay.
  </p>
  { { #link-to 'about' class="button" } }
    About Us
  { { /link-to } }
</div>

{ { #list-filter
   filter=(action 'filterByCity')
   as |rentals| } }
  <ul class="results">
    { { #each rentals as |rentalUnit| } }
      <li>{ { rental-listing rental=rentalUnit } }</li>
    { { /each } }
  </ul>
{ { /list-filter } }

接受组件的输入

我们希望组件简单地提供一个输入域,和显示过滤结果的区域(yield results),因此我们的模板很简单:
app/templates/components/list-filter.hbs:

{ { input value=value
        key-up=(action 'handleFilterEntry')
        class="light"
        placeholder="Filter By City" } }
{ { yield results } }

该模板包含一个{ { input } }助手用于渲染输入框,用户可以在其中输入过滤使用的城市。输入框的value属性将与组件的value属性保持同步。
以另一种说法是,输入框的value属性绑定到组件的value属性。如果属性更改,无论是用户输入,还是通过程序为其分配一个新值,该属性的新值将渲染到网页和体现在代码中。
key-up属性将绑定到handleFilterEntry动作。
这是组件的JavaScript的代码(app/components/list-filter.js):

import Ember from 'ember';

export default Ember.Component.extend({
  classNames: ['list-filter'],
  value: '',

  init() {
    this._super(...arguments);
    this.get('filter')('').then((results) => this.set('results', results));
  },

  actions: {
    handleFilterEntry() {
      let filterInputValue = this.get('value');
      let filterAction = this.get('filter');
      filterAction(filterInputValue).then((filterResults) => this.set('results', filterResults));
    }
  }

});

基于输入过滤数据

在上面的例子中,我们使用init钩子来初始化租赁列表,具体做法是以空值为过滤条件调用filter动作。在handleFilterEntry动作中,调用filter函数,函数参数是输入助手的value值。

filter函数由调用对象传入。这是一种被称为关闭动作的模式。

注意对then函数的调用使用了filter函数的调用结果。该代码期望filter函数返回一个promise。promise是JavaScript对象,它表示一个异步函数的结果。promise在收到时可能已经执行,也可能没有执行。为了解决这个问题,它提供了一些函数,如then函数可以你在promise返回结果时运行一些代码。

要实现这个filter函数来实现城市租赁的实际过滤器,我们将创建一个rentals控制器。 控制器包含可用于其对应路由的模板的操作和属性。在我们的例子中,我们要生成一个名为rentals的控制器。Ember知道名称为rentals的控制器将应用于相同名称的路由。

下列命令为rentals路由生成控制器:

$ ember g controller rentals
installing controller
  create app/controllers/rentals.js
installing controller-test
  create tests/unit/controllers/rentals-test.js

现在定义新的控制器(app/controllers/rentals.js):

import Ember from 'ember';

export default Ember.Controller.extend({
  actions: {
    filterByCity(param) {
      if (param !== '') {
        return this.get('store').query('rental', { city: param });
      } else {
        return this.get('store').findAll('rental');
      }
    }
  }
});

当用户在组件中的文本框输入时,控制器中的filterByCity动作会被调用。此动作取得value属性(来自用户的输入),并过滤rental数据存储中与value匹配的数据。查询的结果返回给调用者。

伪造查询结果

为了使此动作正常工作,我们需要用下来代码覆盖Mirageconfig.js文件,以便它可以响应我们的查询。rentals的Mirage HTTP GET处理程序不再简单地返回租借列表,而是根据URL中的city参数中返回匹配的租赁清单。
mirage/config.js:

export default function() {
  this.namespace = '/api';

  let rentals = [{
      type: 'rentals',
      id: 'grand-old-mansion',
      attributes: {
        title: 'Grand Old Mansion',
        owner: 'Veruca Salt',
        city: 'San Francisco',
        "property-type": 'Estate',
        bedrooms: 15,
        image: 'https://upload.wikimedia.org/wikipedia/commons/c/cb/Crane_estate_(5).jpg',
        description: "This grand old mansion sits on over 100 acres of rolling hills and dense redwood forests."
      }
    }, {
      type: 'rentals',
      id: 'urban-living',
      attributes: {
        title: 'Urban Living',
        owner: 'Mike Teavee',
        city: 'Seattle',
        "property-type": 'Condo',
        bedrooms: 1,
        image: 'https://upload.wikimedia.org/wikipedia/commons/0/0e/Alfonso_13_Highrise_Tegucigalpa.jpg',
        description: "A commuters dream. This rental is within walking distance of 2 bus stops and the Metro."
      }
    }, {
      type: 'rentals',
      id: 'downtown-charm',
      attributes: {
        title: 'Downtown Charm',
        owner: 'Violet Beauregarde',
        city: 'Portland',
        "property-type": 'Apartment',
        bedrooms: 3,
        image: 'https://upload.wikimedia.org/wikipedia/commons/f/f7/Wheeldon_Apartment_Building_-_Portland_Oregon.jpg',
        description: "Convenience is at your doorstep with this charming downtown rental. Great restaurants and active night life are within a few feet."
      }
    }];

  this.get('/rentals', function(db, request) {
    if(request.queryParams.city !== undefined) {
      let filteredRentals = rentals.filter(function(i) {
        return i.attributes.city.toLowerCase().indexOf(request.queryParams.city.toLowerCase()) !== -1;
      });
      return { data: filteredRentals };
    } else {
      return { data: rentals };
    }
  });
}

修改了 mirage 配置后,在应用的首页上显示了输入框,可以按输入过滤城市。

在我们的示例中,您可能会注意到,如果快速输入结果可能与输入的当前过滤器文本不同步。这是因为我们的数据过滤功能是异步的,这意味着函数中的代码将被调度为稍后执行,而调用该函数的代码将继续执行。通常,可能使网络请求的代码设置为异步,因为服务器可能会在不同的时间返回其响应。

让我们添加一些保护代码,以避免查询结果与过滤器输入不同步。为此,我们将简单地将过滤器文本提供给过滤器函数,以便当结果返回时,我们可以将原始过滤器值与当前过滤器值进行比较。只有原始过滤器值和当前过滤器值相同,我们才会在屏幕上更新结果。 app/controllers/rentals.js(注释掉的是原来的代码):

import Ember from 'ember';

export default Ember.Controller.extend({
  actions: {
    filterByCity(param) {
      if (param !== '') {
//        return this.get('store').query('rental', { city: param });
        return this.get('store')
          .query('rental', { city: param }).then((results) => {
            return { query: param, results: results };
          });
      } else {
//        return this.get('store').findAll('rental');
        return this.get('store')
          .findAll('rental').then((results) => {
            return { query: param, results: results };
          });
      }
    }
  }
});

在上述filterByCity租赁控制器的函数中,我们添加了一个新的属性query,而不是像以前一样返回一系列租赁。
app/components/list-filter.js:

import Ember from 'ember';

export default Ember.Component.extend({
  classNames: ['list-filter'],
  value: '',

  init() {
    this._super(...arguments);
//    this.get('filter')('').then((results) => this.set('results', results));
    this.get('filter')('').then((allResults) => {
      this.set('results', allResults.results);
    });
  },

  actions: {
    handleFilterEntry() {
      let filterInputValue = this.get('value');
      let filterAction = this.get('filter');
//      filterAction(filterInputValue).then((filterResults) => this.set('results', filterResults));
      filterAction(filterInputValue).then((resultsObj) => {
        if (resultsObj.query === this.get('value')) {
          this.set('results', resultsObj.results);
        }
      });
    }
  }
});

在我们的列表过滤器组件JavaScript中,我们使用该query属性来比较组件的value属性。该value属性表示输入字段的最新状态。因此,我们现在检查结果与输入字段是否匹配,确保结果与用户输入保持同步。

虽然这种方法将使我们的结果顺序保持一致,但在处理多个并发任务时还需要考虑其他问题,例如限制对服务器发出的请求数量。为了为应用程序创建有效和强大的自动完成行为,我们建议您考虑使用ember-concurrencyaddon项目。

您现在可以继续执行下一个功能,或继续测试我们新创建的过滤器组件。

集成测试

tests/integration/components/list-filter-test.js:

import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
import wait from 'ember-test-helpers/wait';
import RSVP from 'rsvp';

moduleForComponent('list-filter', 'Integration | Component | filter listing', {
  integration: true
});

const ITEMS = [{city: 'San Francisco'}, {city: 'Portland'}, {city: 'Seattle'}];
const FILTERED_ITEMS = [{city: 'San Francisco'}];

test('should initially load all listings', function (assert) {
  // we want our actions to return promises, since they are potentially fetching data asynchronously
  this.on('filterByCity', () => {
    return RSVP.resolve({ results: ITEMS });
  });

  // with an integration test,
  // you can set up and use your component in the same way your application will use it.
  this.render(hbs`
    { { #list-filter filter=(action 'filterByCity') as |results| } }
      <ul>
      { { #each results as |item| } }
        <li class="city">
          { { item.city } }
        </li>
      { { /each } }
      </ul>
    { { /list-filter } }
  `);

  return wait().then(() => {
    assert.equal(this.$('.city').length, 3);
    assert.equal(this.$('.city').first().text().trim(), 'San Francisco');
  });
});

由于我们的组件期望过滤器过程是异步的,我们使用Ember的RSVP库从我们的过滤器返回promise。 this.on将提供的filterByCity函数添加到测试本地范围,我们可以使用它来提供给组件。
filterByCity函数将被装扮成为我们组件的动作函数,实际过滤出租列表。

测试的最后,添加了一个wait调用来检验返回结果。

Ember的wait助手 在运行给定的函数回调之前等待所有异步任务完成。它返回一个从测试返回的promise。

第一测试模拟了空值过滤,返回了所有城市的租赁列表。下面是第二个测试,它将模仿用户输入过滤条件,检测返回的租赁列表是否符合输入的城市。
我们将为filterByCity动作添加一些附加功能,以返回单个租赁,FILTERED_ITEMS变量就是设置的过滤条件。

我们通过keyUp在输入字段上生成一个事件来强制执行该操作,然后检测确保只渲染一个项目。
tests/integration/components/list-filter-test.js:

test('should update with matching listings', function (assert) {
  this.on('filterByCity', (val) => {
    if (val === '') {
      return RSVP.resolve({
        query: val,
        results: ITEMS });
    } else {
      return RSVP.resolve({
        query: val,
        results: FILTERED_ITEMS });
    }
  });

  this.render(hbs`
    { { #list-filter filter=(action 'filterByCity') as |results| } }
      <ul>
      { { #each results as |item| } }
        <li class="city">
          { { item.city } }
        </li>
      { { /each } }
      </ul>
    { { /list-filter } }
  `);

  // The keyup event here should invoke an action that will cause the list to be filtered
  this.$('.list-filter input').val('San').keyup();

  return wait().then(() => {
    assert.equal(this.$('.city').length, 1);
    assert.equal(this.$('.city').text().trim(), 'San Francisco');
  });
});

现在两个集成测试场景都应该能通过。您可以通过ember t -s命令来启动我们的测试套件来验证这一点。

验收测试

现在我们已经测试了list-filter组件的行为与预期的一样,让我们​​来测试一下,页面本身也可以正常地进行验收测试。我们会验证访问租借页面的用户可以在搜索字段中输入文字,并按城市缩小租赁列表。

打开我们现有的验收测试,tests/acceptance/list-rentals-test.js并实施标签为“should filter the list of rentals by city”的测试。 tests/acceptance/list-rentals-test.js:

test('should filter the list of rentals by city.', function (assert) {
  visit('/');
  fillIn('.list-filter input', 'Seattle');
  keyEvent('.list-filter input', 'keyup', 69);
  andThen(function() {
    assert.equal(find('.listing').length, 1, 'should show 1 listing');
    assert.equal(find('.listing .location:contains("Seattle")').length, 1, 'should contain 1 listing with location Seattle');
  });
});

我们在测试中引入了两个新的帮手,fillInkeyEvent

  • fillIn助手“填写”给定的文本到给定的选择相匹配的输入字段。
  • keyEvent助手发送键击事件的UI,模拟用户输入一个按键。

app/components/list-filter.js中,我们有一个被类型是list-filter的组件渲染出来的顶层元素。我们使用选择器在组件内定位搜索输入.list-filter input,因为我们知道列表过滤器组件中只有一个输入元素。

我们的测试填写“Seattle”作为搜索字段中的搜索条件,然后keyup使用69(字母e的按键值)的代码将事件发送到同一个字段,以模拟用户输入。

在测试中通过查找类型是listing的元素,定位出在本教程的“构建简单组件”部分中定义的rental-listing组件。

由于我们的数据在Mirage中是硬编码的,所以我们知道只有一个城市名称为“Seattle”的租金,所以我们断定数量是一个,它显示的位置被命名为“Seattle”。

测试验证在填写“Seattle”搜索输入后,租赁列表从3减少到1,显示的项目显示“Seattle”作为位置。

按原文,到现在应只剩下2个验收测试失败,但我测试3个失败。