my-grocery-price-book/www

View on GitHub
app/javascript/src/EntriesForm/components/entries_form.jsx

Summary

Maintainability
C
1 day
Test Coverage
import React from 'react';
import PropTypes from 'prop-types';

var createReactClass = require('create-react-class');

var EntriesForm = createReactClass({

  propTypes: {
    entry: PropTypes.object,
    package_unit: PropTypes.string,
    back_href: PropTypes.string,
    currency_symbol: PropTypes.string,
    new_store_href: PropTypes.string,
    form_action: PropTypes.string,
    form_method: PropTypes.string,
    error_messages: PropTypes.arrayOf(PropTypes.string),
    local_suggestions: PropTypes.arrayOf(PropTypes.string),
    entry_names_url: PropTypes.string,
    selectable_stores: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
    bloodhoundBuilder: PropTypes.func,
    authenticity_token: PropTypes.string
  },

  getInitialState: function () {
    const entry = this.props.entry;
    return {
      total_price: entry.total_price, date_on: entry.date_on,
      store_id: entry.store_id.toString(), product_name: entry.product_name,
      amount: entry.amount, package_size: entry.package_size, bloodhound_initialized: false,
      suggestions: [], more_suggestions: []
    };
  },

  handleStoreIDChange: function (e) {
    this.setState({store_id: e.target.value});
  },

  handleDateOnChange: function (e) {
    this.setState({date_on: e.target.value});
  },

  setProductNameFromSuggestion: function (click_event) {
    click_event.preventDefault();
    var new_name = click_event.currentTarget.textContent;
    this.setState({product_name: new_name, suggestions: [], more_suggestions: []});
  },

  handleProductNameChange: function (e) {
    this.findSuggestions(e.target.value);
    this.setState({product_name: e.target.value});
  },

  findSuggestions: function (name) {
    if (this.state.bloodhound_initialized) {
      this.bloodhound.search(name,
        this.handleNameSuggestions,
        this.handleAdditionalNameSuggestions)
    }
  },

  handleNameSuggestions: function (suggested_names) {
    this.setState({suggestions: suggested_names});
  },

  handleAdditionalNameSuggestions: function (suggested_names) {
    this.setState({more_suggestions: suggested_names});
  },

  handleAmountChange: function (e) {
    this.setState({amount: e.target.value});
  },

  handlePackageSizeChange: function (e) {
    this.setState({package_size: e.target.value});
  },

  handleTotalPriceChange: function (e) {
    this.setState({total_price: e.target.value});
  },

  componentDidMount: function () {
    this.loadBloodhound();
  },

  EntriesBloodhound: function (local_suggestions, remote_url) {
    var Bloodhound = require('bloodhound-js');
    return new Bloodhound({
      datumTokenizer: Bloodhound.tokenizers.whitespace,
      queryTokenizer: Bloodhound.tokenizers.whitespace,
      sufficient: 3,
      local: local_suggestions,
      remote: {
        url: remote_url + "?query=%QUERY",
        wildcard: '%QUERY',
        transform: function (response) {
          return response.data;
        }
      }
    });
  },

  loadBloodhound: function () {
    var bloodhoundBuilder = this.props.bloodhoundBuilder || this.EntriesBloodhound;
    this.bloodhound = bloodhoundBuilder(this.props.local_suggestions, this.props.entry_names_url);
    this.bloodhound.initialize().then(this.bloodhoundInitialized);
  },

  bloodhoundInitialized: function () {
    this.setState({bloodhound_initialized: true});
  },

  render: function () {
    const props = this.props;
    const state = this.state;
    const component = this;

    var rendered_errors = null;

    if (this.props.error_messages.length > 0) {
      var i = 0;
      const all_errors = this.props.error_messages.map(function (message) {
        i += 1;
        return (
          <li key={"message_" + i}>{message}</li>
        );
      });
      rendered_errors = <div className="error-explanation">
        <ul>{all_errors}</ul>
      </div>;
    }

    const rendered_store_options = this.props.selectable_stores.map(function (store_option) {
      return (
        <option key={"store" + store_option[1]}
                value={store_option[1]}>
          {store_option[0]}
        </option>
      );
    });

    var suggestions = this.state.suggestions.concat(this.state.more_suggestions).slice(0, 3);
    if (state.product_name == "") {
      suggestions = [];
    }

    suggestions.reverse();

    var rendered_suggestions = suggestions.map(function (name) {
      return (
        <button className="bg-info name-suggestion btn" key={"suggested-" + name}
                onClick={component.setProductNameFromSuggestion}>
          {name}
        </button>
      );
    });


    return <form action={props.form_action} method="post">
      <input name="authenticity_token" value={props.authenticity_token} type="hidden"/>
      <input name="_method" value={props.form_method} type="hidden"/>
      {rendered_errors}
      <div className="form-group">
        <label className="sr-only" htmlFor="store_id">Store</label>
        <select id="store_id" name="price_entry[store_id]" className="form-control"
                onChange={this.handleStoreIDChange} value={state.store_id}>
          <option>Select a store</option>
          {rendered_store_options}
        </select>
        <a href={props.new_store_href} className="btn btn-default">New Store</a>
      </div>
      <div className="form-group">
        <label className="sr-only" htmlFor="date_on">Date on</label>
        <input name="price_entry[date_on]" className="form-control"
               value={state.date_on} onChange={this.handleDateOnChange}
               id="date_on" type="date" required placeholder="Date on"/>
      </div>
      <div className="form-group" style={{position: "relative"}}>
        <label className="sr-only" htmlFor="product_name">Product name</label>
        <div className="col-xs-12 shopping-list-suggestions">
          {rendered_suggestions}
        </div>
        <input name="price_entry[product_name]" className="form-control"
               value={state.product_name} onChange={this.handleProductNameChange}
               id="product_name" required placeholder="Product name"
               autoComplete={state.bloodhound_initialized ? 'off' : 'on'}/>
      </div>
      <div className="form-group">
        <label className="sr-only" htmlFor="amount">Amount</label>
        <input name="price_entry[amount]" className="form-control"
               value={state.amount} onChange={this.handleAmountChange}
               id="amount" type="number" min="1" required placeholder="Amount"/>
      </div>
      <div className="form-group">
        <label className="sr-only" htmlFor="package_size">Package size</label>
        <div className="input-group">
          <input name="price_entry[package_size]" className="form-control"
                 value={state.package_size} onChange={this.handlePackageSizeChange}
                 id="package_size" type="number" min="1" aria-describedby="package_unit"
                 required placeholder="Package size"/>
          <span className="input-group-addon" id="package_unit">{props.package_unit}</span>
        </div>
      </div>
      <div className="form-group">
        <label className="sr-only" htmlFor="total_price">Total price</label>
        <div className="input-group">
          <div className="input-group-addon">{props.currency_symbol}</div>
          <input name="price_entry[total_price]" className="form-control"
                 value={state.total_price} onChange={this.handleTotalPriceChange}
                 id="total_price" type="number" min="0" step="0.01" required
                 placeholder="Total price"/>
        </div>
      </div>
      <button className="btn btn-primary">Save</button>
      <a href={props.back_href} className="btn btn-default">Back</a>
    </form>;
  }
});

export default EntriesForm;