Ember翻譯——教程九:創建一個復雜的組件

當用戶搜索租賃信息時,他們或許也想要縮小他們的搜索范圍到某一特定城市。讓我們創建一個組件來讓他們依照城市過濾租賃信息。

一開始,讓我們生成新的組件。我們將其命名為 list-filter,因為我們想要我們的組件對租賃信息基於用戶輸入進行過濾。

ember g component list-filter

和之前一樣,這會創建一個 Handlebars 模板(app/templates/components/list-filter.hbs),一個 JavaScript 文件(app/components/list-filter.js),和一個組件繼承測試(tests/integration/components/list-filter.js)。

讓我們從寫一些測試開始,以便於幫助我們思考我們將做什麼。過濾組件將生成一系列經過過濾的信息,不管它裡面呈現的是什麼,成為其內部模板塊。我們想要我們的組件調用兩種行為:一種是在沒提供過濾信息時提供所有的信息,而另一種是依照城市搜索列表。

對於我們的初次測試,我們將檢查是否我們提供的所有城市都被呈現瞭出來以及是否可以從模板中訪問列表對象。

我們的依照城市調用過濾的行為將異步完成,而我們的測試也將必須適應這一點。我們將利用這裡的 行為 來處理 filterByCity 中異步事件的完成,它是從我們的存根事件中返回一個 promise 實現的。

註意在在測試的末尾,我們也需要添加一個 wait 調用來確保測試結果。Ember 的 wait helper 會在運行給定的回調函數和結束測試之前,先等待所有其它的 promise 切換到 resovle 狀態。

/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', (val) => {
    if (val === '') {
      return RSVP.resolve(ITEMS);
    } else {
      return RSVP.resolve(FILTERED_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}}
  `);

  // the wait function will return a promise that will wait for all promises
  // and xhr requests to resolve before running the contents of the then block.
  return wait().then(() => {
    assert.equal(this.$('.city').length, 3);
    assert.equal(this.$('.city').first().text().trim(), 'San Francisco');
  });
});

對於我們的第二個測試,我們將檢查在過濾器中輸入文字是否真的會調用適當的過濾行為和更新顯示的列表。

我們通過在我們的輸入框生成一個 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(ITEMS);
    } else {
      return RSVP.resolve(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');
  });
});

下一步,在我們的 app/templates/rentals.hbs 文件中,我們將通過類似的方式添加測試中使用的新的 list-filter 組件。我們將使用我們的 rental-listing 組件來展示詳細的租賃信息,而非隻顯示城市。

/app/templates/rentals.hbs

<p class="jumbo">
  <p class="right tomster"></p>
  <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}}
</p>

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

既然我們有瞭失敗的測試和關於我們組件的想法,我們這就來創建我們的組件。我盟想要隻是簡單地提供一個輸入字段和一個結果將展示到模板塊中的字段,所以我們的模板將會很簡單:

/app/remplates/components/list-filter.hbs

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

這個模板包含瞭一個 {{input}} helper,它將渲染為一個輸入框,用戶可以輸入一個指令來過濾搜索中使用的城市列表。input 的 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));
    }
  }

});

我們通過用空值調用 filter行為來使用 init 鉤子,從而設置我們的初始化列表。我們的 handleFilterEntry 行為將基於我們 input helper 設置的 value 屬性來調用過濾行為。

filter 行為是被調用對象傳入其中的,我們將創建一個 rentals 控制器。控制器能夠包含一些行為和屬性,這些行為和屬性可以被控制器相關的路由的模板所讀取。

通過運行下面的代碼為 rentals 路由生成一個控制器:

ember g controller rentals

現在,請像這樣定義你的新控制器:

/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 屬性,然後過濾出 data store 中匹配用戶輸入信息的租賃信息。查詢的結果將會被返回到調用程序中。

為瞭讓這個行為工作,我們需要用下面的代碼替換我們的 Mirage 的 config.js 文件,以便於它能響應我們的查詢。

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',
        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',
        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',
        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 配置之後,我們應該看到通過的測試以及在首頁上出現一個簡單的過濾器,它將隨你的輸入更新租賃信息列表:

發佈留言