КатегорииReactУроки

React урок 5 Маршрутизация

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.

repo-router

Активная ссылка

Компонент 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’)
history.pushState({showGrades: true}, ‘/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}>&#9888; </div>
                {/* &#9888; 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 мы увидим ошибку:

error_router

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 будет содержать форму, она будет выглядит как модальное окно:

conatct_form

Компонент 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);
}

add_button

А теперь кнопка редактирования:

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}/>
                {/* &#9998; is the HTML entity for the utf-8 pencil character (✎) */}
                <div className="card__edit"><Link to={'/edit/'+this.props.id}>&#9998;</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, что поможет нам лучше организовать свой проект.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *