Mis à jour le mardi 7 mars 2017
  • 20 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

Ce cours est en vidéo.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

J'ai tout compris !

Handle user input

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

All that remains is for our user input to update state correctly in our app. It's that simple!

This will be done with event handlers like the ones we used previously in the course. Essentially, we're going to write a bunch of event handlers that just call  setState .

The complexity here lies in letting a child component's events affect its parent's state. The DOM's pattern for this type of communication is to bubble events up the DOM tree. By contrast, instead of explicitly bubbling actions up, we'll pass handlers down – just like any other kind of information, via  props  – that when called, will make the necessary changes to the parent's state.

To illustrate how to handle events by passing handlers down, we'll start at the top of our component tree again.

The  <Products>  component's state is affected by three user actions that are triggered in descendent components:

  • filtering the list of products

  • submitting the new product form

  • destroying products

Let's start with filtering the list of products. We simply want to set our local state to match whatever data is triggered in the  <Filters>  component. This involves

  • writing a simple handler that takes input and passes it to  setState :

  handleFilter(filterInput) {
    this.setState(filterInput);
  }
  • binding the handler in our constructor function so it doesn't lose a reference to  this  when triggered on its child

this.handleFilter = this.handleFilter.bind(this);
  • passing the handler to the child component

        <Filters
          filterText={this.state.filterText}
          inStockOnly={this.state.inStockOnly}
          onFilter={this.handleFilter}
        />

Now, in the  Filters  component, we will bind  onChange  on our inputs, and figure out a way to pass their values to the handler we've passed in. Just as in the parent component, this involves three things:

  • Writing an event handler that passes user input to the handler passed in from the parent. By using the  name  property of the target element, we can handle both inputs in a single handler. This is a matter of preference. You could just as well write two handlers, a  handleFilterTextChange  and a  handleIsStockedOnlyChange  handler, and avoid assigning the  name  properties below. It's up to you. I prefer this approach, because as your number of inputs grows, you don't have to keep writing new handlers.

  handleChange(e) {
    const value = e.target[e.target.type === "checkbox" ? "checked" : "value"]
    const name = e.target.name;

    this.props.onFilter({
      [name]: value
    });
  }
  • Binding the handler in our constructor function so it doesn't lose a reference to  this  when triggered on the event target element

    this.handleChange = this.handleChange.bind(this);
  • Assigning the handler to the  onChange  property of our input element (We'll also assign a  name  property here, so we can reference it in the handler above).

        <input
          type="text"
          placeholder="Search..."
          value={this.props.filterText}
          name="filterText"
          onChange={this.handleChange}
        />
        <p>
          <input
            type="checkbox"
            checked={this.props.inStockOnly}
            name="inStockOnly"
            onChange={this.handleChange}
          />
          &nbsp;
          Only show products in stock
        </p>

The other two actions that concern the  <Products>  component's state are those that add and remove products from the list. We'll start with adding, which happens via the new products form.

The process will be much the same as before!

There are three steps:

  • Write a handler that will take values from the form and use them to modify the state of the data model. Products in our list have unique IDs, so we'll generate one now. When working with data from a server, you would probably not generate an ID client side, and might use some other unique key instead. For our purposes, this is fine. Note the callback form of the arguments to  setState  here. We use the callback because in order to replace the list with a new version of itself, we need a reference to the existing state of the list. Whenever you are using existing state or props to calculate the new state, you need to use this callback form of  setState .

  saveProduct(product) {
    product.id = new Date().getTime();
    this.setState((prevState) => {
      let products = prevState.products;
      products[product.id] = product;
      return { products };
    });
  }
  • Bind that handler in our constructor function so it doesn't lose its reference to  this  when passed to the child component

    this.saveProduct = this.saveProduct.bind(this);
  • Pass the handler to the  <ProductForm> component so it can use it when  submit  is triggered on the form.

        <ProductForm onSave={this.saveProduct} />

Now let's look at the  <ProductForm> . Handling the form submission works like the other bindings we've done:

  • Write a handler for the submit button:

  handleSave(e) {
    this.props.onSave(this.state.product);
    // reset the form values to blank after submitting:
    this.setState({
      product: Object.assign({}, RESET_VALUES)
    });
    // prevent the form submit event from triggering an HTTP Post:
    e.preventDefault();
  }
  • Bind it in the constructor:

    this.handleSave = this.handleSave.bind(this);
  • Pass it to the button elements  onClick  hook:

        <input type="submit" value="Save" onClick={this.handleSave}/>

 So far, so good, but remember that our form inputs don't work right now because we've bound their values to local state in our component. We've got to complete the work to make the form inputs update our state on user input. Once again, we:

  • Write a handler. Again, we're using previous state to calculate the new state, so we'll have to use the callback form of  setState  arguments:

  handleChange(e) {
    const target = e.target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;

    this.setState((prevState) => {
      prevState.product[name] = value;
      return { product: prevState.product };
    });
  }
  • Bind the handler in the constructor:

    this.handleChange = this.handleChange.bind(this);
  • Attach them to the inputs via handler properties. Since we're using a common handler, like in the  <Filters>  component, we'll also add the  name  properties:

        <p>
          <label>
            Name
            <br />
            <input type="text" name="name" onChange={this.handleChange} value={this.state.product.name}/>
          </label>
        </p>
        <p>
          <label>
            Category
            <br />
            <input type="text" name="category" onChange={this.handleChange} value={this.state.product.category} />
          </label>
        </p>
        <p>
          <label>
            Price
            <br />
            <input type="text" name="price" onChange={this.handleChange} value={this.state.product.price} />
          </label>
        </p>
        <p>
          <label>
            <input type="checkbox" name="stocked" onChange={this.handleChange} checked={this.state.product.stocked}/>
            &nbsp;In stock?
          </label>
        </p>

The final event we have to be concerned with in our  <Products>  component is the delete buttons on each product row. As we've defined our components, the product row is actually two steps below the  <Products>  component. No worries, we just have to pass our props an extra time. In  <Products> , we create our handler and pass it as before:

  • Write the handler:

  handleDestroy(productId) {
    this.setState((prevState) => {
      let products = prevState.products;
      delete products[productId];
      return { products };
    });
  }
  • Bind it in the constructor:

    this.handleDestroy = this.handleDestroy.bind(this);
  • Instead of passing the handler to an input's event handler hook, we pass again to a second child component:

      rows.push(<ProductRow product={product} key={product.id} onDestroy={this.handleDestroy}/>);

And finally, to complete the loop, we'll repeat this in the  <ProductRow>  component:

  • Write the handler:

  destroy() {
    this.props.onDestroy(this.props.product.id);
  }
  • Bind it in the constructor function:

    this.handleSort = this.handleSort.bind(this);
  • Pass it to the child component(s):

              <SortableColumnHeader
                onSort={this.handleSort}
                currentSort={this.state.sort}
                column="name"
              />
              <SortableColumnHeader
                onSort={this.handleSort}
                currentSort={this.state.sort}
                column="price"
              ></SortableColumnHeader>

No surprises in the child component either:

  • As in every other case, write a handler that calls the handler that was passed in via  props :

  handleSort(e) {
    this.props.onSort(this.props.column, e.target.name);
  }
  • Bind it:

    this.handleSort = this.handleSort.bind(this);
  • Assign it to the input element:

        <button
          onClick={this.handleSort}
          className={currentSort === 'asc' ? 'SortableColumnHeader-current' : ''}
          name='asc'
        >&#x25B2;</button>
        <button
          onClick={this.handleSort}
          className={currentSort === 'desc' ? 'SortableColumnHeader-current' : ''}
          name='desc'
        >&#x25BC;</button>

And that's it!

Here's how the app ends up:

 Products.js :

import React from 'react';
import Filters from './Filters.js';
import ProductTable from './ProductTable.js';
import ProductForm from './ProductForm';

var PRODUCTS = {
  '1': {id: 1, category: 'Musical Instruments', price: '$459.99', stocked: true, name: 'Clarinet'},
  '2': {id: 2, category: 'Musical Instruments', price: '$5,000', stocked: true, name: 'Harpsicord'},
  '3': {id: 3, category: 'Musical Instruments', price: '$11,000', stocked: false, name: 'Fortepiano'},
  '4': {id: 4, category: 'Furniture', price: '$799', stocked: true, name: 'Chaise Lounge'},
  '5': {id: 5, category: 'Furniture', price: '$1,300', stocked: false, name: 'Dining Table'},
  '6': {id: 6, category: 'Furniture', price: '$100', stocked: true, name: 'Bean Bag'}
};

class Products extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      filterText: '',
      inStockOnly: false,
      products: PRODUCTS
    };

    this.handleFilter = this.handleFilter.bind(this);
    this.handleDestroy = this.handleDestroy.bind(this);
    this.saveProduct = this.saveProduct.bind(this);
  }
  handleFilter(filterInput) {
    this.setState(filterInput);
  }
  saveProduct(product) {
    if (!product.id) {
      product.id = new Date().getTime();
    }
    this.setState((prevState) => {
      let products = prevState.products;
      products[product.id] = product;
      return { products };
    });
  }
  handleDestroy(productId) {
    this.setState((prevState) => {
      let products = prevState.products;
      delete products[productId];
      return { products };
    });
  }
  render() {
    return (
      <div>
        <Filters
          filterText={this.state.filterText}
          inStockOnly={this.state.inStockOnly}
          onFilter={this.handleFilter}
        ></Filters>
        <ProductTable
          products={this.state.products}
          filterText={this.state.filterText}
          inStockOnly={this.state.inStockOnly}
          onDestroy={this.handleDestroy}
        ></ProductTable>
        <ProductForm onSave={this.saveProduct} ></ProductForm>
      </div>
    );
  }
}

export default Products;

  Filters.js :

import React from 'react';

class Filters extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }
  handleChange(e) {
    const value = e.target[e.target.type === "checkbox" ? "checked" : "value"]
    const name = e.target.name;

    this.props.onFilter({
      [name]: value
    });
  }
  render() {
    return (
      <form>
        <input
          type="text"
          placeholder="Search..."
          value={this.props.filterText}
          name="filterText"
          onChange={this.handleChange}
        />
        <p>
          <input
            type="checkbox"
            checked={this.props.inStockOnly}
            name="inStockOnly"
            onChange={this.handleChange}
          />
          &nbsp;
          Only show products in stock
        </p>
      </form>
    );
  }
}

export default Filters;

 ProductForm.js :

import React from 'react';

const RESET_VALUES = {id: '', category: '', price: '', stocked: false, name: ''};

class ProductForm extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.handleSave = this.handleSave.bind(this);
    this.state = {
      product: Object.assign({}, RESET_VALUES),
      errors: {}
    };
  }
  handleChange(e) {
    const target = e.target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;

    this.setState((prevState) => {
      prevState.product[name] = value;
      return { product: prevState.product };
    });
  }
  handleSave(e) {
    this.props.onSave(this.state.product);
    this.setState({
      product: Object.assign({}, RESET_VALUES),
      errors: {}
    });
    e.preventDefault();
  }
  render() {
    return (
      <form>
        <h3>Enter a new product</h3>
        <p>
          <label>
            Name
            <br />
            <input type="text" name="name" onChange={this.handleChange} value={this.state.product.name}/>
          </label>
        </p>
        <p>
          <label>
            Category
            <br />
            <input type="text" name="category" onChange={this.handleChange} value={this.state.product.category} />
          </label>
        </p>
        <p>
          <label>
            Price
            <br />
            <input type="text" name="price" onChange={this.handleChange} value={this.state.product.price} />
          </label>
        </p>
        <p>
          <label>
            <input type="checkbox" name="stocked" onChange={this.handleChange} checked={this.state.product.stocked}/>
            &nbsp;In stock?
          </label>
        </p>
        <input type="submit" value="Save" onClick={this.handleSave}/>
      </form>
    );
  }
}

export default ProductForm;

 ProductTable.js :

import React from 'react';
import ProductRow from './ProductRow.js';
import SortableColumnHeader from './SortableColumnHeader.js';

class ProductTable extends React.Component {
  constructor(props) {
    super(props);
    this.sortByKeyAndOrder = this.sortByKeyAndOrder.bind(this);
    this.handleSort = this.handleSort.bind(this);
    this.handleDestroy = this.handleDestroy.bind(this);
    this.state = {
      sort: {
        column: 'name',
        direction: 'desc'
      }
    };
  }
  sortByKeyAndOrder(objectA, objectB) {
    let isDesc = this.state.sort.direction === 'desc' ? 1 : -1;
    let [a, b] = [objectA[this.state.sort.column], objectB[this.state.sort.column]];
    if (this.state.sort.column === 'price') {
      [a, b] = [a, b].map((value) => parseFloat(value.replace(/[^\d\.]/g, ''), 10));
    }
    if (a > b) {
      return 1 * isDesc;
    }
    if (a < b) {
      return -1 * isDesc;
    }
    return 0;
  }
  sortProducts() {
    let productsAsArray = Object.keys(this.props.products).map((pid) => this.props.products[pid]);
    return productsAsArray.sort(this.sortByKeyAndOrder);
  }
  handleDestroy(id) {
    this.props.onDestroy(id);
  }
  handleSort(column, direction) {
    this.setState({
      sort: {
        column: column,
        direction: direction
      }
    });
  }
  render() {
    var rows = [];
    this.sortProducts().forEach((product) => {
      if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) {
        return;
      }
      rows.push(<ProductRow product={product} key={product.id} onDestroy={this.handleDestroy}></ProductRow>);
    });

    return (
      <div>
        <table>
          <thead>
            <tr>
              <SortableColumnHeader
                onSort={this.handleSort}
                currentSort={this.state.sort}
                column="name"
              ></SortableColumnHeader>
              <SortableColumnHeader
                onSort={this.handleSort}
                currentSort={this.state.sort}
                column="price"
              ></SortableColumnHeader>
            </tr>
          </thead>
          <tbody>{rows}</tbody>
        </table>
      </div>
    );
  }
}

export default ProductTable;

 SortableColumnHeader.js :

import React from 'react';
import './SortableColumnHeader.css';

class SortableColumnHeader extends React.Component {
  constructor(props) {
    super(props);
    this.handleSort = this.handleSort.bind(this);
  }
  handleSort(e) {
    this.props.onSort(this.props.column, e.target.name);
  }
  render() {
    let currentSort = this.props.currentSort.column === this.props.column ? this.props.currentSort.direction : false;
    return(
      <th>
        {this.props.column}
        <button
          onClick={this.handleSort}
          className={currentSort === 'asc' ? 'SortableColumnHeader-current' : ''}
          name='asc'
        >&#x25B2;</button>
        <button
          onClick={this.handleSort}
          className={currentSort === 'desc' ? 'SortableColumnHeader-current' : ''}
          name='desc'
        >&#x25BC;</button>
      </th>
    );
  }
}

export default SortableColumnHeader;

 ProductRow.js :

import React from 'react';

class ProductRow extends React.Component {
  constructor(props) {
    super(props);
    this.destroy = this.destroy.bind(this);
  }
  destroy() {
    this.props.onDestroy(this.props.product.id);
  }
  render() {
    var name = this.props.product.stocked ?
      this.props.product.name :
      <span style={{color: 'red'}}>
        {this.props.product.name}
      </span>;
    return (
      <tr>
        <td>{name}</td>
        <td>{this.props.product.price}</td>
        <td><button onClick={this.destroy}>x</button></td>
      </tr>
    );
  }
}

export default ProductRow;

The whole step of adding interactivity turns out to be extremely regular: we just keep writing handlers, binding them, and passing them down.

The key is having already mapped out state in our app. If you're working on a React app and having trouble handling user input, most likely there is something wrong with how you've located state in your app. Sometimes you might have the same piece of state tracked in two different components.

The fix should involve storing each piece of state in just one place, and having that be at the highest point in the component tree where that state is referenced! 

Exemple de certificat de réussite
Exemple de certificat de réussite