Using Smart.Form for editing a row in a Smart.Grid in React
What is Smart.Form?
The purpose of our Form component is to provide you an awesome and modern looking Form component, which is easy customizable and will help you to deliver better and professional looking web Forms. You do not have to write complex functions and attach event listeners to validate your form, you can do it fast with our Form Validation.
You can read more about our Form component here: Smart.Form
Implementing a React App with Smart.Grid that can interact with the Smart.Form
The main goal of this article is to show you how to bind the Grid React Component in the Form so you can edit and validate a single row with a form.
Setup React Environment
The easiest way is via create-react-app. Open a terminal and type:
npx create-react-app smart-app cd smart-app
-
open src/App.js and everything inside the div with className App
<div className="App"> </div>
- remove import logo from './logo.svg';
- remove everything from App.css
- remove src/logo.svg
Setup Smart UI
Smart UI for React is distributed as smart-webcomponents-react NPM package
-
Open the terminal and install the package:
npm install smart-webcomponents-react
-
Import the CSS in the App.js
import 'smart-webcomponents-react/source/styles/smart.default.css'; import './App.css'; function App() { return ( <div className="App"></div> ); } export default App;
-
import the Grid Component
import { Grid } from 'smart-webcomponents-react/grid';
-
import the DropDownList
import {DropDownList} from 'smart-webcomponents-react/dropdownlist';
Initialize Form
Our grid will have a first name, last name, age and gender columns, so in the form, we will initialize four form controls for these data fields.
-
First, we need to store the form's reference so we can get & set its value when we select & edit a row from the grid.
We need to import useRef from react
import { useRef } from 'react';
Now we can create a ref for the form. See what ref is: here
Basically, it is a mutable object that can store value in its .current property across re-renders & changing it will not trigger re-render
const formRef = useRef(null);
-
The form needs a tag with a selector so we will add a form tag with a wrapper
<div id="form-wrapper"> <form id='form' /> </div>
-
Once we have a form tag with an id we can initialize our form with the Smart.Form constructor and assign the returned object to the form's ref. We will add it in a useEffect, because we do not want to initialize it on every render, but only on mounting.
Check here what useEffect does. (It is a way to catch a lifecycle phase in React)
import { useEffect, useRef } from 'react';
useEffect(() => { formRef.current = new window.Smart.Form('#form', { controls: [ { controlType: 'input', dataField: 'firstName', label: 'First Name', placeholder: 'First Name' }, { controlType: 'input', dataField: 'lastName', label: 'Last Name', placeholder: 'Last Name' }, { controlType: 'number', dataField: 'age', label: 'Age', placeholder: 'select age' }, { controlType: 'dropDownList', dataField: 'gender', label: 'Gender', placeholder: 'select gender', controlOptions: { dataSource: ['male', 'female', 'other'] } }, { controlType: 'group', columns: 2, controls: [ { controlType: 'button', label: 'Edit', name: "editBtn", cssClass: 'success', }, { controlType: 'button', name: "cancelBtn", label: 'Cancel', } ] } ] }); }, []);
-
See that we have buttons, but if we click them, they do not do anything
Let's catch each click of the button by adding an event listener of the form's wrapper and in it check if a button is clicked
const handleEditButtonClick = () => {} const handleCancelButtonClick = () => {} const handleFormClick = (e) => { const target = e.target; switch (target.name) { case 'editBtn': handleEditButtonClick(); break; case 'cancelBtn': handleCancelButtonClick(); break; default: break; } }
<div id="form-wrapper" onClick={handleFormClick}> <form id='form' /> </div>
-
Let's style the form a little bit. Paste this in App.css
.App { margin: 2rem 4rem 6rem 4rem; } #form-wrapper { padding: 20px; border-radius: 10px; width: fit-content; border: 1px solid #333; margin: auto; }
-
We need a button, which shows and hides the form and later begins editing a row. For this, we will use Smart UI button.
First, we need to import the button component
import { Button } from 'smart-webcomponents-react/button';
Now we can add it to the template, but first we should add a click handler for it
const handleStartEditingButtonClick = () => {}
<div id="grids-editing-button-wrapper"> <Button id='edit-grids-row-button' onClick={handleStartEditingButtonClick}>Start Editing</Button> </div>
Paste this css in the App.css to position the button
#grids-editing-button-wrapper { margin: 1rem 0; display: flex; justify-content: center; }
To hide & show the form we need to save its state (shown or hidden) and change this state on clicking the buttons.
We will import useState so we can use state in our component. (See what state is here)
Basically, state is the way to store data between component re-renders and changing it will causes component's re-render.
import { useEffect, useRef, useState } from 'react';
When we click the 'Start Editing' button the form should become visible and on 'Edit' or 'Cancel' the form should become hidden.
We will change the state this way:
const handleEditButtonClick = () => { setIsFormHidden(true); } const handleCancelButtonClick = () => { setIsFormHidden(true); } const handleStartEditingButtonClick = () => { setIsFormHidden(false); }
Depending on the state a style will be applied for the form's wrapper to show and hide it.
<div id="form-wrapper" onClick={handleFormClick} style={{ display: isFormHidden ? 'none' : 'block' }}> <form id='form' /> </div>
-
We are ready with our form. Here is what we have written so far:
import 'smart-webcomponents-react/source/styles/smart.default.css'; import './App.css'; import { Grid } from 'smart-webcomponents-react/grid'; import 'smart-webcomponents-react/source/modules/smart.dropdownlist'; import { useEffect, useRef, useState } from 'react'; import { Button } from 'smart-webcomponents-react/button'; function App() { const formRef = useRef(null); const [isFormHidden, setIsFormHidden] = useState(true); useEffect(() => { formRef.current = new window.Smart.Form('#form', { controls: [ { controlType: 'input', dataField: 'firstName', label: 'First Name', placeholder: 'First Name' }, { controlType: 'input', dataField: 'lastName', label: 'Last Name', placeholder: 'Last Name' }, { controlType: 'number', dataField: 'age', label: 'Age', placeholder: 'select age' }, { controlType: 'dropDownList', dataField: 'gender', label: 'Gender', placeholder: 'select gender', controlOptions: { dataSource: ['male', 'female', 'other'] } }, { controlType: 'group', columns: 2, controls: [ { controlType: 'button', label: 'Edit', name: "editBtn", cssClass: 'success', }, { controlType: 'button', name: "cancelBtn", label: 'Cancel', } ] } ] }); }, []); const handleFormClick = (e) => { const target = e.target; switch (target.name) { case 'editBtn': handleEditButtonClick(); break; case 'cancelBtn': handleCancelButtonClick(); break; default: break; } } const handleEditButtonClick = () => { setIsFormHidden(true); } const handleCancelButtonClick = () => { setIsFormHidden(true); } const handleStartEditingButtonClick = () => { setIsFormHidden(false); } return ( <div className="App"> <div id="grids-editing-button-wrapper"> <Button id='edit-grids-row-button' onClick={handleStartEditingButtonClick}>Start Editing</Button> </div> <div id="form-wrapper" onClick={handleFormClick} style={{ display: isFormHidden ? 'none' : 'block' }}> <form id='form' /> </div> </div> ); } export default App;
Initialize Grid
Currently we have a form that does nothing. Now we will create a Smart.Grid with our React's Grid Component.
We will have 4 columns First Name, Last Name, Age and Gender and the selection will be enabled so we can select which column we want to edit.
Our Grid supports sorting, filtering and paging so we will enable them also.
We will save a ref for the Grid Component, because we will have to use Grid's methods.
- Firstly, we should define our grid's settings.
const [gridSettings, setGridSettings] = useState({ dataSource: new window.Smart.DataAdapter({ dataSource: gridData, dataFields: [ 'firstName: string', 'lastName: string', 'age: number', 'gender: string' ] }), selection: { enabled: true, action: 'click', mode: 'one' }, sorting: { enabled: true, sortMode: 'one' }, filtering: { enabled: true }, paging: { enabled: true, pageSize: 15 }, pager: { visible: true, pageSizeSelector: { visible: true, dataSource: [15, 30, 50] } }, columns: [ { label: 'First Name', dataField: 'firstName' }, { label: 'Last Name', dataField: 'lastName' }, { label: 'Age', dataField: 'age' }, { label: 'Gender', dataField: 'gender' } ] });
-
See that we do not have gridData. Let's add it.
Add this method outside the Component:
const generateGridData = (rows) => { const data = []; const firstNames = ['Peter', 'Alexander', 'George', 'Steve', 'Zlat', 'Philip', 'Michael', 'Oliver', 'Kassandra', 'Maria', 'Iglika']; const lastNames = ['Madison', 'Marley', 'Bolden', 'Raven', 'Steve', 'Plain', 'Michael', 'Hansley', 'Ashley', 'Monroe', 'West']; for (let i = 0; i < rows; i++) { const row = { firstName: firstNames[Math.floor(Math.random() * (firstNames.length - 0) + 0)], lastName: lastNames[Math.floor(Math.random() * (lastNames.length - 0) + 0)], age: Math.floor(Math.random() * (110 - 10) + 10), gender: Math.random() > 0.5 ? 'male' : 'female' } data.push(row); } return data; }
Now initialize a state for the gridData this way or with a desired rows' count
const [gridData] = generateGridData(30);
-
After creating a ref for the Grid, we will be ready to create our Smart.Grid
const gridRef = useRef(null);
<div id="grid-wrapper"> <Grid ref={gridRef} id="grid" {...gridSettings} /> </div>
Again we wrapped the grid to make the positioning easier with the help of the following CSS passed in the App.css
#grid-wrapper { margin: 20px; display: flex; justify-content: center; }
-
Now we have Smart.Form and Smart.Grid. It is time to bind them. Here is the code so far.
import 'smart-webcomponents-react/source/styles/smart.default.css'; import './App.css'; import { Grid } from 'smart-webcomponents-react/grid'; import { Smart } from 'smart-webcomponents-react/form'; import 'smart-webcomponents-react/source/modules/smart.dropdownlist'; import { Button } from 'smart-webcomponents-react/button'; import { useEffect, useRef, useState } from 'react'; const generateGridData = (rows) => { const data = []; const firstNames = ['Peter', 'Alexander', 'George', 'Steve', 'Zlat', 'Philip', 'Michael', 'Oliver', 'Kassandra', 'Maria', 'Iglika']; const lastNames = ['Madison', 'Marley', 'Bolden', 'Raven', 'Steve', 'Plain', 'Michael', 'Hansley', 'Ashley', 'Monroe', 'West']; for (let i = 0; i < rows; i++) { const row = { firstName: firstNames[Math.floor(Math.random() * (firstNames.length - 0) + 0)], lastName: lastNames[Math.floor(Math.random() * (lastNames.length - 0) + 0)], age: Math.floor(Math.random() * (110 - 10) + 10), gender: Math.random() > 0.5 ? 'male' : 'female' } data.push(row); } return data; } function App() { const [gridData] = generateGridData(30); const formRef = useRef(null); const [isFormHidden, setIsFormHidden] = useState(true); const gridRef = useRef(null); const [gridSettings, setGridSettings] = useState({ dataSource: new window.Smart.DataAdapter({ dataSource: gridData, dataFields: [ 'firstName: string', 'lastName: string', 'age: number', 'gender: string' ] }), selection: { enabled: true, action: 'click', mode: 'one' }, sorting: { enabled: true, sortMode: 'one' }, filtering: { enabled: true }, paging: { enabled: true, pageSize: 15 }, pager: { visible: true, pageSizeSelector: { visible: true, dataSource: [15, 30, 50] } }, columns: [ { label: 'First Name', dataField: 'firstName' }, { label: 'Last Name', dataField: 'lastName' }, { label: 'Age', dataField: 'age' }, { label: 'Gender', dataField: 'gender' } ] }); useEffect(() => { formRef.current = new Smart.Form('#form', { controls: [ { controlType: 'input', dataField: 'firstName', label: 'First Name', placeholder: 'First Name' }, { controlType: 'input', dataField: 'lastName', label: 'Last Name', placeholder: 'Last Name' }, { controlType: 'number', dataField: 'age', label: 'Age', placeholder: 'select age' }, { controlType: 'dropDownList', dataField: 'gender', label: 'Gender', placeholder: 'select gender', controlOptions: { dataSource: ['male', 'female', 'other'] } }, { controlType: 'group', columns: 2, controls: [ { controlType: 'button', label: 'Edit', name: "editBtn", cssClass: 'success', }, { controlType: 'button', name: "cancelBtn", label: 'Cancel', } ] } ] }); }, []); const handleFormClick = (e) => { const target = e.target; switch (target.name) { case 'editBtn': handleEditButtonClick(); break; case 'cancelBtn': handleCancelButtonClick(); break; default: break; } } const handleEditButtonClick = () => { setIsFormHidden(true); } const handleCancelButtonClick = () => { setIsFormHidden(true); } const handleStartEditingButtonClick = () => { setIsFormHidden(false); } return ( <div className="App"> <div id="grids-editing-button-wrapper"> <Button id='edit-grids-row-button' onClick={handleStartEditingButtonClick}>Start Editing</Button> </div> <div id="form-wrapper" onClick={handleFormClick} style={{ display: isFormHidden ? 'none' : 'block' }}> <form id='form' /> </div> <div id="grid-wrapper"> <Grid ref={gridRef} id="grid" {...gridSettings} /> </div> </div> ); } export default App;
We are ready to bind our Form to the Grid
-
The first thing is to check whether a row is selected when the 'Start editing' is clicked, if not, we will take the first visible row in the Grid. After that the form's value should be changed to the row data.
The data from the grid is being collected via Smart.Grid's methods
See the Smart.Grid docs here
const handleStartEditingButtonClick = async () => { disableGrid(true); const selectedRowId = (await gridRef.current.getSelectedRowIds())[0] const selectedRow = await gridRef.current.getRowByIndex(selectedRowId); if(selectedRowId && selectedRow.visibleIndex >= 0) { formRef.current.value = await gridRef.current.getRowData(selectedRowId); formRef.current.rowId = selectedRowId; } else { const firstVisibleRow = (await gridRef.current.getVisibleRows())[0]; formRef.current.value = await gridRef.current.getRowData(firstVisibleRow.data.$.id); formRef.current.rowId = firstVisibleRow.data.$.id; gridRef.current.ensureVisible(firstVisibleRow.data.$.id); } setIsFormHidden(false); }
-
The next is to disable the Grid when we are editing and enable it when we finish the editing. For this, we will change our gridSettings state.
const disableGrid = (isDisabled) => { setGridSettings( settings => ({ ...settings, selection: { ...settings.selection, enabled: !isDisabled }, sorting: { ...settings.sorting, enabled: !isDisabled }, filtering: { ...settings.filtering, enabled: !isDisabled } }) ) } const handleEditButtonClick = () => { setIsFormHidden(true); disableGrid(false); } const handleCancelButtonClick = () => { setIsFormHidden(true); disableGrid(false); } const handleStartEditingButtonClick = async () => { disableGrid(true); //Rest of the previously written code }
-
We are almost ready! Our next step will be to commit the change when the editing is finished.
const handleEditButtonClick = () => { setIsFormHidden(true); gridRef.current.updateRow(formRef.current.rowId, formRef.current.value); gridRef.current.ensureVisible(formRef.current.rowId); disableGrid(false); }
-
We can step further and add validation. Each of our Smart.Form controls can accept validation rules.
How to use validation rules here
We will add validation for the first name, last name, age.
First and Last names will be with min length of 3 and the age should be greater than 0 and less that 120
The 'Edit' button will be disabled if the form is invalid
The first thing is to define our validation rules like this:
controls: [ { controlType: 'input', dataField: 'firstName', label: 'First Name', placeholder: 'First Name', validationRules: [ { type: 'stringLength', min: 3, } ] }, { controlType: 'input', dataField: 'lastName', label: 'Last Name', placeholder: 'Last Name', validationRules: [ { type: 'stringLength', min: 3, } ] }, { controlType: 'number', dataField: 'age', label: 'Age', placeholder: 'select age', validationRules: [ { type: 'range', min: 1, max: 120 } ] }
Our next task is to disable and enable our form edit button upon form status changes
We will define a function so it will be easier for us to manage the state of the button.
const validateFormEditButton = () => { const editBtn = formRef.current.element.querySelector('smart-button[name="editBtn"]'); if (formRef.current.status === 'invalid') { return editBtn.disabled = true; } else if (formRef.current.status === 'valid') { return editBtn.disabled = false; } }
Next, we will catch the status change of the form and manage the 'Edit' button's state
This code is placed in the useEffect under the Smart.Form's initialization
formRef.current.onStatusChanges = () => { validateFormEditButton(); }
Now, we will set the state of the 'Edit' button when our 'Start Editing' button is clicked
const handleStartEditingButtonClick = async () => { //REST OF OUR CODE validateFormEditButton(); setIsFormHidden(false); }
The last step is to disable commiting the edit if the form status is invalid. This is done in the 'Edit' button's click handler
const handleEditButtonClick = () => { if (formRef.current.status === 'invalid') { return; } setIsFormHidden(true); gridRef.current.updateRow(formRef.current.rowId, formRef.current.value); gridRef.current.ensureVisible(formRef.current.rowId); disableGrid(false); }
-
Everything is ready and this is the code for everything:
import 'smart-webcomponents-react/source/styles/smart.default.css'; import './App.css'; import { Grid } from 'smart-webcomponents-react/grid'; import { Smart } from 'smart-webcomponents-react/form'; import 'smart-webcomponents-react/source/modules/smart.dropdownlist'; import { Button } from 'smart-webcomponents-react/button'; import { useEffect, useRef, useState } from 'react'; const generateGridData = (rows) => { const data = []; const firstNames = ['Peter', 'Alexander', 'George', 'Steve', 'Zlat', 'Philip', 'Michael', 'Oliver', 'Kassandra', 'Maria', 'Iglika']; const lastNames = ['Madison', 'Marley', 'Bolden', 'Raven', 'Steve', 'Plain', 'Michael', 'Hansley', 'Ashley', 'Monroe', 'West']; for (let i = 0; i < rows; i++) { const row = { firstName: firstNames[Math.floor(Math.random() * (firstNames.length - 0) + 0)], lastName: lastNames[Math.floor(Math.random() * (lastNames.length - 0) + 0)], age: Math.floor(Math.random() * (110 - 10) + 10), gender: Math.random() > 0.5 ? 'male' : 'female' } data.push(row); } return data; } function App() { const [gridData] = useState(generateGridData(30)); const formRef = useRef(null); const [formValue, setFormValue] = useState({ firstName: null, lastName: null, age: null, gender: null, }) const [isFormHidden, setIsFormHidden] = useState(true); const gridRef = useRef(null); useEffect(() => { formRef.current = new Smart.Form('#form', { value: formValue, controls: [ { controlType: 'input', dataField: 'firstName', label: 'First Name', placeholder: 'First Name', validationRules: [ { type: 'stringLength', min: 3, } ] }, { controlType: 'input', dataField: 'lastName', label: 'Last Name', placeholder: 'Last Name', validationRules: [ { type: 'stringLength', min: 3, } ] }, { controlType: 'number', dataField: 'age', label: 'Age', placeholder: 'select age', validationRules: [ { type: 'range', min: 1, max: 120 } ] }, { controlType: 'dropDownList', dataField: 'gender', label: 'Gender', placeholder: 'select gender', controlOptions: { dataSource: ['male', 'female', 'other'] } }, { controlType: 'group', columns: 2, controls: [ { controlType: 'button', label: 'Edit', name: "editBtn", cssClass: 'success', }, { controlType: 'button', name: "cancelBtn", label: 'Cancel', } ] } ] }); formRef.current.onStatusChanges = () => { validateFormEditButton(); } }, [formValue]); const [gridSettings, setGridSettings] = useState({ dataSource: new window.Smart.DataAdapter({ dataSource: gridData, dataFields: [ 'firstName: string', 'lastName: string', 'age: number', 'gender: string' ] }), selection: { enabled: true, action: 'click', mode: 'one' }, sorting: { enabled: true, sortMode: 'one' }, filtering: { enabled: true }, paging: { enabled: true, pageSize: 15 }, pager: { visible: true, pageSizeSelector: { visible: true, dataSource: [15, 30, 50] } }, columns: [ { label: 'First Name', dataField: 'firstName' }, { label: 'Last Name', dataField: 'lastName' }, { label: 'Age', dataField: 'age' }, { label: 'Gender', dataField: 'gender', width: 'auto' } ] }); const disableGrid = (isDisabled) => { setGridSettings( settings => ({ ...settings, selection: { ...settings.selection, enabled: !isDisabled }, sorting: { ...settings.sorting, enabled: !isDisabled }, filtering: { ...settings.filtering, enabled: !isDisabled } }) ) } const validateFormEditButton = () => { const editBtn = formRef.current.element.querySelector('smart-button[name="editBtn"]'); if (formRef.current.status === 'invalid') { return editBtn.disabled = true; } else if (formRef.current.status === 'valid') { return editBtn.disabled = false; } } const handleFormClick = (e) => { const target = e.target; switch (target.name) { case 'editBtn': handleEditButtonClick(); break; case 'cancelBtn': handleCancelButtonClick(); break; default: break; } } const handleEditButtonClick = () => { if (formRef.current.status === 'invalid') { return; } setIsFormHidden(true); gridRef.current.updateRow(formRef.current.rowId, formRef.current.value); gridRef.current.ensureVisible(formRef.current.rowId); disableGrid(false); } const handleCancelButtonClick = () => { setIsFormHidden(true); disableGrid(false); } const handleStartEditingButtonClick = async () => { disableGrid(true); const selectedRowId = (await gridRef.current.getSelectedRowIds())[0] const selectedRow = await gridRef.current.getRowByIndex(selectedRowId); if (selectedRowId && selectedRow.visibleIndex >= 0) { setFormValue(await gridRef.current.getRowData(selectedRowId)) formRef.current.rowId = selectedRowId; } else { const firstVisibleRow = (await gridRef.current.getVisibleRows())[0]; setFormValue(await gridRef.current.getRowData(firstVisibleRow.data.$.id)) formRef.current.rowId = firstVisibleRow.data.$.id; gridRef.current.ensureVisible(firstVisibleRow.data.$.id); } validateFormEditButton(); setIsFormHidden(false); } return ( <div className="App"> <div id="grids-editing-button-wrapper"> <Button id='edit-grids-row-button' onClick={handleStartEditingButtonClick}>Start Editing</Button> </div> <div id="form-wrapper" onClick={handleFormClick} style={{ display: isFormHidden ? 'none' : 'block' }}> <form id='form' /> </div> <div id="grid-wrapper"> <Grid ref={gridRef} id="grid" {...gridSettings} /> </div> </div> ); } export default App;
-
We achieved this so far