Начало работы с Ember.js используя Ember CLI


Цель: создать приложение со списком задач.
Время: 2 часа (дайте мне знать, если ваш результат сильно отличается).
Последнее обновление: 7 сентября 2015 г.

Введение

Я пишу обучающую статью. Такое руководство я и сам хотел бы прочитать год назад, когда начинал работать с Ember. Эта статья не предполагает, что вы уже лет 5 используете jQuery, знаете, что такое состояние, съедаете «обещания» на завтрак или даже понимаете суть термина «hook». Эта статья для новичков в программировании, в работе с JavaScript или фронтэнд фреймворками. Если же вы разбираетесь в этих вещах, то статья может показаться вам слишком растянутой и занудной. Но я буду рад, если вы ее прочитаете и поможете улучшить. Независимо от вашего уровня, дайте мне знать, в каком месте застряли, какие вещи вам непонятны или где не хватает деталей. Проще всего до меня достучаться через Twitter или оставить комментарий внизу.

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

И еще одно примечание: я не настолько остроумный, как сам о себе думаю.

Начало работы

Для начала нужно установить node.js. Node.js — javascript, который используется для написания приложений на сервере. Конкретно с node мы ничего делать не будем, но используем его менеджер пакетов (npm), чтобы установить ember CLI (интерфейс командной строки). Если последние два предложения вам непонятны и в голове засела мысль: «ЧТО? Я думал, эта статья для полных новичков в ember. Почему этот парень говорит о node и менеджерах пакетов? Да и что такое менеджер пакетов? Как он догадался, что я сейчас об этом думаю?», то не беспокойтесь. Все это неважно для обучения. Мы просто установим node из node.js. После установки откройте терминал и наберите:

$ node -v

У вас должна появиться версия (-v) node.js. Например, v0.12.x.

Теперь давайте используем npm, чтобы установить ember CLI, который предоставит нам все возможности Ember.

$ npm install ember-cli -g
$ ember -v

-g на npm install значит, что установка пакета производится глобально, а не в папку, в которой мы сейчас находимся. Таким образом, мы можем использовать ember-cli из любого места на компьютере. В этой статье я использовал версию version: 1.13.x ember-cli. Поэтому вам нужна такая же версия или выше.

В терминале перейдите в папку, где вы хотите хранить приложение. С помощью ember CLI создайте приложение. Это очень просто:

$ ember new todo-mvc

Если вы прежде использовали rails, то этот тип генератора будет вам знаком. Мы указали ember-cli подготовить загрузку файлов и установить пакеты кода, требуемые для создания приложения ember. В результате новое приложение будет создано в папке todo-mvc. Переходите туда.

$ cd todo-mvc

Если вы используете ember-cli ниже версии 2.0.0, то в файле bower.json нужно изменить версию ember и ember-data на 2.0.0. На время написания статьи ember-cli был все еще версии 1.13.8.

"ember": "2.0",
"ember-data": "2.0",

Вам будет нужно переустановить пакеты ember. Вы можете сделать это, набрав следующую строку в терминале:

$ bower install

Bower может спросить вас, какую версию ember нужно установить. Просмотрите варианты и выберите тот, что включает ember 2.0 или выше.

Просмотрите структуру приложения. Большинство файлов, которые мы будем использовать или создавать, будут находиться в папке app. Теперь я предлагаю открыть содержимое этой папки в текстовом редакторе. Хорошей альтернативой могут быть sublime text или atom (только для Mac).

Давайте начнем работу над приложением.

$ ember serve

В терминале мы увидим следующее:

version: 1.13.8
Livereload server on port 35729
Serving on http://localhost:4200/


// Some build information about the time it took to build //

Здесь важная строчка — Serving on http://localhost:4200. Это значит, что если вы перейдете в браузере на localhost:4200, то увидите приложение ember. У вас должна появиться страница с надписью:

Welcome to Ember.js

Изменим это сообщение. Откроем файл app/templates/application.hbs. Расширение hbs — это handlebars, то есть язык шаблонов. В дальнейшем ember расширяет handlebars с помощью так-называемого HTMLbars, который добавляет специфическую функциональность ember. Языки шаблонов позволяют выполнять в html некоторый базовый код. Например, вывод переменной, проход в цикле по массиву или проверка переменной на true и false. Все эти вещи мы будем делать в ходе этой статьи. Поэтому не переживайте, если сейчас вам что-либо непонятно.

<h2 id='title'>Welcome to Ember-CLI tutorial :)</h2>

{{outlet}}

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

Роутер и маршруты

Это первая часть истинного Ember. Я даже взволнован.

Вместе роутер и маршруты определяют структуру приложения и то, как оно отвечает пользователям. Когда пользователь посещает www.myapp.com/about, что должно делать приложение? А если он посетит www.myapp.com/todos/1, что мы ему покажем? В этих примерах нам нужно загрузить информацию о приложении в первом случае и извлечь запись с ID 1 из базы данных во втором.

Есть две концепции, которые необходимо уяснить, чтобы понять суть маршрутизации в Ember. И это очень важно, так как, по моему мнению, именно маршрутизация отделяет ember от остальных фреймворков и делает его отличным решением для создания амбициозных приложений. Итак:

  1. Маршрут: маршрут загружает шаблон и необходимые данные.
  2. Роутер: роутер на основе URL определяет, какой маршрут (или набор маршрутов) загружать.

Давайте взглянем на этот процесс на примере маршрута todos. Сначала, загружаем Ember Inspector, который нам понадобится далее. Это дополнение для chrome или firefox. После установки оно появится в инструментах разработчика как закладка (в chrome на mac открывается сочетанием cmd+alt+i). Мы будем использовать inspector чуть позже.

Откройте роутер app/router.js. Все правильно, роутер настолько важен, что даже не скрыт в папке. Теперь добавим маршрут todos в Router.map.

import Ember from 'ember';  
import config from './config/environment';

var Router = Ember.Router.extend({  
  location: config.locationType
});

Router.map(function() {  
    this.route('todos', { path: '/' });
});

export default Router;

Сейчас у нас есть маршрут todos с URL / (например, www.localhost:4200/). Почему /? Это путь, который мы указали в определении маршрута. Мы могли бы указать путь /shitToDo или оставить его пустым. В последнем случае ember по умолчанию установил бы путь /todos. Мы указываем приложению, что если кто-либо посещает этот URL, должен загружаться маршрут todos. Вскоре мы создадим объект маршрута, который укажет приложению, какие загружать данные и шаблон. Наконец, последняя строка export default Router; — это «магия» ember CLI. Она значит, что вы не получите глобальных переменных. Глобальные переменные все портят. Если вы не знаете почему, то воспользуйтесь поиском google или просто поверьте мне на слово…

Глобальные переменные

Откройте инструменты разработчика на странице приложения в браузере и щелкните по вкладке Ember. Давайте взглянем на наши маршруты.

Ember Inspector

Я отметил «Current Route only» (только текущий маршрут), чтобы убрать лишнее. У нас есть маршрут приложения и ниже — наш маршрут todos. Маршрут приложения мы получаем изначально. В ember он находится на вершине дерева маршрутов. Маршрут приложения — отличное место, куда можно вложить то, что вы хотите увидеть в программе на каждом маршруте, например, header и footer (верхняя и нижняя часть страницы). Мы уже видели шаблон приложения app/templates/application.hbs. Давайте отредактируем его прямо сейчас.

app/templates/application.hbs

<section id="todoapp">  
    <header id="header">
        <h1>todos</h1>
    </header>

    {{outlet}}
</section>

<footer id="info">  
    <p>Double-click to edit a todo</p>
</footer>

У нас есть код html с одной переменной {{outlet}}. В следующем разделе статьи я поясню, что это за переменная. Так что, читаем дальше.

Где находится маршрут приложения? Он не в папке routes. Все потому что он генерируется ember автоматически. Мы могли бы создать его, если бы не хотели придерживаться исходного поведения. Исходное поведение маршрута заключается в отображении шаблона с тем же именем, что и у маршрута.

Application Route -> Renders Application Template
Route app/routes/application.js -> Renders app/templates/application.hbs Template

Другая функция маршрута — это загрузка данных. На уровне маршрута приложения нам не требуются какие-либо данные, поэтому мы можем продолжать без создания этого маршрута.

Маршрут todos

Давайте сделаем маршрут todos по аналогии с маршрутом приложения. Создаем шаблон todos в папке templates.

$ touch app/templates/todos.hbs

В этот файл добавляем html <h2>Todos Template</h2>. Теперь, когда мы посмотрим на приложение, мы увидим этот контент. Шаблоны вложены друг в друга. И у нас есть структура дерева.

Application -> Todos

Маршруты загружаются по порядку, шаблоны вложены в родительский шаблон с помощью {{outlet}}. В Ember Inspector во вкладке View Tree можно увидеть визуальное представление этой структуры.

Ember Inspector

Давайте взглянем на часть маршрута с данными. Нам нужно создать маршрут todos.

$ touch app/routes/todos.js
import Ember from 'ember';

export default Ember.Route.extend({  
    model() {
        let todos = [
            {
                title: "Learn Ember",
                complete: false,
            },
            {
                title: "Solve World Hunger",
                complete: false,
            }
        ];
        return todos;
    }
});

Маршрут todos имеет hook модели. Термин hook означает функцию. Этот hook модели будет вызываться, когда загрузится маршрут. Внутри функции нашей модели мы создаем массив [] объектов {} javascript, которые представляют задачу todo. Массив называется todos, и он возвращается функцией. То есть наш маршрут todos теперь отображает шаблон (по умолчанию) todos.hbs и загружает данные todos. Последняя часть состоит в том, чтобы показать, что шаблон имеет доступ к данным модели. Открываем app/templates/todos.hbs.

<h2>Todos Template</h2>

<ul>  
    {{#each model as |todo|}}
        <li>
            {{todo.title}}
        </li>
    {{/each}}
</ul>

Вы должны видеть что-то подобное:

Todos

Handlebars хелпер {{#each}} проходит в цикле по массиву модели. Каждый элемент мы выводим как {{todo.title}}. Hook модели из маршрута присваивает переменную, которая по умолчанию называется model для шаблона. И снова мы могли бы изменить ее, например, на todos, чтобы стало понятнее. Но сейчас мы этого делать не будем.

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

Отсутствующий стиль

Мы оба подумали об одном, но ни у кого не хватает смелости это сказать. Непонятно, твоя ли это вина или моя, но я произнесу это сейчас… Приложение выглядит отвратительно.

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

Найдите файл CSS здесь, скопируйте и вставьте его в app/styles/app.css.

Давайте добавим разметку и контент в html, чтобы сопоставить таблицу стилей для шаблона todos app/templates/todos.hbs.

app/templates/todos.hbs

<input type="text" id="new-todo" placeholder="What needs to be done?" />

<section id="main">  
    <ul id="todo-list">
        {{#each model as |todo|}}
            <li class="completed">
                <input type="checkbox" class="toggle">
                <label>{{todo.title}}</label><button class="destroy"></button>
            </li>
        {{/each}}
    </ul>

    <input type="checkbox" id="toggle-all">
</section>

<footer id="footer">  
    <span id="todo-count">
        <strong>2</strong> todos left
    </span>
    <ul id="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 id="clear-completed">
        Clear completed (1)
    </button>
</footer>

Теперь приложение выглядит отлично.

Планирование маршрутов

Чем дольше я работаю с Ember, тем становится яснее, как важно в самом начале планировать маршруты приложений. Я не знаю, реально ли это сделать при создании нового проекта, но в нашем случае я знаю конечный результат. Поэтому мы все спланируем.

Нам нужно три маршрута, вложенных в todos: index, incomplete и complete. Первый маршрут выдает список всех задач. Другие два фильтруют список соответствующим образом (завершенные и незавершенные). Они будут иметь следующие URLs:

www.localhost:4200/ -> Index Route
www.localhost:4200/incomplete -> Incomplete route
www.localhost:4200/complete -> Complete route

Мы можем использовать инструмент генерации ember-cli, чтобы создать файл маршрута, шаблон и сделать соответствующую запись в файле router.js. В терминале набираем:

$ ember generate route todos/index   
$ ember generate route todos/complete
$ ember generate route todos/incomplete

В итоге мы получим:

version: 1.13.8
installing route
  create app/routes/todos/complete.js
  create app/templates/todos/complete.hbs
updating router
  add route todos/complete
installing route-test
  create tests/unit/routes/todos/complete-test.js

Генератор создаст все необходимые файлы и изменит роутер.

Наш конечный app/router.js должен выглядеть так:

import Ember from 'ember';  
import config from './config/environment';

var Router = Ember.Router.extend({  
    location: config.locationType
});

Router.map(function() {  
    this.route('todos', { path: '/' }, function() {
        this.route('complete');
        this.route('incomplete');
    });
});

export default Router;

У нас есть два вложенных маршрута в todos. Маршрут index не указывается, но в Ember вложенный набор маршрутов будет автоматически иметь маршрут index. Если другой аргумент пути не предоставлен, URL будет соответствовать имени маршрута, например /complete.

Вы также увидите, что мы сгенерировали тестовые файлы для каждого маршрута…

В этой статье мы не будем разбирать тесты. Так я мягко намекаю, что:

Я не изучал, как писать тесты, поэтому не могу включить такой материал в статью.

Если вы не знаете, что такое тесты в программировании, то я объясню простыми словами. Это код, который тестирует определенный результат выполнения приложения. Например, мы ожидаем, что сможем добавить новый элемент в список задач. Если бы мы имели всеобъемлющие знания и были опытными разработчиками, то написали бы кусок кода, который заставит компьютер заполнить поле ввода и щелкнуть enter. Затем этот код проверит, что в список была добавлена новая задача. Тест провалится, если вы не написали код для осуществления такой процедуры. И мы пишем код, который позволит пройти тест и т. д. Польза тестирования заключается в том, что если в будущем мы изменим код, и программа перестанет работать, тесты автоматически скажут нам об этом. И не придется каждый раз после изменения кода вручную набирать текст и добавлять задачи в список. Надеюсь, сейчас вы понимаете, что такое тестирование и чем оно полезно. Для понимания необязательно уметь писать такие вещи.

И нам осталось перенести список задач из шаблона todos app/templates/todos.hbs в шаблон index app/templates/todos/index.hbs. Помните, что мы используем {{outlet}} в родительском шаблоне, чтобы указать место, куда загружать дочерний шаблон.

app/templates/todos.hbs

<input type="text" id="new-todo" placeholder="What needs to be done?" />

<section id="main">  
    {{!-- This is where our todos will go --}}
    {{outlet}}

    <input type="checkbox" id="toggle-all">
</section>

{!-- The rest of the todos mark up truncated for brevity --}

Теперь создаем шаблон index и добавляем в него список задач.

app/templates/todos/index.hbs

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

Наш маршрут index будет использовать модель от маршрута todos. Открываем app/routes/todos/index.js.

app/routes/todos/index.js

import Ember from 'ember';

export default Ember.Route.extend({  
    model() {
        return this.modelFor('todos');
    }
});

Теперь взглянем на www.localhost:4200/, откроем Ember Inspector и посмотрим на View Tree.

Ember Inspector

Вы увидите, что шаблон index отображается под маршрутом todos.

Теперь у нас есть плотная основа для приложения.

Компоненты

Компоненты — отдельные блоки кода, которые можно использовать повторно. Они генерируют элементы (компоненты), с которыми пользователи могут взаимодействовать. Например, поле ввода — компонент, который используется по всему интернету. Он имеет стандартное поведение, то есть дает пользователю возможность набирать в нем текст. В любом html мы могли бы сгенерировать поле ввода с помощью тега <input>. В сообществе разработчиков стало понятно, что эти распространенные компоненты действительно полезны, и если позволить любому разработчику их писать, то всем это облегчит работу. Например, стандартный тег <calendar> был бы полезен на многих сайтах.

В настоящее время существует стандарт для компонентов http://w3c.github.io/webcomponents/spec/custom/. Ember реализует компоненты и старается придерживаться прогнозируемых стандартов, чтобы в будущем вы могли легко подключать распространенные компоненты.

Давайте подумаем о компонентах в рамках приложения со списком задач. Мне кажется, что у нас есть два основных компонента. Это todo-list (список задач) и todo-item (элемент списка/задача).

Todo-list должен осуществлять следующие функции:

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

Todo-item должен осуществлять такие функции:

  • показывать наименование задачи и указывать ее состояние;
  • отмечать задачу как завершенную или незавершенную;
  • редактировать наименование;
  • удалять задачу.

Сначала сгенерируем компонент todo-item.

$ ember generate component todo-item

version: 1.13.8
installing component
  create app/components/todo-item.js
  create app/templates/components/todo-item.hbs
installing component-test
  create tests/integration/components/todo-item-test.js

Компонент состоит из двух частей: шаблона app/templates/components/todo-item.hbs для отображения и файла javascript app/components/todo-item.js для вычисления переменных и обработки событий пользователя.

Шаблон todo-item содержит разметку html.

app/templates/components/todo-item.hbs

<input type="checkbox" class="toggle" checked="{{if todo.complete 'checked'}}">  
<label class="{{if todo.complete 'completed'}}">{{todo.title}}</label><button class="destroy"></button>

Теперь app/templates/todos/index.hbs.

app/templates/todos/index.hbs

<ul id="todo-list">  
    {{#each model as |todo|}}
        {{todo-item todo=todo}}
    {{/each}}
</ul>

В todo-item мы выводим переменную {{todo.title}}, поэтому нам нужно убедиться, что мы передаем задачу, когда используем компонент. В шаблоне todos.index вы можете видеть, что мы делаем это внутри блока {{#each}}. Мы также проверяем статус todo.complete, и если это так, то проверяем поле и принимаем класс завершенных задач с checked="{{if todo.complete 'checked'}}" и {{if todo.complete 'completed'}}.

И еще одна вещь, которую нужно сделать в javascript app/components/todo-item.js для этого компонента.

app/components/todo-item.js

import Ember from 'ember';

export default Ember.Component.extend({  
    tagName: 'li'
});

Ember вставит каждый компонент в тег html. По умолчанию это будет <div>. Но здесь нам нужно, чтобы компонент заключался в <li>. Поэтому мы указали это здесь со свойством tagName. Мы могли бы поместить <li> в шаблон, что было бы понятнее, но привело к фиктивным дополнительным divs (в основном безвредным) в коде.

Теперь сделаем todo-list.

$ ember g component todo-list
  • g — это сокращение для генерации.

Шаблон todo-list будет содержать большую часть разметки от шаблона todos. Отредактируем app/templates/components/todo-list.hbs.

app/templates/components/todo-list.hbs

<section id="main">  
    {{yield}}

    <input type="checkbox" id="toggle-all">
</section>

<footer id="footer">  
    <span id="todo-count">
        <strong>2</strong> todos left
    </span>
    <ul id="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 id="clear-completed">
        Clear completed (1)
    </button>
</footer>

Помощник {{yield}} похож на {{outlet}}. Он позволит нам ввести другие компоненты и разметку в этот шаблон. Посмотрим, как это выглядит на практике. Открываем app/templates/todos.hbs.

app/templates/todos.hbs

<input type="text" id="new-todo" placeholder="What needs to be done?" />  
{{#todo-list todos=model}}
    {{outlet}}
{{/todo-list}}

{{outlet}} будет отображаться заместо {{yield}} в компоненте todo-list. Помните, что {{outlet}} — это наш вложенный маршрут todos.index. Сейчас мы также передаем модель todos в todo-list. Позднее мы напишем код, который использует эту модель.

Моделирование задачи (todo)

Модель — это как образец для наших данных. Каждая задача в списке будет экземпляром модели Todo. Мы можем использовать генератор ember, чтобы создать модель.

$ ember generate model todo title:string complete:boolean

Открываем app/models/todo.js, чтобы просмотреть сгенерированный образец модели.

app/models/todo.js

import DS from 'ember-data';

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

Каждый экземпляр модели Todo будет иметь title и поле isCompleted. Поле title будет строкой, а поле isComplete булевым значением (true или false). Тут мы применим ember data. Это библиотека, которая используется для управления данными в приложениях ember. Ее поддерживает команда ember, и она должна работать без дополнительных установок.

Data Mirage

Ранее наши данные модели были массивом объектов javascript. Это было быстрое и неаккуратное решение, предоставленное в качестве примера. Но при создании приложения, мы будем запрашивать данные с сервера. В этой статье мы не станем создавать сервер. Мы используем пакет под названием ember-cli-mirage, который сделает фиктивный сервер. Мы легко можем установить его с помощью ember-cli.

$ ember install ember-cli-mirage

Теперь в app/routes/todos.js для запроса данных мы будем использовать ember data вместо переменной массива.

app/routes/todos.js

import Ember from 'ember';

export default Ember.Route.extend({  
    model() {
        return this.store.findAll('todo');
    }
});

Еmber data будет посылать запрос REST GET к /todos. В этой статье я не буду объяснять про запросы REST и GET. Если они вам непонятны, то не беспокойтесь. Прямо сейчас вам это и не нужно.

Мы установим Mirage, чтобы разобраться с этим запросом к todos и, по сути, со всеми запросами, с которыми нашему фиктивному серверу нужно будет иметь дело в этой статье.

Редактируем app/mirage/config.js.

app/mirage/config.js

export default function() {  
    this.get('/todos', function(db, request) {
        return {
            data: db.todos.map(attrs => (
                {type: 'todos', id: attrs.id, attributes: attrs }
            ))
        };
    });
    this.post('/todos', function(db, request) {
        let attrs = JSON.parse(request.requestBody);
        let todo = db.todos.insert(attrs);
        return {
            data: {
                type: 'todos',
                id: todo.id,
                attributs: todo
            }
        };
    });
    this.patch('/todos/:id', function(db, request) {
        let attrs = JSON.parse(request.requestBody);
        let todo = db.todos.update(attrs.data.id, attrs.data.attributes);
        return {
            data: {
                type: "todos",
                id: todo.id,
                attributes: todo
            }
        };
    });
    this.del('/todos/:id');
}

Эта конфигурация установит сервер, который будет отвечать на вызовы от Ember Data, чтобы получать все задачи, а также создавать, редактировать и удалять отдельные задачи.

Мы создаем фабрику, которая будет генерировать данные.

$ touch app/mirage/factories/todo.js

app/mirage/factories/todo.js

import Mirage, {faker} from 'ember-cli-mirage';

export default Mirage.Factory.extend({  
    title(i) { return `Todo title ${i + 1}`; },
    complete: faker.list.random(true, false)
});

Этот код создаст задачу с заголовком "Todo title 1" и случайно-выбранным состоянием true или false.

Мы вызываем фабрику в app/mirage/scenarios/default.js.

app/mirage/scenarios/default.js

export default function(server) {  
    server.createList('todo', 3);
}

Сценарий сообщает нашему фиктивному серверу создать 3 задачи.

Наконец, перезапускаем сервер в терминале.

Вот и все. Теперь мы эмулируем наши данные. Если вы не уследили за происходящим, то, возможно, я недостаточно подробно изложил суть процесса. Но не переживайте, изучение Mirage не является главной целью этой статьи.

Создание новой задачи

Давайте добавим еще одну задачу в список. Чтобы сделать это, нам нужно создать новый компонент todo-input.

$ ember generate component todo-input

Шаблон этого компонента app/templates/components/todo-input.hbs содержит хелпер input от handlebars.

app/templates/components/todo-input.hbs

{{input type="text" id="new-todo" placeholder="What needs to be done?" 
    value=newTitle enter="submitTodo"}}

Здесь мы задаем компоненту действие, которое будет запущено, когда пользователь нажмет enter на поле ввода. Нам нужно обработать это действие в javascript компонента app/components/todo-input.js.

app/components/todo-input.js

import Ember from 'ember';

export default Ember.Component.extend({  
    actions: {
        submitTodo(newTitle) {
            if (newTitle) {
                this.sendAction('action', newTitle);
            }
            this.set('newTitle', '');
        }
    }
});

В этом действии мы проверяем, есть ли newTitle, и при его наличии посылаем действие из компонента. Затем мы очищаем ввод с помощью this.set('newTitle', '');. Действие, которое мы посылаем, это переменная 'action'.

Мы должны передать эту переменную, когда реализуем компонент в шаблоне app/templates/todos.hbs, где заменяем поле ввода на компонент.

app/templates/todos.hbs

{{todo-input action="createTodo"}}
{{#todo-list todos=model}}
    {{outlet}}
{{/todo-list}}

Теперь последовательность событий такая: нажав enter в поле ввода, мы запускаем действие submitTodo, это действие посылает переменную action из компонента. Посылает? Куда посылает? Это хороший вопрос.

Время представить вам процесс распространения действия. Изначально наше действие обрабатывается в компоненте, где оно без сторонней помощи «погибает». Все потому что компоненты изолированы. Но мы поможем действию выбраться, отправив его из компонента к маршруту с помощью sendAction. Компонент todo-input находится на маршруте todos. Поэтому маршрут первым берется за обработку действия createTodo. Если вы не обрабатываете его на маршруте todos, то оно перейдет к маршруту приложения. Если вы откроете inspector, то увидите дерево маршрутов. Если ни один из маршрутов не обработает действие, то в консоли вы получите примерно такую ошибку:

Ember Inspector

Мы посылаем действие из компонента по правилу «данные вниз, действия вверх». Мы посылаем данные вниз, например, отправляя задачу в компонент todo-item. Здесь, чтобы создать новую задачу, мы посылаем действие вверх от todo-input к месту (роутер), которое отвечает за поддержку данных.

Обработаем действие на app/routes/todos.js.

app/routes/todos.js

import Ember from 'ember';

export default Ember.Route.extend({  
    model() {
        return this.store.findAll('todo');
    },
    actions: {
        createTodo(newTitle) {
           this.store.createRecord('todo', {
               title: newTitle,
               complete: false
           }).save();
        }
    }
});

В действии createTodo сначала мы создаем экземпляр задачи с newTitle и изначальным статусом complete на false. После этого мы ставим save() в конец цепочки, чтобы отправить на сервер запрос на сохранение данных.

Завершение задачи

Нам нужна возможность через приложение переключать значение задачи complete с true на false и обратно. Для этого требуется изменить чекбокс ввода. Теперь мы будем использовать хелпер handlebars {{input}}. Компонент app/templates/components/todo-item.hbs тоже изменится.

app/templates/components/todo-item.hbs

{{input type="checkbox" checked=todo.complete class="toggle"}}
<label class="{{if todo.complete 'completed'}}" {{action "editTodo" on="doubleClick"}}>{{todo.title}}</label>  
<button class="destroy"></button>

Когда отображается тег input, он использует текущее значение свойства todos complete, чтобы определить, проверен ли ввод или нет checked=todo.complete. Когда значение проверки меняется, Ember обновляет значение модели без написания дополнительного кода. Да, действие переключения обрабатывается автоматически. Разве не здорово?!? (Очень).

Можно провести быструю проверку. Сейчас мы по идее можем совершить 2 действия от изначального списка требований к компоненту todo-item:

  • должен показывать задачу;
  • должен иметь возможность отмечать задачу как завершенную или незавершенную;
  • (Нужно сделать) должен поддаваться редактированию;
  • (Нужно сделать) должен давать возможность удалять задачу.

Если бы только у меня был список, чтобы отслеживать все эти требования!

Такое поведение наблюдается в приложении ниже (из руководства по ember).

JS Bin on jsbin.com

Редактирование и удаление задач

Нам нужно указать приложению, в какой момент мы редактируем задачу. Для этого необходимо свойство isEditing в компоненте app/templates/components/todo-item.hbs.

app/templates/components/todo-item.hbs

{{#if editing}}
    <input class="edit">
{{else}}
    {{input type="checkbox" checked=todo.complete class="toggle"}}
    <label class="{{if todo.complete 'completed'}}" {{action "editTodo" on="doubleClick"}}>{{todo.title}}</label>
    <button class="destroy"></button>
{{/if}}

Мы подключаем класс editing к свойству isEditing. Мы использовали блок хелпера {{#if}}. Это дает нам простой блок if else. Если isEditing true, то отображается <input class='edit'>, если isEditing false, то отображается другая часть после {{else}}. Если вы знакомы с логикой if/else, то важно отметить, что в handlebars можно только проверить, соответствует ли свойство true или false*, и здесь нет варианта else if. В блоке else у нас есть еще один хелпер {{action}}. Он вызывается editTodo и запускается по doubleClick на элементе.

Чтобы расширить блок if, посмотрите дополнение truth helpers.

Если вы дважды щелкните по задаче, то увидите в консоли ошибку обработки действия. Все точно так же, как мы делали с действием createTodo.

Uncaught Error: <ember-cli-todo-mvc@component:todo-item::ember543> had no action handler for: editTodo

Обработаем действие в javascript компонента app/components/todo-item.js.

app/components/todo-item.js

import Ember from 'ember';

export default Ember.Component.extend({  
    tagName: 'li',
    classNameBindings: ['editing'],
    editing: false,
    actions: {
        editTodo() {
            this.toggleProperty('editing');
        }
    }
});

Обратите внимание, что в компоненте мы создаем свойство под названием editing. Это состояние отображения компонента. То есть оно не имеет значения для остальной части приложения, поэтому мы управляем им в компоненте. Компоненты должны отвечать за управление их состоянием. Действие editTodo переключает свойство editing. Наконец, classNameBindings применит класс 'editing' к тегу <li> компонента. Это хорошая возможность просмотреть документацию API по Ember.Component, которая объяснит, как работает свойство classNameBindings.

Теперь двойной щелчок отображает поле ввода.

Следующий шаг — изменить тег <input> в помощнике handlebars, что изменит значение модели title. Наш компонент теперь выглядит примерно как в app/templates/components/todo-item.hbs.

app/templates/components/todo-item.hbs

{{#if editing}}
    {{input class="edit" value=todo.title action="submitTodo"}}
{{else}}
    {{input type="checkbox" checked=todo.complete class="toggle"}}
    <label class="{{if todo.complete 'completed'}}" {{action "editTodo" on="doubleClick"}}>{{todo.title}}</label><button class="destroy"></button>
{{/if}}

Действие submitTodo в app/components/todo-item.js.

app/components/todo-item.js

actions: {  
    editTodo() {/** action **/},
    submitTodo() {
        let todo = this.get('todo');
        if (todo.get('title') === "") {
            this.sendAction('deleteTodo', todo);
        } else {
            this.sendAction('updateTodo', this.get('todo'));
        }
        this.set('editing', false);
    }
}

В этом действии мы получаем задачу. Затем мы проверяем, пустой ли title или нет. Если title пустой, то мы удаляем задачу или обновляем ее, отправляя задачу как переменную.

Теперь запомните, что нам нужно обновить шаблон, который имеет todo-item app/templates/todos/index.hbs, и передать две новые переменные действия.

app/templates/todos/index.hbs

<ul id="todo-list">  
    {{#each model as |todo|}}
        {{todo-item todo=todo updateTodo="updateTodo" deleteTodo="deleteTodo"}}
    {{/each}}
</ul>

Нам нужно обработать эти действия на маршруте todos app/routes/todos.js.

app/routes/todos.js

import Ember from 'ember';

export default Ember.Route.extend({  
    model() {
        return this.store.findAll('todo');
    },
    actions: {
        createTodo(newTitle) {
            this.store.createRecord('todo', {
               title: newTitle,
               complete: false
            }).save();
        },
        updateTodo(todo) {
            todo.save();
        },
        deleteTodo(todo) {
            todo.destroyRecord();
        }
    }
});

В действии updateTodo мы просто сохраняем новую задачу, которая запустит запрос на сервер для сохранения изменений. В действии deleteTodo мы удаляем запись. Это действие убирает запись локально и отправляет на сервер запрос сделать то же самое.

Мы уже написали действия для удаления задач, поэтому давайте просто закрепим его на кнопке 'X' справа от задачи. Нам нужно добавить еще одно {{action}} для кнопки удаления в хелпер handlebars в app/templates/components/todo-item.hbs.

app/templates/components/todo-item.hbs

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

Мы добавили для кнопки действие deleteTodo. Нам нужно сделать так, чтобы наш javascript посылал это действие на маршрут todos от действий компонентов. Открываем app/components/todo-item.js.

app/components/todo-item.js

actions: {  
    editTodo() { /* truncated */ },
    submitTodo() { /* truncated */ },
    deleteTodo() {
        let todo = this.get('todo');
        this.sendAction('deleteTodo', todo);
    }
}

Все будет работать, так как у нас уже есть действие для удаления задачи на маршруте todos.

Вложенные маршруты

В начале статьи мы создали в роутере вложенные маршруты. Давайте используем их, чтобы отфильтровать завершенные и незавершенные задачи. Сначала сделаем это в маршруте complete app/routes/todos/complete.js.

app/routes/todos/complete.js

import Ember from 'ember';

export default Ember.Route.extend({  
    model() {
        return this.store.filter('todo', function(todo) {
            return todo.get('complete');
        });
    },
    renderTemplate(controller, model) {
        this.render('todos.index', {
            model: model
        });
    }
});

Теперь, если вы посетите localhost:4200/complete, то увидите только завершенные задачи из списка. Hook model() сначала возвращает модель todos с this.store.filter('todo', затем проходит по каждой задаче и возвращает те, в которых todo.get('complete') === true. Hook renderTemplate() указывает, чтобы ember отобразил шаблон для todos.index (app/templates/todos/index.hbs) и применил модель для этого маршрута как модель.

Маршрут incomplete будет почти идентичным, но с return !todo.get('complete'); в функции фильтра. Файл app/routes/todos/incomplete.js будет выглядеть так.

app/routes/todos/incomplete.js

import Ember from 'ember';

export default Ember.Route.extend({  
    model() {
        return this.store.filter('todo', function(todo) {
            return !todo.get('complete');
        });
    },
    renderTemplate(controller, model) {
        this.render('todos.index', {
            model: model
        });
    }
});

Теперь нам нужно, чтобы пользователь мог добраться до этих маршрутов. В todos-list необходимо сделать ссылки на маршруты.

app/templates/components/todo-list.hbs

<ul id="filters">  
    <li>
        {{#link-to "todos.index" activeClass="selected"}}All{{/link-to}}
    </li>
    <li>
        {{#link-to "todos.incomplete" activeClass="selected"}}Active{{/link-to}}
    </li>
    <li>
        {{#link-to "todos.complete" activeClass="selected"}}Complete{{/link-to}}
    </li>
</ul>

Помощник {{#link-to}} позволяет нам создать ссылки на маршруты приложения в соответствии с роутером. И мы можем применить специальный класс, когда маршрут активен. То есть, когда мы находимся на localhost:4200/incomplete, эта ссылка будет иметь класс selected, который к ней применили.

Здесь представлен пример доступной на этом этапе функциональности приложения.

JS Bin on jsbin.com

Последние штрихи

В последнем разделе мы сделаем три вещи:

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

Теперь мы можем поработать с тремя последними пунктами, которые будет обрабатывать компонент todo-list.

Сначала изменим жестко заданное значение '1' на свойство компонента {{remaining}} и добавим свойство {{inflection}} в app/templates/components/todo-list.hbs.

app/templates/components/todo-list.hbs

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

Теперь добавим свойства к app/components/todo-list.js.

app/components/todo-list.js

import Ember from 'ember';

export default Ember.Component.extend({  
    remaining: Ember.computed('todos.@each.complete', function() {
        let todos = this.get('todos');
        return todos.filterBy('complete', false).get('length');
    }),
    inflection: Ember.computed('remaining', function() {
        var remaining = this.get('remaining');
        return (remaining === 1) ? 'item' : 'items';
    })
});

Свойство remaining содержит вычисляемое свойство Ember. Вычисляемое свойство зависит от другого свойства или свойств. Оно отслеживает их. Если они меняются, то меняется и вычисляемое свойство.

В свойстве remaining мы отслеживаем todos.@each.complete. Это значит, что мы смотрим на каждый экземпляр (@each) задач и свойство complete каждого экземпляра. То есть если свойство complete на любой задаче меняется, вычисляемое свойство пересчитывается. Если вы изменили свойство title, то эта функция не пересчитывает значение. Текущее вычисляемое свойство — это число (.get('length')) моделей, где complete принимает false (model.filterBy('complete', false)).

В свойстве inflection мы отслеживаем другое вычисляемое свойство — remaining. Вычисляемое свойство будет 'item', если останется только одна задача, и 'items' во всех остальных случаях.

Чтобы добавить кнопку, которая убирает завершенные задачи, нам нужно выяснить, есть ли у нас завершенные задачи, сколько их, и написать действие для их удаления. Сначала изменим app/components/todo-list.js.

app/components/todo-list.js

import Ember from 'ember';

export default Ember.Component.extend({  
    remaining: Ember.computed('todos.@each.complete', function() { /* truncated */}),
    inflection: Ember.computed('remaining', function() { /* truncated */ }),
    completed: Ember.computed('todos.@each.complete', function() {
        var todos = this.get('todos');
        return todos.filterBy('complete', true).get('length');
    }),
    hasCompleted: Ember.computed('completed', function() {
        return this.get('completed') > 0;
    }),
    actions: {
        clearCompleted() {
            let completed = this.get('todos').filterBy('complete', true);
            completed.forEach((todo) => {
                this.sendAction('deleteTodo', todo);
            });
        }
    }
});

У нас есть действие clearCompleted, которое получает все завершенные задачи с помощью метода filterBy. Мы видели его ранее. Затем мы посылаем действие deleteTodo из компонента forEach, чтобы задача обрабатывалась на маршруте todos.

Мы добавили два вычисляемых свойства. hasCompleted выясняет, есть ли у нас this.get('completed') > 0, то есть как минимум одна завершенная задача. Это свойство будет использоваться в шаблоне, чтобы определять, показывать или нет кнопку удаления завершенных задач с помощью блока handlebars {{#if}}. Второе вычисляемое свойство — completed. Оно считает количество завершенных задач. Нам также нужно повесить действие на кнопку.

Теперь добавим необходимые компоненты к app/templates/components/todo-list.hbs.

app/templates/components/todo-list.hbs

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

Мы также посылаем действие deleteTodo из компонента, поэтому нужно отправить переменную в шаблон todos app/templates/todos.hbs.

app/templates/todos.hbs

{{todo-input action="createTodo"}}
{{#todo-list todos=model deleteTodo="deleteTodo"}}
    {{outlet}}
{{/todo-list}}

Последнее взаимодействие и финишная прямая. Если вы добрались до этого места, то отступать поздно.

Для создания возможности переключения между завершенными и незавершенными задачами мы начнем с HTML компонентов и затем проработаем javascript. Открываем app/templates/components/todo-list.hbs.

app/templates/components/todo-list.hbs

<section id="main">  
    {{yield}}

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

Мы добавили чекбокс {{input}} в handlebars. Он будет переключать свойство allAreDone.

Давайте используем это свойство в javascript app/components/todo-list.js, чтобы устанавливать все задачи на завершенные или незавершенные.

app/components/todo-list.js

import Ember from 'ember';

export default Ember.Component.extend({  
    /* truncated for brevity */
    didInsertElement() {
        let todos = this.get('todos');
        if (todos.get('length') > 0 && todos.isEvery('complete', true)) {
            this.set('allAreDone', true);
        } else {
            this.set('allAreDone', false);
        }
    },
    allAreDoneObserver: Ember.observer('allAreDone', function() {
        let completeValue = this.get('allAreDone');
        let todos = this.get('todos');
        todos.forEach((todo) => {
            todo.set('complete', completeValue)
            this.sendAction('updateTodo', todo);
        });
    }),
    actions: { /* truncated for brevity */ }
});

Здесь мы используем hook didInsertElement(), который вызывается каждый раз, когда компонент отображается или изменяется (при изменении данных). В этой функции мы вычисляем, есть ли задачи todos.get('length') > 0, и если они все завершены todos.isEvery('complete', true), то мы устанавливаем allAreDone на true. Во всех остальных случаях allAreDone будет false.

allAreDoneObserver — наблюдатель ember, который отслеживает значение allAreDone, и когда оно меняется, вызывает необходимую функцию. В функции мы проходим по каждой задаче и устанавливаем свойство complete, чтобы сопоставить его со свойством allAreDone.

Вот и все, мы закончили. Дай пять.

Заключение

Всему хорошему когда-нибудь приходит конец, но вы даже близко к нему не подобрались. Вы еще только в начале создания амбициозных веб-приложений с помощью ember.js. Я раскрыл здесь только некоторые основы фреймворка, но он скрывает гораздо больше возможностей.

Я оставил код на github, чтобы вы могли его загрузить.

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

Перевод статьи - Getting Started with Ember.js using Ember CLI


Комментарии (9)

  1. -_- 30 января 2016, 18:24 # 0
    Хорошая статья, спасибо
    1. Князев А 04 февраля 2016, 18:36 # 0
      Не работает вставка в шаблон todos шаблона index. Делал все по инструкции.
      1. Александр 04 февраля 2016, 18:47 # 0
        Отпишите подробнее, что делаете, какая ошибка появляется.
        1. Азат 18 марта 2016, 08:39 # 0
          там ошибка в главе «вложенные маршруты», app/routes/complete.js не правильный путь… правильный app/routes/todos/complete.js
          1. Александр 18 марта 2016, 10:18 # 0
            Спасибо, поправили.
            1. Азат 24 марта 2016, 11:38 # 0
              для app/routes/incomplete.js там такая же проблема… отсылал ошибку через кнопку «нашли ошибку?», результат зеро…
              1. Александр 24 марта 2016, 14:30 # 0
                Спасибо, исправили.
        2. Алексей 30 мая 2016, 18:53 # 0
          Я сгенерировал helloworld-приложение под ember 2.5, но в нём нет файла app/routes/application.js. Вместо него теперь app/app.js?
          1. Азат 24 августа 2016, 12:48 # 0
            сгенерируйте его командой
            ember g route application
          Выделите опечатку и нажмите Ctrl + Enter, чтобы отправить сообщение об ошибке.