Несколько недель назад я решил заняться Ember.js и попробовать свои силы в создании полностью клиентского приложения. Учебник на сайте Ember, посвященный реализации TodoMVC, хорошо написан и имеет хорошую глубину, но несколько устарел. Он включает в себя некоторые устаревшие элементы и не использует ember-cli, что является рекомендуемой реализацией в текущей версии.

Чтобы научиться работать с новым фреймворком и дать другим хорошую основу для начала работы, я решил переработать существующее руководство, чтобы обновить его.

Этот урок в значительной степени заимствован из оригинальной работы, выполненной Trek Glowacki. Когда у меня будет возможность, я возьму документацию по Ember и обновлю ее содержимым из этой статьи.

Инструкции по установке можно найти на сайте Ember.js.

Оригинальный туториал можно найти на сайте Ember.js.

Исходный код этого урока доступен на github.

Создайте скелет приложения

Перейдите в папку, в которой вы хотите создать свой проект, и выполните следующее.

ember new ember-cli-todomvc
cd ember-cli-todomvc
ember serve

Это создаст структуру приложения и даст вам отправную точку для дальнейшего развития. Все последующие команды bash находятся в каталоге приложения. Когда файлы упоминаются в этом руководстве, им будет предоставлен путь, предполагая, что каталог приложения является базовым.

ember serve создает экземпляр сервера, к которому вы можете получить доступ по адресу localhost:4200, и будет отслеживать изменения в каталоге приложения.

Установить базу TodoMVC

Отличные ребята из TodoMVC предоставляют CSS для приложения, поэтому нам не нужно создавать его с нуля.

ember install:bower todomvc-app-css
ember install:bower todomvc-common

Все зависимости добавляются в Brocfile.js перед module.exports = app.toTree();. Вы можете импортировать активы только из каталогов bower_components и vendor.

app.import('bower_components/todomvc-common/base.css');
app.import('bower_components/todomvc-app-css/index.css');

Создайте статичный макет приложения

Прежде чем добавлять какой-либо код, создайте статический макет приложения в app/templates/application.hbs. Нет необходимости добавлять ссылки на файлы CSS, так как это будет обрабатываться встроенной в Ember системой управления зависимостями.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Ember.js • TodoMVC</title>
  </head>
  <body>
    <section class="todoapp">
      <header class="header">
        <h1>todos</h1>
        <input type="text" class="new-todo" placeholder="What needs to be done?" />
      </header>
      <section class="main">
        <ul class="todo-list">
          <li class="completed">
            <input type="checkbox" class="toggle">
            <label>Learn Ember.js</label><button class="destroy"></button>
          </li>
          <li>
            <input type="checkbox" class="toggle">
            <label>...</label><button class="destroy"></button>
          </li>
          <li>
            <input type="checkbox" class="toggle">
            <label>Profit!</label><button class="destroy"></button>
          </li>
        </ul>
        <input type="checkbox" class="toggle-all">
      </section>
      <footer class="footer">
        <span class="todo-count">
          <strong>2</strong> todos left
        </span>
        <ul class="filters">
          <li>
            <a href="all" class="selected">All</a>
          </li>
          <li>
            <a href="active">Active</a>
          </li>
          <li>
            <a href="completed">Completed</a>
          </li>
        </ul>
        <button class="clear-completed">
          Clear completed (1)
        </button>
      </footer>
    </section>
    <footer class="info">
      <p>Double-click to edit a todo</p>
    </footer>
  </body>
</html>

Это будет изменено на протяжении всего руководства по мере реализации функций.

Добавление первого маршрута и шаблона

ember-cli использует генераторы, похожие на Rails. Генераторы вызываются с использованием ember generate или ember g для краткости. В этом уроке будет использоваться краткая форма.

Генератор маршрутов имеет два параметра:

  • type может быть либо маршрут, либо ресурс.
  • путь, указывающий путь, отличный от того, который можно вывести напрямую, используя имя маршрута.

ресурс не меняет URI, в отличие от маршрута.

Используйте генератор маршрутов для создания ресурса в корне URI.

ember g route todos --type=resource --path '/'

Генератор создаст три файла:

  • приложение/маршруты/todos.js
  • приложение/шаблоны/todos.hbs
  • тесты/модуль/маршруты/todos-test.js

Генератор также изменяет app/router.js, чтобы отразить новый маршрут/ресурс.

Скопируйте весь HTML-код между ‹body› и ‹/body› в app/templates/todos.hbs. Замените скопированный HTML-код в app/templates/application.hbs на {{outlet}}.

//...
<body>
    {{outlet}}
</body>
//...

Данные моделирования

Используйте генератор моделей, чтобы создать новую модель todo.

ember generate model todo

Это создаст два новых файла:

  • /приложение/модели/todo.js
  • тесты/модуль/модели/todo-test.js

Измените app/models/todo.js на следующее

export default DS.Model.extend({
    title: DS.attr('string'),
    isCompleted: DS.attr('boolean')
});

Создать макет для данных прибора

Предыдущие версии Ember использовали фикстуры для фиктивных данных. Хотя фикстуры все еще доступны, теперь рекомендуется использовать http-mock, который создает простой сервер Express.js, который будет работать, когда вы используете ember serve.

http-mock очень прост в настройке и использовании. Генератор делает большую часть работы за вас.

ember g http-mock todos

Добавьте фиктивные данные в формате JSON между [] в server/mocks/todos.js.

{
    id: 1,
    title: 'Learn Ember.js',
    isCompleted: true
},
{
    id: 2,
    title: '...',
    isCompleted: false
},
{
    id: 3,
    title: 'Profit!',
    isCompleted: false
}

Используйте генератор для создания нового адаптера.

ember g adapter application

Адаптер будет создан в app/adapters/application.js. Откройте этот файл и измените его, чтобы он читался следующим образом.

import DS from 'ember-data';
export default DS.RESTAdapter.extend({
    namespace: 'api'
});

По умолчанию http-mock обслуживает данные из /api/todos, что требует пространства имен.

Отображение данных модели

Теперь, когда данные доступны для приложения, код будет изменен для отображения динамических данных, а не статического макета.

Чтобы передать данные в шаблон, нам сначала нужно изменить app/routes/todos.js, чтобы он знал, какие данные извлекать. Добавьте следующее в блок extend.

model: function() {
    return this.store.find('todo');
}

Теперь app/templates/todos.hbs можно изменить, заменив статические элементы ‹li› помощником Handlebars {{each}}.

//...
<ul class="todo-list">
    {{#each todo in model}}
        <li>
            <input type="checkbox" class="toggle">
            <label>{{todo.title}}</label><button class="destroy"></button>
        </li>
    {{/each}}
</ul>
//...

Поскольку на данный момент от контроллера не требуется специального поведения, мы можем полагаться на контроллер по умолчанию, предоставляемый фреймворком.

Отображение полного состояния модели

TodoMVC использует класс completed для зачеркивания завершенных задач. Измените элемент ‹li› в app/templates/todos.hbs, чтобы применить класс, когда свойство isCompleted задачи имеет значение true.

<li {{bind-attr class="todo.isCompleted:completed"}}>

Создание нового экземпляра модели

Теперь, когда мы можем отображать данные, мы можем внести некоторые изменения, чтобы добавить элементы в список задач.

Сначала замените элемент input в apps/templates/todos.hbs вспомогательным элементом {{input}}.

<h1>todos</h1>
{{input type="text" class="new-todo" placeholder="What needs to be done?" value=newTitle action="createTodo"}}
//...

Помощник связывает свойство newTitle контроллера с атрибутом value ввода.

Затем используйте генератор для создания контроллера todos для реализации пользовательской логики.

ember g controller todos

Во вновь сгенерированном app/controllers/todos.js измените Ember.Controller.extend на Ember.ArrayController.extend, чтобы он обрабатывал массив данные, которые мы ему передаем.

Добавьте следующее в блок расширения

actions: {
    createTodo: function() {
        var title = this.get('newTitle');
        if (!title.trim()) { return; }
        var todo = this.store.createRecord('todo', {
            title: title,
            isCompleted: false
        });
        this.set('newTitle', '');
        todo.save();
    }
}

createTodo получает свойство newTitle и создает новую запись задачи, используя входные данные в качестве заголовка и устанавливая для isCompleted значение false. Затем он очищает ввод и сохраняет запись в хранилище.

Пометка модели как полной или неполной

Далее мы добавим возможность пометить задачу как завершенную или неполную и сохранить обновление.

Сначала обновите pp/templates/todos.hbs, добавив itemController в помощник {{each}}. Кроме того, преобразуйте статический элемент ‹input› в помощник {{input}}.

{{#each todo in model itemController="todo"}}
    <li {{bind-attr class="todo.model.isCompleted:completed"}}>
        {{input type="checkbox" checked=todo.model.isCompleted class="toggle"}}
        <label >{{todo.model.title}}</label><button class="destroy"></button>
    </li>
{{/each}}

Обратите внимание, что todo.isCompleted был изменен на todo.model.isCompleted. Это результат того, как устроен новый контроллер todo. Ember недавно отказался от класса ObjectController в пользу более легкого для запоминания класса Controller, что является одной из причин этого изменения.

Необходимо создать новый контроллер для работы с отдельными задачами. Используйте генератор для создания контроллера.

ember g controller todo

В блоке extend файла app/controllers/todo.js добавьте этот код.

isCompleted: function(key, value) {
    var model = this.get('model');
    if (value === undefined) {
        return model.get('isCompleted');
    } else {
        model.set('isCompleted', value);
        model.save();
        return value;
    }
}.property('model.isCompleted')

Свойство isCompleted контроллера — это вычисляемое свойство, значение которого зависит от значения свойства isCompleted экземпляра модели.

При вызове со значением, поскольку пользователь установил флажок, контроллер переключит свойство isCompleted экземпляра модели и сохранит изменение. При вызове без значения, например при загрузке страницы, он просто возвращает значение свойства isCompleted экземпляра модели.

Отображение количества незавершенных задач

Обновите app/templates/todos.hbs, как показано ниже.

<span class="todo-count">
    <strong>{{remaining}}</strong> {{inflection}} left
</span>

Реализуйте свойства остальные и изменение в app/controllers/todos.js.

actions: {
    // ...
},
remaining: function() {
    return this.filterBy('isCompleted', false).get('length');
}.property('@each.isCompleted'),
inflection: function() {
    var remaining = this.get('remaining');
    return remaining === 1 ? 'item' : 'items';
}.property('remaining')

Свойство осталось возвращает количество задач, для которых свойство isCompleted имеет значение false. Каждый раз, когда свойство isCompleted задачи изменяется, свойство оставшееся будет пересчитываться.

Свойство inflection отслеживает свойство остальное и будет обновляться всякий раз, когда обновляется остальное. Если осталось равно 1, будет возвращено единственное число, в противном случае будет возвращено множественное число.

Переключение между отображением и редактированием состояний

TodoMVC позволяет пользователям дважды щелкнуть задачу, чтобы отредактировать заголовок. Чтобы реализовать эту функцию, мы добавим свойство isEditing в контроллер задач, который будет использоваться для классификации элемента ‹li› и предоставления входных данных для редактирования задачи.

Обновите app/templates/todos.hbs следующим образом.

<ul class="todo-list">
  {{#each todo in model itemController="todo"}}
    <li {{bind-attr class="todo.model.isCompleted:completed todo.model.isEditing:editing"}}>
      {{#if todo.model.isEditing}}
        <input class="edit">
      {{else}}
        {{input type="checkbox" checked=todo.model.isCompleted class="toggle"}}
        <label {{action "editTodo" on="doubleClick"}}>{{todo.model.title}}</label><button class="destroy"></button>
      {{/if}}
    </li>
  {{/each}}
</ul>

В app/controllers/todo.js добавьте действие editTodo и свойство isEditing.

actions: {
  editTodo: function() {
    this.set('isEditing', true);
  },
  isEditing: false,
  //...
}

Это предоставляет метод editTodo, который переключает свойство isEditing в значение true. Теперь, когда вы дважды щелкаете по задаче, вам предоставляется пустой ввод. Не совсем то, что мы ищем.

Принятие правок

Теперь, когда у нас есть вход, нам нужно сделать его функциональным. Теперь мы добавим логику, чтобы сосредоточиться на вводе, когда он виден, принять ввод пользователя и сохранить изменения, когда пользователь нажимает Enter.

Создайте компонент, который будет модифицированной реализацией текстового поля.

ember g component edit-todo

Измените только что созданный файл app/components/edit-todo.js, чтобы он соответствовал следующему.

//...
export default Ember.TextField.extend({
  didInsertElement: function() {
    this.$().focus();
  }
});

Это автоматически фокусируется на элементе, когда он вставляется на страницу.

Затем замените статический элемент input компонентом {{edit-todo}} в app/templates/todos.hbs.

{{#if todo.model.isEditing}}
  {{edit-todo class="edit" value=todo.model.title focus-out="acceptChanges" insert-newline="acceptChanges"}}
{{else}}

Метод acceptChanges будет вызываться, если пользователь нажмет Enter или иным образом уберет фокус с ввода. value ввода привязано к свойству title экземпляра модели.

Наконец, необходимо добавить метод acceptChanges в app/controllers/todo.js.

//...
editTodo: function() {
  this.model.set('isEditing', true);
},
acceptChanges: function() {
  this.model.set('isEditing', false);
  if (Ember.isEmpty(this.model.get('title'))) {
    this.send('removeTodo');
  } else {
    this.get('model').save();
  }
}
//...

Удаление модели

Обновите статический элемент ‹button› в app/templates/todos.hbs, чтобы использовать действие под названием removeTodo.

<button {{action "removeTodo"}} class="destroy"></button>

Добавьте метод removeTodo в app/controllers/todo.js.

//...
acceptChanges: function() {
  this.model.set('isEditing', false);
  if (Ember.isEmpty(this.model.get('title'))) {
    this.send('removeTodo');
  } else {
    this.get('model').save();
  }
},
removeTodo: function() {
  var todo = this.get('model');
  todo.deleteRecord();
  todo.save();
}
//...

Добавление дочерних маршрутов

Добавление дочерних маршрутов к ресурсу todos в маршрутизаторе позволит реализовать отфильтрованные представления Active и Completed, которые связаны внизу списка. . Используйте генератор, чтобы создать новый маршрут index в ресурсе todos.

ember g route todos/index

В дополнение к маршруту генератор также дает нам app/templates/todos/index.hbs. Переместите весь блок списка задач ‹ul› в этот файл и замените его на {{outlet}} в app/templates/todos.hbs. .

<section class="main">
  {{outlet}}
  <input type="checkbox" class="toggle-all">
</section>

Добавьте следующее в блок extend в файле app/routes/todos/index.js.

model: function() {
  return this.modelFor('todos');
}

Переход к показу только незавершенных задач

Создайте новый активный маршрут в ресурсе todos с помощью генератора.

ember g route todos/active

Этот маршрут будет использовать модель todos с примененным фильтром. Он также будет отображаться в шаблоне todos/index, который был создан ранее. Добавьте следующее в app/routes/todos/active.js.

model: function() {
  return this.store.filter('todo', function(todo) {
    return !todo.get('isCompleted');
  });
},
renderTemplate: function(controller) {
  this.render('todos/index', {controller: controller});
}

Функция модели возвращает задачи со свойством isCompleted, равным false.

Измените app/templates/todos.hbs, заменив ссылку Active вспомогательной функцией {{link-to}}.

<li>
  <a href="all">All</a>
</li>
<li>
  {{#link-to "todos.active" activeClass="selected"}}Active{{/link-to}}
</li>
<li>
  <a href="completed">Completed</a>
</li>

Переход на отображение только выполненных задач

Создайте новый маршрут completed в ресурсе todos с помощью генератора.

ember g route todos/completed

Добавьте следующее в app/routes/todos/completed.js.

model: function() {
  return this.store.filter('todo', function(todo) {
    return todo.get('isCompleted');
  });
},
renderTemplate: function(controller) {
  this.render('todos/index', {controller: controller});
}

Измените app/templates/todos.hbs, заменив ссылку Completed вспомогательной функцией {{link-to}}.

<li>
  <a href="all">All</a>
</li>
<li>
  {{#link-to "todos.active" activeClass="selected"}}Active{{/link-to}}
</li>
<li>
  {{#link-to "todos.completed" activeClass="selected"}}Completed{{/link-to}}
</li>

Возврат к просмотру всех задач

Измените app/templates/todos.hbs, заменив ссылку All вспомогательной функцией {{link-to}}.

<li>
  {{#link-to "todos.index" activeClass="selected"}}All{{/link-to}}
</li>
<li>
  {{#link-to "todos.active" activeClass="selected"}}Active{{/link-to}}
</li>
<li>
  {{#link-to "todos.completed" activeClass="selected"}}Completed{{/link-to}}
</li>

Отображение кнопки для удаления всех выполненных задач

TodoMVC позволяет пользователям удалять все задачи, отмеченные как выполненные, одним нажатием кнопки. В app/templates/todos.js обновите статический элемент ‹button› с помощью {{action}}.

{{#if hasCompleted}}
  <button class="clear-completed" {{action "clearCompleted"}}>
    Clear completed ({{completed}})
  </button>
{{/if}}

app/controllers/todos.js необходимо обновить, чтобы добавить свойства hasCompleted и completed, а также clearCompleted. > метод.

actions: {
  clearCompleted: function() {
    var completed = this.filterBy('isCompleted', true);
    completed.invoke('deleteRecord');
    completed.invoke('save');
  },
  //...
},
hasCompleted: function() {
  return this.get('completed') > 0;
}.property('completed'),
completed: function() {
  return this.filterBy('isCompleted', true).get('length');
}.property('@each.isCompleted'),
//...

filterBy возвращает объект EmberArray, содержащий только те элементы, которые возвращают значение true. Метод вызова является частью EmberArray API и выполняет метод для каждого объекта в массиве.

Указание, когда все задачи завершены

Обновите статический флажок в app/templates/todos.hbs. Этот флажок укажет, когда все задачи будут выполнены.

<section class="main">
  {{outlet}}
  {{input type="checkbox" class="toggle-all" checked=allAreDone}}
</section>

Этот флажок будет установлен, если для параметра allAreDone установлено значение true. Реализуйте allAreDone в app/controllers/todos.js.

allAreDone: function(key, value) {
  return !!this.get('length') && this.isEvery('isCompleted');
}.property('@each.isCompleted')

Переключение всех задач между завершенными и незавершенными

Флажок, который был только что реализован, полезен, но было бы гораздо полезнее, если бы он мог переключать все задачи между завершенными и незавершенными.

Для этого измените allAreDone в app/controllers/todos.js.

allAreDone: function(key, value) {
  if (value === undefined) {
    return !!this.get('length') && this.isEvery('isCompleted');
  } else {
    this.setEach('isCompleted', value);
    this.invoke('save');
    return value;
  }
}.property('@each.isCompleted')

Замена HTTP-Mock на localStorage

Адаптеры легко заменяются без ущерба для всего приложения. Теперь, когда приложение завершено, мы добавим адаптер localStorage, чтобы информация могла сохраняться вне сеанса без сервера.

Установите адаптер localStorage и создайте сериализатор для управления строками данных JSON для хранения.

ember install:bower ember-localstorage-adapter
ember g serializer application

Перейдите к Brocfile.js и добавьте следующее под импортом CSS, который будет включать логику адаптера в качестве зависимости.

app.import('bower_components/ember-localstorage-adapter/localstorage_adapter.js');

Теперь нам нужно сообщить приложению, что мы используем localStorage, а не REST.

Откройте app/serializers/application.js и замените RESTSerializer на LSSerializer.

Откройте app/adapters/application.js и измените RESTAdapter на LSAdapter.