Вы уже знаете, как создаются пользовательские React-компоненты, как составляется (отображается) пользовательский интерфейс с применением стандартных DOM-компонентов и ваших собственных пользовательских компонентов, как задаются свойства, осуществляется работа с состоянием, производится внедрение в жизненный цикл компонента и оптимизируется производительность путем отказа от отображения при отсутствии необходимости.
Объединим все это (и в процессе приобретем новые значения о React) путем создания более интересного компонента — таблицы данных.
Это будет нечто вроде раннего прототипа Microsoft Excel v.0.1.beta, позволяющего редактировать содержимое таблицы данных, а также производить сортировку, поиск (фильтрацию) и экспорт данных в виде загружаемых файлов.
Таблицы целиком завязаны на данных, поэтому компонент необычной таблицы (почему бы не назвать его Excel?) должен получать массив данных и массив заголовков. В целях тестирования позаимствуем список книг-бестселлеров из «Википедии»:
var headers = [
"Book", "Author", "Language", "Published", "Sales"
];
var data = [
["The Lord of the Rings", "J. R.R. Tolkien",
"English", "1954–1955", "150 million"],
["Le Petit Prince (The Little Prince)", "Antoine de Saint-Exupéry",
"French", "1943", "140 million"],
["Harry Potter and the Philosopher's Stone", "J. K. Rowling",
"English", "1997", "107 million"],
["And Then There Were None", "Agatha Christie",
"English", "1939", "100 million"],
["Dream of the Red Chamber", "Cao Xueqin",
"Chinese", "1754–1791", "100 million"],
["The Hobbit", "J. R.R. Tolkien",
"English", "1937", "100 million"],
["She: A History of Adventure", "H. Rider Haggard",
"English", "1887", "100 million"],
];
Первый шаг (просто чтобы было с чего начать) заключается в отображении одних лишь заголовков. Простейшая реализация должна выглядеть следующим образом:
var Excel = React.createClass({
render: function() {
return (
React.DOM.table(null,
React.DOM.thead(null,
React.DOM.tr(null,
this.props.headers.map(function(title) {
return React.DOM.th(null, title);
})
)
)
)
);
}
});
Теперь, располагая работоспособным компонентом, можно посмотреть, как им пользоваться:
ReactDOM.render(
React.createElement(Excel, {
headers: headers,
initialData: data,
}),
document.getElementById("app")
);
Результат этого начального примера показан на рис. 3.1.
Здесь появилось кое-что новое — применяемый с массивами метод map(), который используется для возвращения массива дочерних компонентов. Метод map() берет каждый элемент (в данном случае из массива headers) и передает его функции обратного вызова. Здесь функция обратного вызова создает и возвращает новый компонент <th>.
В этом и проявляется красота React: вы используете JavaScript для создания вашего пользовательского интерфейса и вам доступна вся мощь JavaScript. Циклы и задания условий работают в обычном порядке и вам для создания пользовательского интерфейса не нужно учить еще один язык создания шаблонов или синтаксис.
Рис. 3.1. Отображение заголовков таблицы
Вместо того, что вы видели до сих пор (как каждый дочерний компонент передавался в виде отдельных аргументов), дочерние компоненты можно передать компоненту в виде одного аргумента, представляющего собой массив. То есть работают оба варианта:
// отдельные аргументы
React.DOM.ul(
null,
React.DOM.li(null, 'one'),
React.DOM.li(null, 'two')
);
// массив
React.DOM.ul(
null,
[
React.DOM.li(null, 'one'),
React.DOM.li(null, 'two')
]
);
Копия экрана на рис. 3.1 показывает предупреждение, выведенное в консоли. О чем оно сообщает и как можно исправить ситуацию? В предупреждении говорится, что каждый дочерний компонент в массиве или итераторе должен иметь уникальное свойство key и что нужно проверить вызов функции отображения верхнего уровня, использующий <tr> (Warning: Each child in an array or iterator should have a unique “key” prop. Check the top-level render call using <tr>).
Проверить вызов функции отображения, использующий <tr>? Поскольку в приложении имеется только один компонент, нетрудно прийти к выводу, что проблема заключается в нем, но в реальной жизни у вас может быть множество компонентов, создающих элементы <tr>. Excel является простой переменной, которой присваивается React-компонент за пределами мира React, следовательно, React не в состоянии определить имя этого компонента. Вы можете ему помочь, объявив свойство displayName:
var Excel = React.createClass({
displayName: 'Excel',
render: function() {
// ...
}
};
Теперь React может идентифицировать источник проблемы и выдать вам предупреждение, сообщающее, что у каждого дочернего компонента в массиве должно быть уникальное свойство ключа key и что вам следует проверить метод отображения, принадлежащий компоненту Excel. Уже гораздо лучше. Но предупреждение все равно появляется. Чтобы исправить ситуацию теперь, когда известно, какой из методов render() «виноват», нужно просто сделать то, о чем говорится в предупреждении:
this.props.headers.map(function(title, idx) {
return React.DOM.th({key: idx}, title);
})
Что здесь происходит? Функции обратного вызова, передаваемые методу Array.prototype.map(), снабжаются тремя аргументами: значением массива, его индексом (0, 1, 2 и т.д.), а также всем массивом. Чтобы дать React свойство key, вы можете использовать индекс (idx) элемента массива и покончить с этим делом. Уникальность ключей должна соблюдаться только внутри этого массива, но не во всем React-приложении.
Теперь, когда проблема с ключами решена, можно с помощью небольшого участия CSS получить удовольствие от работы версии 0.0.1 вашего нового компонента — все имеет весьма достойный вид и дело обходится без предупреждения (рис. 3.2).
Рис. 3.2. Вывод на экран заголовков таблицы без появления предупреждения
Добавление displayName только в целях отладки может показаться лишними хлопотами, но их можно избежать: при использовании технологии JSX (рассматриваемой в главе 4) вам уже не придется определять это свойство, поскольку имя генерируется автоматически.
После получения вполне подходящего заголовка таблицы настало время добавить в нее основное наполнение. Содержимое заголовка представляет собой одномерный массив (одну строку), а вот данные наполнения имеют два измерения. Следовательно, вам нужны два цикла: один для прохода по строкам, второй — для прохода по данным (ячейкам) каждой строки. Это можно выполнить, используя те же самые циклы .map(), о порядке применения которых вы уже знаете:
data.map(function(row) {
return (
React.DOM.tr(null,
row.map(function(cell) {
return React.DOM.td(null, cell);
})
)
);
})
Еще нужно рассмотреть содержимое переменной data и ответить на вопрос: откуда оно берется и как изменяется? Код, вызывающий ваш компонент Excel, должен иметь возможность передавать данные для инициализации таблицы. Но позже, после появления таблицы, данные будут меняться, поскольку пользователь должен иметь возможность их сортировать, редактировать и т.д. Иными словами, состояние компонента будет изменяться. Поэтому воспользуемся свойством this.state.data для отслеживания изменений и свойством this.props.initialData, чтобы позволить вызывающему коду инициализировать компонент. Теперь полноценная реализация может приобрести следующий вид (результат показан на рис. 3.3):
getInitialState: function() {
return {data: this.props.initialData};
},
render: function() {
return (
React.DOM.table(null,
React.DOM.thead(null,
React.DOM.tr(null,
this.props.headers.map(function(title, idx) {
return React.DOM.th({key: idx}, title);
})
)
),
React.DOM.tbody(null,
this.state.data.map(function(row, idx) {
return (
React.DOM.tr({key: idx},
row.map(function(cell, idx) {
return React.DOM.td({key: idx}, cell);
})
)
);
})
)
)
);
}
В коде можно увидеть повторяющийся фрагмент {key: idx}, дающий уникальный ключ каждому элементу массива компонентов. Хотя все циклы .map() начинаются с индекса 0, проблемы не возникает, поскольку ключи должны быть уникальными только в текущем цикле, а не во всем приложении.
Функция render() уже приобретает трудноконтролируемый вид, особенно если дело касается отслеживания закрывающих символов } и ). Но не стоит переживать — технология JSX готова исправить ситуацию!
Рис. 3.3. Вывод на экран всей таблицы
В предыдущем фрагменте кода отсутствует свойство propTypes (чье присутствие необязательно, но зачастую желательно). Оно способствует проверке допустимости данных и документированию компонента. Применим конкретный подход и попробуем максимально сократить вероятность предоставления кем-либо нежелательных данных нашему привлекательному компоненту Excel. Объект React.PropTypes предлагает метод проверки массива, позволяющий убедиться в том, что свойство всегда является массивом. Но этим дело не заканчивается, в arrayOf можно также указать тип элементов массива. В данном случае зададим возможность использования для названия заголовков и для данных только строковых значений:
propTypes: {
headers: React.PropTypes.arrayOf(
React.PropTypes.string
),
initialData: React.PropTypes.arrayOf(
React.PropTypes.arrayOf(
React.PropTypes.string
)
),
},
Теперь наведен полный порядок!
Как можно усовершенствовать компонент? Использование в универсальной электронной таблице Excel одних только строковых значений можно назвать излишней ограничительной мерой. В качестве упражнения вы можете разрешить применение более широкого набора типов данных (React.PropTypes.any) и осуществить их отображение по-разному в зависимости от типа (например, применить выравнивание чисел по правому краю).
Сколько раз вам доводилось смотреть на таблицу на веб-странице и испытывать острое желание отсортировать ее данные по-другому? К счастью, с React это делается довольно просто. Это станет отличным примером того, как React с блеском справляется с поставленными задачами, поскольку вам нужно лишь отсортировать массив данных, а все обновления пользовательского интерфейса будут выполнены без вашего участия.
Сначала добавим в строку заголовка обработчик щелчка:
React.DOM.table(null,
React.DOM.thead({onClick: this._sort},
React.DOM.tr(null,
// ...
Теперь реализуем функцию _sort. Нужно знать, по какому столбцу производить сортировку, и эти сведения удобнее всего будет извлечь, воспользовавшись свойством cellIndex цели события (целью события является заголовок таблицы <th>):
var column = e.target.cellIndex;
Использование свойства cellIndex при разработке приложений встречается довольно редко. Оно определено еще в спецификации DOM Level 1 в качестве «индекса ячейки в строке», а чуть позже, в спецификации DOM Level 2, объявлено доступным только для чтения.
Вам также понадобится копия сортируемых данных. В противном случае, если воспользоваться принадлежащим массиву методом sort() напрямую, он внесет в массив изменения, а значит, метод this.state.data.sort() изменит значение this.state. Как вы уже знаете, значение this.state не должно изменяться напрямую, это осуществляется только с помощью метода setState():
// копирование данных
var data = this.state.data.slice();
// или 'Array.from(this.state.data)' в ES6
Теперь сортировка выполняется с помощью функции обратного вызова метода sort():
data.sort(function(a, b) {
return a[column] > b[column] ? 1 : -1;
});
И наконец, в следующей строке состояние настраивается под новые отсортированные данные:
this.setState({
data: data,
});
Теперь при щелчке на заголовке содержимое сортируется в алфавитном порядке (рис. 3.4).
Рис. 3.4. Сортировка книг по названию
Вот, собственно, и все — касаться отображения пользовательского интерфейса вам вообще не требуется. В методе render() вы уже раз и навсегда определили, как должен выглядеть компонент с получением данных. Когда данные изменяются, изменяется и пользовательский интерфейс, но это уже не ваша забота.
Как можно усовершенствовать компонент? Эта сортировка не отличается особой сложностью, но ее вполне хватает для рассмотрения возможностей React. Вы можете развивать фантазию, анализируя содержимое на предмет наличия числовых значений, с единицами измерения или без них и т.д.
Таблица отсортирована подходящим образом, но по какому именно столбцу — непонятно. Обновим пользовательский интерфейс, чтобы в нем на основе отсортированного столбца были показаны стрелки. А попутно еще реализуем сортировку по убыванию.
Чтобы отслеживать новое состояние, нужны два новых свойства.
• this.state.sortby. Индекс столбца, подвергаемого сортировке.
• this.state.descending. Булево значение для определения порядка выполняемой сортировки — по возрастанию или по убыванию.
getInitialState: function() {
return {
data: this.props.initialData,
sortby: null,
descending: false,
};
},
В функции _sort() нужно определить, в каком порядке будет вестись сортировка. По умолчанию она будет выполняться по возрастанию, если только индекс нового столбца не будет таким же, как и индекс столбца, по которому уже была произведена сортировка, и если эта сортировка не была выполнена в порядке убывания:
var descending = this.state.sortby === column && !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: column,
descending: descending,
});
Осталось лишь обновить функцию render() для обозначения направления сортировки. Просто добавим к названию текущего столбца, по которому выполняется сортировка, символ стрелки:
this.props.headers.map(function(title, idx) {
if (this.state.sortby === idx) {
title += this.state.descending ? ' \u2191' : ' \u2193'
}
return React.DOM.th({key: idx}, title);
}, this)
Вот теперь сортировку можно считать функционально завершенной — ее можно проводить по любому столбцу, щелкнув один раз для сортировки в порядке возрастания и тут же еще один раз для сортировки в порядке убывания; пользовательский интерфейс обновится с предоставлением визуальной индикации (рис. 3.5).
Рис. 3.5. Сортировка в порядке возрастания и убывания
Следующий шаг в создании компонента Excel — предоставить пользователям возможность редактировать содержимое таблицы. Один из способов включает следующие шаги.
1. Вы дважды щелкаете кнопкой мыши на ячейке. Компонент Excel определяет, на какой именно ячейке был сделан двойной щелчок, и превращает ее в поле ввода с ранее введенным в него содержимым (рис. 3.6).
Рис. 3.6. Ячейка таблицы превращается в поле ввода после двойного щелчка
2. Редактируете содержимое (рис. 3.7).
Рис. 3.7. Редактирование содержимого
3. Нажимаете клавишу Enter. Поле ввода исчезает, и таблица обновляется (прежний текст заменяется новым) (рис. 3.8).
Рис. 3.8. Содержимое обновляется после нажатия клавиши Enter
Первое, что нужно сделать, — настроить простой обработчик событий. По двойному щелчку компонент «запоминает» выбранную ячейку:
React.DOM.tbody({onDoubleClick: this._showEditor}, ....)
Обратите внимание на дружелюбную, легко читаемую форму записи onDoubleClick вместо определяемого в спецификации W3C ondblclick.
Посмотрим, как выглядит _showEditor:
_showEditor: function(e) {
this.setState({edit: {
row: parseInt(e.target.dataset.row, 10),
cell: e.target.cellIndex,
}});
},
Что здесь происходит?
• Функция устанавливает свойство edit состояния this.state. Этот свойство имеет значение null, когда редактирование не выполняется, а затем превращается в объект со свойствами row и cell, в которых содержатся индекс строки и индекс редактируемой ячейки. Итак, если вы выполните двойной щелчок на самой первой ячейке, this.state.edit получит значение {row: 0, cell: 0}.
• Чтобы определить индекс ячейки, используется, как и раньше, свойство e.target.cellIndex, где в качестве e.target фигурирует элемент <td>, на котором был сделан двойной щелчок.
• DOM-модель не предоставляет никакого свойства для индекса строки вроде rowIndex, поэтому получить этот индекс нужно самостоятельно через атрибут data-. У каждой ячейки должен быть атрибут data-row с индексом строки, который с помощью метода parseInt() можно проанализировать и превратить в целочисленное значение для получения обратно индекса строки.
Есть еще несколько уточнений и предварительных условий. Свойства edit прежде не существовало, и оно также должно быть инициализировано в методе getInitialState(), который теперь приобретет следующий вид:
getInitialState: function() {
return {
data: this.props.initialData,
sortby: null,
descending: false,
edit: null, // {row: index, cell: index}
};
},
Для отслеживания индексов строк понадобится свойство data-row. Посмотрим, на что похожа вся конструкция tbody():
React.DOM.tbody({onDoubleClick: this._showEditor},
this.state.data.map(function(row, rowidx) {
return (
React.DOM.tr({key: rowidx},
row.map(function(cell, idx) {
var content = cell;
// TODO (что нужно сделать) – превратить 'содержимое'
// в поле ввода, если 'idx' и 'rowidx' соответствуют
// редактируемой ячейке; в противном случае просто
// показать содержимое
return React.DOM.td({
key: idx,
'data-row': rowidx
}, content);
}, this)
)
);
}, this)
)
И наконец, нужно сделать то, что предписано комментарием TODO. Создадим поле ввода там, где это требуется. Из-за вызова setState(), устанавливающего свойство edit, снова вызывается вся функция render(). React отображает таблицу, которая предоставляет возможность обновления ячейки, на которой будет сделан двойной щелчок.
Посмотрим на код, заменяющий комментарий TODO. Сначала нужно вспомнить о состоянии редактирования:
var edit = this.state.edit;
Проверяем, установлено ли свойство edit, и если да, то именно ли эта ячейка редактируется:
if (edit && edit.row === rowidx && edit.cell === idx) {
// ...
}
Если это целевая ячейка, создадим форму и поле ввода с содержимым этой ячейки:
content = React.DOM.form({onSubmit: this._save},
React.DOM.input({
type: 'text',
defaultValue: content,
})
);
Как видите, это форма с одним полем ввода, которое предварительно уже заполнено текстом из ячейки. При отправке формы управление перехватится и будет передано закрытому методу _save().
Последний фрагмент пазла редактирования — сохранение изменений содержимого после того, как пользователь завершит набор текста и отправит форму (путем нажатия клавиши Enter):
_save: function(e) {
e.preventDefault();
// ... выполнение сохранения
},
После исключения возможности поведения по умолчанию (чтобы страница не перезагружалась) нужно получить ссылку на поле ввода:
var input = e.target.firstChild;
Клонируем данные, чтобы не пришлось работать с this.state напрямую:
var data = this.state.data.slice();
Обновляем часть данных, используя новое значение и индексы ячейки и строки, сохраненные в свойстве edit состояния state:
data[this.state.edit.row][this.state.edit.cell] = input.value;
И наконец, устанавливаем состояние, вызывая тем самым повторное отображение пользовательского интерфейса:
this.setState({
edit: null, // редактирование выполнено
data: data,
});
Итак, с редактированием покончено. Уложились в весьма скромный объем кода. Нам для этого потребовалось всего лишь следующее:
• отследить с помощью this.state.edit, какую из ячеек редактировать;
• отобразить поле ввода при показе таблицы, если индексы строки и ячейки соответствуют ячейке, на которой пользователь сделал двойной щелчок;
• обновить массив данных новым значением из поля ввода.
Как только метод setState() будет вызван с новыми данными, React вызовет принадлежащий компоненту метод render() — и пользовательский интерфейс волшебным образом обновится. Можно подумать, что нерационально было бы отображать всю таблицу из-за изменения содержимого всего одной ячейки. Фактически React только одну ячейку и обновляет.
Если открыть инструментарий разработчика вашего браузера, можно увидеть, какая часть DOM-дерева обновлена в ходе взаимодействия с вашим приложением. На рис. 3.9 показана область инструментария разработчика, в которой выделена та часть DOM-модели, которая была изменена при внесении правки в поле языка для книги с названием The Lord of the Rings, превращающей English в Engrish.
Рис. 3.9. Выделение изменений в DOM-модели
Закулисно React вызывает метод render() и создает упрощенное представление дерева желаемого результата, достигаемого в DOM-модели. Это представление известно как виртуальное DOM-дерево.
Когда метод render() вызывается снова (к примеру, после вызова setState()), React сравнивает виртуальное дерево до и после этого вызова и вычисляет различие. Основываясь на этом различии, React определяет для выполнения нужного изменения в DOM-модели браузера минимально требуемые DOM-операции (например, appendChild(), textContent и т.д.).
На рис. 3.9 показано, что потребовалось только одно изменение ячейки и нет необходимости повторно отображать всю таблицу. Вычисляя минимальный набор изменений и объединяя DOM-операции в пакеты, React тем самым очень бережно относится к DOM-модели в силу известной проблемы с неспешным выполнением DOM-операций (по сравнению с чистыми операциями JavaScript, вызовами функций и т.д.), поскольку зачастую узким местом солидных веб-приложений является производительность операций, связанных с отображением данных.
Короче говоря, когда дело касается производительности и обновления пользовательского интерфейса, React подставляет вам свое плечо путем:
• бережного отношения к DOM-модели;
• использования делегирования событий при взаимодействии с пользователем.
А теперь добавим к компоненту Excel функцию поиска, позволяющую пользователям выполнять фильтрацию содержимого таблицы. План таков:
• добавить кнопку для включения и выключения новой функции (рис. 3.10);
Рис. 3.10. Кнопка поиска
• если поиск включен, добавить строку полей ввода, каждое из которых предназначено для поиска в соответствующем столбце (рис. 3.11);
Рис. 3.11. Строка полей ввода для поиска и фильтрации
• по мере того как пользователь набирает текст в поле ввода, выполнять фильтрацию массива state.data, чтобы показывалось только соответствующее содержимое (рис. 3.12).
Рис. 3.12. Результаты поиска
Сначала нужно добавить к объекту this.state свойство search, чтобы отслеживать, включена или нет функция поиска:
getInitialState: function() {
return {
data: this.props.initialData,
sortby: null,
descending: false,
edit: null, // [row index, cell index],
search: false,
};
},
Затем наступает очередь обновления пользовательского интерфейса. Чтобы упростить сопровождение кода, разобьем функцию render() на небольшие специализированные части. До сих пор функция render() занималась только отображением таблицы. Переименуем ее в _renderTable(). Далее кнопка поиска (Search) должна стать частью полноценной панели инструментов (вскоре к ней добавится кнопка экспорта Export), поэтому сделаем ее отображение частью функции отображения панели инструментов под названием _renderToolbar().
Результат выглядит следующим образом:
render: function() {
return (
React.DOM.div(null,
this._renderToolbar(),
this._renderTable()
)
);
},
_renderToolbar: function() {
// TODO
},
_renderTable: function() {
// тот же код, что и у функции,
// прежде известной как 'render()'
},
Как видите, новая функция render() возвращает контейнер div с двумя дочерними элементами: панелью инструментов и таблицей. Вы уже знаете, как выглядит отображение таблицы, а панель инструментов пока что представлена всего лишь одной кнопкой:
_renderToolbar: function() {
return React.DOM.button(
{
onClick: this._toggleSearch,
className: 'toolbar',
},
'search'
);
},
Если поиск включен (это означает, что у свойства this.state.search значение true), вам нужна новая строка таблицы, заполненная полями ввода. Создадим функцию _renderSearch(), которая позаботится о появлении этой строки:
_renderSearch: function() {
if (!this.state.search) {
return null;
}
return (
React.DOM.tr({onChange: this._search},
this.props.headers.map(function(_ignore, idx) {
return React.DOM.td({key: idx},
React.DOM.input({
type: 'text',
'data-idx': idx,
})
);
})
)
);
},
Как видите, если поиск не включен, функции не нужно ничего отображать — и она возвращает null. Другой вариант, разумеется, заключается в том, чтобы решение было принято в вызывающем эту функцию коде — и она вообще бы не вызывалась, если поиск не включен. Но предыдущий пример помогает немного упростить уже загруженную работой функцию _renderTable(). Вот что нужно сделать функции _renderTable():
До:
React.DOM.tbody({onDoubleClick: this._showEditor},
this.state.data.map(function(row, rowidx) { // ...
После:
React.DOM.tbody({onDoubleClick: this._showEditor},
this._renderSearch(),
this.state.data.map(function(row, rowidx) { // ...
Поля ввода поиска всего лишь еще один дочерний узел перед основным циклом data (тем, который создает все строки и ячейки таблицы). Когда _renderSearch() возвращает null, React просто не отображает дополнительный дочерний узел и переходит к отображению таблицы.
Итак, все об обновлениях пользовательского интерфейса уже сказано. С вашего позволения, рассмотрим сам механизм поиска, «деловую логику», то есть фактический поиск.
Функция поиска представляется довольно простой: взять массив данных, вызвать в отношении него метод Array.prototype.filter() и возвратить отфильтрованный массив с элементами, соответствующими строке поиска.
Пользовательский интерфейс по-прежнему использует для отображения this.state.data, но это значение this.state.data является урезанной версией предыдущего значения.
Прежде чем вести поиск, нужно скопировать данные, чтобы они не были утеряны навсегда. Это позволит пользователю вернуться к полной таблице или изменить строку поиска для получения другого соответствия. Назовем эту копию (фактически ссылку) _preSearchData:
var Excel = React.createClass({
// содержимое...
_preSearchData: null,
// еще содержимое...
});
Когда пользователь нажимает кнопку Search, вызывается функция _toggleSearch(). Задача этой функции — запускать и останавливать поиск. Свою задачу она выполняет путем:
• установки для this.state.search значения true или false соответственно;
• запоминания прежних данных, когда поиск включается;
• возвращения к старым данным при выключении поиска.
Функция может принять следующий вид:
_toggleSearch: function() {
if (this.state.search) {
this.setState({
data: this._preSearchData,
search: false,
});
this._preSearchData = null;
} else {
this._preSearchData = this.state.data;
this.setState({
search: true,
});
}
},
Остается лишь реализовать функцию _search(), вызываемую при каждом изменении в строке поиска, означающем, что пользователь набрал что-то в одном из полей ввода. Полная реализация с некоторыми дополнительными особенностями имеет следующий вид:
_search: function(e) {
var needle = e.target.value.toLowerCase();
if (!needle) { // строка поиска будет удалена
this.setState({data: this._preSearchData});
return;
}
var idx = e.target.dataset.idx; // в каком столбце искать
var searchdata = this._preSearchData.filter(function(row) {
return row[idx].toString().toLowerCase().indexOf(needle)
> -1;
});
this.setState({data: searchdata});
},
Строка поиска берется из измененной цели события (являющейся полем ввода):
var needle = e.target.value.toLowerCase();
Если там нет строки поиска (пользователь стер набранное), функция берет исходные кэшированные данные — и они становятся новым состоянием:
if (!needle) {
this.setState({data: this._preSearchData});
return;
}
Если строка поиска имеется, происходит фильтрация исходных данных — и отфильтрованные результаты устанавливаются в качестве нового состояния данных:
var idx = e.target.dataset.idx;
var searchdata = this._preSearchData.filter(function(row) {
return row[idx].toString().toLowerCase().indexOf(needle) > -1;
});
this.setState({data: searchdata});
И на этом функцию поиска можно считать выполненной. Для реализации данной функции вам пришлось сделать всего лишь следующее:
• добавить пользовательский интерфейс поиска;
• показывать или скрывать новый пользовательский интерфейс по мере надобности;
• создать «деловую логику», представляющую собой вызов метода filter() в отношении массива.
Ничего из исходного отображения таблицы менять не пришлось. Как и всегда, все заботы свелись к состоянию ваших данных и к разрешению React позаботиться об отображении (и о работе, связанной с DOM-моделью) при каждом изменении состояния данных.
Это был простой рабочий пример, использованный для иллюстрации возможностей. Можно ли усовершенствовать функцию?
Простым усовершенствованием может стать переключение надписи на кнопке поиска. Чтобы, когда поиск включен (this.state.search === true), она сообщала: «Выполняется поиск».
Можно еще попробовать реализовать дополнительный поиск в нескольких полях, то есть фильтровать уже отфильтрованные данные. Если пользователь в строке языка набирает Eng, а затем ведет поиск, используя другое поле ввода данных для поиска, почему бы не провести поиск только в результатах предыдущего поиска? Как бы вы реализовали такую функцию?
Теперь уже известно, что ваши компоненты «заботятся» о своем состоянии и позволяют React выполнять свое отображение и повторное отображение при соответствующих обстоятельствах. Это означает, что при одних и тех же данных (состоянии и свойствах) приложение будет выглядеть абсолютно одинаково независимо от того, что изменилось до или после этого конкретного состояния данных. Это предоставляет вам отличную возможность для проведения отладки в реальных условиях.
Представим, что кто-то при использовании вашего приложения обнаружил ошибку; он может щелкнуть на кнопке для создания отчета об ошибке без объяснения того, что случилось. Этот отчет может отправить вам копию this.state и this.props — и вы сможете воссоздать точное состояние приложения и посмотреть на визуальный результат.
Откат может стать еще одной функцией на основе того, что React отображает ваше приложение одинаково при задании тех же самых свойств и состояния. Откат реализуется довольно просто: нужно всего лишь вернуться к предыдущему состоянию.
Ради интереса немного разовьем эту мысль. Мы будем записывать каждое изменение состояния в компоненте Excel, а затем воспроизводить его. Весьма интересно будет понаблюдать за разворачивающейся перед вами картиной всех ваших действий в обратном порядке.
В плане реализации не станем задаваться вопросом, когда произошло изменение, а просто проиграем изменения состояний приложения с односекундным интервалом. Для реализации данной функции вам понадобится всего лишь добавить метод _logSetState() и осуществить поиск-замену всех вызовов setState() вызовами новой функции.
Итак, все вызовы:
this.setState(newSate);
становятся:
this._logSetState(newState);
Методу _logSetState предстоит выполнять два действия: регистрировать новое состояние, а затем передавать его по эстафете методу setState(). Вот как выглядит один из примеров, где делается глубокая копия состояния, добавляемая к this._log:
var Excel = React.createClass({
_log: [],
_logSetState: function(newState) {
// запоминание старого состояния в клоне
this._log.push(JSON.parse(JSON.stringify(
this._log.length === 0 ? this.state : newState
)));
this.setState(newState);
},
// ...
});
Теперь, после регистрации всех изменений состояния, проиграем их назад. Для запуска проигрывания добавим простой отслеживатель события, перехватывающий действия с клавиатурой и запускающий функцию _replay():
componentDidMount: function() {
document.onkeydown = function(e) {
if (e.altKey && e.shiftKey && e.keyCode === 82) {
// ALT+SHIFT+R(eplay)
this._replay();
}
}.bind(this);
},
И наконец, добавим метод _replay(). Он использует setInterval() и один раз в секунду считывает следующий объект из регистрационного журнала и передает его методу setState():
_replay: function() {
if (this._log.length === 0) {
console.warn('Состояния для проигрывания отсутствуют');
return;
}
var idx = -1;
var interval = setInterval(function() {
idx++;
if (idx === this._log.length - 1) { // конец
clearInterval(interval);
}
this.setState(this._log[idx]);
}.bind(this), 1000);
},
Что если реализовать функцию «откат-возвращение»? Скажем, когда нажимают сочетание клавиш Alt+Z, происходит откат на один шаг в журнале состояния, а при нажатии сочетания Alt+Shift+Z выполняется один шаг вперед по записям журнала.
Существует ли другой способ реализации функциональных возможностей типа «воспроизведение-откат» без изменения всех ваших вызовов setState()? Может быть, стоит для этого воспользоваться соответствующим методом управления жизненным циклом компонента (рассмотренным в главе 2)?
После всех сортировок, редактирований и поисков пользователь наконец-то доволен состоянием данных в таблице. Было бы неплохо, если бы он мог скачать результат своих усилий, чтобы с этим можно было поработать в другое время.
К счастью, в React это сделать проще простого. Нужно всего лишь забрать текущее значение this.state.data и вернуть его назад в JSON- или CSV-формате.
На рис. 3.13 показан конечный результат щелчка пользователем на кнопке Export CSV, скачивания файла data.csv (посмотрите в нижний левый угол окна браузера) и открытия этого файла в Microsoft Excel.
Рис. 3.13. Экспорт данных таблицы в Microsoft Excel с использованием CSV-формата
Сначала нужно добавить новые возможности на панель инструментов. Воспользуемся для этого небольшой магией HTML5, заставляющей ссылки, образуемые тегом <a>, запускать скачивание файлов, и сделаем так, чтобы с использованием технологии CSS новые «кнопки» становились ссылками с видом кнопок:
_renderToolbar: function() {
return React.DOM.div({className: 'toolbar'},
React.DOM.button({
onClick: this._toggleSearch
}, 'Search'),
React.DOM.a({
onClick: this._download.bind(this, 'json'),
href: 'data.json'
}, 'Export JSON'),
React.DOM.a({
onClick: this._download.bind(this, 'csv'),
href: 'data.csv'
}, 'Export CSV')
);
},
Теперь обратимся к функции скачивания _download(). По сравнению с легкостью экспортирования в формате JSON для использования формата CSV нужно приложить немного больше усилий. Проще говоря, надо последовательно перебрать все строки и ячейки в каждой строке, создавая длинное строковое значение. Как только это будет сделано, функция инициирует скачивание с помощью атрибута download и большого двоичного объекта href, созданного с использованием window.URL:
_download: function(format, ev) {
var contents = format === 'json'
? JSON.stringify(this.state.data)
: this.state.data.reduce(function(result, row) {
return result
+ row.reduce(function(rowresult, cell, idx) {
return rowresult
+ '"'
+ cell.replace(/"/g, '""')
+ '"'
+ (idx < row.length - 1 ? ',' : '');
}, '')
+ "\n";
}, '');
var URL = window.URL || window.webkitURL;
var blob = new Blob([contents], {type: 'text/' + format});
ev.target.href = URL.createObjectURL(blob);
ev.target.download = 'data.' + format;
},