web-dev-qa-db-fra.com

Comment définir initialValues ​​en fonction d'une source asynchrone telle qu'un appel ajax avec redux-form

Sur les pages officielles et dans les numéros de GitHub pour redux-form , il existe plus d'un exemple sur la façon de travailler avec initialValues ​​. Cependant, je ne trouve pas un seul qui explique en détail comment initialValues ​​ peut être défini en réponse à une source asynchrone. 

Le cas principal auquel je pense est quelque chose comme une simple application CRUD où un utilisateur va éditer une entité qui existe déjà. Lorsque la vue est ouverte pour la première fois et que le composant redux-form est monté, mais avant que le composant soit rendu, les valeurs initialValues ​​doivent être définies. Disons que dans cet exemple, les données sont chargées à la demande lorsque le composant est monté et rendu pour la première fois. Les exemples montrent la définition de initialValues ​​sur la base de valeurs codées en dur ou de l’état du magasin redux, mais aucune d’entre elles ne me permet de déterminer comment définir les initialValues ​​sur une base asynchrone telle qu’un appel à XHR ou à une extraction.

Je suis sûr que je manque juste quelque chose de fondamental alors s'il vous plaît dirigez-moi dans la bonne direction.

Références:

21
jpierson

EDIT: Solution mise à jour à partir de la documentation ReduxForm

Ceci est maintenant documenté dans la dernière version de ReduxForm, et est beaucoup plus simple que ma réponse précédente.

La clé consiste à connect votre composant de formulaire après l'avoir décoré avec ReduxForm. Vous pourrez alors accéder à l'accessoire initialValues comme tout autre accessoire de votre composant.

// Decorate with reduxForm(). It will read the initialValues prop provided by connect()
InitializeFromStateForm = reduxForm({
  form: 'initializeFromState'
})(InitializeFromStateForm)

// now set initialValues using data from your store state
InitializeFromStateForm = connect(
  state => ({
    initialValues: state.account.data 
  })
)(InitializeFromStateForm)

J'ai accompli cela en utilisant la méthode redux-form reducer plugin .

Les démonstrations suivantes récupèrent des données asynchrones et pré-remplissent un formulaire utilisateur avec une réponse.

const RECEIVE_USER = 'RECEIVE_USER';

// once you've received data from api dispatch action
const receiveUser = (user) => {
    return {
       type: RECEIVE_USER,
       payload: { user }
    }
}

// here is your async request to retrieve user data
const fetchUser = (id) => dispatch => {
   return fetch('http://getuser.api')
            .then(response => response.json())
            .then(json => receiveUser(json));
}

Ensuite, dans votre réducteur racine où vous incluez votre réducteur redux-form, vous devez inclure votre plug-in réducteur qui remplace les valeurs des formulaires avec les données extraites renvoyées.

const formPluginReducer = {
   form: formReducer.plugin({
      // this would be the name of the form you're trying to populate
      user: (state, action) => {
         switch (action.type) {
             case RECEIVE_USER:
                return {
                  ...state,
                  values: {
                      ...state.values,
                      ...action.payload.user
                  }
               }
            default:
               return state;
         }
      }
   })
};

const rootReducer = combineReducers({
   ...formPluginReducer,
   ...yourOtherReducers
});

Enfin, vous incluez vous combinez votre nouveau réducteur de formulaire avec les autres réducteurs de votre application. 

Remarque Ce qui suit suppose que les clés de l'objet utilisateur récupéré correspondent aux noms des champs du formulaire utilisateur. Si ce n'est pas le cas, vous devrez effectuer une étape supplémentaire sur les données pour mapper les champs.

11
ryandrewjohnson

Par défaut, vous ne pouvez initialiser un composant de formulaire qu'une seule fois via initialValues. Il existe deux méthodes pour réinitialiser le composant de formulaire avec de nouvelles valeurs "immaculées":

Passez un paramètre de configuration enableReinitialize prop ou reduxForm () à true pour autoriser la réinitialisation du formulaire avec de nouvelles valeurs "pristine" chaque fois que la valeur initiale propValues ​​est modifiée. Pour conserver les valeurs de formulaire modifiées lors de la réinitialisation, vous pouvez définir keepDirtyOnReinitialize sur true. Par défaut, la réinitialisation du formulaire remplace toutes les valeurs modifiées par des valeurs "immaculées".

Envoie l'action INITIALIZE (en utilisant le créateur de l'action fourni par redux-form).

Référencé à partir de: http://redux-form.com/6.1.1/examples/initializeFromState/

3
chen Jacky

Pourriez-vous déclencher la répartition sur composantWillMount () et définir l'état sur chargement.

Pendant le chargement, affichez un spinner, par exemple, et uniquement lorsque la demande renverra les valeurs, mettez à jour l'état, puis retransformez le formulaire avec les valeurs ??

2
luanped

Voici un exemple de travail minimal sur la manière de définir initialValues ​​en fonction d'une source asynchrone.
Il utilise initialize créateur d’actions. 

Toutes les valeurs de initialValues ​​ ne doivent pas être indéfinies, sinon vous obtiendrez une boucle infinie .

// import { Field, reduxForm, change, initialize } from 'redux-form';

async someAsyncMethod() {
  // fetch data from server
  await this.props.getProducts(),

  // this allows to get current values of props after promises and benefits code readability
  const { products } = this.props;

  const initialValues = { productsField: products };

  // set values as pristine to be able to detect changes
  this.props.dispatch(initialize(
    'myForm',
    initialValues,
  ));
}
1
Artem Bernatskyi

Bien que cette méthode ne soit peut-être pas la meilleure solution, elle fonctionne assez bien pour mes besoins:

  • Demande AJAX à l'API à l'entrée
  • Initialise le formulaire avec les données lorsque la demande est remplie ou affiche une erreur de serveur
  • La réinitialisation du formulaire restaure les données initiales.
  • Permet au formulaire d'être réutilisé à d'autres fins (par exemple, une simple instruction if peut contourner les valeurs initiales): Ajouter une publication et Modifier une publication ou Ajouter un commentaire et Modifier un commentaire ... etc.
  • Les données sont supprimées du formulaire Redux à la sortie (aucune raison de stocker de nouvelles données dans Redux, car elles sont restituées par un composant Blog). 

Form.jsx:

import React, { Component } from 'react';
import { Field, reduxForm } from 'redux-form';
import { connect } from 'react-redux';
import { browserHistory, Link } from 'react-router';

import { editPost, fetchPost } from '../../actions/BlogActions.jsx';
import NotFound from '../../components/presentational/notfound/NotFound.jsx';
import RenderAlert from '../../components/presentational/app/RenderAlert.jsx';   
import Spinner from '../../components/presentational/loaders/Spinner.jsx'; 

// form validation checks
const validate = (values) => {
  const errors = {}
  if (!values.title) {
    errors.title = 'Required';
  }

  if (!values.image) {
    errors.image = 'Required';
  }

  if (!values.description) {
    errors.description = 'Required';
  } else if  (values.description.length > 10000) {
    errors.description = 'Error! Must be 10,000 characters or less!';
  }

  return errors;
}

// renders input fields
const renderInputField = ({ input, label, type, meta: { touched, error } }) => (
  <div>
    <label>{label}</label>
    <div>
      <input {...input} className="form-details complete-expand" placeholder={label} type={type}/>
      {touched && error && <div className="error-handlers "><i className="fa fa-exclamation-triangle" aria-hidden="true"></i> {error}</div>}
    </div>
  </div>
)

// renders a text area field
const renderAreaField = ({ textarea, input, label, type, meta: { touched, error } }) => (
  <div>
    <label>{label}</label>
    <div>
      <textarea {...input} className="form-details complete-expand" placeholder={label} type={type}/>
      {touched && error && <div className="error-handlers"><i className="fa fa-exclamation-triangle" aria-hidden="true"></i> {error}</div>}
    </div>
  </div>
)

class BlogPostForm extends Component {   
  constructor() {
    super();

    this.state = {
      isLoaded: false,
      requestTimeout: false,
    };
  }

  componentDidMount() {
    if (this.props.location.query.postId) {
      // sets a 5 second server timeout
      this.timeout = setInterval(this.timer.bind(this), 5000);
      // AJAX request to API 
      fetchPost(this.props.location.query.postId).then((res) => {
        // if data returned, seed Redux form
        if (res.foundPost) this.initializeForm(res.foundPost);
        // if data present, set isLoaded to true, otherwise set a server error
        this.setState({
          isLoaded: (res.foundPost) ? true : false,
          serverError: (res.err) ? res.err : ''
        });
      });
    }
  }

  componentWillUnmount() {
    this.clearTimeout();
  }

  timer() {
    this.setState({ requestTimeout: true });
    this.clearTimeout();
  }

  clearTimeout() {
    clearInterval(this.timeout);
  }

  // initialize Redux form from API supplied data
  initializeForm(foundPost) {

    const initData = {
      id: foundPost._id,
      title: foundPost.title,
      image: foundPost.image,
      imgtitle: foundPost.imgtitle,
      description: foundPost.description
    }

    this.props.initialize(initData);
  }

  // onSubmit => take Redux form props and send back to server
  handleFormSubmit(formProps) {
    editPost(formProps).then((res) => {
      if (res.err) {
        this.setState({
          serverError: res.err
        });
      } else {
        browserHistory.Push(/blog);
      }
    });
  }

  renderServerError() {
    const { serverError } = this.state;
    // if form submission returns a server error, display the error
    if (serverError) return <RenderAlert errorMessage={serverError} />
  }

  render() {
    const { handleSubmit, pristine, reset, submitting, fields: { title, image, imgtitle, description } } = this.props;
    const { isLoaded, requestTimeout, serverError } = this.state;

    // if data hasn't returned from AJAX request, then render a spinner 
    if (this.props.location.query.postId && !isLoaded) {
      // if AJAX request returns an error or request has timed out, show NotFound component
      if (serverError || requestTimeout) return <NotFound />

      return <Spinner />
     }

    // if above conditions are met, clear the timeout, otherwise it'll cause the component to re-render on timer's setState function
    this.clearTimeout();

    return (
      <div className="col-sm-12">
        <div className="form-container">
          <h1>Edit Form</h1>
          <hr />
          <form onSubmit={handleSubmit(this.handleFormSubmit.bind(this))}>
            <Field name="title" type="text" component={renderInputField} label="Post Title" />
            <Field name="image" type="text" component={renderInputField} label="Image URL" />
            <Field name="imgtitle" component={renderInputField} label="Image Description" />
            <Field name="description" component={renderAreaField} label="Description" />
            <div>
              <button type="submit" className="btn btn-primary partial-expand rounded" disabled={submitting}>Submit</button>
              <button type="button" className="btn btn-danger partial-expand rounded f-r" disabled={ pristine || submitting } onClick={ reset }>Clear Values</button>
            </div>
          </form>
         { this.renderServerError() }
        </div>
      </div>
    )
  }
}

BlogPostForm = reduxForm({
  form: 'BlogPostForm',
  validate,
  fields: ['name', 'image', 'imgtitle', 'description']
})(BlogPostForm);


export default BlogPostForm = connect(BlogPostForm);

BlogActions.jsx:

import * as app from 'axios';

const ROOT_URL = 'http://localhost:3001';

// submits Redux form data to server
export const editPost = ({ id, title, image, imgtitle, description, navTitle }) => {
 return app.put(`${ROOT_URL}/post/edit/${id}?userId=${config.user}`, { id, title, image, imgtitle, description, navTitle }, config)
 .then(response => {
   return { success: response.data.message }
  })
  .catch(({ response }) => {
    if(response.data.deniedAccess) {
      return { err: response.data.deniedAccess }
    } else {
      return { err: response.data.err }
    }
  });
}

// fetches a single post from the server for front-end editing     
export const fetchPost = (id) => {
  return app.get(`${ROOT_URL}/posts/${id}`)
  .then(response => {
     return { foundPost: response.data.post}
   })
   .catch(({ response }) => {
     return { err: response.data.err };
   });
}    

RenderAlert.jsx:

import React, { Component } from 'react';

const RenderAlert = (props) => {   
    const displayMessage = () => {
      const { errorMessage } = props;

      if (errorMessage) {
        return (
          <div className="callout-alert">
            <p>
              <i className="fa fa-exclamation-triangle" aria-hidden="true"/>
              <strong>Error! </strong> { errorMessage }
            </p>
          </div>
        );
      }
    }

    return (
      <div>
        { displayMessage() }
      </div>
    );
  }


export default RenderAlert;

Reducers.jsx

import { routerReducer as routing } from 'react-router-redux';
import { reducer as formReducer } from 'redux-form';
import { combineReducers } from 'redux';  

const rootReducer = combineReducers({
  form: formReducer,
  routing
});

export default rootReducer;
0
Matt Carlotta