How to Construct a UI Dynamically
Nov 21, 2019 • 10 Minute Read
Introduction
React's component-driven approach makes it easy for us to build dynamic UI components. Have you ever wondered how CMSs like Wordpress and Drupal build UI dynamically with page builders? Well, that's what we are going to learn in this guide; of course, we’ll be working in the context of development in the React Ecosystem.
For this guide, we are going to explore using a CMS Rest API that will provide a "JSON-ified" version of our UI. More specifically, we are going to build a form and its components dynamically.
The following would be the JSON response from the server:
{
"form_heading": "Contact Form",
"fields": [
{
"label": {
"text": "Name"
},
"input": {
"type": "text",
"name": "name",
"validations": {
"required": true,
"minlength": 3,
"maxlength": 60
}
}
},
{
"label": {
"text": "Email"
},
"input": {
"type": "email",
"name": "email",
"validations": {
"required": true
}
}
},
{
"label": {
"text": "Message"
},
"input": {
"type": "textarea",
"name": "message",
"validations": {
"required": true,
"minlength": 30,
"maxlength": 255
}
}
}
],
"submit_button": {
"text": "Submit"
}
}
Start the Project with create-react-app
Run the following command to create a React project with create-react-app:
create-react-app dynamic-form
Let's Create Our Form Component
Create a <ContactForm /> component inside the src folder and add the following code to fetch the data from the server. If you are unfamiliar with the browser Fetch API, you can find additional information my previous guide on All You Need to Know About Axios.
const SERVER_URL = "https://61m46.sse.codesandbox.io"; // enter your server URL here
class ContactForm extends Component {
constructor(props) {
super(props);
this.state = {
form: {}
};
}
componentDidMount() {
fetch(`${SERVER_URL}/forms/1`)
.then(res => res.json())
.then(data => this.setState({ forms: data }));
}
render() {
return <div>{JSON.stringify(this.state.forms)}</div>;
}
}
Function to Render the Field
Let's create a utility function that will render the input fields for us based on the field type.
renderField = field => {
const { input } = field;
const { validations } = input;
switch (field.input.type) {
case "textarea":
return (
<textarea
type={input.type}
name={input.name}
onChange={this.handleInput}
value={this.state[field.name]}
minLength={validations.minlength}
maxLength={validations.maxlength}
required={validations.required}
rows={input.rows}
id={input.name}
/>
);
default:
return (
<input
type={input.type}
name={input.name}
onChange={this.handleInput}
value={this.state[field.name]}
minLength={validations.minlength}
maxLength={validations.maxlength}
required={validations.required}
id={input.name}
/>
);
}
};
We are using the switch and case statements to return the respective field according to the field input type specified in the JSON response.
For handling the input changes, we will write a onChange handler function that will store the input values in the state as a key-value pair where the key would be the name of the input field.
class ContactForm extends Component {
// ...
handleInput = e =>
this.setState({ [e.currentTarget.name]: e.currentTarget.value });
render() {
// ...
}
}
Let's Render the Form
In the render() function of the component we will loop through each field and render it using the renderField() utility function.
class ContactForm extends Component {
// ...
render() {
const { form } = this.state;
if (Object.keys(form).length < 1) {
return "Loading...";
}
return (
<>
<h1>{form.form_heading}</h1>
<div>
<form onSubmit={() => console.log(this.state)}>
{form.fields.map(field => (
<div className="input-group" key={field.input.name}>
<label for={field.input.name}>{field.label.text}</label>
{this.renderField(field)}
</div>
))}
<button type="submit">{form.submit_button.text}</button>
</form>
</div>
</>
);
}
}
Import the Form Component in the index.js File
import ContactForm from "./ContactForm";
function App() {
return (
<div className="App">
<ContactForm />
</div>
);
}
Complete Code
ContactForm.js File
import React, { Component } from "react";
const SERVER_URL = "https://61m46.sse.codesandbox.io"; // enter your server URL here
class ContactForm extends Component {
constructor(props) {
super(props);
this.state = {
form: {}
};
}
componentDidMount() {
fetch(`${SERVER_URL}/forms/1`)
.then(res => res.json())
.then(data => this.setState({ form: data }));
}
handleInput = e =>
this.setState({ [e.currentTarget.name]: e.currentTarget.value });
renderField = field => {
const { input } = field;
const { validations } = input;
switch (field.input.type) {
case "textarea":
return (
<textarea
type={input.type}
name={input.name}
onChange={this.handleInput}
value={this.state[field.name]}
minLength={validations.minlength}
maxLength={validations.maxlength}
required={validations.required}
rows={input.rows}
id={input.name}
/>
);
default:
return (
<input
type={input.type}
name={input.name}
onChange={this.handleInput}
value={this.state[field.name]}
minLength={validations.minlength}
maxLength={validations.maxlength}
required={validations.required}
id={input.name}
/>
);
}
};
render() {
const { form } = this.state;
if (Object.keys(form).length < 1) {
return "Loading...";
}
return (
<>
<h1>{form.form_heading}</h1>
<div>
<form onSubmit={() => console.log(this.state)}>
{form.fields.map(field => (
<div className="input-group" key={field.input.name}>
<label for={field.input.name}>{field.label.text}</label>
{this.renderField(field)}
</div>
))}
<button type="submit">{form.submit_button.text}</button>
</form>
</div>
</>
);
}
}
export default ContactForm;
index.js file
import React from "react";
import ReactDOM from "react-dom";
import ContactForm from "./ContactForm";
import "./styles.css";
function App() {
return (
<div className="App">
<ContactForm />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
styles.css file
.App {
font-family: sans-serif;
}
.input-group {
margin-bottom: 10px;
}
.input-group label {
display: block;
margin-bottom: 5px;
}
button {
border: none;
padding: 8px 24px;
}
Bonus Section - Let's Add a Select Field
Let's have a look at how we can dynamically create a select input field.
For the select field, we can assume the following JSON response:
{
"label": {
"text": "Role"
},
"input": {
"type": "select",
"name": "role",
"options": [
{
"value": "frontend-developer",
"text": "Frontend Developer"
},
{
"value": "backend-developer",
"text": "Backend Developer"
},
{
"value": "fullstack-developer",
"text": "Fullstack Developer"
}
]
}
}
To render the select field, we will loop through the options using the map() function.
Lets update our renderField() method:
renderField = field => {
const { input } = field;
const { validations } = input;
switch (field.input.type) {
case "textarea":
return (
<textarea
type={input.type}
name={input.name}
onChange={this.handleInput}
value={this.state[field.name]}
minLength={validations.minlength}
maxLength={validations.maxlength}
required={validations.required}
rows={input.rows}
id={input.name}
/>
);
// This case is for rendering the select field
case "select":
return (
<select name={input.name}>
{input.options.map(option => (
<option value={option.value}>{option.text}</option>
))}
</select>
);
default:
return (
<input
type={input.type}
name={input.name}
onChange={this.handleInput}
value={this.state[field.name]}
minLength={validations.minlength}
maxLength={validations.maxlength}
required={validations.required}
id={input.name}
/>
);
}
};
Conclusion
I hope you liked this guide on creating UI dynamically with React. Until next time, goodbye and don't forget to code like a beast.