I miss simple_form already. Forms in React are such a pain and I hope to recreate the simple_form experience. We’ll take it a step at a time.
We’ll start by shoving a form onto the main page. We’ll tidy it up later.
// app/javascript/application/Application.jsx export function Application(_props) { return <div id='application'> <h1 className='site-name'>Blogging</h1> <form> <input type='text' name='title' /> <textarea name='body' /> <input type='submit' /> </form> <Post id={1} /> </div> }
Oh, dear. That’s ugly.
Come to think of it, the whole app is pretty ugly. Let’s tidy.
That’s better. It won’t win any prizes but it’s good enough for now.
I won’t show you the CSS (and you probably won’t like it anyway) but it’s all checked in if you want to peek.
I added a couple of divs and some CSS classes to the top-level elements too. I’ll show you in a bit, but first I am gonna move that form to its own component. I’m not TDDing this bit because I’m kind of stumbling around to find something that works. I’ll test it later.
Here’s the code after tidying the page structure and extracting an Editor.
export function Application() { return <div id='application'> <div id='header' className='header'> <h1 className='site-name'>Blogging</h1> </div> <div id='main'> <Editor /> <Post /> </div> </div> }
Let’s extract a Header and Main section too before we get back to the form.
The Application now looks like this.
export function Application() { return <div id='application'> <Header /> <Main /> </div> }
The header looks like this.
// javascript/application/Header.jsx export function Header() { return <div id='header' className='header'> <h1 className='site-name'>Blogging</h1> </div> }
The Main component looks like this.
export function Main() { return <div id='main'> <Editor /> <Post id={1} /> </div> }
And the Editor looks like this.
export function Editor() { return <form> <input type='text' name='title' className='title'/> <textarea name='body' className='body' /> <div className='actions'> <input type='submit' className='button'/> </div> </form> }
I wrote a test for the editor. It doesn’t do much yet.
// test/javascript/posts/Editor.test.jsx describe('The post editor', () => { test('displays a form', () => { const component = display(<Editor />) assert_select(component, '.title') assert_select(component, '.body') }) }
Controlled Components
React has this concept of controlled components versus uncontrolled components. This concept comes to the fore when you use form elements.
Long story short, when we are writing plain HTML forms, we rely on the form elements to manage their own state that they magically send to the server when you hit SUBMIT. In React, we usually manage the state ourselves and do our own submitting.
A component is said to be controlled if its state changes are managed in a containing component. The basic idea is that you store the values from your form elements in React state, then pass them back in as properties when you render the form.
You can read more about controlled components here: https://reactjs.org/docs/forms.html
Here. Let me just show you.
export default class Editor extends React.Component { constructor(props) { super(props) this.state = { post: {} } this.handleChange = this.handleChange.bind(this) this.handleSubmit = this.handleSubmit.bind(this) } render() { const {post} = this.state return <form onChange={this.handleChange} onSubmit={this.handleSubmit}> <input type='text' name='title' className='title' defaultValue={post.title} /> <textarea name='body' className='body' defaultValue={post.body} /> <div className='actions'> <input type='submit' className='button' /> </div> </form> } handleChange(event) { const {name, value} = event.target const post = { ...this.state.post, [name]: value } this.setState({post}) } handleSubmit(event) { event.preventDefault() const {post} = this.state const {onSubmit} = this.props onSubmit && onSubmit(post) } }
I’ve converted our Editor function to a class so that we can manage its state. I’ve set up some state for the form elements and I’ve added an event handler for the onChange event.
When the user changes the value of the title element or the body element, the onChange event bubbles up to the form element where we handle it and store the value with this.setState(post)
.
The {…} in the handler is a spread operator. I’m using it to make a copy of post and overwriting one property. React really doesn’t like it when you mutate objects directly and the spread operator provides an easy way to make a copy without changing the original object.
When you submit the form, Editor calls an onSubmit function that was passed in as a property. It calls event.default() first to prevent the page refresh and then writes the current state of the post to the console.
That’s all quite straightforward, I think, except: let me draw your attention to the bind code in the constructor.
this.handleChange = this.handleChange.bind(this)
In Javascript, the value of this is elusive and fleeting and often not what you think it is. In this situation, when our event handler tries to handle the onChange event, the value of this will be null and weird errors result unless you call bind().
Mozilla has more details about binding here.
Now we have something we can test.
// test/javascript/posts/Editor.test.jsx test('updates the post details', () => { const submit = jest.fn() const component = display(<Editor onSubmit={submit} />) const title = component.find('.title') const body = component.find('.body') const form = component.find('form') title.simulate('change', {target: {name: 'title', value: 'The title'}}) body.simulate('change', {target: {name: 'body', value: 'The body'}}) form.simulate('submit') const expected = { title: 'The title', body: 'The body', } expect(submit).toBeCalledWith(expected) })
I’ve mocked the onSubmit function and I check that, when I edit the fields and submit the form, the right thing comes out the other end.
In the next episode, we’ll send this new post to the server.