webentwicklung-frage-antwort-db.com.de

React + Redux - Wie gehe ich am besten mit CRUD in einer Formularkomponente um?

Ich habe ein Formular, das zum Erstellen, Lesen, Aktualisieren und Löschen verwendet wird. Ich habe 3 Komponenten mit der gleichen Form erstellt, aber ich übergebe ihnen verschiedene Requisiten. Ich habe CreateForm.js, ViewForm.js (schreibgeschützt mit der Schaltfläche Löschen) und UpdateForm.js.

Ich habe früher mit PHP gearbeitet, also habe ich diese immer in einer Form gemacht.

Ich verwende React und Redux, um den Shop zu verwalten.

Wenn ich in der CreateForm-Komponente bin, übergebe ich diese Requisiten an meine Unterkomponenten createForm={true}, um die Eingaben nicht mit einem Wert zu füllen und sie nicht zu deaktivieren. In meiner ViewForm-Komponente übergebe ich diese Requisiten readonly="readonly".

Und ich habe ein weiteres Problem mit einem Textbereich, der mit einem Wert gefüllt und nicht aktualisierbar ist. Textbereich mit Wert reagieren ist schreibgeschützt, muss aber aktualisiert werden

Was ist die beste Struktur, um nur eine Komponente zu haben, die diese verschiedenen Zustände des Formulars verarbeitet?

Haben Sie Ratschläge, Tutorials, Videos, Demos zum Teilen?

126
Mike Boutin

Ich habe das Paket Redux Form gefunden. Es macht einen wirklich guten Job!

Sie können also Redux mit React-Redux verwenden.

Zuerst müssen Sie (offensichtlich) eine Formularkomponente erstellen:

import React from 'react';
import { reduxForm } from 'redux-form';
import validateContact from '../utils/validateContact';

class ContactForm extends React.Component {
  render() {
    const { fields: {name, address, phone}, handleSubmit } = this.props;
    return (
      <form onSubmit={handleSubmit}>
        <label>Name</label>
        <input type="text" {...name}/>
        {name.error && name.touched && <div>{name.error}</div>}

        <label>Address</label>
        <input type="text" {...address} />
        {address.error && address.touched && <div>{address.error}</div>}

        <label>Phone</label>
        <input type="text" {...phone}/>
        {phone.error && phone.touched && <div>{phone.error}</div>}

        <button onClick={handleSubmit}>Submit</button>
      </form>
    );
  }
}

ContactForm = reduxForm({
  form: 'contact',                      // the name of your form and the key to
                                        // where your form's state will be mounted
  fields: ['name', 'address', 'phone'], // a list of all your fields in your form
  validate: validateContact             // a synchronous validation function
})(ContactForm);

export default ContactForm;

Anschließend verbinden Sie die Komponente, die das Formular verarbeitet:

import React from 'react';
import { connect } from 'react-redux';
import { initialize } from 'redux-form';
import ContactForm from './ContactForm.react';

class App extends React.Component {

  handleSubmit(data) {
    console.log('Submission received!', data);
    this.props.dispatch(initialize('contact', {})); // clear form
  }

  render() {
    return (
      <div id="app">
        <h1>App</h1>
        <ContactForm onSubmit={this.handleSubmit.bind(this)}/>
      </div>
    );
  }

}

export default connect()(App);

Und fügen Sie den Redux-Form-Reduzierer in Ihre kombinierten Reduzierer ein:

import { combineReducers } from 'redux';
import { appReducer } from './app-reducers';
import { reducer as formReducer } from 'redux-form';

let reducers = combineReducers({
  appReducer, form: formReducer // this is the form reducer
});

export default reducers;

Und das Validator-Modul sieht so aus:

export default function validateContact(data, props) {
  const errors = {};
  if(!data.name) {
    errors.name = 'Required';
  }
  if(data.address && data.address.length > 50) {
    errors.address = 'Must be fewer than 50 characters';
  }
  if(!data.phone) {
    errors.phone = 'Required';
  } else if(!/\d{3}-\d{3}-\d{4}/.test(data.phone)) {
    errors.phone = 'Phone must match the form "999-999-9999"'
  }
  return errors;
}

Wenn Sie nach dem Ausfüllen des Formulars alle Felder mit Werten füllen möchten, können Sie die Funktion initialize verwenden:

componentWillMount() {
  this.props.dispatch(initialize('contact', {
    name: 'test'
  }, ['name', 'address', 'phone']));
}

Eine andere Möglichkeit, die Formulare auszufüllen, besteht darin, die initialValues ​​festzulegen.

ContactForm = reduxForm({
  form: 'contact',                      // the name of your form and the key to
  fields: ['name', 'address', 'phone'], // a list of all your fields in your form
  validate: validateContact             // a synchronous validation function
}, state => ({
  initialValues: {
    name: state.user.name,
    address: state.user.address,
    phone: state.user.phone,
  },
}))(ContactForm);

Wenn Sie eine andere Möglichkeit haben, hinterlassen Sie einfach eine Nachricht! Vielen Dank.

114
Mike Boutin

UPDATE: Es ist 2018 und ich werde immer nur Formik (oder Formik-ähnliche Bibliotheken) verwenden

Es gibt auch React-Redux-Form ( Step-by-Step ), das anscheinend etwas von Redux-Form s Javascript austauscht (& Heizplatte) mit Auszeichnungserklärung. Es sieht gut aus, aber ich habe es noch nicht benutzt.

Ein Ausschnitt aus der Readme-Datei:

import React from 'react';
import { createStore, combineReducers } from 'redux';
import { Provider } from 'react-redux';
import { modelReducer, formReducer } from 'react-redux-form';

import MyForm from './components/my-form-component';

const store = createStore(combineReducers({
  user: modelReducer('user', { name: '' }),
  userForm: formReducer('user')
}));

class App extends React.Component {
  render() {
    return (
      <Provider store={ store }>
        <MyForm />
      </Provider>
    );
  }
}

./components/my-form-component.js

import React from 'react';
import { connect } from 'react-redux';
import { Field, Form } from 'react-redux-form';

class MyForm extends React.Component {
  handleSubmit(val) {
    // Do anything you want with the form value
    console.log(val);
  }

  render() {
    let { user } = this.props;

    return (
      <Form model="user" onSubmit={(val) => this.handleSubmit(val)}>
        <h1>Hello, { user.name }!</h1>
        <Field model="user.name">
          <input type="text" />
        </Field>
        <button>Submit!</button>
      </Form>
    );
  }
}

export default connect(state => ({ user: state.user }))(MyForm);

Bearbeiten: Vergleich

Die Dokumentation zum React-Redux-Formular bietet einen Vergleich zum Redux-Formular:

https://davidkpiano.github.io/react-redux-form/docs/guides/compare-redux-form.html

10
Ashley Coolman

Für diejenigen, die sich nicht für eine riesige Bibliothek zur Behandlung von Formularproblemen interessieren, würde ich redux-form-utils empfehlen.

Es kann Werte generieren und Handler für Ihre Formularsteuerelemente ändern, Reduzierungen des Formulars generieren, praktische Aktionsersteller zum Löschen bestimmter (oder aller) Felder usw.

Alles, was Sie tun müssen, ist, sie in Ihrem Code zusammenzusetzen.

Durch die Nutzung redux-form-utils, am Ende haben Sie die Formmanipulation wie folgt:

import { createForm } from 'redux-form-utils';

@createForm({
  form: 'my-form',
  fields: ['name', 'address', 'gender']
})
class Form extends React.Component {
  render() {
    const { name, address, gender } = this.props.fields;
    return (
      <form className="form">
        <input name="name" {...name} />
        <input name="address" {...address} />
        <select {...gender}>
          <option value="male" />
          <option value="female" />
        </select>
      </form>
    );
  }
}

Diese Bibliothek löst jedoch nur die Probleme C und U, für R und D ist möglicherweise eine stärker integrierte Table Komponente zu verfälschen .

4
jasonslyvia

Eine weitere Möglichkeit für Benutzer, die eine vollständig kontrollierte Formularkomponente erstellen möchten, ohne eine übergroße Bibliothek zu verwenden.

ReduxFormHelper - eine kleine ES6-Klasse mit weniger als 100 Zeilen:

class ReduxFormHelper {
  constructor(props = {}) {
    let {formModel, onUpdateForm} = props
    this.props = typeof formModel === 'object' &&
      typeof onUpdateForm === 'function' && {formModel, onUpdateForm}
  }

  resetForm (defaults = {}) {
    if (!this.props) return false
    let {formModel, onUpdateForm} = this.props
    let data = {}, errors = {_flag: false}
    for (let name in formModel) {
      data[name] = name in defaults? defaults[name] :
        ('default' in formModel[name]? formModel[name].default : '')
      errors[name] = false
    }
    onUpdateForm(data, errors)
  }

  processField (event) {
    if (!this.props || !event.target) return false
    let {formModel, onUpdateForm} = this.props
    let {name, value, error, within} = this._processField(event.target, formModel)
    let data = {}, errors = {_flag: false}
    if (name) {
      value !== false && within && (data[name] = value)
      errors[name] = error
    }
    onUpdateForm(data, errors)
    return !error && data
  }

  processForm (event) {
    if (!this.props || !event.target) return false
    let form = event.target
    if (!form || !form.elements) return false
    let fields = form.elements
    let {formModel, onUpdateForm} = this.props
    let data = {}, errors = {}, ret = {}, flag = false
    for (let n = fields.length, i = 0; i < n; i++) {
      let {name, value, error, within} = this._processField(fields[i], formModel)
      if (name) {
        value !== false && within && (data[name] = value)
        value !== false && !error && (ret[name] = value)
        errors[name] = error
        error && (flag = true)
      }
    }
    errors._flag = flag
    onUpdateForm(data, errors)
    return !flag && ret
  }

  _processField (field, formModel) {
    if (!field || !field.name || !('value' in field))
      return {name: false, value: false, error: false, within: false}
    let name = field.name
    let value = field.value
    if (!formModel || !formModel[name])
      return {name, value, error: false, within: false}
    let model = formModel[name]
    if (model.required && value === '')
      return {name, value, error: 'missing', within: true}
    if (model.validate && value !== '') {
      let fn = model.validate
      if (typeof fn === 'function' && !fn(value))
        return {name, value, error: 'invalid', within: true}
    }
    if (model.numeric && isNaN(value = Number(value)))
      return {name, value: 0, error: 'invalid', within: true}
    return {name, value, error: false, within: true}
  }
}

Es erledigt nicht die ganze Arbeit für Sie. Es erleichtert jedoch die Erstellung, Validierung und Handhabung einer kontrollierten Formularkomponente. Sie können den obigen Code einfach kopieren und in Ihr Projekt einfügen oder stattdessen die entsprechende Bibliothek einbinden - redux-form-helper (plug!).

Wie benutzt man

Der erste Schritt ist das Hinzufügen spezifischer Daten zum Redux-Status, die den Status unseres Formulars darstellen. Diese Daten enthalten aktuelle Feldwerte sowie eine Reihe von Fehlerflags für jedes Feld im Formular.

Der Formularstatus kann zu einem vorhandenen Reduzierer hinzugefügt oder in einem separaten Reduzierer definiert werden.

Darüber hinaus ist es erforderlich, eine bestimmte Aktion zu definieren, die die Aktualisierung des Formularstatus auslöst, sowie den jeweiligen Aktionsersteller.

Aktionsbeispiel:

export const FORM_UPDATE = 'FORM_UPDATE' 

export const doFormUpdate = (data, errors) => {
  return { type: FORM_UPDATE, data, errors }
}
...

Reduzierbeispiel:

...
const initialState = {
  formData: {
    field1: '',
    ...
  },
  formErrors: {
  },
  ...
}

export default function reducer (state = initialState, action) {
  switch (action.type) {
    case FORM_UPDATE:
      return {
        ...ret,
        formData: Object.assign({}, formData, action.data || {}),
        formErrors: Object.assign({}, formErrors, action.errors || {})
      }
    ...
  }
}

Der zweite und letzte Schritt besteht darin, eine Containerkomponente für unser Formular zu erstellen und mit dem jeweiligen Teil des Redux-Status und der entsprechenden Aktionen zu verbinden.

Außerdem müssen wir ein Formularmodell definieren, das die Validierung von Formularfeldern spezifiziert. Jetzt instanziieren wir das Objekt ReduxFormHelper als Mitglied der Komponente und übergeben dort unser Formularmodell und eine Callback-Dispatching-Aktualisierung des Formularstatus.

Dann müssen wir in der render() -Methode der Komponente die onChange -Ereignisse jedes Felds und die onSubmit -Ereignisse des Formulars mit processField() und processForm() binden. Methoden sowie Anzeigen von Fehlerblöcken für jedes Feld in Abhängigkeit von den Formularfehlerflags im Status.

Das folgende Beispiel verwendet CSS von Twitter Bootstrap Framework.

Beispiel einer Containerkomponente:

import React, {Component} from 'react';
import {connect} from 'react-redux'
import ReduxFormHelper from 'redux-form-helper'

class MyForm extends Component {
  constructor(props) {
    super(props);
    this.helper = new ReduxFormHelper(props)
    this.helper.resetForm();
  }

  onChange(e) {
    this.helper.processField(e)
  }

  onSubmit(e) {
    e.preventDefault()
    let {onSubmitForm} = this.props
    let ret = this.helper.processForm(e)
    ret && onSubmitForm(ret)
  }

  render() {
    let {formData, formErrors} = this.props
    return (
  <div>
    {!!formErrors._flag &&
      <div className="alert" role="alert">
        Form has one or more errors.
      </div>
    }
    <form onSubmit={this.onSubmit.bind(this)} >
      <div className={'form-group' + (formErrors['field1']? ' has-error': '')}>
        <label>Field 1 *</label>
        <input type="text" name="field1" value={formData.field1} onChange={this.onChange.bind(this)} className="form-control" />
        {!!formErrors['field1'] &&
        <span className="help-block">
          {formErrors['field1'] === 'invalid'? 'Must be a string of 2-50 characters' : 'Required field'}
        </span>
        }
      </div>
      ...
      <button type="submit" className="btn btn-default">Submit</button>
    </form>
  </div>
    )
  }
}

const formModel = {
  field1: {
    required: true,
    validate: (value) => value.length >= 2 && value.length <= 50
  },
  ...
}

function mapStateToProps (state) {
  return {
    formData: state.formData, formErrors: state.formErrors,
    formModel
  }
}

function mapDispatchToProps (dispatch) {
  return {
    onUpdateForm: (data, errors) => {
      dispatch(doFormUpdate(data, errors))
    },
    onSubmitForm: (data) => {
      // dispatch some action which somehow updates state with form data
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(MyForm)

Demo

1
hindmost