Простое ToDo приложение на Ember
Старт проекта
Откроем терминал
npm install -g ember-cli
ember new todo-app
cd todo-app
ember serve
Первое, на что следует обратить внимание, — это структура папок и файлов в Ember по сравнению с React.
Самое простое приложение в React будет иметь
index.html
style.css
index.js
Вы можете закинуть все в index.js (функции, вызовы API и т. д.), не трогая файлы HTML и CSS, и это будет работать.
В Ember каждое новое приложение будет иметь:
App
Components
Controllers
Helpers
Models
Routes
Styles
Templates
Application.hbs
App.js
Index.html
Router.js
Чтобы напечатать «Hello World» на экране, перейдите в application.hbs, удалите
{{outlet}}
и вставьте
<h1>Hello World</h1>
теперь для нашего приложения измените его обратно на
{{outlet}}
Все, что попадет в один компонент в React, будет разбросано между Route, Component и Template в Ember.
Template — это ваш html. Hbs означает handlebar. Главное, что нужно знать, — это то, что Handlebar — это нелогика, поэтому в вашем html нет сопоставления или фильтрации.
Route… лучше всего думать об этом так: Route — это ваш компонент, который будет монтироваться (на самом деле это не так, но для практических целей выполнения чего-то думайте об этом именно так). Данные, которые вы хотите видеть на экране при загрузке страницы, извлекаются/аксиируются/запрашиваются в Route.
Component — это то место, куда вы помещаете любые функции, которые будут реагировать на любой пользовательский ввод, нажатия кнопок, по сути, любые взаимодействия с пользователем.
Helpers — это то место, куда помещаются небольшие вспомогательные функции многократного использования. Например, если вы конвертируете градусы Фаренгейта в градусы Цельсия, то вот куда идет функция, которая это делает.
Что касается контроллеров, то процитирую вопрос, заданный в Ember Guide: «Стоит ли использовать контроллеры в моем приложении? Я слышал, что они уходят!»
Шаг 1 — Создаем route
В терминале пишем
ember g route todo
Вывод в вашем терминале будет следующим:
installing route
create app/routes/todo.js
create app/templates/todo.hbs
updating router
add route todo
installing route-test
create tests/unit/routes/todo-test.js
Шаг 2 — Отображение списка дел в консоли
Давайте начнем с добавления некоторых существующих дел.
Перейдите в app/routes/todo.js, шаблон должен выглядеть примерно так:
import Route from '@ember/routing/route';
export default class TodoRoute extends Route {
}
Чтобы добавить набор данных todo, добавьте модель к маршруту:
import Route from "@ember/routing/route";
export default class TodoRoute extends Route {
model() {
return [
{
id: 1,
todo: "todo 1"
},
{
id: 2,
todo: "todo 2"
},
{
id: 3,
todo: "todo 3"
}
];
}
}
Теперь перейдите в app/templates/todo.hbs, удалите все, что там есть, и добавьте:
<h1>TODO app</h1>
{{log this.model}}
В терминале запустите ember serve
Откройте веб-браузер, перейдите по адресу http://localhost:4200/todo на странице должно появиться приложение TODO. Откройте Inspector -> Console. В консоли вы должны увидеть массив вашей модели.
Шаг 3 — Отображение списка дел на экране
Итак, я принимаю решение, что построю все приложение в одном компоненте. Можете свободно реорганизовать его, чтобы он был в отдельных компонентах. Я бы сказал, что «список дел» и «кнопка добавления нового дела» должны быть двумя отдельными компонентами, но я предоставлю вам возможность разобраться, как это реорганизовать.
Часть 3.1
В Терминале выполните:
ember g component todo-app
В вашем терминале вы увидите следующее:
installing component
create app/components/todo-app.hbs
skip app/components/todo-app.js
tip to add a class, run `ember generate component-class todo-app`
installing component-test
Продолжайте следовать «совету» и выполните команду ember generate component-class todo-app .
Теперь, если вы перейдете в app/components, вы найдете todo-app.hbs и todo-app.js .
Todo-app.hbs — это ваш HTML, а todo-app.js — это ваша логика и часть обработки действий.
Давайте перейдем к todo-app.hbs, удалим все, что там есть, и добавим
<p>sanity check</p>
Если вы сейчас перейдете по адресу http://localhost:4200/todo или http://localhost:4200 вы не увидите проверку работоспособности на экране.
Чтобы отобразить todo-app.hbs на экране, перейдите в todo.hbs и добавьтев файл, чтобы у вас было
<h1>TODO app</h1>
<TodoApp />
{{log this.model}}
Теперь перейдите по адресу http://localhost:4200/todo — вуаля! Проверка работоспособности отображается.
Часть 3.2
Итак, если вы зайдете на todo.hbs и возьмете
{{log this.model}}
и зайдите в todo-app.hbs и добавьте его туда
<p>sanity check</p>
{{log this.model}}
Теперь вы получите undefined в консоли, а не в модели.
Давайте передадим модель из todo в компонент todo-app, изменив
<TodoApp />
к
<TodoApp @model={{this.model}}/>
Несмотря на это изменение, вы все равно получите undefined , поскольку this.model был передан компоненту как @model .
Так что давайте изменим
{{log this.model}}
к
{{log @model}}
Ура! Вы вернулись к исходной точке 0 с моделью, отображаемой в консоли. Теперь давайте ее отобразим.
Часть 3.3
В React самым простым решением для отображения 3 задач будет следующее:
<ul>
<li>{this.state.model[0].todo}</li>
<li>{this.state.model[1].todo}</li>
<li>{this.state.model[2].todo}</li>
</ul>
Вы можете попробовать написать что-то в этом роде в todo-app.hbs, но это не сработает. Поэтому другой вариант отображения этого в React — использовать .map.
Итак, что-то вроде этого:
<ul>
{this.state.model.map(todo => {
return <li key={todo.id}>{todo.todo}</li>;
})}
</ul>
Шаблоны лишены логики, и это означает, что javascript .map не будет работать. Однако у шаблонов есть помощники, которые могут привнести некоторую логику в шаблон.
Мы сделаем что-то похожее на .map, используя вспомогательный метод «each» .
Итак, давайте перейдем в todo-app.hbs и добавим:
<ul>
{{#each @model as |item|}}
<li>{{item.todo}}</li>
{{/each}}
</ul>
Отлично! Список дел отображается.
Шаг 4 — Добавление действий
Теперь давайте добавим текстовое поле и кнопку, чтобы вы могли добавлять новые задачи в список.
На стороне html теги будут идентичны тем, которые вы бы использовали в компоненте React. Итак, давайте добавим это в todo-app.hbs :
<ul>
{{#each @model as |item|}}
<li>{{item.todo}}</li>
{{/each}}
</ul>
<form>
<input placeholder='Add todo' type='text' />
<button type='submit'>Add</button>
</form>
Это отобразит поле ввода и кнопку, но, конечно, это ничего не сделает, так что пришло время наконец взглянуть на todo-app.js . Но прежде чем мы это сделаем, давайте посмотрим, как это выглядело бы в React.
Реагировать вид
import ReactDOM from "react-dom";
import React, { Component } from "react";
class Todo extends Component {
constructor(props) {
super(props);
this.state = {
model: [
{
id: 1,
todo: "todo 1"
},
{
id: 2,
todo: "todo 2"
},
{
id: 3,
todo: "todo 3"
}
],
text: ""
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
handleSubmit(e) {
e.preventDefault();
const i = this.state.model[this.state.model.length - 1].id + 1;
let newTodo = {
todo: this.state.text,
id: i
};
this.setState(prevState => ({
model: prevState.model.concat(newTodo),
text: ""
}));
}
handleChange(e) {
this.setState({
text: e.target.value
});
}
render() {
return (
<div>
<h1>TODO LIST</h1>
<ul>
{this.state.model.map(todo => {
return <li key={todo.id}>{todo.todo}</li>;
})}
</ul>
<form onSubmit={this.handleSubmit}>
<input value={this.state.text} onChange={e => this.handleChange(e)} />
<button>Add</button>
</form>
</div>
);
}
}
ReactDOM.render(<Todo />, document.getElementById("root"));
Вернуться к Эмберу
Теперь давайте напишем handleChange и handleSubmit в Ember.
Реакция
handleChange(e) {
this.setState({
text: e.target.value
});
}
почти не меняется, поскольку становится:
@tracked
text = "";
@action
onChange(e) {
this.text = e.target.value;
}
@tracked — это ваше состояние, но лучше всего прочитать о @tracker и @action в руководстве Ember .
А handleSubmit идет от:
handleSubmit(e) {
e.preventDefault();
const i = this.state.model[this.state.model.length - 1].id + 1;
let newTodo = {
todo: this.state.text,
id: i
};
this.setState(prevState => ({
model: prevState.model.concat(newTodo),
text: ""
}));
}
к:
@action
submit(model, e) {
e.preventDefault();
const i = model[model.length - 1].id + 1;
const newTodo = {
id: i,
todo: this.text
};
model.pushObject(newTodo);
}
Итак, todo-app.js в конечном итоге выглядит так:
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
export default class TodoAppComponent extends Component {
@tracked
text = "";
@action
submit(model, event) {
event.preventDefault();
const i = model[model.length - 1].id + 1;
const newTodo = {
id: i,
todo: this.text
};
model.pushObject(newTodo);
console.log("add", model);
}
@action
onChange(e) {
this.text = e.target.value;
}
}
Шаг 5. Соединение .js с .hbs
Рендеринг React выглядит так:
<form onSubmit={this.handleSubmit}>
<input value={this.state.text} onChange={e =>
this.handleChange(e)} />
<button>Add</button>
</form>
в Ember ваш todo-app.hbs должен выглядеть так:
<form onsubmit={{fn this.submit @model}}>
<input placeholder='Add todo' type='text' value={{this.text.todo}}
onchange={{fn this.onChange}} />
<button type='submit'>Add</button>
</form>
Итак, полный todo-app.hbs выглядит так:
<ul>
{{#each @model as |item|}}
<li>{{item.todo}}</li>
{{/each}}
</ul>
<form onsubmit={{fn this.submit @model}}>
<input placeholder='Add todo' type='text' value={{this.text.todo}} onchange={{fn this.onChange}} />
<button type='submit'>Add</button>
</form>
Шаг 6. Добавление функции «Удалить»
Я должен признать, что удаление не будет выглядеть красиво в React или Ember, потому что мы имеем дело с массивом объектов. Если бы это был массив строк, жизнь была бы проще.
В React это выглядит так:
removeItem(id) {
let model = this.state.model;
const index = model
.map((file, index) => {
if (file.id === id) {
return index;
} else return undefined
})
.filter(id => id !== undefined);
model.splice(index, 1);
this.setState([...model]);
}
В какой-то степени Ember следует той же общей идее. Вы вычисляете индекс, затем сращиваете массив.
Есть одно но. HTML-сторона не будет перерисовываться, если вы удалите «todo» из массива. Поэтому мой обходной путь: заменить элемент, который должен быть удален, пустым объектом и добавить в шаблон «if empty don't show».
todo-app.js
@action
delete(model, item) {
const index = model
.map((file, index) => {
if (file.id === item.id) {
set(item, "id", null);
set(item, "todo", null);
return index;
}
})
.filter(id => id !== undefined);
model.splice(index[0], 1);
}
и todo-hbs.js
<ul>
{{#each @model as |item|}}
{{#if item.id}}
<li>{{item.todo}}</li>
<button onclick={{fn this.delete @model item}} type='button'>delete</button>
{{/if}}
{{/each}}
</ul>
Приложение Todo готово.