Menu
Menu Sheet Overlay
Search
Search Sheet

Working with Forms

    Building Forms #

    When building forms with the Progressive Mobile Web SDK, we recommend using the Redux Form library to manage form state and handle validation. Redux Form works alongside the form components included in the SDK: CardInput, FieldSet, FieldRow, Field, and Stepper.

    Redux Form has its own Field component which is used to connect the value of the field to the state. You can use its component prop to determine what it renders as that field. You should use this prop to render the SDK’s Field component, like so:

    <ReduxForm.Field
        component={Field}
        label="First Name" // The Field component will render this label
        name="first-name" // Redux form needs the name to be able to connect this field to the store
    >
        <input type="text" /> // The Field component expects the input as a single child
    </ReduxForm.Field>
    

    This will render an SDK Field that is connected to the store. For more information, see the full example on the Field component.

    Validating Forms #

    Redux Form offers two kinds of validation: sync validation and submit validation.

    Sync Validation #

    Sync validation occurs when the form first mounts and whenever the user changes a value within the form. It does not run when the user submits the form. If the value within a field fails sync validation, an error is shown for that field and form submission is disabled. The SDK’s Field component automatically works with Redux Form’s validation and displays these errors. By default, errors are hidden for a field if the user hasn’t interacted with or is currently editing the field. This can be configured using the Field component’s props. Redux Form’s documentation explains how to add sync validation to your form.

    You might run into a situation where you have a form with different sections that need to be validated separately. For example, you might have a payment form with multiple payment methods such as credit card and gift card. You only want to validate the credit card form if the credit card payment option is selected.

    In these cases, it’s often best to break the form up into two smaller forms. This makes the logic you need to write for validation much simpler. When you need to submit the forms, you can determine which data should be submitted based on which payment option is used.

    Submit Validation #

    Submit validation occurs when the form is submitted. To use submit validation, your submit action must return a Promise rejected with a SubmissionError. Redux Form will use the SubmissionError to show errors on the fields and the form itself.

    Because SubmissionErrors are related to the user interface, we recommend using SubmissionErrors within the UI action, rather than within the connector command. For an example of how this might work, see Form Submission Example.

    Submitting Forms #

    One of the most important steps when submitting a form is ensuring that you’re sending it all of the correct data. To get the data that you need for the form submission, we suggest using selectors within the submit command to get the data from the state. This is easier than trying to pass all of the data that you need to the submit command, especially when that data may come from several different sources.

    Form Submission Example #

    // web/app/containers/login/container.jsx
    <form onSubmit={handleSubmit(submit)}>
    
    // web/app/containers/login/actions.js
    import {SubmissionError} from 'redux-form'
    import {login} from '../../integration-manager/connector/login/commands'
    import {getLoginFormValues} from '../../containers/login/selectors'
    
    export const submit = () => (dispatch, getState) => {
        const selector = createPropsSelector({
            formValues: getLoginFormValues
        })
    
        const data = selector(getState())
    
        const {
            username,
            password,
            rememberMe
        } = data.formValues
    
        return dispatch(login(username, password, rememberMe))
            .then(() => {
                // Handle the successful form submission
            })
            .catch((errors) => new SubmissionError(errors))
    }
    
    // web/app/integration-manager/connector/login/commands.js
    import {makeJsonEncodedRequest} from 'progressive-web-sdk/dist/utils/fetch-utils'
    
    export const login = (username, password, rememberMe) => (dispatch, getState) => {
        const url = 'api/login'
        const formData = {
            username,
            password,
            rememberMe
        }
    
        return makeJsonEncodedRequest(url, formData)
            .then((response) => response.json())
            .then((json) => {
                if (json.errors) {
                    throw json.errors
                }
            })
    }
    
    // web/app/containers/login/selectors.js
    import {getForm} from '../../store/selectors'
    
    export const getLoginForm = createSelector(
        getForm,
        ({loginForm}) => loginForm
    )
    
    export const getLoginFormValues = createSelector(
        getLoginForm,
        ({values}) => values
    )
    

    Initial Values #

    Redux Form’s official documentation offers examples for supplying initial values into fields, but it might be more useful to see examples of how to do this in our progressive web app instead.

    Below are two examples of forms that use initial values in two distinct ways: the first demonstrates how to set static initial values, and the second example demonstrates how to use selectors to provide the initial values dynamically.

    // web/app/containers/my-form/container.jsx
    import React from 'react'
    import PropTypes from 'prop-types'
    import * as ReduxForm from 'redux-form'
    import {connect} from 'react-redux'
    import FormFields from './partials/form-fields.jsx'
    
    class MyForm extends React.Component {
        constructor(props) { /* ... */ }
        onSubmit(values) { /* ... */ }
        render() {
            return (<FormFields />)
        }
    }
    
    MyForm.propTypes = { /* ... */ }
    
    const mapStateToProps = createPropsSelector({/* ... */})
    const mapDispatchToProps = { /* ... */ }
    
    // This is where we pass in our static initial values. This object's
    // keys reference a field's name prop, and the object values
    // refer to the field's initial values
    const MyFormReduxForm = ReduxForm.reduxForm({
        form: 'my-form',
        initialValues: {
            textFieldName: 'Text Field Initial Value',
            checkboxFieldName: true,
            selectFieldName: 'Select Field\'s Initial Option Value'
        }
    })(MyForm)
    export default connect(mapStateToProps, mapDispatchToProps)(MyFormReduxForm)
    

    There is a pattern that is used in the progressive web apps to help simplify syncing form initial values in relation to other aspects of the UI. Consider the following example: a checkout payment form that uses the same data that the user submitted to a shipping form earlier in the checkout process.

    // web/app/containers/my-form/container.jsx
    import React from 'react'
    import PropTypes from 'prop-types'
    import * as ReduxForm from 'redux-form'
    import {connect} from 'react-redux'
    import Field from 'progressive-web-sdk/dist/components/field'
    import FieldRow from 'progressive-web-sdk/dist/components/field-row'
    
    // We import a shared form partial that is used in both the
    // shipping form, and the payment form. It's format can be thought
    // of as similar as the fields in the previous form, above.
    import AddressFields from './partials/address-fields.jsx'
    
    // Here we import the payment form selector that fetches the
    // initial form values that the user already submitted during
    // the shipping part of the checkout process.
    import {getPaymentFormInitialValues} from '../../../store/checkout/billing/selectors'
    
    class PaymentForm extends React.Component {
        constructor(props) { /* ... */ }
        onSubmit(values) { /* ... */ }
        render() {
            return (
                <div>
                    <AddressFields />
                    {/* and so on... */}
                </div>
            )
        }
    }
    
    PaymentForm.propTypes = { /* ... */ }
    
    // Note this: instead of passing in static values, we fetch the
    // values from the Redux state. This way, we are able to use the
    // same values that the user submitted to the shipping form
    // earlier in the checkout process.
    const mapStateToProps = createPropsSelector({
        initialValues: getPaymentFormInitialValues
    })
    
    const mapDispatchToProps = { /* ... */ }
    const PaymentFormReduxForm = ReduxForm.reduxForm( /* ... */)(PaymentForm)
    export default connect(mapStateToProps, mapDispatchToProps)(PaymentFormReduxForm)
    

    The above example of the payment form is a common use case in eCommerce applications, but the possibilities do not end there. Other possibilities include filling a form’s fields based on user interactions, such as a user selecting options on a product before submitting a “add to cart” action, or a guest user having empty form fields where a logged in user would have them pre-filled, and so on.

    Debugging forms #

    Silent Submission Errors #

    You might be experiencing this issue if:

    Redux Form expects your submit function to return a Promise. It uses the Promise you return to determine if your submission has succeeded. If the onSubmit function throws an exception, your submission has failed. However, one side effect of this is that Redux Form can end up silently swallowing exceptions that occur in your onSubmit function. When this happens, start by looking for any exceptions that could be thrown inside your onSubmit function. It can be helpful to step through this function line by line to determine exactly what’s going wrong. You could also wrap the code inside your onSubmit function in a try {} catch(e) {} so you can see the exception thrown inside the catch.

    Missing Hidden Inputs #

    You might be experiencing this issue if:

    HTML forms often use hidden inputs to send extra data to the back end during a form submission. This extra data is sometimes required for the form to be able to submit correctly. In this case, you should check to make sure your form is including all of these hidden input values in the submission. A good way to check this is to compare the network requests for form submissions with the desktop site. If the desktop site’s submission includes data that isn’t in your submission, it’s likely due to a missing hidden input.

    Thanks to Redux Form, you do not need to render these hidden inputs within the form itself. Instead, you can add their values to the form submission request when you send it.

    // web/app/integration-manager/connector/login/commands.js
    export const login = (username, password, rememberMe) => (dispatch, getState) => {
        const url = 'api/login'
        const selector = createPropsSelector({
            hiddenInputs: getHiddenInputs
        })
    
        const props = selector(getState())
        const formData = {
            ...hiddenInputs,
            username,
            password,
            rememberMe
        }
    
        return makeJsonEncodedRequest(url, formData)
            .then((response) => response.json())
            .then((json) => {
                if (json.errors) {
                    throw json.errors
                }
            })
    }
    

    Incorrect Encoding Type #

    You might be experiencing this issue if:

    Form endpoints usually expect data sent to them to be formatted in a particular way. It’s important to ensure that the data that you’re sending is using the correct Content-Type header so the endpoint is able to use the data you’re sending it.

    When using an HTML form, check out which Content-Type the desktop form is using. In most cases, the Content-Type used will be application/x-www-form-urlencoded. This is the Content-Type used in the makeFormEncodedRequest utility in fetch-utils.js.

    When using an API form endpoint, take a look at the documentation to determine the correct encoding. In most cases, the Content-Type used will be application/json. This is the Content-Type used in the makeJsonEncodedRequest utility in fetch-utils.js.

    User Experience #

    Contextual Keyboard #

    We strongly recommend always using the correct input types and contextual keyboards for your form fields. Some examples of the correct input types include:

    Autocomplete Attributes #

    Browser autofill can help users fill out forms much faster, but it’s not always accurate. The browser has to do a lot of work to infer what each field is and what data should be entered into it. However, it’s possible to add autocomplete attributes to your input elements to fix this. You can use autocomplete attributes to tell the browser what the data you’re expecting for each field. We recommend using autocomplete attributes for all of the fields in a checkout form.

    You can find the full list of autocomplete attributes here.

    Pasting Passwords

    Not allowing the user to paste into the password field makes form entry slower, and it breaks password managers! As a result, the user should always be able to paste into the password field.

    Performance #

    One common performance pitfall when using forms is rerendering a large section of the app whenever the value of a form changes. This can make the app laggy and unresponsive. This can happen if you include selectors for form values in the mapStateToProps of your component. In general, you shouldn’t need to include these selectors as Redux Form will handle rendering the values inside the fields. It only becomes necessary when you want to display form values somewhere else in the component, outside of the input. Whenever possible, try to design your forms in such a way that this isn’t necessary.

    Analytics #

    The Analytics Manager makes it easy to instrument events for all form fields. To instrument these events, the following requirements must be met:

    When the ID of the form is the same as the name of the corresponding Redux form, the Analytics Manager will send analytics events for any submission errors that occur on this form.

    The SDK comes with a list of predefined names for common ecommerce buttons and form fields. Those names can be imported from progressive-web-sdk/dist/analytics/data-objects. If none of the provided names match the field or button you’re working with, you can define a custom name. Define this value as a constant within your project and reuse it wherever necessary.

    The following example shows a form with the appropriate markup.

    // JSX
    <form id={SIGN_IN_FORM_NAME} data-analytics-name={UI_NAME.login}>
        <FieldRow>
            <ReduxForm.Field
                component={Field}
                name="username"
                label="Email"
            >
                <input type="email" data-analytics-name={UI_NAME.email} />
            </ReduxForm.Field>
        </FieldRow>
    
        <FieldRow>
            <ReduxForm.Field
                component={Field}
                name="password"
                label="Password"
            >
                <input type="password" data-analytics-name={UI_NAME.password} />
            </ReduxForm.Field>
        </FieldRow>
    </form>
    
    // Creating the Redux form
    const ReduxSignInForm = reduxForm({
        form: SIGN_IN_FORM_NAME
    })(SignInForm)