React for Back-End Developers (Part 1)
The GitHub repository for this blog post is https://github.com/gbozee/react-for-backend-dev.
React is a front-end web library that can be used to build rich front-end or single page applications (SPA), but that doesn't mean it can't be used alongside an existing back-end web framework.
I would say that this is one of the core strengths of React. With its power, we can spice up our boring back-end web apps/sites with ease.
For the next few blog posts, I'll be discussing some areas where we can use React to augment functionalities provided by the Django Python web framework. This can also be applied to any other server-side web framework.
Our sample scenario consists of implementing a formset in Django. One pain point that Django doesn't solve is the ability to dynamically add a new form to a Django formset on the browser.
One solution to this is using jQuery, but it tends to be difficult to follow. This is a perfect use case to try out React and see how it helps solve this problem. In the scenario we're exploring together, we'll ensure that the following requirements are satisfied:
- We can successfully add a new form to the formset.
- We can enforce validations imposed by the formset on the server.
To get started, we create a virtual environment to house our web app. I am using git bash for Windows to ensure that the environment is as similar as possible across OS platforms.
$ mkdir react_django
$ cd react_django
$ virtualenv venv
$ source venv/Scripts/activate # in mac and linux source venv/bin/activate
$ pip install django
$ django-admin startproject formset_in_react
$ cd formset_in_react
$ python formset_in_react/manage.py runserver
With the above, we should have a functional Django project up and running.
Let's create our index view
that will house our formset in the urls.py
provided by Django.
I am going to create a single form in Django from which our formset will be based. This form consists of two simple fields, school
and degree
.
From Django import forms:
class EducationForm(forms.Form):
school = forms.CharField()
course = forms.CharField()
EducationFormset = forms.formset_factory(EducationForm,max_num=2)
The formset ensures that the maximum number of forms is no more than two.
Before we venture into React land, let's see the generated HTML from the formset, generated by Django, and hook everything up in our view.
Using the Python interactive shell, we get the following HTML generated from the formset.
$ python formset_in_react/manage.py shell
>>> from formset_in_react.urls import EducationFormset
>>> sample_form = EducationFormset()
>>> sample_form.as_ul()
u'<input type="hidden" name="form-TOTAL_FORMS" value="1" id="id_form-TOTAL_FORMS" /><input type="hidden" name="form-INITIAL_FORMS" value="0" id="id_form-INITIAL_FORMS
" /><input type="hidden" name="form-MIN_NUM_FORMS" value="0" id="id_form-MIN_NUM_FORMS" /><input type="hidden" name="form-MAX_NUM_FORMS" value="2" id="id_form-MAX_
NUM_FORMS" />\n<li><label for="id_form-0-school">School:</label> <input type="text" name="form-0-school" id="id_form-0-school" /></li>\n<li><label for="id_form-0-cour
se">Course:</label> <input type="text" name="form-0-course" id="id_form-0-course" /></li>`
Prettifying the above output, we get the following:
<input type="hidden" name="form-TOTAL_FORMS" value="1" id="id_form-TOTAL_FORMS" />
<input type="hidden" name="form-INITIAL_FORMS" value="0" id="id_form-INITIAL_FORMS
" />
<input type="hidden" name="form-MIN_NUM_FORMS" value="0" id="id_form-MIN_NUM_FORMS" />
<input type="hidden" name="form-MAX_NUM_FORMS" value="2" id="id_form-MAX_
NUM_FORMS" />
<li>
<label for="id_form-0-school">School:</label>
<input type="text" name="form-0-school" id="id_form-0-school" />
</li>
<li>
<label for="id_form-0-course">Course:</label>
<input type="text" name="form-0-course" id="id_form-0-course" />
</li>`
We can see from the output that the generated HTML contains a lot of hidden fields but no button/link to help us create a new form in the formset. We are supposed to use the information in the hidden form fields with a JavaScript library to implement the add
functionality.
Let's set up our view and HTML and try to make our form a little bit nicer with Bootstrap.
The complete urls.py
content:
I'll be making a config change to the Django's settings.py
so that Django can find our templates
location.
settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, "templates")], # the change
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
Observe where I created the templates
folder:
The index page should be displaying and accessible in the browser now.
The content of the index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<!-- Optional theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
<title>Formset with django</title>
<style>
.margin-top{
margin-top: 50px;
}
</style>
</head>
<body>
<div class="container margin-top">
<div class="row">
<div class="col-md-10 col-md-offset-1">
<form method="post">
{% csrf_token %}
<div id="formset">
{{form.as_ul}}
</div>
</form>
</div>
</div>
</div>
</body>
</html>
We can see here that I created an id
before using the default rendering of the formset provided by Django. I'll be hijacking this id
with React and replacing it with the modified formset.
We should get the following on our browser:
Now it's time to set up React. I'll be using the easy approach explained in my last blog post Getting Started in React the Easy Way.
index.html
<head>
...
<script src="https://unpkg.com/react@15.6.0/dist/react.min.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@15.6.0/dist/react-dom.min.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/create-react-class@15.6.0/create-react-class.min.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/babel-standalone@6.24.2/babel.min.js"></script>
<script type="text/babel">
// Our source code would go here
</script>
</head>
Remember the original content of the formset we found out about from the terminal. We'll be creating a React component that houses that before making modifications.
// Our source code would go here
const React = window.React;
const ReactDOM = window.ReactDOM;
const createReactClass = window.createReactClass;
const Formset = createReactClass({
render(){
return (
<div>
<input type="hidden" name="form-TOTAL_FORMS" value="1" id="id_form-TOTAL_FORMS" />
<input type="hidden" name="form-INITIAL_FORMS" value="0" id="id_form-INITIAL_FORMS" />
<input type="hidden" name="form-MIN_NUM_FORMS" value="0" id="id_form-MIN_NUM_FORMS" />
<input type="hidden" name="form-MAX_NUM_FORMS" value="2" id="id_form-MAX_NUM_FORMS" />
<li>
<label for="id_form-0-school">School:</label>
<input type="text" name="form-0-school" id="id_form-0-school" />
</li>
<li>
<label for="id_form-0-course">Course:</label>
<input type="text" name="form-0-course" id="id_form-0-course" />
</li>
</div>
)}
})
ReactDOM.render(<Formset/>,document.getElementById('formset'))
</script>
Refreshing our page, we should still get exactly the same view, but now, we have taken control of the rendering of the form with React. Our custom React component is being injected into the formset
id element.
Let's simplify our component a little bit.
const Form = createReactClass({
render(){
return (
<div>
<li>
<label for="id_form-0-school">School:</label>
<input type="text" name="form-0-school" id="id_form-0-school" />
</li>
<li>
<label for="id_form-0-course">Course:</label>
<input type="text" name="form-0-course" id="id_form-0-course" />
</li>
</div>
)}
})
const Formset = createReactClass({
render(){
const management_data = [
{name: "TOTAL", value:1,},
{name: "INITIAL", value: 0},
{name: "MIN_NUM", value: 0},
{name: "MAX_NUM",value: 2}
]
return (
<div>
{management_data.map((d,index)=>
<input key={`management-${index}`}
type="hidden" name={`form-${d.name}_FORMS`}
value={d.value} id={`id_form-${id.name}_FORMS`} />
)}
<Form />
</div>
)}
})
I pulled out the actual form into a seperate component and avoided duplicating all of the hidden fields. Now, I can move the management_data
out of the component and pass it as a props
to the Formset
component. It looks like this:
...
const Formset = createReactClass({
render(){
return (
<div>
{this.props.management_data.map((d,index)=>
<input key={`management-${index}`}
type="hidden" name={`form-${d.name}_FORMS`}
value={d.value} id={`id_form-${id.name}_FORMS`} />
)}
<Form />
</div>
)}
})
const management_data = [
{name: "TOTAL", value:1,},
{name: "INITIAL", value: 0},
{name: "MIN_NUM", value: 0},
{name: "MAX_NUM",value: 2}
]
ReactDOM.render(
<Formset managemend_data={managemend_data} />,
document.getElementById('formset')
)
Going back to our Form
component, because of the way the Django formset actually requires formset data, the id of each form field follows a particular convention, id_form-0-school
where the 0
represents fields in the same form instance. We can take advantage of this.
We need to keep track of the number of forms we have successfully added so that we can add a local state to our Formset
component.
In the above, we created a local state called noOfForms
and set the default value to 1. Then, in the render
method of the formset component, we dynamically created an array based on the current local state, filled it with undefined
by using the fill
method on the array prototype, and then used map
to return the individual form.
I also added a button that would be responsible for adding a new form to the screen. Right now, it doesn't do anything.
In the Form
component, I'm getting the index of the form passed from the parent Formset
and using that to generate the id
and name
of fields in the form so that the Django way isn't broken.
Let's finish up the implementation by implementing the button add
and remove
actions.
In the Form
component, I'm expecting a props
that would determine if I want to remove the particular form or not from the parent, as well as the action that should occur when the remove
button is clicked.
const Form = createReactClass({
removeForm(e){
e.preventDefault()
this.props.removeForm(this.props.index);
},
render(){
const school = `form-${this.props.index}-school`
const course = `form-${this.props.index}-course`
return (
<div>
<li>
<label for={`id_${school}`}>School:</label>
<input type="text" name={school} id={`id_${school}`} />
</li>
<li>
<label for={`id_${course}`}>Course:</label>
<input type="text" name={course} id={`id_${course}`} />
</li>
{this.props.displayRemoveButton ?
<button onClick={this.removeForm}>Remove Form</button>
: null}
</div>
)}
})
The Remove Form
button has an onClick
event handler that calls a method within the component called removeForm
, which contains the logic for calling the passed function from the parent that knows how to delete the form. We are preventing any default browser event action from happening by callig e.preventDefault
, since we do not want our form to get submitted to the server by mistake.
We'll then need to create the method for removing a form from the parent Formset
.
const Formset = createReactClass({
getInitialState(){
return {
noOfForms:1
}
},
addNewForm(e){
e.preventDefault()
this.setState({noOfForms:this.state.noOfForms+1})
},
removeForm(index){
this.setState({noOfForms: this.state.noOfForms-1})
},
render(){
return (
<div>
{this.props.management_data.map((d,index)=>
<input key={`management-${index}`}
type="hidden" name={`form-${d.name}_FORMS`}
value={d.value} id={`id_form-${d.name}_FORMS`} />
)}
{Array(this.state.noOfForms).fill().map((form,index)=>
<Form key={`form-${index}`} index={index}
displayRemoveButton={this.state.noOfForms > 1}
removeForm={this.removeForm} />
)}
<button onClick={this.addNewForm}>Add new form</button>
</div>
)}
})
Okay, our Form
component is now receiving more props. The displayRemoveButton
props determine whether the removeButton on the form shows or not. The removeForm
props take the function that should be called by the child form when the removeForm button is clicked.
With the above implemented and the browser refreshed, we should be able to add and remove forms to the formset. We could inspect elements by using the browser dev tools to ensure that the id
and name
for field in each form have the index of the form and are consistent with the Django implementation.
In the next blog post, we'll look at enforcing that the maximum number of forms required by the server isn't exceeded and ensuring that everything works as expected when the form is eventually submitted. Have a lovely day!
The GitHub repo for this post is hosted at https://github.com/gbozee/react-for-backend-dev.