1
\$\begingroup\$

I created a simple react todo-like application (GitHub). There are several questions/problems:

  • How do I separate logic from components (methods such removeHandler etc)?
  • Should the top component have state?
  • Where should I place the logic of validation?

For simplicity I put everything in one file (main.js):

var React = require('react/addons'),
    _ = require('underscore'),
    IM = require('immutable'),
    log = require('debug')('main');

window.myDebug = require("debug");

var D = React.DOM;

class Form extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    var {errors} = this.props;
    var getFormGroup = (propName, name) => {
      return D.div(
        {className: 'form-group'},
        D.label(null, name),
        errors.has(propName) ? 
          D.label({className: 'pull-right'}, errors.get(propName)) : null,
        D.input({
          className: 'form-control',
          value: this.props.form.get(propName),
          onChange: _.partial(this.props.changeInputHandler, propName)
        })
      );
    };
    return D.form(
      {
        onSubmit: this.props.submitHandler.bind(this)
      },
      getFormGroup('firstName', 'First Name'),
      getFormGroup('lastName', 'Last Name'),
      getFormGroup('phone', 'Phone'),
      D.button({type: 'submit', className: 'btn btn-default'}, 'Submit')
    );
  }
}

class Table extends React.Component {
    constructor(props) {
      super(props);
    }
    render() {
      var {items, changeSortingHandler, sortBy, removeHandler} = this.props;
      var getHeadCell = (key, name) => {
        var span;
        if(key === sortBy.get('key')) {
          var direction = sortBy.get('isAsc') ? 'down' : 'up';
          span = D.span({
            className: `glyphicon glyphicon-chevron-${direction}`
          });
        }
        return D.th({onClick: _.partial(changeSortingHandler, key)}, name, span);
      };
      // TODO: refactor
      var sorted = this.props.items.map(
        (el, i) => _.extend(el.toObject(), {id: i})
      ).sort(
        (l, r) => {
          var {key, isAsc} = sortBy.toObject(),
              res,
              mul = isAsc ? 1 : -1;
          if (l[key] > r[key]) { res = 1; }
          else if (l[key] === r[key]) { res = 0; }
          else { res = -1; }
          return res * mul;
        }
      );
      var rows = sorted.map((el) => {
          return D.tr(
            {key: el.id},
            D.th({scope: 'row'}, el.id),
            D.td(null, el.firstName),
            D.td(null, el.lastName),
            D.td(null, el.phone),
            D.td(
              null,
              D.span({
                onClick: _.partial(removeHandler, el.id),
                className: 'glyphicon glyphicon-remove-sign'
              })
            )
          );
      });
      return D.table(
        {className: 'table'},
        D.thead(
          null,
          D.tr(
            null,
            getHeadCell('id', '#'),
            getHeadCell('firstName', 'First Name'),
            getHeadCell('lastName', 'Last Name'),
            getHeadCell('phone', 'Phone'),
            D.th(null, '')
          )
        ),
        D.tbody(
          null,
          rows
        )
      );
    }
}

class App extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        form: IM.Map({
          firstName: '',
          lastName: '',
          phone: ''
        }),
        items: IM.fromJS([1, 2, 3].map((el) => {
          return {
            firstName: 'firstName' + el,
            lastName: 'lastName' + el,
            phone: 'phone' + el
          };
        })),
        sortBy: IM.Map({
          key: 'id',
          isAsc: true
        }),
        errors: IM.Map()
      };
    }
    submitHandler(e) {
      e.preventDefault();
      this.setState(function(prevState) {
        var {form, items} = prevState;
        var errors = this.validateNewItem(form);
        log(errors);
        if (_.isEmpty(errors)) {
          return _.extend(prevState, {
            items: items.push(form),
            form: form.clear(), 
            errors: IM.Map()
          });
        } else {
          return _.extend(prevState, {
            errors: IM.Map(errors)
          });
        }
      });
    }
    validateNewItem (newItem) {
      var errors = {};
      var {firstName, lastName, phone} = newItem.toJS();
      log(firstName);
      log(lastName);
      if (_.isUndefined(firstName) || firstName.length === 0) {
        errors.firstName = 'This field is required';
      }
      if (_.isUndefined(lastName) || lastName.length === 0) {
        errors.lastName = 'This field is required';
      }
      var phoneRegEx = /\+?\(?\d{2,4}\)?[\d\s-]{3,}/;
      if (phoneRegEx.test(phone)) {
        errors.phone = 'The phone number is invalid';
      }
      return errors;
    }
    changeInputHandler(propName, e) {
      var value = e.target.value;
      var newForm = this.state.form.merge({[propName]: value});
      this.setState(function(prevState) {
        return _.extend(prevState, {form: newForm});
      });
    }
    changeSortingHandler(key) {
      this.setState(function(prevState) {
        var {sortBy} = prevState;
        if (key === sortBy.get('key')) {
          sortBy = sortBy.update('isAsc', val => !val);
        } else {
          sortBy = sortBy.merge({key, isAsc:true});
        }
        return _.extend(prevState, {sortBy});
      });
    }
    removeHandler(i) {
      this.setState(function(prevState) {
        var {items} = prevState;
        items = items.delete(i);
        return _.extend(prevState, {items});
      });
    }
    render() {
      return D.div(
        {className: 'container'},
        D.div(
          {className: 'col-lg-offset-3 col-lg-6'},
          React.createElement(Form, {
            form: this.state.form,
            errors: this.state.errors,
            submitHandler: this.submitHandler.bind(this),
            changeInputHandler: this.changeInputHandler.bind(this)
          }),
          React.createElement(Table, {
            items: this.state.items,
            sortBy: this.state.sortBy,
            changeSortingHandler: this.changeSortingHandler.bind(this),
            removeHandler: this.removeHandler.bind(this)
          })
        )
      );
    }
}

React.render(
    React.createElement(App),
    document.body
);
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.