<file_sep>/**
 * Серверная часть
 */
const express = require('express');
const path = require('path');
const app = express();
const port = process.env.PORT || 7000;

if (process.env.NODE_ENV === 'production') {
  app.use(express.static(path.join(__dirname, 'build')));
}

app.listen(port, () => console.log(`Listening on port ${ port }`));<file_sep>/**
 * Компонент тела таблицы
 */
import React, {Component} from 'react';
import {arrayOf, func, number, shape, string} from 'prop-types';
import Cell from './Cell/Cell';
import styles from './TBody.scss';
import TextInput from '../../TextInput/TextInput';
import {EDIT_TABLE_PROP_TYPES} from '../../common/globalPropTypes';
import {BTN_DEL_TITLE, BTN_DEL_VALUE, DEF_PLACE_HOLDER, UPDATE_DELAY} from '../../common/settings';

class TBody extends Component {
  constructor(props) {
    super(props);
    this.changeTimer = null;
    this.state = {
      changedRows: {},
      error: false,
    };
  }

  componentDidCatch(error, info) {
    this.setState({error, info});
  }

  componentWillReceiveProps(nextProps) {
    if (this.props !== nextProps) {
      clearTimeout(this.changeTimer);
      this.setState({changedRows: {}});
    }
  }

  componentWillUnmount() {
    clearTimeout(this.changeTimer);
  }

  shouldComponentUpdate(nextProps, nextState) {
    return (this.props !== nextProps);
  }

  /**
   * Обработчик удаления строик
   * @param e - event
   * @param {number} rowIndex - номер строки
   */
  onDelRow =
    (e, rowIndex) => {
      const changedRows_ = this.state.changedRows;

      // прерываем таймер ожидания на разрешение ререндеринга
      clearTimeout(this.changeTimer);

      if (Object.keys(changedRows_).length === 0) {
        this.props.delRow(rowIndex);
      } else {
        this.props.updateRow(changedRows_, rowIndex);
      }
    };

  /**
   * Функция записи нового значения в список изменений, которые будут учтены при простое изменения за UPDATE_DELAY мсек
   * @param {string} value - значение
   * @param {number} rowIndex - номер строки
   * @param {number} colIndex - номер поля
   */
  doStateChangedCell = (value, rowIndex, colIndex) => {
    const changedRows = this.state.changedRows;

    if (!changedRows.hasOwnProperty(rowIndex)) {
      changedRows[rowIndex] = {};
    }

    changedRows[rowIndex][colIndex] = value;

    return changedRows;
  };
  /**
   * Обработчик изменения значения текстового поля. Минимум через UPDATE_DELAY мсек запускается ререндеринг
   * @param {string} value - значение
   * @param {number} rowIndex - номер строки
   * @param {number} colIndex - номер поля
   */
  handleChange = (value, rowIndex, colIndex) => {
    // прерываем таймер ожидания на разрешение ререндеринга
    clearTimeout(this.changeTimer);

    // записываем обновленное значение в список изменений, которые будут учтены при простое изменения за UPDATE_DELAY мсек
    const changedRows = this.doStateChangedCell(value, rowIndex, colIndex);

    this.setState({changedRows});

    // и запускаем таймер ожидания на разрешение ререндеринга
    this.changeTimer = setTimeout(() => this.props.updateRow(changedRows), UPDATE_DELAY);
  };

  render() {
    if (this.state.info && this.state.info.componentStack) {
      console.log(this.state.info.componentStack);
    }

    if (this.state.error) {
      return <h2>Error: {this.state.error.toString()}</h2>;
    }

    const {
      rows,
    } = this.props;

    return (
      <tbody className={styles['t-body']}>
      {rows.map((rowData, rowIndex) => {
        const uniqID = rowData.id;
        const inputEditID1 = `ed1_${rowIndex}`;
        const inputEditValue1 = rowData.value1;
        const inputEditID2 = `ed2_${rowIndex}`;
        const inputEditValue2 = rowData.value2;

        return (
          <tr key={rowIndex}>
            <Cell key={`${rowIndex}.1`} value={`${uniqID}`}>{uniqID}</Cell>
            <Cell key={`${rowIndex}.2`} value={inputEditValue1}>
              <label htmlFor={inputEditID1}>{inputEditValue1}</label>
              <TextInput
                id={inputEditID1}
                placeholder={DEF_PLACE_HOLDER}
                value={inputEditValue1}
                onChange={value => this.handleChange(value, rowIndex, 1)}
              />
            </Cell>
            <Cell key={`${rowIndex}.3`} value={inputEditValue2}>
              <label htmlFor={inputEditID2}>{inputEditValue2}</label>
              <TextInput
                id={inputEditID2}
                placeholder={DEF_PLACE_HOLDER}
                value={inputEditValue2}
                onChange={value => this.handleChange(value, rowIndex, 2)}
              />
            </Cell>
            <Cell key={`${rowIndex}.4`} value={BTN_DEL_VALUE}>
              <input type="button" key={`${rowIndex}.4.1`} onClick={e => this.onDelRow(e, rowIndex)}
                     value={BTN_DEL_VALUE} title={BTN_DEL_TITLE}/>
            </Cell>
          </tr>
        )
      })}
      </tbody>
    )
  }
}

TBody.propTypes = {
  ...EDIT_TABLE_PROP_TYPES,
};

export default TBody;<file_sep>/**
 * Компонент текстового поля
 */
import React, {Component} from 'react'
import {func, string} from 'prop-types';
import {MAX_LENGTH} from '../common/settings';

class Input extends Component {
  constructor(props) {
    super(props);
    this.timer = null;
    this.state = {
      value: props.value,
    };
  }

  componentDidCatch(error, info) {
    this.setState({error, info});
  }

  componentWillReceiveProps(nextProps) {
    if (this.props.value !== nextProps.value) {
      this.setState({
        value: nextProps.value,
      });
    }
  }

  /**
   * Обработчик изменения значения
   * @param e
   */
  handleChange = e => {
    this.setState({value: e.target.value});
  };

  /**
   * Обработчик потери фокуса
   * @param e
   */
  handleBlur = e => {
    this.setState({value: e.target.value});
    this.triggerChange();
  };

  /**
   * Обработчик зажатия клавиши
   * @param e
   */
  handleKeyDown = e => {
    if (e.key === 'Escape') {
      this.triggerChange();
    }
  };

  /**
   * Обработчик отпускания клавиши
   * @param e
   */
  handleKeyUp = e => this.triggerChange();

  /**
   * Триггер вызова изменения значения
   */
  triggerChange = () => {
    this.props.onChange(this.state.value);
  };

  render() {
    if (this.state.info && this.state.info.componentStack) {
      console.log(this.state.info.componentStack);
    }

    if (this.state.error) {
      return <h2>Error: {this.state.error.toString()}</h2>;
    }

    const {
      id,
      placeholder,
    } = this.props;

    return (
      <input
        id={id}
        type="text"
        placeholder={placeholder}
        value={this.state.value}
        onChange={this.handleChange}
        onBlur={this.handleBlur}
        onKeyDown={this.handleKeyDown}
        onKeyUp={this.handleKeyUp}
        maxLength={MAX_LENGTH}
      />
    )
  }
}

Input.propTypes = {
  id: string,
  placeholder: string,
  value: string.isRequired,
  onChange: func.isRequired,
};

export default Input;<file_sep>/**
 * Компонент таблицы
 */
import React from 'react';
import {arrayOf, shape, number, string, func} from 'prop-types';
import styles from './EditTable.scss';
import HeaderCell from './TBody/Cell/Cell';
import TBody from './TBody/TBody';
import {EDIT_TABLE_PROP_TYPES} from '../common/globalPropTypes';

class EditTable extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      error: false,
    };
  }

  componentDidCatch(error, info) {
    this.setState({error, info});
  }

  shouldComponentUpdate(nextProps, nextState) {
    return (this.props !== nextProps);
  }

  render() {
    if (this.state.info && this.state.info.componentStack) {
      console.log(this.state.info.componentStack);
    }

    if (this.state.error) {
      return <h2>Error: {this.state.error.toString()}</h2>;
    }

    const {
      ...props
    } = this.props;

    return (
      <table className={styles.table}>
        <thead>
        <tr>
          <HeaderCell isTh={true} value="ID">ID</HeaderCell>
          <HeaderCell isTh={true} value="Значение 1">Значение 1</HeaderCell>
          <HeaderCell isTh={true} value="Значение 2">Значение 2</HeaderCell>
          <HeaderCell isTh={true} value="-" updateRow={props.updateRow}/>
        </tr>
        </thead>
        {props.rows.length > 0 && <TBody {...props}/>}
      </table>
    )
  }
}

EditTable.propTypes = {
  ...EDIT_TABLE_PROP_TYPES,
};

export default EditTable;<file_sep>/**
 * Библиотека PropTypes
 */
import {arrayOf, func, number, shape, string} from 'prop-types';

/**
 * PropTypes для компонента EditTable
 * @type {{rows: *, updateRow: *, delRow: *}}
 */
export const EDIT_TABLE_PROP_TYPES = {
  rows: arrayOf(
    shape({
      id: number,
      value2: string,
      value1: string
    })
  ).isRequired,
  updateRow: func.isRequired,
  delRow: func.isRequired,
};<file_sep>Проект **«D-table»** позволяет манипулировать данными и исследовать рендеринг элементов.
В правом верхнем углу страницы расположен навигатор с тремя кнопками: 
- кнопка добавления строки,
- кнопка генерации таблицы со случайными данными,
- кнопка очистки таблицы.<br>

Если у текстового поля возникло событие потери фокуса - начинается таймер ререндеринга. Но если какое-то иное поле получило потерю фокуса и при этом не успел запуститься ререндеринг - перезапускается таймер (запуск ренрендеринга откладывается).
При ререндеринге элемента меняется цвет фона элемента. Таким образом, можно наблюдать, какой элемент перерисован при манипулировании данными.<br>
*Также присутствует простая серверная часть приложения для возможного расширения функционала в будущем.*

Данный проект разработан на базе [Create React App](https://github.com/facebookincubator/create-react-app).

## Содержание

- [Настройки приложния](#Настройки-приложния)
- [Доступные команды](#Доступные-команды)
  - [npm run client](#npm-run-client)
  - [npm run build](#npm-run-build)
  - [npm run server](#npm-run-server)
  - [npm run start-dev](#npm-run-start-dev)
  - [npm run start-prod](#npm-run-start-prod)

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

Настройки с комментариями приложения хранятся в файле `src/app/common/settings.js`.
В частности, можно настраивать длительность задержки ререндеринга при редактировании (`UPDATE_DELAY`), настраивать варианты цветов для наблюдения ререндеринга элементов (`COLORS`), указать количество строк для построения таблицы со случайными данными (`ROWS_DEF_COUNT`) и т.д.

## Доступные команды

Команды запускаются в консоле. В корневом каталоге Вы можете использовать следующие команды:

### `npm run client`

Запускается клиентская часть приложения в режиме разработки (development). Открывается страница [http://localhost:3000](http://localhost:3000) в браузере.<br>
В случае внесения изменений в исходниках страница будет перезагружена автоматически. Также Вы можете увидеть lint ошибки в консоле. 

### `npm run build`

Собирается клиентская часть приложения в режиме продакшена (production) с минифицированными исходниками для лучшей производительности в каталоге `build`.<br>

Ваше клиентское приложение готово для деплоя!

### `npm run server`

Запускается серверная часть приложения.<br>
После успешного запуска в консоле будет выводиться строка `Listening on port 7000`.

### `npm run start-dev`

Запускается серверная и клиентская части приложения в режиме разработки (development).<br>
При успешном запуске серверной и клиентской части приложения открывается страница [http://localhost:3000](http://localhost:3000) в браузере.
В случае внесения изменений в исходниках страница будет перезагружена автоматически.<br>
Также Вы можете увидеть lint ошибки в консоле. 

### `npm run start-prod`

Собирается и запускается приложение в режиме продакшена (production).<br>
Сначала собирается клиентская часть приложения с минифицированными исходниками для лучшей производительности в каталоге `build`. Затем запускается серверная и клиентская части приложения. После успешного запуска в консоле будет выводиться строка `Listening on port 7000`. Вы можете открыть страницу [http://localhost:7000](http://localhost:7000) в браузере.<br>

Ваше приложение готово для деплоя!
<file_sep>/**
 * Главный компонент приложения
 */
import React from 'react';
import styles from './App.scss';
import EditTable from './EditTable/EditTable';
import {getRandomInt} from './common/lib';
import Nav from './Nav/Nav';
import {ROWS_DEF_COUNT} from './common/settings';

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      rows: [],
      lastID: 0,
      error: false,
    };
  }

  componentDidCatch(error, info) {
    this.setState({error, info});
  }

  shouldComponentUpdate(nextProps, nextState) {
    return (this.state !== nextState);
  }

  /**
   * Генератор массива со случайными значениями
   * @param {number} countRow - длина массива
   * @returns {{id: number, value1: string, value2: string}[]}
   */
  genRandomArray = countRow => Array.from(Array(countRow), (_, x) => ({
    id: x + 1,
    value1: 'Значение ' + getRandomInt(x, x + countRow),
    value2: 'Значение ' + getRandomInt(x, x + countRow),
  }));

  /**
   * Генератор строк со случайными значениями
   * @param {number} rowCount - количество строк
   * @returns {{rows: {id: number, value1: string, value2: string}[], lastID: number}}
   */
  genRows = rowCount => ({
    rows: this.genRandomArray(rowCount),
    lastID: rowCount,
  });

  /**
   * Регенератор строк со случайными значениями
   * @param {number} countRows - количество генерируемых строк
   */
  reGenRows = countRows => {
    this.setState({...this.genRows(countRows)});
  };

  /**
   * Обработчик добавления строки
   */
  onAddRow = () => {
    let _lastID = this.state.lastID;

    this.setState({
      rows: [
        ...this.state.rows,
        {
          id: ++_lastID,
          value1: '',
          value2: '',
        },
      ],
      lastID: _lastID,
    });
  };

  /**
   * Функция обновления значения
   * @param {object} changedRows - данных об обновленных значениях
   * @param {number} delIndexRow - номер строки
   */
  updateRow = (changedRows, delIndexRow) => {
    const rows = this.state.rows;
    let rowIndex, colIndex, rowData, value;

    for (rowIndex in changedRows) {
      if (changedRows.hasOwnProperty(rowIndex)) {
        rowData = changedRows[rowIndex];

        for (colIndex in rowData) {
          if (rowData.hasOwnProperty(colIndex)) {
            value = rowData[colIndex];

            rows[rowIndex]['value' + colIndex] = value;
          }
        }
      }
    }

    if (delIndexRow) {
      this.setState(
        {rows},
        () => {
          this.setState({
            rows: [...rows.slice(0, delIndexRow), ...rows.slice(delIndexRow + 1)]
          });
        }
      );
    } else {
      this.setState({rows});
    }
  };

  /**
   * Функция удаления строки
   * @param {number} rowIndex - номер строки
   */
  delRow = rowIndex => {
    const rows = this.state.rows;

    this.setState({
      rows: [...rows.slice(0, rowIndex), ...rows.slice(rowIndex + 1)]
    });
  };

  /**
   * Удаление всех строк
   */
  delAllRows = () => {
    this.reGenRows(0);
  };

  render() {
    if (this.state.info && this.state.info.componentStack) {
      console.log(this.state.info.componentStack);
    }

    if (this.state.error) {
      return <div className={styles.App}>
        <h2>Error: {this.state.error.toString()}</h2>
      </div>;
    }

    const {
      rows,
    } = this.state;

    return (
      <div className={styles.App}>
        <EditTable
          rows={rows}
          updateRow={this.updateRow}
          delRow={this.delRow}
        />
        <Nav
          onAddRow={this.onAddRow}
          reGenRows={() => this.reGenRows(ROWS_DEF_COUNT)}
          delAllRows={this.delAllRows}
        />
      </div>
    );
  }
}

export default App;