Добавить новость

ОБНОВЛЕНИЕ 2018: в учебнике хорошая теория, но ему уже два года. Проверяйте версии пакетов. За выходом нового учебника можно следить в telegram канале или twitter

На канале так же проводятся бесплатные вебинары, публикуются переводы и авторские материалы, присоединяйтесь!

Добавить новость

Что такое добавление новости?

  1. Это форма, в которую мы вводим необходимые данные.

  2. Это "лента новостей", которая отображает наши данные.

После создания новости должно генерироваться событие, на которое должна быть подписана новостная лента. Иначе, как лента узнает о том, что нужно показать новую новость?

Очевидно, что нам нужна какая-то "система событий", чтобы научить компонент <Add /> генерировать событие, а компонент <News /> отображать. Так как компонент <News /> имеет статичный атрибут data, было бы логично предположить, что в data нужно "скидывать" динамически сформированную переменную. Конечно же, это место где нужно использовать state.

Подытожим:

  1. компонент <App /> должен иметь переменную в state - "новости", которую передавать в компонент <News />.

  2. Компонент <App /> должен уметь слушать событие "добавлена новость".

  3. Компонент <App /> так же должен уметь отписываться от прослушивания события.

var App = React.createClass({
  getInitialState: function() {
    return {
      news: my_news
    };
  },
  componentDidMount: function() {
    /* Слушай событие "Создана новость"
      если событие произошло, обнови this.state.news
    */
  },
  componentWillUnmount: function() {
    /* Больше не слушай событие "Создана новость" */
  },
  render: function() {
    console.log('render');
    return (
      <div className='app'>
        <Add />
        <h3>Новости</h3>
        <News data={this.state.news} />
      </div>
    );
  }
});

Мы добавили переменную в state (с начальным состоянием новостей - массивом my_news), ее же стали передавать в качестве свойств, для компонента <News />. А также, "как будто подписались" на прослушивание события в момент примонтирования компонента, и "как будто отписались" в момент "перед удалением компонента". В нашем примере, компонент <App /> не может быть удален, но тем не менее "не забывать отписываться" - хорошая практика.

На втором берегу, у нас компонент <Add />, который должен уметь генерировать событие в обработчике onBtnClickHandler.

Кстати, переименуйте кнопку "показать alert" -> "Добавить новость", так как мы уже почти готовы!

Глобальная система событий

Напомню, у нас возник вопрос о необходимости взаимодействия двух компонентов. Эти компоненты не состоят в отношении родитель-потомок.

Предлагаю воспользоваться решением EventEmitter, для этого скачайте/добавьте библиотеку (.min версия) в index.html, перед app.js.

index.html

<!DOCTYPE html>
<html>
  <head>
    <title>React [RU] Tutorial</title>
    <link rel="stylesheet" href="css/app.css">
  </head>
  <body>
    <div id="root"></div>

    <script src="js/react/react.js"></script>
    <script src="js/react/react-dom.js"></script>
    <script src="js/react/browser.min.js"></script>
    <script src="js/EventEmitter.js"></script>
    <script type="text/babel" src="js/app.js"></script>
  </body>
</html>

Теперь мы можем в app.js добавить глобальную переменную:

...
window.ee = new EventEmitter();
...

Благодаря которой, можем генерировать событие в обработчике onBtnClickHandler компонента <Add />

...
onBtnClickHandler: function(e) {
  e.preventDefault();
  var author = ReactDOM.findDOMNode(this.refs.author).value;
  var text = ReactDOM.findDOMNode(this.refs.text).value;

  var item = [{
    author: author,
    text: text,
    bigText: '...'
  }];

  window.ee.emit('News.add', item);
},
...

window.ee.emit('News.add', item); = сгенерируй событие 'News.add' и передай в качестве данных - item.

И наконец, благодаря window.ee мы можем подписываться/отписываться в <App />:

...
componentDidMount: function() {
  var self = this;
  window.ee.addListener('News.add', function(item) {
    var nextNews = item.concat(self.state.news);
    self.setState({news: nextNews});
  });
},
componentWillUnmount: function() {
  window.ee.removeListener('News.add');
},
...

window.ee.addListener - принимает в качестве аргументов имя события и функцию-обработчик. Чтобы внутри функции-обработчика (callback) использовать this - мы сохранили его чуть выше в переменную self.

Интересный момент: var nextNews = item.concat(self.state.news);

Мы создали новый массив, в котором первым элементом поставили новую новость, чтобы она была верхней в списке.

Кстати, было бы неплохо частично очищать форму после добавления новости: а именно, удалять текст новости, но оставлять "чекбокс" и автора. Внесите эти изменения в onBtnClickHandler:

...
onBtnClickHandler: function(e) {
  e.preventDefault();
  var textEl = ReactDOM.findDOMNode(this.refs.text);

  var author = ReactDOM.findDOMNode(this.refs.author).value;
  var text = textEl.value;

  var item = [{
    author: author,
    text: text,
    bigText: '...'
  }];

  window.ee.emit('News.add', item);

  textEl.value = '';
  this.setState({textIsEmpty: true});
},
...

Удобно, что кнопка "дизейблится" (disable) после очистки.

Кхм, итоговый сценарий:

js/app.js

'use strict';

var my_news = [
  {
    author: 'Саша Печкин',
    text: 'В четчерг, четвертого числа...',
    bigText: 'в четыре с четвертью часа четыре чёрненьких чумазеньких чертёнка чертили чёрными чернилами чертёж.'
  },
  {
    author: 'Просто Вася',
    text: 'Считаю, что $ должен стоить 35 рублей!',
    bigText: 'А евро 42!'
  },
  {
    author: 'Гость',
    text: 'Бесплатно. Скачать. Лучший сайт - http://localhost:3000',
    bigText: 'На самом деле платно, просто нужно прочитать очень длинное лицензионное соглашение'
  }
];

window.ee = new EventEmitter();

var Article = React.createClass({
  propTypes: {
    data: React.PropTypes.shape({
      author: React.PropTypes.string.isRequired,
      text: React.PropTypes.string.isRequired,
      bigText: React.PropTypes.string.isRequired
    })
  },
  getInitialState: function() {
    return {
      visible: false
    };
  },
  readmoreClick: function(e) {
    e.preventDefault();
    this.setState({visible: true});
  },
  render: function() {
    var author = this.props.data.author,
        text = this.props.data.text,
        bigText = this.props.data.bigText,
        visible = this.state.visible;

    return (
      <div className='article'>
        <p className='news__author'>{author}:</p>
        <p className='news__text'>{text}</p>
        <a href="#"
          onClick={this.readmoreClick}
          className={'news__readmore ' + (visible ? 'none': '')}>
          Подробнее
        </a>
        <p className={'news__big-text ' + (visible ? '': 'none')}>{bigText}</p>
      </div>
    )
  }
});

var News = React.createClass({
  propTypes: {
    data: React.PropTypes.array.isRequired
  },
  getInitialState: function() {
    return {
      counter: 0
    }
  },
  render: function() {
    var data = this.props.data;
    var newsTemplate;

    if (data.length > 0) {
      newsTemplate = data.map(function(item, index) {
        return (
          <div key={index}>
            <Article data={item} />
          </div>
        )
      })
    } else {
      newsTemplate = <p>К сожалению новостей нет</p>
    }

    return (
      <div className='news'>
        {newsTemplate}
        <strong
          className={'news__count ' + (data.length > 0 ? '':'none') }>Всего новостей: {data.length}</strong>
      </div>
    );
  }
});

var Add = React.createClass({
  getInitialState: function() {
    return {
      agreeNotChecked: true,
      authorIsEmpty: true,
      textIsEmpty: true
    };
  },
  componentDidMount: function() {
    ReactDOM.findDOMNode(this.refs.author).focus();
  },
  onBtnClickHandler: function(e) {
    e.preventDefault();
    var textEl = ReactDOM.findDOMNode(this.refs.text);

    var author = ReactDOM.findDOMNode(this.refs.author).value;
    var text = textEl.value;

    var item = [{
      author: author,
      text: text,
      bigText: '...'
    }];

    window.ee.emit('News.add', item);

    textEl.value = '';
    this.setState({textIsEmpty: true});
  },
  onCheckRuleClick: function(e) {
    this.setState({agreeNotChecked: !this.state.agreeNotChecked});
  },
  onFieldChange: function(fieldName, e) {
    if (e.target.value.trim().length > 0) {
      this.setState({[''+fieldName]:false})
    } else {
      this.setState({[''+fieldName]:true})
    }
  },
  render: function() {
    var agreeNotChecked = this.state.agreeNotChecked,
        authorIsEmpty = this.state.authorIsEmpty,
        textIsEmpty = this.state.textIsEmpty;
    return (
      <form className='add cf'>
        <input
          type='text'
          className='add__author'
          onChange={this.onFieldChange.bind(this, 'authorIsEmpty')}
          placeholder='Ваше имя'
          ref='author'
        />
        <textarea
          className='add__text'
          onChange={this.onFieldChange.bind(this, 'textIsEmpty')}
          placeholder='Текст новости'
          ref='text'
        ></textarea>
        <label className='add__checkrule'>
          <input type='checkbox' ref='checkrule' onChange={this.onCheckRuleClick}/>Я согласен с правилами
        </label>

        <button
          className='add__btn'
          onClick={this.onBtnClickHandler}
          ref='alert_button'
          disabled={agreeNotChecked || authorIsEmpty || textIsEmpty}
          >
          Опубликовать новость
        </button>
      </form>
    );
  }
});

var App = React.createClass({
  getInitialState: function() {
    return {
      news: my_news
    };
  },
  componentDidMount: function() {
    var self = this;
    window.ee.addListener('News.add', function(item) {
      var nextNews = item.concat(self.state.news);
      self.setState({news: nextNews});
    });
  },
  componentWillUnmount: function() {
    window.ee.removeListener('News.add');
  },
  render: function() {
    console.log('render');
    return (
      <div className='app'>
        <Add />
        <h3>Новости</h3>
        <News data={this.state.news} />
      </div>
    );
  }
});

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

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

Не торопите события! На данный момент было важно показать вам именно работу с реактом, как с "просто еще одной библиотекой". Организация кода "на современный лад" - входит в мои планы. Быть может, когда вы будете читать этот текст, соответствующая глава уже будет написана.

Полезная ссылка по вопросу организации взаимодействия между компонентами http://stackoverflow.com/questions/21285923/reactjs-two-components-communicating/31563614#31563614

Итого: Мы научили компоненты совместной работе. Посмотрели на реализацию EventEmitter для браузера.

Данный урок хорош тем, что он показывает подход к реализации глобальной системы событий в React.js

Мне очень симпатичен Redux, который элегантно решает эту проблему. По нему тоже есть подробный туториал на русском. Рекомендую к изучению.

Исходный код на данный момент.

Last updated