模型高級特性,引入模型關聯關係

文章來源:模型高級特性,引入模型關聯關係css

接着前面五篇:html

  1. 環境搭建以及使用Ember.js建立第一個靜態頁面
  2. 引入計算屬性、action、動態內容
  3. 模型,保存數據到數據庫
  4. 發佈項目,加入CRUD功能
  5. 從服務器獲取數據,引入組件

前言

本篇主要是介紹模型直接的關聯關係,好比:一對1、一對多關係。會建立兩個模型authorbook,設置它們的關係,並增長測試數據。git

建立模型並設置關聯

關聯關係設置API:github

  1. belongsTo
  2. hasMany

模型關係:一個library對應多個book,一個author對應多個book。關係圖以下:數據庫

library模型關係圖

使用Ember CLI命令建立模型。bootstrap

ember g model book title:string releaseYear:date library:belongsTo author:belongsTo
ember g model author name:string books:hasMany

手動在library中增長hasMany關聯關係。ubuntu

import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import { hasMany } from 'ember-data/relationships';
import Ember from 'ember';

export default Model.extend({
  name: attr('string'),
  address: attr('string'),
  phone: attr('string'),

  books: hasMany('books'),

  isValid: Ember.computed.notEmpty('name'),
});

建立一個後臺管理頁面「Seeder」

ember g route admin/seeder

檢查router.js看看路由是否成功建立。相關代碼以下:vim

//  其餘代碼不變,省略
this.route('admin', function() {
    this.route('invitations');
    this.route('contacts');
    this.route('seeder');
});
//  其餘代碼不變,省略

修改導航模板navbar.hbs增長新建路由的入口連接。api

<ul class="dropdown-menu">
  {{#nav-link-to 'admin.invitations'}}Invitations{{/nav-link-to}}
  {{#nav-link-to 'admin.contacts'}}Contacts{{/nav-link-to}}
  {{#nav-link-to 'admin.seeder'}}Seeder{{/nav-link-to}}
</ul>

使用Ember.RSVP.hash()在一個路由中返回多個模型的數據

Ember支持在一個路由的model回調中返回多個模型的數據。有關方法發API請看Ember.RSVP.hash()瀏覽器

// app/routes/admin/seeder.js
import Ember from 'ember';

export default Ember.Route.extend({

  model() {
    return Ember.RSVP.hash({
      libraries: this.store.findAll('library'),
      books: this.store.findAll('book'),
      authors: this.store.findAll('author')
    })
  },

  setupController(controller, model) {
    controller.set('libraries', model.libraries);
    controller.set('books', model.books);
    controller.set('authors', model.authors);
  }
});

上述model()回調中返回了三個模型的數據:librarybookauthor。須要注意的是:上述代碼中方法Ember.RSVP.hash()會發送3個請求,而且只有三個請求都成功纔會執行成功。
setupController()回調中,把三個模型分別設置到controller中。

路由內置方法調用次序

每一個路由內都內置了不少方法,好比前面介紹的modelsetupControllerrenderTemplate,這些都是內置在路由類中的方法,那麼這些方法調用次序又是如何的呢?請看下面的代碼:

// app/routes/test.js

import Ember from 'ember';

export default Ember.Route.extend({

  init() {
    debugger;
  },

  beforeModel(transition) {
    debugger;
  },

  model(params, transition) {
    debugger;
  },

  afterModel(model, transition) {
    debugger;
  },

  activate() {
    debugger;
  },

  setupController(controller, model) {
    debugger;
  },

  renderTemplate(controller, model) {
    debugger;
  }
});

打開瀏覽器的debug模式並在執行到這個路由中http://localhost:4200/test。能夠看到方法的執行次序與上述代碼方法的次序是一致的。有關API請看下面網址的介紹:

  1. init()
  2. beforeModel(transition)
  3. model(params, transition)
  4. activate()
  5. setupController(controller, model)
  6. renderTemplate(controller, model)

數量顯示功能

建立一個組件用於顯示各個模型數據的總數。

ember g component number-box

組件建立完畢以後在組件類中增長css類,使用屬性classNames設置。

// app/components/number-box.js
import Ember from 'ember';

export default Ember.Component.extend({

  classNames: ['panel', 'panel-warning']

});

而後在組件模板中增長代碼:

<!-- app/templates/components/number-box.hbs -->
<div class="panel-heading">
  <h3 class="text-center">{{title}}</h3>
  <h1 class="text-center">{{if number number '...'}}</h1>
</div>

在修改app/templates/admin/seeder.hbs

<!-- app/templates/admin/seeder.hbs -->
<h1>Seeder, our Data Center</h1>

<div class="row">
  <div class="col-md-4">{{number-box title="Libraries" number=libraries.length}}</div>
  <div class="col-md-4">{{number-box title="Authors" number=authors.length}}</div>
  <div class="col-md-4">{{number-box title="Books" number=books.length}}</div>
</div>

等待項目重啓完成,進入到後臺的seeder下能夠看到三個小圓點,請記得,必定要在setupController中設置數據,model回調會自動從服務器獲取數據,obj.length意思是調用length()方法獲取數據長度,而後直接顯示到模板上,效果以下截圖,因爲後面兩個模型尚未數據因此顯示省略號。

結果截圖

構建表單生成測試數據

前面已經介紹過屬性的傳遞,下面的代碼將爲讀者介紹一些更加高級的東西!!一大波代碼即未來臨!!!

ember g component seeder-block
ember g component fader-label
// app/components/seeder-block.js
import Ember from 'ember';

export default Ember.Component.extend({

  actions: {
    generateAction() {
      this.sendAction('generateAction');
    },

    deleteAction() {
      this.sendAction('deleteAction');
    }
  }
});
<!-- app/templates/components/seeder-block.hbs -->
<div class="row">
  <div class="col-md-12">
    <h3>{{sectionTitle}}</h3>

    <div class="row">
      <div class="form-horizontal">
        <label class="col-sm-2 control-label">Number of new records:</label>
        <div class="col-sm-2">
          {{input value=counter class='form-control'}}
        </div>
        <div class="col-sm-4">
          <button class="btn btn-primary" {{action 'generateAction'}}>Generate {{sectionTitle}}</button>
          {{#fader-label isShowing=createReady}}Created!{{/fader-label}}
        </div>
        <div class="col-sm-4">
          <button class="btn btn-danger" {{action 'deleteAction'}}>Delete All {{sectionTitle}}</button>
          {{#fader-label isShowing=deleteReady}}Deleted!{{/fader-label}}
        </div>
      </div>

    </div>
  </div>
</div>
// app/components/fader-label.js
import Ember from 'ember';

export default Ember.Component.extend({
  tagName: 'span',

  classNames: ['label label-success label-fade'],
  classNameBindings: ['isShowing:label-show'],

  isShowing: false,

  isShowingChanged: Ember.observer('isShowing', function() {
    Ember.run.later(() => {
      this.set('isShowing', false);
    }, 3000);
  })
});

代碼 classNames: ['label label-success label-fade']的做用是綁定三個CSS類到標籤span上,獲得html如<span class="label label-success label-fade">xxx</span>
代碼classNameBindings: ['isShowing:label-show']的做用是根據屬性isShowing的值判斷是否添加CSS類label-show到標籤span上。更多有關信息請看Ember.js 入門指南之十二handlebars屬性綁定

<!-- app/templates/components/fader-label.hbs -->
{{yield}}
// app/styles/app.scss
@import 'bootstrap';

body {
  padding-top: 20px;
}

html {
  overflow-y: scroll;
}

.library-item {
  min-height: 150px;
}

.label-fade {
  opacity: 0;
  @include transition(all 0.5s);
  &.label-show {
    opacity: 1;
  }
}

最主要、最關鍵的部分來了。

<!-- app/templates/admin/seeder.hbs -->
<h1>Seeder, our Data Center</h1>

<div class="row">
  <div class="col-md-4">{{number-box title="Libraries" number=libraries.length}}</div>
  <div class="col-md-4">{{number-box title="Authors" number=authors.length}}</div>
  <div class="col-md-4">{{number-box title="Books" number=books.length}}</div>
</div>

{{seeder-block
    sectionTitle='Libraries'
    counter=librariesCounter
    generateAction='generateLibraries'
    deleteAction='deleteLibraries'
    createReady=libDone
    deleteReady=libDelDone
}}

{{seeder-block
  sectionTitle='Authors with Books'
  counter=authorCounter
  generateAction='generateBooksAndAuthors'
  deleteAction='deleteBooksAndAuthors'
  createReady=authDone
  deleteReady=authDelDone
}}

屬性generateActiondeleteAction用於關聯控制器中的action方法,屬性createReadydeleteReady是標記屬性。

等待項目重啓完畢,頁面結果以下:

result

底部的兩個輸入框用於獲取生成的數據條數。

安裝faker.js構建測試數據

使用faker.js構建測試數據。

ember install ember-faker

安裝完畢以後擴展各個模型,並在模型中調用randomize()方法產生數據。下面是各個模型的代碼。

// app/models/library.js
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import { hasMany } from 'ember-data/relationships';
import Ember from 'ember';
import Faker from 'faker';

export default Model.extend({
  name: attr('string'),
  address: attr('string'),
  phone: attr('string'),

  books: hasMany('book', {inverse: 'library', async: true}),

  isValid: Ember.computed.notEmpty('name'),

  randomize() {
    this.set('name', Faker.company.companyName() + ' Library');
    this.set('address', this._fullAddress());
    this.set('phone', Faker.phone.phoneNumber());

    // If you would like to use in chain.
    return this;
  },

  _fullAddress() {
    return `${Faker.address.streetAddress()}, ${Faker.address.city()}`;
  }
});
// app/models/book.js
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import { belongsTo } from 'ember-data/relationships';
import Faker from 'faker';

export default Model.extend({

  title:        attr('string'),
  releaseYear:  attr('date'),

  author:       belongsTo('author', {inverse: 'books', async: true}),
  library:      belongsTo('library', {inverse: 'books', async: true}),

  randomize(author, library) {
    this.set('title', this._bookTitle());
    this.set('author', author);
    this.set('releaseYear', this._randomYear());
    this.set('library', library);

    return this;
  },

  _bookTitle() {
    return `${Faker.commerce.productName()} Cookbook`;
  },

  _randomYear() {
    return new Date(this._getRandomArbitrary(1900, 2015));
  },

  _getRandomArbitrary(min, max) {
    return Math.random() * (max - min) + min;
  }
});
// app/models/author.js
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import { hasMany } from 'ember-data/relationships';
import Faker from 'faker';

export default Model.extend({

  name: attr('string'),

  books: hasMany('book', {inverse: 'author', async: true}),

  randomize() {
    this.set('name', Faker.name.findName());
    return this;
  }

});

上述代碼中。 async設置爲true的做用是:在獲取book的同時會把關聯的author也加載出來,默認是不加載(延遲加載)。

// app/controllers/admin/seeder.js
import Ember from 'ember';
import Faker from 'faker';

export default Ember.Controller.extend({

  libraries: [],
  books: [],
  authors: [],

  actions: {

    generateLibraries() {
      const counter = parseInt(this.get('librariesCounter'));

      for (let i = 0; i < counter; i++) {
        this.store.createRecord('library').randomize().save().then(() => {
          if (i === counter-1) {
            this.set('librariesCounter', 0);
            this.set('libDone', true);
          }
        });
      }
    },

    deleteLibraries() {
      this._destroyAll(this.get('libraries'));

      this.set('libDelDone', true);
    },

    generateBooksAndAuthors() {
      const counter = parseInt(this.get('authorCounter'));

      for (let i = 0; i < counter; i++) {
        let newAuthor = this.store.createRecord('author');
        newAuthor.randomize()
          .save().then(() => {
             if (i === counter-1) {
               this.set('authorCounter', 0);
               this.set('authDone', true);
             }
          }
        );

        this._generateSomeBooks(newAuthor);
      }
    },

    deleteBooksAndAuthors() {
      this._destroyAll(this.get('books'));
      this._destroyAll(this.get('authors'));

      this.set('authDelDone', true);
    }
  },

  // Private methods

  _generateSomeBooks(author) {
    const bookCounter = Faker.random.number(10);

    for (let j = 0; j < bookCounter; j++) {
      const library = this._selectRandomLibrary();
      this.store.createRecord('book')
        .randomize(author, library)
        .save();
      author.save();
      library.save();
    }
  },

  _selectRandomLibrary() {
    const libraries = this.get('libraries');
    const librariesCounter = libraries.get('length');

    // Create a new array from IDs
    const libraryIds = libraries.map((lib) => {return lib.get('id');});
    const randomNumber = Faker.random.number(librariesCounter-1);

    const randomLibrary = libraries.findBy('id', libraryIds[randomNumber]);
    return randomLibrary;
  },

  _destroyAll(records) {
    records.forEach((item) => {
      item.destroyRecord();
    });
  }

});

重啓項目,進入到http://localhost:4200/admin/seeder。在輸入框內輸入要生成的測試數據條數,而後點擊右邊的藍色按鈕,若是生成成功能夠在按鈕右邊看到綠色的「created」提示文字。以下圖:

生成成功

而後到firebase上查看。能夠看到數據已經存在了,而且是隨機的數據。

firebase數據截圖1

firebase數據截圖2

而且是實現了數據表之間的關聯關係,好比一個author對應多個book,以下圖。

author一對多book

或者是直接在http://localhost:4200/libraries下查看。

在接下來的一篇文章中將介紹如何遍歷關聯關係中的對象,使用起來也是很是簡單的,直接使用面向對象的方式遍歷便可。

家庭做業

本篇的家庭做業仍然是好好理解組件!參考下面的文章認真學習、理解組件。

  1. Ember.js 入門指南之二十八組件定義
  2. Ember.js 入門指南之二十九屬性傳遞
  3. Ember.js 入門指南之三十包裹內容
  4. Ember.js 入門指南之三十一自定義包裹組件的HTML標籤
  5. Ember.js 入門指南之三十二處理事件
  6. Ember.js 入門指南之三十三action觸發變化



爲了照顧懶人我把完整的代碼放在GitHub上,若有須要請參考參考。博文通過屢次修改,博文上的代碼與github代碼可能有出入,不過影響不大!若是你以爲博文對你有點用,請在github項目上給我點個star吧。您的確定對我來講是最大的動力!!

相關文章
相關標籤/搜索