Tutorial

Creating a Custom Form with Persistent State

  • 21 March 2023
  • 0 replies
  • 87 views

Userlevel 4
Badge +4

Insight Cards are great to provide customized views of the data in context, but they are also so helpful to create custom forms for data entry, add additional input validation logic or use data from an external system, there are many possibilities here. In this blog we will explore the fundamentals on creating a custom form, how to properly add custom input fields and process form submission to update a conversation custom attributes, and finally, add persistent state will give us a commonly asked feature to keep the input values when the user navigates out of the context before submitting the form or the Insight Card get automatically refreshed after an object modification by another user session.

 

1. Create a basic Form component

 

The following is a custom class-based component that will implement our form. A class-based component allows us to implement local state, which is essential to preserve the input data in the form.

 

```javascript

class CustomForm extends React.Component {

 constructor(props) {

   super(props);

 }

 

 onSubmit = (event) => {

   event.preventDefault();

 }

 

 render() {

   return (

     <Segment style={{ padding: '0px 1em' }}>

       <form onSubmit={this.onSubmit}>

           <BasicRow

              label="Notes"

              value={<input type="text" name="notesStr" />}

           />

           <BasicRow

              label="Comments"

              value={<input type="text" name="commentsStr" />}

           />

           <BasicRow

             value={<Button onClick={this.onSubmit}>Submit</Button>}

           />

       </form>

     </Segment>

   );

 }

}

<CustomForm />

```

 

2. Make inputs a controlled components

 

Intrinsic components like `<input> `and `<select>` maintain their own state as a regular HTML page does. However, the state is reset during the next React render cycle, this is called an Uncontrolled component.

 

Uncontrolled components are not practical for many reasons in a regular React application. Additionally, an Insight Card can re-render itself when the Conversation object is modified by another user session.

 

In order to make the `<input>` a controlled component, we need to implement local state management in our `CustomForm` component. The field values (`formData`) will be stored in the component state, so the values will be kept among the render cycles. The `onInputHandler` will update the field values when the user changes the input.

 

The `state` is initialized in the component's `constructor`, the `formData` object is populated with the current values of the attributes from the `conversation` object which is provided by the Insight Card context.

 

```javascript

class CustomForm extends React.Component {

 constructor(props) {

   super(props);

   const formData = {

     notesStr: conversation.custom?.notesStr ?? '',

     commentsStr: conversation.custom?.commentsStr ?? ''

   };

   this.state = { formData };

 }

 

 onSubmit = (event) => {

   event.preventDefault();

 }

 

 onInputHandler = (event) => {

   const fieldName = event.target.name;

   const value = event.target.value;

 

   let formData = {

     ...this.state.formData,

     [fieldName]: value

   };

   this.setState({

     formData: { ...formData },

     message: null

   });

 }

 

 render() {

   const formData = this.state.formData;

   return (

     <Segment style={{ padding: '0px 1em' }}>

       <form onSubmit={this.onSubmit}>

           <BasicRow

              label="Notes"

              value={<input type="text" name="notesStr" onChange={this.onInputHandler} value={formData['notesStr']} />}

           />

           <BasicRow

              label="Comments"

              value={<input type="text" name="commentsStr" onChange={this.onInputHandler} value={formData['commentsStr']} />}

           />

           <BasicRow

             value={<Button onClick={this.onSubmit}>Submit</Button>}

           />

       </form>

     </Segment>

   );

 }

}

<CustomForm />

```

 

3. Handle the Form Submission

 

At this point our form is capable of rendering the form inputs with current values, handling the user input and keeping the form fields value changes. Now the form is ready to be submitted and complete the processing. Here we use two sample custom attributes that will be updated in the conversation object. To update the conversation will use the Kustomer API endpoint `PATCH /v1/conversations/{id}`, the method `PATCH` is equivalent to `update` as we are only required to provide the attributes values being modified.

 

The method `patchFormData` was added here to handle the details of API calls using the `KustomerRequest` function available in all Insight Cards to allow us to make calls to the Kustomer API endpoints.


 

```javascript

class CustomForm extends React.Component {

 constructor(props) {

   super(props);

   const formData = {

     notesStr: conversation.custom?.notesStr ?? '',

     commentsStr: conversation.custom?.commentsStr ?? ''

   };

   this.state = { formData };

 }

 

 patchFormData = async (url, body) => {

   return new Promise((resolve, reject) => {

     KustomerRequest({

       url,

       method: 'PATCH',

       body,

     },

       (err, response) => {

         if (err) reject(err);

         else resolve(response);

       });

   });

 }

 

 onSubmit = (event) => {

   event.preventDefault();

   const postData = {

     custom: {

       commentsStr: this.state.formData.commentsStr,

       notesStr: this.state.formData.notesStr,

     }

   };

 

   this.patchFormData(`/v1/conversations/${conversation.id}`, postData)

     .then(resp => {

       const { commentsStr, notesStr } = resp?.custom;

       this.setState({

         formData: { commentsStr, notesStr },

         message: 'Submit successfully.'

       }

       );

     })

     .catch(err => {

       this.setState({ message: err?.toString() });

     })

 }

 

 onInputHandler = (event) => {

   const fieldName = event.target.name;

   const value = event.target.value;

 

   let formData = {

     ...this.state.formData,

     [fieldName]: value

   };

   this.setState({

     formData: { ...formData },

     message: null

   });

 }

 

 render() {

   const formData = this.state.formData;

   const messageStyle = { padding: "10px", display: 'flex', flexDirection: 'row', justifyContent: 'space-between', width: '100%' };

   return (

     <Segment style={{ padding: '0px 1em' }}>

       <form onSubmit={this.onSubmit}>

           {this.state.message && <div style={messageStyle}>

             {this.state.message}

           </div>}

           <BasicRow

              label="Notes"

              value={<input type="text" name="notesStr" onChange={this.onInputHandler} value={formData['notesStr']} />}

           />

           <BasicRow

              label="Comments"

              value={<input type="text" name="commentsStr" onChange={this.onInputHandler} value={formData['commentsStr']} />}

           />

           <BasicRow

             value={<Button onClick={this.onSubmit}>Submit</Button>}

           />

       </form>

     </Segment>

   );

 }

}

<CustomForm />

```

 

4. Add persistent local state

 

There are certain cases when we want to keep unsaved form values in a persistent storage. A persistent storage is secondary data storage that keep the data even after the component is unmounted, this is useful in circumstances where the user by mistake navigate to another page section in the timeline, or another user session modifies the current conversation and the Kustomer platform updates the Insight Card context so you can see the latest object version.

 

We will use standard LocalStorage which is available in all modern browsers. The `localStorage` lives in the `window` object and it's easy to use with a few methods to read (`getItem`), update (`setItem`) and remove (`removeItem`) an object by a key. The form field values will saved to the `localStorage` as soon they are input (onInputHandler), when the `CustomForm` is initialized (`constructor`), the previously saved `formData` is read and forms values are restored if input values were not submitted.

 

After the form is successfully submitted, the data in the `localStorage` needs to be removed to avoid showing old values instead of most current values coming from the `conversation` in context.

 

```javascript

class CustomForm extends React.Component {

 constructor(props) {

   super(props);

   let formData = localStorage.getItem(`formwithstate-formdata-${conversation.id}`);

   if (formData) {

     formData = JSON.parse(formData);

   } else {

     formData = {

       notesStr: conversation.custom?.notesStr ?? '',

       commentsStr: conversation.custom?.commentsStr ?? ''

     };

   }

   this.state = { formData };

 }

 

 patchFormData = async (url, body) => {

   return new Promise((resolve, reject) => {

     KustomerRequest({

       url,

       method: 'PATCH',

       body,

     },

       (err, response) => {

         if (err) reject(err);

         else resolve(response);

       });

   });

 }

 

 onSubmit = (event) => {

   event.preventDefault();

   const postData = {

     custom: {

       commentsStr: this.state.formData.commentsStr,

       notesStr: this.state.formData.notesStr,

     }

   };

 

   this.patchFormData(`/v1/conversations/${conversation.id}`, postData)

     .then(resp => {

       const { commentsStr, notesStr } = resp?.custom;

       this.setState({

         formData: { commentsStr, notesStr },

         message: 'Submit successfully.'

       }

       );

       localStorage.removeItem(`formwithstate-formdata-${conversation.id}`);

     })

     .catch(err => {

       this.setState({ message: err?.toString() });

     })

 }

 

 onInputHandler = (event) => {

   const fieldName = event.target.name;

   const value = event.target.value;

 

   let formData = {

     ...this.state.formData,

     [fieldName]: value

   };

   this.setState({

     formData: { ...formData },

     message: null

   });

 

   localStorage.setItem(`formwithstate-formdata-${conversation.id}`, JSON.stringify(formData));

 }

 

 render() {

   const formData = this.state.formData;

   const inputStyle = { minWidth: '35px', marginRight: '16px' };

   const messageStyle = { padding: "10px", display: 'flex', flexDirection: 'row', justifyContent: 'space-between', width: '100%' };

   return (

     <Segment style={{ padding: '0px 1em' }}>

       <form onSubmit={this.onSubmit}>

         {this.state.message && <div style={messageStyle}>

           {this.state.message}

         </div>}

         <BasicRow

           label="Notes"

           value={<input style={inputStyle} type="text" name="notesStr" onChange={this.onInputHandler} value={formData['notesStr']} />}

         />

         <BasicRow

           label="Comments"

           value={<input style={inputStyle} type="text" name="commentsStr" onChange={this.onInputHandler} value={formData['commentsStr']} />}

         />

         <BasicRow

           value={<Button onClick={this.onSubmit}>Submit</Button>}

         />

       </form>

     </Segment>

   );

 }

}

<CustomForm />

```

 

We have completed our custom form component that you can use as a starting point to implement your own solutions, add custom validations logic or even integrate with external API systems.

 

If you have any general questions about how to better understand Workflows as a whole, or need assistance with debugging an issue yourself, feel free to respond to this post and we’ll be in touch shortly. 


0 replies

Be the first to reply!

Reply