URL является важной частью веб-приложений. Изначально задумывался как простой указатель на файл, который лежит на сервере, но с появлением веб-приложений лучше думать о нем, как о текущем состоянии приложения. Глядя на него пользователь может понять где он в настоящее время находиться, а также может скопировать его для последующего использования или кому-то передать.
Реализация маршрутизации «наивный» путь
Чтобы понять, как работает маршрутизация, давайте рассмотрим пример. Мы создадим приложение, которое будет использовать GitHub API, получения списка репозиториев. Помимо этого добавим домашнюю страницу и «О проекте». Давайте начнем с главного компонента App:
import React, {Component} from 'react'; import {render} from 'react-dom'; import About from './About'; import Home from './Home'; import Repos from './Repos'; class App extends Component { constructor(){ super(...arguments); this.state= { route: window.location.hash.substr(1) }; } componentDidMount() { window.addEventListener('hashchange', () => { this.setState({ route: window.location.hash.substr(1) }); }); } render() { return ( <h1>Hello World</h1> ); } } render(<App />, document.getElementById('root'));
Код довольно прост. В конструкторе компонента, мы получаем текущее значение хеш URL-адрес и присваиваем состояние маршрута. Затем, при создании компонента, добавляем обработчик события на изменение URL, в нем обновляется текущее состояние нашего компонента. Теперь нам надо обновить метод render:
... render() { var Child; switch (this.state.route) { case '/about': Child = About; break; case '/repos': Child = Repos; break; default: Child = Home; } return ( <div> <header>App</header> <menu> <ul> <li><a href="#/about">About</a></li> <li><a href="#/repos">Repos</a></li> </ul> </menu> <Child/> </div> ) } ...
Тут все тоже достаточно просто, проверяется текущее состояние route и в зависимости от него Child присваивается определенный компонент. Все дочерние компоненты которые представляют собой внутренние страницы приложения просты и имеют одинаковую структуру:
import React, { Component } from 'react'; class Home extends Component { render() { return ( <h1>HOME</h1> ); } } export default Home;
import React, { Component } from 'react'; class About extends Component { render() { return ( <h1>ABOUT</h1> ); } } export default About;
import React, { Component } from 'react'; class Repos extends Component { render() { return ( <h1>Github Repos</h1> ); } } export default Repos;
Добавим немного красоты:
body { margin: 0; font: 16px/1 sans-serif; } menu ul{ margin: 0; padding: 0; } menu li { display: inline-block; padding: 5px; } a.active { color: #444; font-weight: bold; text-decoration: none; } header { padding: 10px; background-color: #333; color: #ccc; font-size: 20px; font-weight: bold; } menu { background-color: #ccc; padding: 5px; margin-top: 0; margin-bottom: 10px; }
Несмотря на то, что это работает, в данном подходе есть по крайней мере две проблемы:
- В данном примере, содержимое URL заняло центральное место: вместо того чтобы автоматически обновление URL и состояние приложения;
- Код маршрутизации может расти в геометрической прогрессии при сложных нетривиальных сценариях. Представим себе, например, что внутри страницы Repos может быть список репозиториев со ссылкой внутрь, что-то вроде /repos/repo_id.
Для сценариев более сложных, чем одноуровневая маршрутизация, рекомендуемый подход заключается в использовании библиотеки React Router.
React Router
React Router является наиболее популярным решением для добавления маршрутизации React приложению. Он помогает держать в синхронизации UI c URL с помощью компонентов, связанных с маршрутами ( на любом уровне вложенности). При изменении URL адреса React Router автоматически монтирует и демонтирует нужные компоненты. Поскольку React Router является внешней библиотекой, то она должна быть установлена с npm:
npm install —save react-router
React Router предоставляет три компонента для начала работы:
- Router и Route: Используются для декларативного описания карты маршрутов приложения;
- Link: Используется для создания ссылок с заданным href. Это не единственный способ навигации проекта, но все же основной.
Давайте изменим наш пример, вооружившись React Router:
import React, {Component} from 'react'; import {render} from 'react-dom'; import { Router, Route, Link } from 'react-router'; import About from './About'; import Home from './Home'; import Repos from './Repos'; class App extends Component { render() { return ( <div> <header>App</header> <menu> <ul> <li><Link to="/about">About</Link></li> <li><Link to="/repos">Repos</Link></li> </ul> </menu> {this.props.children} </div> ); } } render(<App />, document.getElementById('root'));
Здесь мы импортировали React Router и обновили метод render добавив туда Link, а также избавились от конструктора и componentDidMount. Теперь давайте обновим нижний метод render:
import React, {Component} from 'react'; import {render} from 'react-dom'; import { Router, Route, Link } from 'react-router'; import About from './About'; import Home from './Home'; import Repos from './Repos'; class App extends Component { render() { return ( <div> <header>App</header> <menu> <ul> <li><Link to="/about">About</Link></li> <li><Link to="/repos">Repos</Link></li> </ul> </menu> {this.props.children} </div> ); } } render(( <Router> <Route path="/" component={App}> <Route path="about" component={About}/> <Route path="repos" component={Repos}/> </Route> </Router> ), document.getElementById('root'));
Index Route
Если попробовать запустить, то мы увидим, что все работает как раньше, но если зайти «/» то не увидим результат компонента «Home». Первый что приходит на ум это использовать Route, но какой путь указать?
... <Router> <Route path="/" component={App}> <Route path="???" component={Home}/> <Route path="about" component={About}/> <Route path="repos" component={Repos}/> </Route> </Router> ...
Вместо этого, можно использовать IndexRoute. Надо просто импортировать его и использовать его для настройки маршрут индекса:
Маршруты с параметрами
Теперь у нас есть реализация на одном уровне нашим первым «наивным» примером маршрутизации, давайте расширим его извлекая данные из GitHub.
import React, { Component } from 'react'; import 'whatwg-fetch'; class Repos extends Component { constructor(){ super(...arguments); this.state = { repositories: [] }; } componentDidMount(){ fetch('https://api.github.com/users/pro-react/repos') .then((response) => response.json()) .then((responseData) => { this.setState({repositories:responseData}); }); } render() { let repos = this.state.repositories.map((repo) => ( <li key={repo.id}>{repo.name}</li> )); return ( <div> <h1>Github Repos</h1> <ul> {repos} </ul> </div> ); } } export default Repos;
Теперь есть попробовать зайти на страницу Repos мы увидим список репозиториев. Далее, нам неплохо бы добавить для каждого элемента списка репозиториев ссылку на подробности имеющую примерно такой вид /repos/details/repo_name. Создадим для этих целей новый компонент RepoDetails, но перед этим давайте обновим список добавим Link:
import React, {Component} from 'react'; import 'whatwg-fetch'; import {Link} from 'react-router'; class Repos extends Component { constructor() { super(...arguments); this.state = { repositories: [] }; } componentDidMount() { fetch('https://api.github.com/users/pro-react/repos') .then((response) => response.json()) .then((responseData) => { this.setState({repositories: responseData}); }); } render() { let repos = this.state.repositories.map((repo) => ( <li key={repo.id}> <Link to={'/repos/details/' + repo.name}>{repo.name}</Link> </li> )); return ( <div> <h1>Github Repos</h1> <ul> {repos} {this.props.children} </ul> </div> ); } } export default Repos;
При создании RepoDetails, есть две особенности:
- React Router будет передавать параметр URL через реквизит params значения параметров URL в нашем случае repo_name. Мы может использовать его для получения деталей о репозитории.
- Во вторых мы будем обновлять данные о репозитории не только при создании компонента (componentDidMount), но и при изменении реквизитов (componentWillReceiveProps).
Давай приступим:
import React, {Component} from 'react'; import 'whatwg-fetch'; class RepoDetails extends Component { constructor() { super(...arguments); this.state = { repository: {} }; } fetchData(repo_name) { fetch('https://api.github.com/repos/pro-react/' + repo_name) .then((response) => response.json()) .then((responseData) => { this.setState({repository: responseData}); }); } componentDidMount() { // The Router injects the key "repo_name" inside the params prop let repo_name = this.props.params.repo_name; this.fetchData(repo_name) } componentWillReceiveProps(nextProps) { // The Router injects the key "repo_name" inside the params prop let repo_name = nextProps.params.repo_name; this.fetchData(repo_name) } render() { let stars = []; for (var i = 0; i < this.state.repository.stargazers_count; i++) { stars.push('★'); } return ( <div> <h2>{this.state.repository.name}</h2> <p>{this.state.repository.description}</p> <span>{stars}</span> </div> ); } } export default RepoDetails;
Для завершения нам надо RepoDerails добавить в App.js:
import React, {Component} from 'react'; import {render} from 'react-dom'; import {Router, Route, Link, IndexRoute} from 'react-router'; import About from './About'; import Home from './Home'; import Repos from './Repos'; import RepoDetails from './RepoDetails'; class App extends Component {...} render(( <Router> <Route path="/" component={App}> <IndexRoute component={Home}/> <Route path="about" component={About}/> <Route path="repos" component={Repos}> {/* Add the route, nested where we want the UI to nest */} <Route path="details/:repo_name" component={RepoDetails}/> </Route> </Route> </Router> ), document.getElementById('root'));
Здесь мы импортировали RepoDetails и добавили в Route Repos новый вложенный Route c шаблоном url details/:repo_name и компонентом RepoDetails.
Активная ссылка
Компонент Link имеет дополнительный атрибут activeClassName. Когда он установлен и текущий адрес URL совпадает с ссылкой, ссылке присваивается указанный CSS класс. Давайте добавим этот атрибут к нашим ссылкам:
... class App extends Component { render() { return ( <div> <header>App</header> <menu> <ul> <li><Link to="/about" activeClassName="active">About</Link></li> <li><Link to="/repos" activeClassName="active">Repos</Link></li> </ul> </menu> {this.props.children} </div> ); } } ...
Передача реквизитов
Есть небольшая проблема: мы делаем не нужный запрос GitHub API при переходе по ссылке к деталям репозиториев, потому как при https://api.github.com/users/pro-react/repos уже передаются нужные нам данные. Но это поправимо мы можем передать все данные о репозиториях вниз в качестве реквизита для отображения компонента repoDetails.
Есть два способа передачи реквизитов в React Router: указав реквизит при определении Route или путем инъекции реквизита.
Реквизиты Route
Route компонент предоставляет простой способ настроить маршрут, он не отображается как обычный компонент. Когда компонент активен, он отображает взамен себя указанный компонент и передает в него все свои реквизиты, а это значит, что любые дополнительные реквизиты которые указаны в Route доступны дочернему компоненту.
Давайте попробуем попробуем, добавим title к About:
... render(( <Router> <Route path="/" component={App}> <IndexRoute component={Home}/> <Route path="about" component={About} title="About Us"/> <Route path="repos" component={Repos}> {/* Add the route, nested where we want the UI to nest */} <Route path="details/:repo_name" component={RepoDetails}/> </Route> </Route> </Router> ), document.getElementById('root')); ...
А теперь давайте используем внутри About:
import React, { Component } from 'react'; class About extends Component { render() { return ( <h1>{this.props.route.title}</h1> ); } } export default About;
Клонирование и Инъекция реквизитов
Этот подход хорош для динамических реквизитов. Внутри Repo, вместо того, чтобы просто рендерить this.props.childre, можно клонировать его с передачей дополнительных реквизитов, в нашем случае это список репозиториев:
... render() { let repos = this.state.repositories.map((repo) => ( <li key={repo.id}> <Link to={'/repos/details/' + repo.name}>{repo.name}</Link> </li> )); let child = this.props.children && React.cloneElement(this.props.children, {repositories: this.state.repositories} ); return ( <div> <h1>Github Repos</h1> <ul> {repos} {child} </ul> </div> ); } ...
Теперь давайте поправим RepoDetails:
import React, {Component} from 'react'; import 'babel-polyfill'; class RepoDetails extends Component { render() { let repository = this.props.repositories.find( (repo) => repo.name === this.props.params.repo_name ); let stars = []; for (var i = 0; i < repository.stargazers_count; i++) { stars.push('★'); } return ( <div> <h2>{repository.name}</h2> <p>{repository.description}</p> <span>{stars}</span> </div> ); } } export default RepoDetails;
Изменение вложенного URL
/repos/details/:repo_name кажется немного длинным, не плохо было бы иметь что-то типа /repos/:repo_name. Чтобы так сделать надо указать абсолютный путь в path компонента Route:
... render(( <Router> <Route path="/" component={App}> <IndexRoute component={Home}/> <Route path="about" component={About} title="About Us"/> <Route path="repos" component={Repos}> {/* Add the route, nested where we want the UI to nest */} <Route path="/repo/:repo_name" component={RepoDetails} /> </Route> </Route> </Router> ), document.getElementById('root')); ...
Еще надо поправить ссылку в Repos:
... render() { let repos = this.state.repositories.map((repo) => ( <li key={repo.id}> <Link to={'/repos/' + repo.name}>{repo.name}</Link> </li> )); ...
Изменение маршрутов программным способом
Компонент Link клевая штука, но иногда может потребоваться управление текущим положением программно. Например вернуться по какому-то событию назад или перенаправить пользователя на другой маршрут. Для этого React Router внедряет объект history во все компоненты указанные в Router. Объект history отвечает за управление историей браузера и обеспечивает методы навигации:
pushState | Основной метод добавления в историю перехода но новый URL. При желании можно пропустить первый аргумент:
history.pushState(null, ‘/users/123’) |
replaceState | Имеет тот же синтаксис что и pushState, он заменяем текущий URL на новый. Это аналогично переадресации, так как он заменяет URL, не влияя на длину истории |
goBack | Возвращаемся назад по истории браузера |
goForward | Переходим вперед по истории браузера |
Go | Переход на n шагов в истории, если n положительно то вперед, если нет то назад |
createHref | Создает ссылку, используя настройки маршрутов |
Чтобы разобраться, как это работает, давайте создадим новый маршрут Server Error. И если в компоненте Repo при загрузке списка репозиториев произойдет ошибка, то перенаправим на наш новый маршрут. Начнем с создания нового компонента ServerError:
import React, {Component} from 'react'; const styles = { root: { textAlign: 'center' }, alert: { fontSize: 80, fontWeight: 'bold', color: '#e9ab2d' } }; class ServerError extends Component { render() { return ( <div style={styles.root}> <div style={styles.alert}>⚠ </div> {/* ⚠ is the html entity code for the warning character: ⚠ */} <h1>Ops, we have a problem</h1> <p>Sorry, we could't access the repositories. Please try again in a few moments.</p> </div> ); } } export default ServerError;
А теперь давайте объявим его в App.js:
import React, {Component} from 'react'; import {render} from 'react-dom'; import {Router, Route, Link, IndexRoute} from 'react-router'; import About from './About'; import Home from './Home'; import Repos from './Repos'; import ServerError from './ServerError'; import RepoDetails from './RepoDetails'; class App extends Component {...} render(( <Router> <Route path="/" component={App}> <IndexRoute component={Home}/> <Route path="about" component={About} title="About Us"/> <Route path="repos" component={Repos}> {/* Add the route, nested where we want the UI to nest */} <Route path="/repo/:repo_name" component={RepoDetails} /> </Route> <Route path="error" component={ServerError} /> </Route> </Router> ), document.getElementById('root'));
Теперь давайте перейдем к Repos:
import React, {Component} from 'react'; import 'whatwg-fetch'; import {Link} from 'react-router'; class Repos extends Component { constructor() {} componentDidMount() { fetch('https://api.github.com/users/pro-react/repos') .then((response) => { if (response.ok) { return response.json(); } else { throw new Error("Server response wasn't OK"); } }) .then((responseData) => { this.setState({repositories: responseData}); }) .catch((error) => { this.props.history.pushState(null, '/error'); }); } render() {...} } export default Repos;
Если отключить интернет при переходе Repo мы увидим ошибку:
Histories
React Router построен на основе библиотеки History. Ее целью является абстракция над управление URL и историей в различных браузерах и других платформ. History по умолчанию реагирует на маршруты использующие хеш (#) часть URL, например /#/path. И это хорошо работает для старых браузеров IE 8 9 и не требует ни какой конфигурации сервера. Если вашему приложению не нужно работать в старом браузере и вы можете настроить свой сервер, идеальный подход в таком случае будет использование browserHistory, которая создает реальные URL — адреса, например example.com/path.
import React, {Component} from 'react'; import {render} from 'react-dom'; import {browserHistory, Router, Route, Link, IndexRoute} from 'react-router'; import About from './About'; import Home from './Home'; import Repos from './Repos'; import ServerError from './ServerError'; import RepoDetails from './RepoDetails'; class App extends Component {} render(( <Router history={browserHistory}> <Route path="/" component={App}> <IndexRoute component={Home}/> <Route path="about" component={About} title="About Us"/> <Route path="repos" component={Repos}> {/* Add the route, nested where we want the UI to nest */} <Route path="/repo/:repo_name" component={RepoDetails} /> </Route> <Route path="error" component={ServerError} /> </Route> </Router> ), document.getElementById('root'));
Тут мы импортировали и указали что хотим использовать browserHistory.
Канбан доска: Маршрутизация
До сих пор наше приложение было очень эфективным в качестве упражнения, но без полезно само по себе, потому что мы не можем ни редактировать карточки не создавать их. Давайте реализуем эти две функции с помощью маршрутов. URL /new будет показывать окно создания новой карточки, а /edit/:card_id будет открывать окно редактирования. Для этого мы создадим компоненты NewCard и EditCard, а так как оба компонента разделяют множество общих характеристик (набор полей), мы создадим так же СardForm. Давайте разберем план действий:
- Начинаем снизу в верх, с создания CardForm
- Затем создадим Newcard и EditCard
- Добавляем новые маршруты в App.js
- В KanbanBoardContainer добавим методы редактирования и создания карточек. И передадим эти методы в Newcard и EditCard.
Но сначала нам надо установить React Router:
npm install —save react-router
Компонент CardForm
CardForm будет содержать форму, она будет выглядит как модальное окно:
Компонент CardForm будет чистым, без состояния. Оба компонента Newcard и EditCard должны будут предоставить следующие реквизиты:
- Объект, содержащий значение для отображения на карточке.
- Заголовок кнопки подтверждения.
- Функция обработки формы.
- Функция обработки закрытия окна.
Код:
import React, {Component, PropTypes} from 'react'; class CardForm extends Component { handleChange(field, e) { this.props.handleChange(field, e.target.value); } handleClose(e) { e.preventDefault(); this.props.handleClose(); } render() { return ( <div> <div className="card big"> <form onSubmit={this.props.handleSubmit.bind(this)}> <input type='text' value={this.props.draftCard.title} onChange={this.handleChange.bind(this,'title')} placeholder="Title" required={true} autoFocus={true}/> <textarea value={this.props.draftCard.description} onChange={this.handleChange.bind(this,'description')} placeholder="Description" required={true}/> <label htmlFor="status">Status</label> <select id="status" value={this.props.draftCard.status} onChange={this.handleChange.bind(this,'status')}> <option value="todo">To Do</option> <option value="in-progress">In Progress</option> <option value="done">Done</option> </select> <br /> <label htmlFor="color">Color</label> <input id="color" value={this.props.draftCard.color} onChange={this.handleChange.bind(this,'color')} type="color" defaultValue="#ff0000"/> <div className='actions'> <button type="submit">{this.props.buttonLabel}</button> </div> </form> </div> <div className="overlay" onClick={this.handleClose.bind(this)}> </div> </div> ); } } CardForm.propTypes = { buttonLabel: PropTypes.string.isRequired, draftCard: PropTypes.shape({ title: PropTypes.string, description: PropTypes.string, status: PropTypes.string, color: PropTypes.string }).isRequired, handleChange: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired, handleClose: PropTypes.func.isRequired }; export default CardForm;
Теперь давайте добавим немного красоты CSS:
... .overlay { position: absolute; width: 100%; height: 100%; top: 0; left: 0; bottom: 0; right: 0; z-index: 2; background-color: rgba(0, 0, 0, 0.6); } .card.big { position: absolute; width: 450px; height: 200px; margin: auto; padding: 15px; top: 0; left: 0; bottom: 100px; right: 0; z-index: 3; } .card.big input[type=text], textarea { width : 100%; margin: 3px 0; font-size: 13px; border: none; } .card.big input[type=text] { font-size: 20px; font-weight: bold;} .card.big input[type=text]:focus, .card.big textarea:focus { outline: dashed thin #999; outline-offset: 2px; } .card.big label { margin: 3px 0 7px 3px; color: #a7a7a7; display: inline-block; width: 60px; } .actions { margin-top: 10px; text-align: right; } .card.big button { font-size:14px; padding: 8px; }
Компоненты NewCard и EditCard
Давайте перейдем к компонентам NewCard и EditCard.
import React, {Component, PropTypes} from 'react'; import CardForm from './CardForm' class NewCard extends Component { componentWillMount() { this.setState({ id: Date.now(), title: '', description: '', status: 'todo', color: '#c9c9c9', tasks: [] }); } handleChange(field, value) { this.setState({[field]: value}); } handleSubmit(e) { e.preventDefault(); this.props.cardCallbacks.addCard(this.state); this.props.history.pushState(null, '/'); } handleClose(e) { this.props.history.pushState(null, '/'); } render() { return ( <CardForm draftCard={this.state} buttonLabel="Create Card" handleChange={this.handleChange.bind(this)} handleSubmit={this.handleSubmit.bind(this)} handleClose={this.handleClose.bind(this)}/> ); } } NewCard.propTypes = { cardCallbacks: PropTypes.object }; export default NewCard;
import React, {Component, PropTypes} from 'react'; import CardForm from './CardForm'; class EditCard extends Component { componentWillMount() { let card = this.props.cards.find((card)=>card.id == this.props.params.card_id); this.setState({...card}); } handleChange(field, value) { this.setState({[field]: value}); } handleSubmit(e) { e.preventDefault(); this.props.cardCallbacks.updateCard(this.state); this.props.history.pushState(null, '/'); } handleClose(e) { this.props.history.pushState(null, '/'); } render() { return ( <CardForm draftCard={this.state} buttonLabel="Edit Card" handleChange={this.handleChange.bind(this)} handleSubmit={this.handleSubmit.bind(this)} handleClose={this.handleClose.bind(this)}/> ) } } EditCard.propTypes = { cardCallbacks: PropTypes.object }; export default EditCard;
Настройка маршрутов
Давайте пока пропустим KanbanBoardContainer и перейдем к App.js. Это поможет лучше понять как модифицировать KanbanBoardContainer. Мы создадим три маршрута: ведущая к доске, созданию новой карточки и редактированию карточки.
import React, {Component} from 'react'; import {render} from 'react-dom'; import {browserHistory, Router, Route } from 'react-router'; import KanbanBoardContainer from './KanbanBoardContainer'; import KanbanBoard from './KanbanBoard'; import EditCard from './EditCard'; import NewCard from './NewCard'; render(( <Router history={browserHistory}> <Route component={KanbanBoardContainer}> <Route path="/" component={KanbanBoard}> <Route path="new" component={NewCard} /> <Route path="edit/:card_id" component={EditCard} /> </Route> </Route> </Router> ), document.getElementById('root'));
KanbanBoardContainer обратные вызовы
Давайте добавим два новых метода создания и удаления карточек.
... addCard(card) { // Keep a reference to the original state prior to the mutations // in case we need to revert the optimistic changes in the UI let prevState = this.state; // Add a temporary ID to the card if (card.id === null) { let card = Object.assign({}, card, {id: Date.now()}); } // Create a new object and push the new card to the array of cards let nextState = update(this.state.cards, {$push: [card]}); // set the component state to the mutated object this.setState({cards: nextState}); // Call the API to add the card on the server fetch(`${API_URL}/cards`, { method: 'post', headers: API_HEADERS, body: JSON.stringify(card) }) .then((response) => { if (response.ok) { return response.json() } else { // Throw an error if server response wasn't 'ok' // so we can revert back the optimistic changes // made to the UI. throw new Error("Server response wasn't OK") } }) .then((responseData) => { // When the server returns the definitive ID // used for the new Card on the server, update it on React card.id = responseData.id; this.setState({cards: nextState}); }) .catch((error) => { this.setState(prevState); }); } updateCard(card) { // Keep a reference to the original state prior to the mutations // in case we need to revert the optimistic changes in the UI let prevState = this.state; // Find the index of the card let cardIndex = this.state.cards.findIndex((c)=>c.id == card.id); // Using the $set command, we will change the whole card let nextState = update( this.state.cards, { [cardIndex]: {$set: card} }); // set the component state to the mutated object this.setState({cards: nextState}); // Call the API to update the card on the server fetch(`${API_URL}/cards/${card.id}`, { method: 'put', headers: API_HEADERS, body: JSON.stringify(card) }) .then((response) => { if (!response.ok) { // Throw an error if server response wasn't 'ok' // so we can revert back the optimistic changes // made to the UI. throw new Error("Server response wasn't OK") } }) .catch((error) => { console.error("Fetch error:", error); this.setState(prevState); }); } ...
И, наконец, нам надо обновить метод render.
render() { let kanbanBoard = this.props.children && React.cloneElement(this.props.children, { cards: this.state.cards, taskCallbacks:{ toggle: this.toggleTask.bind(this), delete: this.deleteTask.bind(this), add: this.addTask.bind(this) }, cardCallbacks:{ addCard: this.addCard.bind(this), updateCard: this.updateCard.bind(this), updateStatus: this.updateCardStatus.bind(this), updatePosition: throttle(this.updateCardPosition.bind(this),500), persistMove: this.persistCardDrag.bind(this) } }); return kanbanBoard; }
Рендеринг Card Forms
Чтобы появилось окно, осталось отредактировать рендеринг KanbanBoard:
... class KanbanBoard extends Component { render() { let cardModal=this.props.children && React.cloneElement(this.props.children, { cards: this.props.cards, cardCallbacks: this.props.cardCallbacks }); return ( <div className="app"> <List ... /> <List ... /> <List ... /> {cardModal} </div> ); } } ...
Последние штрихи: переход
Если в ручную ввести /new, то мы увидим нашу форму, но это не дело что бы так вызывать ее, давайте добавим кнопку:
import React, {Component, PropTypes} from 'react'; import { DragDropContext } from 'react-dnd'; import HTML5Backend from 'react-dnd-html5-backend'; import { Link } from 'react-router'; import List from './List'; class KanbanBoard extends Component { render() { let cardModal=this.props.children && React.cloneElement(this.props.children, { cards: this.props.cards, cardCallbacks: this.props.cardCallbacks }); return ( <div className="app"> <Link to='/new' className="float-button">+</Link> <List taskCallbacks={this.props.taskCallbacks} cardCallbacks={this.props.cardCallbacks} id='todo' title="To Do" cards={this.props.cards.filter((card) => card.status === "todo")} /> <List taskCallbacks={this.props.taskCallbacks} cardCallbacks={this.props.cardCallbacks} id='in-progress' title="In Progress" cards={this.props.cards.filter((card) => card.status === "in-progress")} /> <List taskCallbacks={this.props.taskCallbacks} cardCallbacks={this.props.cardCallbacks} id='done' title='Done' cards={this.props.cards.filter((card) => card.status === "done")} /> {cardModal} </div> ); } } KanbanBoard.propTypes = {...}; export default DragDropContext(HTML5Backend)(KanbanBoard);
.float-button { position: absolute; height: 56px; width: 56px; z-index: 2; right: 20px; bottom: 20px; background: #D43A2F; color: white; border-radius: 100%; font-size: 34px; text-align: center; text-decoration: none; line-height: 50px; box-shadow: 0 5px 10px rgba(0, 0, 0, 0.5); }
А теперь кнопка редактирования:
import React, {Component, PropTypes} from 'react'; import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; import CheckList from './CheckList'; import marked from 'marked'; import { DragSource, DropTarget } from 'react-dnd'; import constants from './constants'; import {Link} from 'react-router'; let titlePropType = (props, propName, componentName) => {...}; const cardDropSpec = {...}; let collectDrop = (connect, monitor) => {...}; const cardDragSpec = {...}; let collectDrag = (connect, monitor) => {...}; class Card extends Component { constructor() {...} toggleDetails() {...} render() { const { connectDragSource, connectDropTarget } = this.props; let cardDetails; if (this.state.showDetails) {...} let sideColor = {...}; return connectDropTarget(connectDragSource( <div className="card"> <div style={sideColor}/> {/* ✎ is the HTML entity for the utf-8 pencil character (✎) */} <div className="card__edit"><Link to={'/edit/'+this.props.id}>✎</Link></div> <div className={ this.state.showDetails ? "card__title card__title--is-open" : "card__title" } onClick={this.toggleDetails.bind(this)} > {this.props.title} </div> <ReactCSSTransitionGroup transitionName="toggle" transitionEnterTimeout={250} transitionLeaveTimeout={250} > {cardDetails} </ ReactCSSTransitionGroup> </div> )) } } Card.propTypes = {...}; const dragHighOrderCard = DragSource(constants.CARD, cardDragSpec, collectDrag)(Card); const dragDropHighOrderCard = DropTarget(constants.CARD, cardDropSpec, collectDrop)(dragHighOrderCard); export default dragDropHighOrderCard
... .card__edit{ position: absolute; top:10px; right: 10px; opacity: 0; transition: opacity .25s ease-in; } .card:hover .card__edit{ opacity: 1; } .card__edit a{ text-decoration: none; color: #999; font-size: 17px; }
Итоги
Сегодня мы разобрали с вами маршрутизацию в React приложениях. Видели какие могу быть сложности с вложенными путями. Рассмотрели замечательную библиотеку React Route для построение маршрутизации в наших приложения. Узнали как можно управлять программно перемещением по маршрутам. В итоге наше приложение Канбан доска становиться все более крупным и начинают чувствоваться «боли» роста. В следующий раз мы разберем, как строить приложения с помощью Flux, что поможет нам лучше организовать свой проект.