Книга: React.js. Быстрый старт
Назад: 5. Настройки для разработки приложения
Дальше: 7. Проверка качества кода, соответствия типов, тестирование, повтор

6. Создание приложения

Теперь, когда вы уже постигли все основы создания пользовательских React-компонентов (и использования встроенных компонентов), применения технологии JSX (или работы без нее) для определения пользовательских интерфейсов, а также сборки и развертывания результатов своей работы, настало время потрудиться над созданием более совершенного приложения.

Оно будет называться Whinepad (что-то вроде карты отзывов) и позволит пользователям делать заметки и давать оценку всем дегустируемым винам (на самом деле это не обязательно должны быть вина, можно оценивать что угодно, о чем захочется оставить отзыв). Это должно быть CRUD-приложение, умеющее делать все, что от него ожидается, то есть создавать, считывать, обновлять и удалять (create, read, update и delete — CRUD). Оно также должно быть приложением, выполняемым на стороне клиента и сохраняющим на его же стороне свои данные. Цель его создания — изучение React, поэтому информация, не относящаяся к React (например, хранение, презентация), представлена в минимальном объеме.

В процессе работы над приложением вы узнаете:

о сборке приложения из небольших пригодных к многократному использованию компонентов;

об обмене данными между компонентами и организации их совместной работы.

Whinepad v.0.0.1

Основываясь на стандартах, усвоенных в предыдущей главе, запустим процесс создания Whinepad — приложения выставления оценок, где оставляются заметки относительно новых вещей. Вы ведь не против, если в качестве экрана приветствия будет список уже прошедших оценку вещей в форме привлекательной таблицы? Это означает, что нужно заново воспользоваться компонентом <Excel> из главы 3.

Подготовка к работе

Сначала скопируйте шаблонное приложение reactbook-boiler, на основе которого будет строиться работа (копию можете получить по адресу /), и переименуйте его в whinepad v0.0.1. Затем запустите сценарий watch, чтобы сборка могла производиться при внесении любых изменений:

$ cd ~/reactbook/whinepad\ v0.0.1/

$ sh scripts/watch.sh

Приступим к программированию

Обновите в index.html содержимое тега title и установите идентификатор id="pad", чтобы данные элементы соответствовали новому приложению:

<!DOCTYPE html>

<html>

  <head>

    <title>Whinepad v.0.0.1</title>

    <meta charset="utf-8">

    <link rel="stylesheet" type="text/css" href="bundle.css">

  </head>

  <body>

    <div id="pad"></div>

    <script src="bundle.js"></script>

  </body>

</html>

Воспользуемся JSX-версией компонента Excel (показанной в конце главы 4) и скопируем ее в файл js/source/components/Excel.js:

import React from 'react';

 

var Excel = React.createClass({

 

  // Реализация...

 

  render: function() {

    return (

      <div className="Excel">

        {this._renderToolbar()}

        {this._renderTable()}

      </div>

    );

  },

 

  // продолжение реализации ...

});

 

export default Excel

Здесь можно увидеть ряд отличий от прежнего вида Excel:

инструкции import и export;

у основной части компонента теперь имеется атрибут classNa­me="Excel", чтобы соответствовать недавно принятому соглашению.

По той же причине используются префиксы и во всех элементах CSS:

.Excel table {

  border: 1px solid black;

  margin: 20px;

}

 

.Excel th {

  /* и т.д. */

}

 

/* и т.д. */

Теперь осталось только включить <Excel>, обновив основной файл app.js. Чтобы ничего не усложнять и не выходить за рамки клиентской стороны, воспользуемся хранилищем на стороне клиента (localStorage). Для начала установим ряд исходных значений:

var headers = localStorage.getItem('headers');

var data = localStorage.getItem('data');

 

if (!headers) {

  headers = ['Title', 'Year', 'Rating', 'Comments'];

  data = [['Test', '2015', '3', 'meh']];

}

Теперь передадим данные компоненту <Excel>:

ReactDOM.render(

  <div>

    <h1>

      <Logo /> Welcome to Whinepad!

    </h1>

    <Excel headers={headers} initialData={data} />

  </div>,

  document.getElementById('pad')

);

И, немного подправив Logo.css, завершим работу над версией 0.0.1 (рис. 6.1).

06_01.tif 

Рис. 6.1. Whinepad v.0.0.1

Компоненты

Повторное использование компонента <Excel> позволило упростить начало работы, но этот компонент перегружен задачами. К нему лучше применить принцип «разделяй и властвуй» — разбить на небольшие пригодные для многократного использования компоненты. Например, кнопки должны стать самостоятельными компонентами, чтобы их можно было использовать вне контекста таблицы Excel.

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

Настроимся на работу с новым приложением и воспользуемся еще одним вспомогательным средством — исследователем компонентов.

Его задачи:

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

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

Настройка

Воспользуйтесь комбинацией клавиш Ctrl+C, чтобы остановить работу ранее запущенного сценария отслеживателя и получить возможность запуска нового отслеживателя. Скопируйте исходный минимально жизнеспособный продукт — minimum viable product (MVP) whinepad v.0.0.1 в новую папку whinepad:

$ cp -r ~/reactbook/whinepad\ v0.0.1/ ~/reactbook/whinepad

$ cd ~/reactbook/whinepad

$ sh scripts/watch.sh

 

> Watching js/source/

> Watching css/

js/source/app.js -> js/build/app.js

js/source/components/Excel.js -> js/build/components/Excel.js

js/source/components/Logo.js -> js/build/components/Logo.js

Sun Jan 24 11:10:17 PST 2016

Исследование

Назовем средство исследования компонентов discovery.html и поместим его в корневой каталог:

$ cp index.html discovery.html

Целиком приложение загружать не нужно, поэтому вместо файла app.js воспользуемся файлом discover.js, который содержит все примеры компонентов. Следовательно, вместо принадлежащего приложению файла bundle.js требуется включить отдельный пакет с названием discover-bundle.js:

<!DOCTYPE html>

<html>

  <!-- аналогично index.html -->

  <body>

    <div id="pad"></div>

    <script src="discover-bundle.js"></script>

  </body>

</html>

Попутное создание нового пакета особого труда не представляет, нужно лишь добавить к сценарию build.sh еще одну строку:

# пакет js

browserify js/build/app.js -o bundle.js

browserify js/build/discover.js -o discover-bundle.js

И наконец, добавим к средству исследования (js/build/discover.js) пример <Logo>:

'use strict';

 

import Logo from './components/Logo';

import React from 'react';

import ReactDOM from 'react-dom';

 

ReactDOM.render(

  <div style={ {padding: '20px'} }>

    <h1>Component discoverer</h1>

 

    <h2>Logo</h2>

    <div style={ {display: 'inline-block', background:  

      'purple'} }>

      <Logo />

    </div>

 

    {/* сюда помещаются дополнительные компоненты... */}

 

  </div>,

  document.getElementById('pad')

);

Ваше новое средство исследования компонентов (рис. 6.2) является местом запуска новых компонентов по мере их создания. Приступим к работе и поэтапно создадим нужные нам компоненты.

06_02.tif 

Рис. 6.2. Средства исследования компонентов  для приложения Whinepad

Компонент <Button>

Скажу без преувеличения: кнопки нужны всем приложениям. Зачастую это красиво стилизованные обычные элементы <button>, но иногда в качестве кнопки должна выступать гиперссылка <a> (именно этот элемент был необходим в главе 3 для создания кнопок скачивания файлов). А что, если создать новую привлекательную кнопку Button, принимающую необязательное свойство href? Если оно присутствует, то в основе отображения будет фигурировать гиперссылка <a>.

В духе разработки на основе тестирования — test-driven development (TDD) — можно приступить к работе, выбрав обратное направление и определив пример использования компонента в средстве discovery.js.

До:

import Logo from './components/Logo';

{/* ... */}

{/* сюда помещаются дополнительные компоненты... */}

После:

import Button from './components/Button';

import Logo from './components/Logo';

 

{/* ... */}

 

<h2>Buttons</h2>

<div>Button with onClick: <Button onClick={() =>

  alert('ouch')}>Click me</Button></div>

<div>A link: <Button href="

  me</Button></div>

<div>Custom class name: <Button className="custom">I do

  nothing</Button></div>

 

{/* сюда помещаются дополнительные компоненты... */}

(А нельзя ли тогда назвать это разработкой на основе исследований — discovery-driven development, DDD?)

161363.png

Обратили внимание на новый шаблон () => alert(‘ouch’)? Это пример использования функции стрелки из спецификации ES2015.

Вот другие варианты применения этой функции:

выражение () => {} является пустой функцией (наподобие function() {});

выражение (what, not) => console.log(what, not) является функцией с параметрами;

выражение (a, b) => { var c = a + b; return c;} используется, когда в теле функции больше одной строки, в этом случае нужны фигурные скобки {};

выражение let fn = arg => {} применяется при получении только одного аргумента, круглые скобки () можно не использовать.

Button.css

Согласно требованиям принятого соглашения код, определя­ющий стиль компонента <Button>, должен размещаться в файле /css/components/Button.css. В нем нет ничего необычного, там просто свойства CSS, которые придадут кнопке дополнительную привлекательность. Рассмотрим их здесь полностью и договоримся, что не станем тратить время на код CSS для других компонентов:

.Button {

  background-color: #6f001b;

  border-radius: 28px;

  border: 0;

  box-shadow: 0px 1px 1px #d9d9d9;

  color: #fff;

  cursor: pointer;

  display: inline-block;

  font-size: 18px;

  font-weight: bold;

  padding: 5px 15px;

  text-decoration: none;

  transition-duration: 0.1s;

  transition-property: transform;

}

.Button:hover {

  transform: scale(1.1);

}

Button.js

Теперь рассмотрим полную версию кода в файле /js/source/components/Button.js:

import classNames from 'classnames';

import React, {PropTypes} from 'react';

function Button(props) {

  const cssclasses = classNames('Button', props.className);

  return props.href

    ? <a {...props} className={cssclasses} />

    : <button {...props} className={cssclasses} />;

}

 

Button.propTypes = {

  href: PropTypes.string,

};

 

export default Button

Код этого компонента изложен кратко, но при этом полон новых идей и передового синтаксиса. Проведем исследования, начиная с самой верхней строчки!

Пакет classnames

import classNames from 'classnames';

Пакет classnames (устанавливается с помощью команды npm i --save-dev classnames) предоставляет полезную функцию для работы с именами классов CSS. Задача использования вашим компонентом своих собственных классов при сохранении достаточной гибкости, позволяющей проводить настройки посредством имен классов, передаваемых родительским компонентом, ставится довольно часто. Ранее в пакете дsополнительных средств React для этого была специальная утилита, но она вышла из употребления, и ее место занял этот более удачный пакет от стороннего производителя. Из данного пакета используется только одна функция:

const cssclasses = classNames('Button', props.className);

Она предназначена для объединения имени класса Button с любыми (если таковые будут) именами классов, переданными в виде свойств при создании компонента (рис. 6.3).

06_03.tif 

Рис. 6.3. <Button> с пользовательским именем класса

 

161369.png

Объединить имена классов можно и самостоятельно, но classnames очень маленький пакет, который удобно применять для решения этой часто встречающейся задачи. Он также позволяет выполнять такую полезную операцию, как условное присваивание имен классам:

<div className={classNames({

  'mine': true, // безусловное присваивание

  'highlighted': this.state.active,

// присваивание в зависимости

// от состояния компонента…

  'hidden': this.props.hide,        // ... или его

                                    // свойств

})} />

Реструктуризующее  присваивание

Код:

import React, {PropTypes} from 'react';

является просто лаконичным способом следующего объявления:

import React from 'react';

const PropTypes = React.PropTypes;

Функциональный компонент,  не имеющий состояния

Когда компонент настолько прост (и такое бывает!) и не нуждается в поддержке состояния, для его определения можно воспользоваться функцией. Тело этой функции станет заменой вашего метода render(). Функция в качестве своего первого аргумента получает все свойства, поэтому в ее теле используется не this.props.href, как в версии класса или объекта, а props.href.

Используя форму определения с помощью стрелки, эту функцию можно переписать следующим образом:

const Button = props => {

  // ...

};

И если есть стойкое желание выразить тело одной строкой, можно даже отказаться от использования {}, ; и return:

const Button = props =>

  props.href

    ? <a {...props} className={classNames('Button',

      props.className)} />

    : <button {...props} className={classNames('Button',

      props.className)} />

propTypes

Если используются синтаксис классов, специфицированный в ES2015, или функциональные компоненты, вы должны после определения компонента определить любые свойства (такие как propTypes) как статичные. Иными словами, до (ES3, ES5):

var PropTypes = React.PropTypes;

 

var Button = React.createClass({

  propTypes: {

    href: PropTypes.string

  },

  render: function() {

    /* отображение */

  }

});

После (класс ES2015):

import React, {Component, PropTypes} from 'react';

 

class Button extends Component {

  render() {

    /* отображение */

  }

}

 

Button.propTypes = {

  href: PropTypes.string,

};

То же самое при работе функционального компонента, не использующего состояние:

import React, {Component, PropTypes} from 'react';

 

const Button = props => {

  /* отображение */

};

 

Button.propTypes = {

  href: PropTypes.string,

};

Формы

На данный момент компонент <Button> нас вполне устраивает. Перейдем к выполнению другой задачи, важной для любого приложения, в котором вводятся данные, — к работе с формами. Разработчиков приложений редко устраивает внешний вид встроенных в браузер форм ввода данных, что побуждает их создавать собственные версии. Не исключение в этом плане и приложение Whinepad.

Создадим универсальный компонент <FormInput> с методом getValue(), предоставляющим вызывающему коду доступ к записи в поле ввода. В зависимости от значения свойства type этот компонент должен делегировать создание поля ввода более узкоспециализированным компонентам, например компонентам ввода данных <Suggest>, <Rating> и т.д.

Начнем с компонентов более низкого уровня, которым нужны лишь методы render() и getValue().

Компонент <Suggest>

Необычные поля ввода с автопредложением (известные как поля с упреждающим заполнением) встречаются в веб-приложениях довольно часто, но не будем ничего усложнять (рис. 6.4) и позаимствуем то, что уже предлагается браузером, а именно HTML-элемент <datalist>.

06_04.tif 

Рис. 6.4. Компонент ввода данных <Suggest> в работе

Прежде всего обновим наше исследовательское приложение:

<h2>Suggest</h2>

<div><Suggest options={['eenie', 'meenie', 'miney', 'mo']} /></div>

Теперь приступим к реализации компонента в файле /js/source/components/Suggest.js:

import React, {Component, PropTypes} from 'react';

 

class Suggest extends Component {

 

  getValue() {

    return this.refs.lowlevelinput.value;

  }

 

  render() {

    const randomid = Math.random().toString(16).substring(2);

    return (

      <div>

        <input

          list={randomid}

          defaultValue={this.props.defaultValue}

          ref="lowlevelinput"

          id={this.props.id} />

        <datalist id={randomid}>{

          this.props.options.map((item, idx) =>

            <option value={item} key={idx} />

          )

        }</datalist>

      </div>

    );

  }

}

 

Suggest.propTypes = {

  options: PropTypes.arrayOf(PropTypes.string),

};

 

export default Suggest

Как видно из предыдущего кода, в этом компоненте нет абсолютно ничего особенного, он является всего лишь оболочкой вокруг прикрепленных к нему (с использованием randomid) элементов <input> и <datalist>.

Что касается нового ES-синтаксиса, можно увидеть появившуюся возможность использования реструктуризующего присваивания для назначения переменной более одного свойства:

// до

import React from 'react';

const Component = React.Component;

const PropTypes = React.PropTypes;

 

// после

import React, {Component, PropTypes} from 'react';

А что касается нововведений, появившихся в React, можно увидеть использование атрибута ref.

Атрибут ref. Рассмотрим следующий код:

<input ref="domelement" id="hello">

/* и чуть ниже... */

console.log(this.refs.domelement.id === 'hello'); // true

Атрибут ref позволяет присваивать имя конкретному экземпляру компонента React, после чего ссылаться на него по этому имени. Атрибут ref можно добавить к любому компоненту, но обычно он используется для ссылки на DOM-элементы, когда действительно нужно обратиться к исходной DOM-модели. Зачастую использование ref является обходным маневром и должны быть другие способы выполнения той же самой задачи.

В предыдущем случае нужно было получить возможность забирать при необходимости значение поля ввода <input>. Учитывая, что изменения в поле ввода могут считаться изменениями состояния компонента, вы можете переключиться на использование с целью отслеживания ситуации объекта this.state:

class Suggest extends Component {

 

  constructor(props) {

    super(props);

    this.state = {value: props.defaultValue};

  }

 

  getValue() {

    return this.state.value; // 'ref' больше не используется

  }

 

  render() {}

}

Тогда полю ввода <input> атрибут ref больше не понадобится, но для обновления состояния нужен будет обработчик события onChange:

<input

  list={randomid}

  defaultValue={this.props.defaultValue}

  onChange={e => this.setState({value: e.target.value})}

  id={this.props.id} />

Обратите внимание на применение в методе constructor() выражения this.state = {};: это замена для метода getInitialState(), который использовался до появления ES6.

Компонент <Rating>

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

Этот компонент, предназначенный для многократного использования, должен быть настроен:

на применение любого количества звезд (их исходное количество равно пяти, но почему бы не использовать, скажем, 11 звезд?);

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

Протестируйте компонент в нашем исследовательском приложении (рис. 6.5):

<h2>Rating</h2>

<div>No initial value: <Rating /></div>

<div>Initial value 4: <Rating defaultValue={4} /></div>

<div>This one goes to 11: <Rating max={11} /></div>

<div>Read-only: <Rating readonly={true}

  defaultValue={3} /></div>

06_05.tif 

Рис. 6.5. Виджет для отображения рейтинга

Минимальные потребности реализации включают установки типов свойств и обслуживаемого состояния:

import classNames from 'classnames';

import React, {Component, PropTypes} from 'react';

 

class Rating extends Component {

 

  constructor(props) {

    super(props);

    this.state = {

      rating: props.defaultValue,

      tmpRating: props.defaultValue,

    };

  }

  /* другие методы... */

}

 

Rating.propTypes = {

  defaultValue: PropTypes.number,

  readonly: PropTypes.bool,

  max: PropTypes.number,

};

 

Rating.defaultProps = {

  defaultValue: 0,

  max: 5,

};

 

export default Rating

Названия свойств говорят сами за себя: max представляет собой количество звезд, а readonly делает виджет, как нетрудно догадаться, доступным только для чтения. В состоянии фигурируют rating, текущее значение количества присвоенных звезд, и tmpRating, значение, используемое при перемещении пользователем указателя мыши над компонентом, когда еще не выражена готовность щелк­нуть кнопкой мыши и отправить выбранный показатель рейтинга.

Далее следуют вспомогательные методы, занимающиеся обслуживанием актуальности состояния в ходе работы пользователя с компонентом:

getValue() { // предоставляется всеми нашими полями ввода

  return this.state.rating;

}

 

setTemp(rating) { // при проходе над элементом указателя мыши

  this.setState({tmpRating: rating});

}

 

setRating(rating) { // при щелчке

  this.setState({

    tmpRating: rating,

    rating: rating,

  });

}

 

reset() { // возвращение к реальному рейтингу

          // при уводе указателя мыши

  this.setTemp(this.state.rating);

}

 

componentWillReceiveProps(nextProps) {

// реагирование на внешние изменения

  this.setRating(nextProps.defaultValue);

}

И наконец, метод render(). У него имеются:

цикл, создающий звезды от одной и до this.props.max. Все звезды отображаются с помощью символа с кодом &#9734;. Когда применяется стиль RatingOn, звезды становятся желтыми;

скрытое поле ввода для работы в качестве настоящего, принадлежащего форме поля ввода (позволяет задавать значение в общем виде — подобно любому полю ввода <input>):

render() {

  const stars = [];

  for (let i = 1; i <= this.props.max; i++) {

    stars.push(

      <span

        className={i <= this.state.tmpRating ?

          'RatingOn' : null}

        key={i}

        onClick={!this.props.readonly &&

          this.setRating.bind(this, i)}

        onMouseOver={!this.props.readonly &&

          this.setTemp.bind(this, i)}

      >

        &#9734;

      </span>);

  }

  return (

    <div

      className={classNames({

        'Rating': true,

        'RatingReadonly': this.props.readonly,

      })}

      onMouseOut={this.reset.bind(this)}

    >

      {stars}

      {this.props.readonly || !this.props.id

        ? null

        : <input

          type="hidden"

          id={this.props.id}

          value={this.state.rating} />

        }

     </div>

  );

}

Можно заметить еще одну особенность — использование метода привязки bind. В цикле отображения звезд имеет смысл запомнить текущее значение i, но зачем используется выражение this.reset.bind(this)? Его необходимо задействовать при использовании синтаксиса классов, определенного в ES. При создании привязки можно выбрать один из трех вариантов:

как видно из предыдущего примера, выражение this.me­thod.bind(this);

функция стрелки, выполняющей автопривязку, например (_unused_event_) => this.method();

однократная привязка в конструкторе.

Для использования третьего варианта нужно сделать следующее:

class Comp extents Component {

  constructor(props) {

    this.method = this.method.bind(this);

  }

 

  render() {

    return <button onClick={this.method}>

  }

}

Одно из преимуществ заключается в том, что вы, как и раньше, используете ссылку this.method (как с компонентами, созданными с помощью React.createClass({})). Еще одно преимущество заключается в том, что метод привязывается раз и навсегда, в отличие от привязки при каждом вызове render(). К недостаткам можно отнести наличие более объемного шаблона в контроллере.

Компонент <FormInput>

Настал черед универсального компонента <FormInput>, способного создавать различные компоненты ввода на основе заданных свойств. Все создаваемые компоненты ввода ведут себя согласованно (когда нужно, предоставляется метод getValue()).

Протестируйте компонент в нашем исследовательском приложении (рис. 6.6):

<h2>Form inputs</h2>

<table><tbody>

  <tr>

    <td>Vanilla input</td>  // Обычный ввод

    <td><FormInput /></td>

  </tr>

  <tr>

    <td>Prefilled</td>  // С предварительным заполнением

    <td><FormInput defaultValue="it's like a default" /></td>

  </tr>

  <tr>

    <td>Year</td>  // Год

    <td><FormInput type="year" /></td>

  </tr>

  <tr>

    <td>Rating</td> // Рейтинг

    <td><FormInput type="rating" defaultValue={4} /></td>

  </tr>

  <tr>

    <td>Suggest</td>  // Предложение

    <td><FormInput

      type="suggest"

      options={['red', 'green', 'blue']}

      defaultValue="green" />

    </td>

  </tr>

  <tr>

    <td>Vanilla textarea</td> // Обычная текстовая область

    <td><FormInput type="text" /></td>

  </tr>

</tbody></table>

06_06.tif 

Рис. 6.6. Элементы ввода, используемые в форме

Реализация <FormInput> (js/source/components/FormInput.js) требует для корректности обычного шаблонного использования import, export и propTypes:

import Rating from './Rating';

import React, {Component, PropTypes} from 'react';

import Suggest from './Suggest';

 

class FormInput extends Component {

  getValue() {}

  render() {}

}

 

FormInput.propTypes = {

  type: PropTypes.oneOf(['year', 'suggest', 'rating', 'text',

    'input']),

  id: PropTypes.string,

  options: PropTypes.array,

  // как в вариантах автозаполнения <option>

  defaultValue: PropTypes.any,

};

 

export default FormInput

Метод render() представляет собой одну большую инструкцию switch, которая делегирует создание отдельно взятого элемента ввода узкоспециализированному компоненту или же прибегает к использованию встроенных DOM-элементов <input> и <textarea>:

render() {

  const common = { // свойства, применимые для всех компонентов

    id: this.props.id,

    ref: 'input',

    defaultValue: this.props.defaultValue,

  };

  switch (this.props.type) {

    case 'year':

      return (

        <input

          {...common}

          type="number"

          defaultValue={this.props.defaultValue ||

            new Date().getFullYear()} />

      );

      case 'suggest':

        return <Suggest {...common}

          options={this.props.options} />;

      case 'rating':

        return (

          <Rating

            {...common}

            defaultValue={parseInt(this.props.defaultValue,

              10)} />

        );

      case 'text':

        return <textarea {...common} />;

      default:

        return <input {...common} type="text" />;

  }

}

Заметили использование свойства ref? Оно может оказаться полезным при присваивании значений элемента ввода:

getValue() {

  return 'value' in this.refs.input

    ? this.refs.input.value

    : this.refs.input.getValue();

}

Здесь this.refs.input является ссылкой на исходный элемент DOM-модели. Для обычных DOM-элементов вроде <input> и <textarea> вы получаете DOM-значение с помощью this.refs.input.value (как при использовании традиционно применяемого в DOM выражения document.getElementById('some-input').value). В иных случаях для необычных пользовательских компонентов ввода (вроде <Suggest> и <Rating>) вы добираетесь до их собственных методов getValue().

Компонент <Form>

Теперь у вас имеются:

пользовательские компоненты ввода (например, <Rating>);

встроенные элементы ввода (например, <textarea>);

<FormInput> — фабрика, создающая компоненты ввода на основе значения свойства type.

Настало время заставить их всех работать вместе в составе компонента <Form> (рис. 6.7).

Компонент <Form> должен быть пригодным к многократному использованию, также в нем не должно быть ничего жестко заданного относительно приложения для составления рейтинга вин. (Если копнуть глубже, ничего не должно быть жестко задано относительно вина, чтобы приложение могло быть перенацелено на оценку любой другой категории вещей.) Компонент <Form> может быть настроен через массив полей, где каждое поле определяется:

типом ввода (по умолчанию "input");

идентификатором, чтобы это поле ввода потом можно было найти;

надписью, чтобы ее можно было поставить рядом с полем ввода;

дополнительными вариантами для передачи их полю ввода с автопредложением.

06_07.tif 

Рис. 6.7. Формы

Компонент <Form> также получает отображение исходных значений; он может отображаться в режиме только для чтения, чтобы пользователь не мог воспользоваться редактированием:

import FormInput from './FormInput';

import Rating from './Rating';

import React, {Component, PropTypes} from 'react';

 

class Form extends Component {

  getData() {}

  render() {}

}

 

Form.propTypes = {

  fields: PropTypes.arrayOf(PropTypes.shape({

  id: PropTypes.string.isRequired,

  label: PropTypes.string.isRequired,

  type: PropTypes.string,

  options: PropTypes.arrayOf(PropTypes.string),

  })).isRequired,

  initialData: PropTypes.object,

  readonly: PropTypes.bool,

};

 

export default Form

Обратите внимание на использование выражения PropTypes.shape. Оно позволяет вам конкретизировать свои ожидания относительно содержимого отображения. Оно более строгое, чем обобщение вроде fields: PropTypes.arrayOf(PropTypes.object) или fields: PropTypes.array и, конечно же, позволит отловить больше ошибок еще до того, как они проявятся (в самом начале использования ваших компонентов другими разработчиками).

Свойство initialData является отображением вида {имя_поля: значение}; это также формат данных, возвращенных принадлежащим компоненту методом getData().

Пример использования компонента <Form> для исследовательского приложения имеет следующий вид:

<Form

  fields={[

    {label: 'Rating', type: 'rating', id: 'rateme'},

    {label: 'Greetings', id: 'freetext'},

  ]}

  initialData={ {rateme: 4, freetext: 'Hello'} } />

Теперь вернемся к реализации. Компоненту нужны методы getData() и render():

getData() {

  let data = {};

  this.props.fields.forEach(field =>

    data[field.id] = this.refs[field.id].getValue()

  );

  return data;

}

Как видите, вам надо лишь пройтись в цикле по всем методам getValue(), принадлежащим компонентам ввода данных, воспользовавшись для этого свойствами ref, установленными в методе render().

Сам метод render() особой сложностью не отличается, и в нем не используются какой-либо не встречавшийся вам до этого синтаксис или другие шаблоны:

render() {

  return (

    <form className="Form">{this.props.fields.map(field => {

      const prefilled = this.props.initialData &&

        this.props.initial

        Data[field.id];

      if (!this.props.readonly) {

        return (

          <div className="FormRow" key={field.id}>

            <label className="FormLabel"

              htmlFor={field.id}>{field.label}:</label>

            <FormInput {...field} ref={field.id}

              defaultValue={prefilled} />

          </div>

        );

      }

      if (!prefilled) {

        return null;

      }

      return (

        <div className="FormRow" key={field.id}>

          <span className="FormLabel">{field.label}:</span>

          {

            field.type === 'rating'

              ? <Rating readonly={true}

                defaultValue={parseInt(prefilled, 10)} />

              : <div>{prefilled}</div>

          }

        </div>

      );

    }, this)}</form>

  );

}

Компонент <Actions>

Рядом с каждой строкой в таблице данных должны быть действия (actions) (рис. 6.8), которые можно предпринять в отношении этой строки: удаление, редактирование, просмотр (когда не вся информация может поместиться в строке).

06_08.tif 

Рис. 6.8. Компонент Actions

Тестируемый в исследовательском средстве Discovery компонент Actions имеет следующий вид:

<h2>Actions</h2>

<div><Actions onAction={type => alert(type)} /></div>

А вот его весьма простая реализация:

import React, {PropTypes} from 'react';

 

const Actions = props =>

  <div className="Actions">

    <span

      tabIndex="0"

      className="ActionsInfo"

      title="More info"

      onClick={props.onAction.bind(null,

        'info')}>&#8505;</span>

    <span

      tabIndex="0"

      className="ActionsEdit"

      title="Edit"

      onClick={props.onAction.bind(null,

        'edit')}>&#10000;</span>

    <span

      tabIndex="0"

      className="ActionsDelete"

      title="Delete"

      onClick={props.onAction.bind(null, 'delete')}>x</span>

  </div>

 

Actions.propTypes = {

  onAction: PropTypes.func,

};

 

Actions.defaultProps = {

  onAction: () => {},

};

 

export default Actions

Actions — очень простой компонент, требующий лишь отображения и не поддерживающий состояния. Поэтому он может быть определен как функциональный компонент, не имеющий состояния, с использованием функции стрелки с самым емким из всех возможных синтаксисом: без возвращаемого значения, без {}, без инструкции function (в прежние времена такую форму записи трудно было бы даже посчитать функцией!).

Код, вызывающий компонент, может подписаться на событие «действия», используя свойство onAction. Это простая схема для дочернего компонента, чтобы оповестить свой родительский компонент об изменении внутри компонента. Как видите, применять пользовательские события (например, на случай действия — onAction, на случай нападения пришельцев — onAlienAttack и т.д.) проще простого.

Диалоги

Следующий на очереди универсальный компонент диалога, предназначенный для сообщений или для использования диалоговых окон любого вида (вместо alert()) (рис. 6.9). Например, все формы добавления и редактирования данных могут быть представлены в модальном диалоговом окне в верхней части таблицы данных.

Использование:

<Dialog

  header="Out-of-the-box example"

  onAction={type => alert(type)}>

    Hello, dialog!

</Dialog>

 

<Dialog

  header="No cancel, custom button"

  hasCancel={false}

  confirmLabel="Whatever"

  onAction={type => alert(type)}>

    Anything goes here, see:

    <Button>A button</Button>

</Dialog>

06_09.tif 

Рис. 6.9. Диалоги

Реализация очень похожа на реализацию компонента Actions — без состояния (нужен лишь метод render()) и с функцией обратного вызова onAction, инициируемой при нажатии пользователем кнопки в нижней части диалогового окна:

import Button from './Button';

import React, {Component, PropTypes} from 'react';

 

class Dialog extends Component {

 

}

 

Dialog.propTypes = {

  header: PropTypes.string.isRequired,

  confirmLabel: PropTypes.string,

  modal: PropTypes.bool,

  onAction: PropTypes.func,

  hasCancel: PropTypes.bool,

};

 

Dialog.defaultProps = {

  confirmLabel: 'ok',

  modal: false,

  onAction: () => {},

  hasCancel: true,

};

 

export default Dialog

Но этот компонент объявляется как класс, а не как функция стрелки, поскольку ему требуется определить два дополнительных метода управления жизненным циклом:

componentWillUnmount() {

  document.body.classList.remove('DialogModalOpen');

}

 

componentDidMount() {

  if (this.props.modal) {

    document.body.classList.add('DialogModalOpen');

  }

}

Они понадобятся при создании модального диалогового окна: компонент добавляет имя класса к телу документа, следовательно, документ может получить стилевое оформление (обесцветиться).

И наконец, воспользуемся методом render(), чтобы составить модальную оболочку для заголовка, тела и нижней части. Тело вмещает любой другой компонент (или простой текст); когда речь заходит о его содержимом, диалоговое окно демонстрирует свой абсолютно неприхотливый характер:

render() {

  return (

    <div className={this.props.modal ?

      'Dialog DialogModal' : 'Dialog'}>

      <div className={this.props.modal ?

        'DialogModalWrap' : null}>

        <div className="DialogHeader">{this.props.header}</div>

        <div className="DialogBody">{this.props.children}</div>

        <div className="DialogFooter">

          {this.props.hasCancel

            ? <span

              className="DialogDismiss"

              onClick={this.props.onAction.bind(this,

                'dismiss')}>

              Cancel

            </span>

          : null

        }

        <Button onClick={this.props.onAction.bind(this,

          this.props.hasCancel ? 'confirm' : 'dismiss')}>

          {this.props.confirmLabel}

        </Button>

      </div>

    </div>

  </div>

  );

}

Альтернативы:

вместо использования одного вызова onAction есть другой вариант: предоставление вызова onConfirm (по щелчку пользователя на кнопке OK) и вызова onDismiss;

можно улучшить работу с диалоговым окном, заставив его исчезать после нажатия пользователем клавиши Esc. (А как бы вы смогли реализовать эту функцию?);

у контейнера div имеется условное и безусловное имя класса. В компоненте можно воспользоваться модулем classnames, сделав это следующим образом.

До:

<div className={this.props.modal ? 'Dialog DialogModal' :

  'Dialog'}>

После:

<div className={classNames({

  'Dialog': true,

  'DialogModal': this.props.modal,

})}>

Настройка приложения

На данный момент в нашем распоряжении уже имеются все низкоуровневые компоненты, осталось получить еще два компонента: новую, усовершенствованную таблицу данных Excel и самый высокоуровневый родительский компонент Whinepad. Оба они настраиваются через объект «схемы» — описание типа данных, с которыми нужно работать в приложении. Вот как выглядит пример (js/source/schema.js), позволяющий приступить к формированию приложения, ориентированного на предоставление отзывов о дегустируемых винах:

import classification from './classification';

 

export default [

  {

    id: 'name',

    label: 'Name',

    show: true, // показать в таблице 'Excel'

    sample: '$2 chuck',

    align: 'left', // выравнивание в 'Excel'

  },

  {

    id: 'year',

    label: 'Year',

    type: 'year',

    show: true,

    sample: 2015,

  },

  {

    id: 'grape',

    label: 'Grape',

    type: 'suggest',

    options: classification.grapes,

    show: true,

    sample: 'Merlot',

    align: 'left',

  },

  {

    id: 'rating',

    label: 'Rating',

    type: 'rating',

    show: true,

    sample: 3,

  },

  {

    id: 'comments',

    label: 'Comments',

    type: 'text',

    sample: 'Nice for the price',

  },

]

Это пример одного из самых простых ECMAScript-модулей, которые только можно себе представить (один из тех, что экспортирует только одну переменную). Он также импортирует еще один простой модуль, содержащий некоторые длинные варианты предварительного заполнения форм (js/source/classification.js):

export default {

  grapes: [

    'Baco Noir',

    'Barbera',

    'Cabernet Franc',

    'Cabernet Sauvignon',

    // ....

  ],

}

Теперь с помощью модуля schema вы можете настроить тип данных, с которыми сможете работать в приложении.

<Excel>: новый и усовершенствованный

Компонент Excel из главы 3 обладает избыточными для нашей новой задачи возможностями. Его усовершенствованная версия должна стать более пригодной для многократного использования. Избавимся от функции поиска (переместив ее в компонент самого верхнего уровня Whinepad) и от функций загрузки (эти функции вы можете добавить в Whinepad самостоятельно). Компонент должен заниматься только RUD-частью из набора функций, обозначаемого акронимом CRUD (то есть только чтением, обновлением и удалением) (рис. 6.10). Это редактируемая таблица, и она должна предоставить своему родительскому компоненту Whinepad возможность получения уведомлений при изменении данных, используя для этого метод onDataChange.

Компонент Whinepad должен позаботиться о поиске, о функции C (создании новой записи) из набора функций, обозначаемого акронимом CRUD, и о постоянном хранении данных с использованием localStorage. (В версии приложения, предназначенной для реальной работы, должно быть предусмотрено хранение данных на сервере.)

06_10.tif 

Рис. 6.10. Excel

Для настройки на типы данных оба компонента используют коллекцию из модуля schema.

Итак, приготовьтесь к полноценной реализации компонента Excel (за исключением некоторых особенностей он близок к тому компоненту, который вам знаком по главе 3):

import Actions from './Actions';

import Dialog from './Dialog';

import Form from './Form';

import FormInput from './FormInput';

import Rating from './Rating';

import React, {Component, PropTypes} from 'react';

import classNames from 'classnames';

 

class Excel extends Component {

  constructor(props) {

    super(props);

    this.state = {

      data: this.props.initialData,

      sortby: null, // schema.id

      descending: false,

      edit: null, // [row index, schema.id],

      dialog: null, // {type, idx}

    };

  }

 

  componentWillReceiveProps(nextProps) {

    this.setState({data: nextProps.initialData});

  }

 

  _fireDataChange(data) {

    this.props.onDataChange(data);

  }

 

  _sort(key) {

    let data = Array.from(this.state.data);

    const descending = this.state.sortby === key &&

      !this.state.descending;

    data.sort(function(a, b) {

      return descending

        ? (a[column] < b[column] ? 1 : -1)

        : (a[column] > b[column] ? 1 : -1);

    });

    this.setState({

      data: data,

      sortby: key,

      descending: descending,

    });

    this._fireDataChange(data);

  }

 

  _showEditor(e) {

    this.setState({edit: {

      row: parseInt(e.target.dataset.row, 10),

      key: e.target.dataset.key,

    }});

  }

 

  _save(e) {

    e.preventDefault();

    const value = this.refs.input.getValue();

    let data = Array.from(this.state.data);

    data[this.state.edit.row][this.state.edit.key] = value;

    this.setState({

      edit: null,

      data: data,

    });

    this._fireDataChange(data);

  }

 

  _actionClick(rowidx, action) {

    this.setState({dialog: {type: action, idx: rowidx}});

  }

 

  _deleteConfirmationClick(action) {

    if (action === 'dismiss') {

      this._closeDialog();

      return;

    }

    let data = Array.from(this.state.data);

    data.splice(this.state.dialog.idx, 1);

    this.setState({

      dialog: null,

      data: data,

    });

    this._fireDataChange(data);

  }

 

  _closeDialog() {

    this.setState({dialog: null});

  }

 

  _saveDataDialog(action) {

    if (action === 'dismiss') {

      this._closeDialog();

      return;

    }

    let data = Array.from(this.state.data);

    data[this.state.dialog.idx] = this.refs.form.getData();

    this.setState({

      dialog: null,

      data: data,

    });

    this._fireDataChange(data);

  }

 

  render() {

    return (

      <div className="Excel">

        {this._renderTable()}

        {this._renderDialog()}

      </div>

    );

  }

 

  _renderDialog() {

    if (!this.state.dialog) {

      return null;

    }

    switch (this.state.dialog.type) {

      case 'delete':

        return this._renderDeleteDialog();

      case 'info':

        return this._renderFormDialog(true);

      case 'edit':

        return this._renderFormDialog();

      default:

        throw Error('Unexpected dialog type

          ${this.state.dialog.type}');

    }

  }

 

  _renderDeleteDialog() {

    const first = this.state.data[this.state.dialog.idx];

    const nameguess = first[Object.keys(first)[0]];

    return (

      <Dialog

        modal={true}

        header="Confirm deletion"

        confirmLabel="Delete"

        onAction={this._deleteConfirmationClick.bind(this)}

      >

        {'Are you sure you want to delete "${nameguess}"?'}

      </Dialog>

    );

  }

 

  _renderFormDialog(readonly) {

    return (

      <Dialog

        modal={true}

        header={readonly ? 'Item info' : 'Edit item'}

        confirmLabel={readonly ? 'ok' : 'Save'}

        hasCancel={!readonly}

        onAction={this._saveDataDialog.bind(this)}

      >

      <Form

        ref="form"

        fields={this.props.schema}

        initialData={this.state.data[this.state.dialog.idx]}

        readonly={readonly} />

      </Dialog>

    );

  }

 

  _renderTable() {

    return (

      <table>

        <thead>

          <tr>{

            this.props.schema.map(item => {

              if (!item.show) {

                return null;

              }

              let title = item.label;

              if (this.state.sortby === item.id) {

                title += this.state.descending ? '

                  \u2191' : ' \u2193';

              }

              return (

                <th

                   className={'schema-${item.id}'}

                   key={item.id}

                   onClick={this._sort.bind(this, item.id)}

                >

                  {title}

                </th>

              );

            }, this)

          }

          <th className="ExcelNotSortable">Actions</th>

          </tr>

        </thead>

        <tbody onDoubleClick={this._showEditor.bind(this)}>

          {this.state.data.map((row, rowidx) => {

            return (

              <tr key={rowidx}>{

                 Object.keys(row).map((cell, idx) => {

                   const schema = this.props.schema[idx];

                   if (!schema || !schema.show) {

                     return null;

                   }

                   const isRating = schema.type === 'rating';

                   const edit = this.state.edit;

                   let content = row[cell];

                   if (!isRating && edit && edit.row === rowidx &&

                       edit.key === schema.id) {

                     content = (

                       <form onSubmit={this._save.

                         171006.pngbind(this)}>

                         <FormInput ref="input" {...schema}

                           defaultValue={content} />

                       </form>

                      );

                    } else if (isRating) {

                      content = <Rating readonly={true}

                        defaultValue={Number(content)} />;

                    }

                    return (

                    <td

                      className={classNames({

                        ['schema-${schema.id}']: true,

                        'ExcelEditable': !isRating,

                        'ExcelDataLeft': schema.align ===

                          'left',

                        'ExcelDataRight': schema.align ===

                          'right',

                        'ExcelDataCenter': schema.align !==

                          'left' &&

                          schema.align !== 'right',

                      })}

                      key={idx}

                      data-row={rowidx}

                      data-key={schema.id}>

                      {content}

                    </td>

                  );

                }, this)}

                <td className="ExcelDataCenter">

                   <Actions onAction={this._actionClick.

                     171011.pngbind(this, rowidx)} />

                </td>

              </tr>

            );

          }, this)}

       </tbody>

     </table>

    );

  }

}

 

Excel.propTypes = {

  schema: PropTypes.arrayOf(

     PropTypes.object

  ),

  initialData: PropTypes.arrayOf(

    PropTypes.object

  ),

  onDataChange: PropTypes.func,

};

 

export default Excel

Кое-что здесь требует более подробного разбора…

render() {

  return (

    <div className="Excel">

      {this._renderTable()}

      {this._renderDialog()}

    </div>

  );

}

Компонент выводит таблицу и (возможно) диалоговое окно. Диалоговое окно может быть окном подтверждения с вопросом наподобие «Вы действительно хотите выполнить удаление?», или формой для редактирования, или формой только для чтения, позволяющей лишь прочитать информацию об элементе. Или же может не быть никакого диалогового окна (исходное состояние). Когда устанавливается свойство dialog состояния this.state, данные отображаются заново, в результате чего, если это необходимо, появляется диалоговое окно.

А свойство dialog в состоянии устанавливается, когда пользователь выполняет щелчок на одной из кнопок компонента <Action>:

_actionClick(rowidx, action) {

  this.setState({dialog: {type: action, idx: rowidx}});

}

Когда данные в таблице изменяются (с использованием инструкции this.setState({data: /**/})), вы инициируете выдачу события изменения, уведомляющего родительский компонент, что он может обновить содержимое постоянного хранилища:

_fireDataChange(data) {

  this.props.onDataChange(data);

}

Обмен данными в обратном направлении (от родительского компонента Whinepad к дочернему Excel) осуществляется путем изменения родительским компонентом свойства initialData. Компонент Excel готов реагировать на такие изменения, используя код:

componentWillReceiveProps(nextProps) {

  this.setState({data: nextProps.initialData});

}

А как создается форма ввода данных (рис. 6.11)? Или окно просмотра данных (рис. 6.12)? Вы открываете компонент Dialog с компонентом Form внутри него. Эти настройки для формы берутся из schema, а данные для полей ввода — из this.state.data:

_renderFormDialog(readonly) {

  return (

    <Dialog

06_11.tif 

Рис. 6.11. Диалоговое окно редактирования  данных (U из акронима CRUD)

06_12.tif 

Рис. 6.12. Диалоговое окно просмотра данных  (R из акронима CRUD)

     modal={true}

      header={readonly ? 'Item info' : 'Edit item'}

      confirmLabel={readonly ? 'ok' : 'Save'}

      hasCancel={!readonly}

      onAction={this._saveDataDialog.bind(this)}

    >

    <Form

      ref="form"

      fields={this.props.schema}

      initialData={this.state.data[this.state.dialog.idx]}

      readonly={readonly} />

    </Dialog>

  );

}

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

_saveDataDialog(action) {

  if (action === 'dismiss') {

    this._closeDialog();

    // всего лишь устанавливает this.state.dialog в null

    return;

  }

  let data = Array.from(this.state.data);

  data[this.state.dialog.idx] = this.refs.form.getData();

  this.setState({

    dialog: null,

    data: data,

  });

  this._fireDataChange(data);

}

Если применять новый ES-синтаксис, то все сводится к более широкому использованию шаблона строк:

// До

"Are you sure you want to delete " + nameguess + "?"

 

// После

{'Are you sure you want to delete "${nameguess}"?'}

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

// До

<th className={"schema-" + item.id}}>

 

// После

<th className={'schema-${item.id}'}>

Возможно, самым странным синтаксисом будет тот, в котором строка шаблона заключается в квадратные скобки [] и используется как имя свойства в объекте. Это не имеет отношения к React, но вы можете посчитать любопытным тот факт, что следующий код также можно использовать со строками шаблона:

{

  ['schema-${schema.id}']: true,

  'ExcelEditable': !isRating,

  'ExcelDataLeft': schema.align === 'left',

  'ExcelDataRight': schema.align === 'right',

  'ExcelDataCenter': schema.align !== 'left' &&

    schema.align !== 'right',

}

Компонент <Whinepad>

Настал черед последнего компонента, являющегося родительским для всех других (рис. 6.13). Он проще табличного компонента Excel и имеет меньше зависимостей:

import Button from './Button';

// <- для добавления новой записи: "add new item"

import Dialog from './Dialog';

// <- для вывода на экран формы добавления новой

// записи: "add new item"

import Excel from './Excel';   // <- таблица со всеми записями

import Form from './Form';     // <- форма для добавления

                               // новой записи: "add new item"

import React, {Component, PropTypes} from 'react';

Компонент получает только два свойства — схему данных и существующие записи:

Whinepad.propTypes = {

  schema: PropTypes.arrayOf(

    PropTypes.object

  ),

  initialData: PropTypes.arrayOf(

    PropTypes.object

  ),

};

 

export default Whinepad;

06_13.tif 

Рис. 6.13. Компонент Whinepad в работе с операцией C из акронима CRUD

После того как вы досконально изучили весь код реализации компонента Excel, освоение этого кода не должно вызвать у вас никаких затруднений:

class Whinepad extends Component {

 

  constructor(props) {

    super(props);

    this.state = {

      data: props.initialData,

      addnew: false,

    };

    this._preSearchData = null;

  }

 

  _addNewDialog() {

    this.setState({addnew: true});

  }

 

  _addNew(action) {

    if (action === 'dismiss') {

      this.setState({addnew: false});

      return;

    }

    let data = Array.from(this.state.data);

    data.unshift(this.refs.form.getData());

    this.setState({

      addnew: false,

      data: data,

    });

    this._commitToStorage(data);

  }

 

  _onExcelDataChange(data) {

    this.setState({data: data});

    this._commitToStorage(data);

  }

 

  _commitToStorage(data) {

    localStorage.setItem('data', JSON.stringify(data));

  }

 

  _startSearching() {

    this._preSearchData = this.state.data;

  }

 

  _doneSearching() {

    this.setState({

      data: this._preSearchData,

    });

  }

 

  _search(e) {

    const needle = e.target.value.toLowerCase();

    if (!needle) {

      this.setState({data: this._preSearchData});

      return;

    }

    const fields = this.props.schema.map(item => item.id);

    const searchdata = this._preSearchData.filter(row => {

      for (let f = 0; f < fields.length; f++) {

        if (row[fields[f]].toString().toLowerCase().

          171017.pngindexOf(needle) > -1) {

          return true;

        }

      }

      return false;

    });

    this.setState({data: searchdata});

  }

  render() {

    return (

      <div className="Whinepad">

        <div className="WhinepadToolbar">

          <div className="WhinepadToolbarAdd">

            <Button

              onClick={this._addNewDialog.bind(this)}

              className="WhinepadToolbarAddButton">

              + add

            </Button>

          </div>

          <div className="WhinepadToolbarSearch">

            <input

              placeholder="Search..."

              onChange={this._search.bind(this)}

              onFocus={this._startSearching.bind(this)}

              onBlur={this._doneSearching.bind(this)} />

            </div>

          </div>

          <div className="WhinepadDatagrid">

            <Excel

              schema={this.props.schema}

              initialData={this.state.data}

              onDataChange={this._onExcelDataChange.

                171022.pngbind(this)} />

          </div>

          {this.state.addnew

            ? <Dialog

              modal={true}

              header="Add new item"

              confirmLabel="Add"

              onAction={this._addNew.bind(this)}

            >

            <Form

              ref="form"

              fields={this.props.schema} />

            </Dialog>

          : null}

      </div>

    );

  }

}

Обратите внимание на подписку компонента на изменение данных в Excel с использованием метода onDataChange, а также на то, что все данные сохраняются в локальном хранилище localStorage:

_commitToStorage(data) {

  localStorage.setItem('data', JSON.stringify(data));

}

В данном случае для сохранения данных не только на стороне клиента, но и на сервере можно было бы воспользоваться асинхронными запросами (также известными как технологии XHR, XMLHttpRequest, Ajax).

Подведение итогов

Как было показано в начале главы, основная точка входа в приложение — app.js. Сценарий app.js не является ни компонентом, ни модулем, и он ничего не экспортирует. На него возлагается лишь работа по инициализации — считывание имеющихся данных из localStorage и настройка компонента <Whinepad>:

'use strict';

 

import Logo from './components/Logo';

import React from 'react';

import ReactDOM from 'react-dom';

import Whinepad from './components/Whinepad';

import schema from './schema';

 

let data = JSON.parse(localStorage.getItem('data'));

 

// исходные данные примера, считывание из схемы

if (!data) {

  data = {};

  schema.forEach(item => data[item.id] = item.sample);

  data = [data];

}

 

ReactDOM.render(

  <div>

    <div className="app-header">

      <Logo /> Welcome to Whinepad!

    </div>

    <Whinepad schema={schema} initialData={data} />

  </div>,

  document.getElementById('pad')

);

И на этом создание приложения завершается. Вы можете поработать с ним на сайте и просмотреть его код по адресу /.

Назад: 5. Настройки для разработки приложения
Дальше: 7. Проверка качества кода, соответствия типов, тестирование, повтор

DalExcax
Весьма полезная штука ---- продажа оборудования окон пвх Здесь не может быть ошибки? ---- магазин большой одежды для женщин Я думаю, что Вы ошибаетесь. Пишите мне в PM, пообщаемся. ---- купить землю в сочи под строительство Теперь всё понятно, большое спасибо за информацию. ---- фонарь bailong bl Это просто замечательный ответ ---- отдых на море краснодарский край Присоединяюсь. Всё выше сказанное правда. Можем пообщаться на эту тему. ---- купить дом в сочи Я извиняюсь, но, по-моему, Вы не правы. Пишите мне в PM, пообщаемся. ---- как правильно кидать снюс Конечно. И я с этим столкнулся. Можем пообщаться на эту тему. ---- пицца краснодар меню Офигенно! Спасибо!!! ---- где купить аккаунт фейсбук для рекламы По моему мнению Вы не правы. ---- колумб играть бесплатно без регистрации
AnthonyTap
Это условность, ни больше, ни меньше minecraft who killed noob
stilnyeokna
Высококачественные пластиковые окошки плюс двери ото прямого изготовителя дают возможность заказчику сделать вполне индивидуальный приобретение в надобном наличие, потому наш магазин хотим клиентам сделать самые качественный сервис по производству и доставке окошек к тому же дверных проемов от иркутского изготовителя. На сайте указанного компании изготовление окон Вы можете посмотреть изготавливаемый ассортимент такой уникальной дверей затем сделать оснащение для индивидуальное помещение, коттедж либо большого жилого жилья. Наша компания указана единственным изготовщиком, в которого покупатель напрямую сможете купить пластиковые или алюминиевые окошки или двери, остекление лоджии, фасадные ансамбли у помещение, двойные входные концепции также иные виды застекления для личного помещения. Не включая низких расценок без наценок плюс диллеров компания предоставляем новым заказчикам гибкую систему уценок до сорока пяти процентов, потому по нашем сайте пользователи имеют возможность заказать окна и двери вовсе не только в Иркутске, но и на ближнюю район страны.
ecorfru
Разработка проектов касательно экологической консультации также подобная услуги, сведенная на полевыми-экологическими анализом плюс разработкой плана всяческих направлений –такая обязательная процедура, что исполняется только по закону и предоставляет выполнение норм исходя с охране внешней сферы. Компания Сиблидер выполняет полноценный величину действий именно в теме окружающей среды и предоставляет именно Вам лучшие условия сотрудничества касательно исполнение системного аспекта разработки всех видов документов и аутсорсинга на счет экологии нашими профессионалами. На источнике указанной фирмы организация государственной экологической экспертизы Вы имеют возможность пролистать всяческие подтверждающие удостоверения также разрешение, еще и заказать рекомендации от наших менеджеров либо услуги, какие фирма предоставляет. Наша компания максимально качественно просмотрим ваш заказ затем указываем наилучшие пути резолюции любой случая в небольшой промежуток времени, также еще специалисты совершаем хорошее инжиниринг юрилического лица от самого старта также к завершению договора. Заполняйте номер телефону на страничке и делайте работу в руки специалистами в области экологии каковы клиентам содействуют.
LikefilmsNet
Киносайт – является место, собственно где кинозритель найдет индивидуально такое-же по своей вкусу, определенно не найдете лучшего отвлечения от вашего рутины, чем включение захватывающего сериала обожаемого почерка, тот что зритель выбираете по личное предпочтение. Сайт качественных кинотеатра Зверополис 2 (2022) смотреть мультфильм онлайн бесплатно вмещает огромную число известных кинолент последних выпусков плюс анонсы данного 2021 года, какие постоянные посетители смогут просмотреть полностью в открытом доступе плюс без личного кабинета. Приспособленная навигация по сайту фильмов, мультфильмов либо сериалов легко обозначит кино у хорошем разрешении и звук, также Вы постоянно можете разделить популярное фильм из личными знакомыми с помощью нажатия кнопки социальной сети также написать свой отзыв, есле ж предполагаете разделить первые впечатлениями из другими зрителями. Здесь на презентованном сайте разных фильмов зритель сумеет подобрать фильмы именно по стилю, годам либо рейтингу, заходите и всегда пересматривайте лучшим кинокартиной с большим восторгом на нашему Like Films .
hd1080pred
День кинофильмов сейчас есть целостная порция вашего досуга по вечернее время и в течении дня, на свободных днях и регулярно, в момент изоляции или же небольшим сообщества потому вполне ценно найти сайт высококачественных фильмов, что постоянно рядом с добавленных. На нашем веб-сайте сборника кино Смотреть фильм Гарри Поттер и Тайная комната (2002) онлайн бесплатно и в хорошем качестве, ценители высококачественных показа смогут найти пред вышедшее показ, серийное кино или мультики, или определить видеофильм по жанру. В случае если посетитель всемирный обожатель фильмов, тогда благодаря веб-сайте фильмов есть возможность создать личный аккаунт, чтоб сохранять примечания, обозначить фильм, тот что надо посмотреть. На нашей визитной шапке всегда можно отследить популярные сюжеты, каковы ожидают на больших экранах к тому же увидеть видеоролик, а ожидаемое фильмы мы предоставляем под наших читателей только с хорошем hd разрешении, потому уверенно можете входить на ресурс потом кликнуть «Старт» указанный кино.
KinogoBlue
Зачастую мучаетесь касательно предмету, что стоит запустить интересное на вечерний досуг? На нашем онлайн кинотеатре бесплатного плюс новых кино Киного Я иду искать (2019) смотреть онлайн бесплатно пользователи можете мгновенно обнаружить хороший вариант кинофильма распространенного стиля с подмогой функционала навигации, отбора или же окна ввода. Сайт все это определил за посетителей также разработали просмотр нового кино более проще, именно на начальной стороне Вы сможет обозреть вновь свежую кино, известные многосерийное кино также предельно высокие показы, ну а во время когда надумаете увидеть трейлеры мировых фильмов этого времени, тогда кликайте в шапку «В скором времени в кино» и включайте известные спешные фильмы в прокате. Короткое воссоздание сюжета, сформированный показатель со стороны пользователей и подходящие примечания подсказывают польователю подобрать кино, какое понравится вовсе не лишь посетителю, однако и всем близким. Кликайте и ищите новую фильмы непосредственно сегодня!