Sparragus Sparragus -4 years ago 126
HTML Question

Read <input> in a functional (stateless) component

I have two components: one container component and one presentational component.

The container fetches all the information and actions needed to display a Post using the presentational component. On the presentational component I have a way where I can toggle between presenting the information and editing it. When I edit and submit the data about the post, I want to read the values from all the different inputs I have so I can dispatch an action.

However, the inputs are not inside a

<form id='theForm' onSubmit={onHandleSubmit}>
tag. Instead, all the
<input>
and
<button type='submit'>
that are outside the
<form>
have a
form='theForm'
attribute.

I thought that I could have many
<input>
outside the form, but as long as the
form
attribute pointed towards the corresponding
<form>
, I could read it values using the event that
handleOnSubmit (e) {...}
recieves. However, I haven't figured out how to do that.

How can I read the values of the inputs on my
handleOnSubmit
function? Or this a completely wrong idea I should abandon?

// PostSummaryContainer.js
import React, { PropTypes, Component } from 'react'
import { connect } from 'react-redux'

import { loadPost, editPost, editPostCancel, editPostSubmit } from '../../actions/posts'

import PostSummaryView from '../../views/details/summary'

class PostSummaryContainer extends Component {
constructor (props) {
super(props)
this.handleOnSubmit = this.handleOnSubmit.bind(this)
this.handleOnCancel = this.handleOnCancel.bind(this)
this.handleOnSubmit = this.handleOnSubmit.bind(this)
}

handleOnEdit (e) {
e.preventDefault()
this.props.editPost()
}

handleOnCancel (e) {
e.preventDefault()
this.props.editPostCancel()
}

handleOnSubmit (e) {
e.preventDefault()

// How do I read the input values? <--------------------
}

componentWillMount () {
const {
id,
loadPost
} = this.props

loadPost(id)
}

render () {
const {
post,
isLoading,
isEditing
} = this.props

const viewProps = {
bacon,
isLoading,
isEditing,
handleOnEdit: this.handleOnEdit,
handleOnCancel: this.handleOnCancel,
handleOnSubmit: this.handleOnSubmit
}

return (
<PostSummaryView {...viewProps} />
)
}
}

const mapStateToProps = (state, ownProps) => {
const {
params: {
id
}
} = ownProps

const post = state.entities.post[id]

const {
isLoading,
isEditing
} = state.posts

return {
id,
post,
isLoading,
isEditing
}
}

export default connect(
mapStateToProps,
{ loadPost, editPost, editPostCancel, editPostSubmit }
)(PostSummaryContainer)


On my presentation component:

// PostSummmaryView.js
import React from 'react'
import moment from 'moment'

function PostSummaryView (props) {
const {
post,
isLoading,
isEditing,
handleOnEdit,
handleOnCancel,
handleOnSubmit
} = props

const formId = 'editPostForm'

return (
isLoading
? <div>Loading...</div>
: <div className='row'>
{isEditing && <form id={formId} onSubmit={handleOnSubmit}><input type='text' name='name' /></form>}

<div className='col-md-6'>
<img src={post.media.url} className='img-responsive' />
{isEditing && <input type='file' name='media' form={formId}/>}
</div>
<div className='col-md-6'>

<h1>{post.name}</h1>
<p>
{moment(post.publicationDate).format('dddd, MMMM Do')}
</p>

<hr />

<p className='text-left'>
{post.description || 'Lorem ipsum dolor sit amet, consectetur adipisici elit...'}
</p>

{isEditing
? <div>
<button className='btn btn-lg btn-default' onClick={handleOnCancel}>Cancel</button>
<button type='submit' className='btn btn-lg btn-default' form={formId}>Submit</button>
</div>
: <button className='btn btn-lg btn-default' onClick={handleOnEdit}>Edit</button>
}

</div>
</div>
)
}

export default PostSummaryView

Answer Source

Disclaimer: I'm still new to React/Redux, so take this answer with a big grain of salt.

I think your approach is slightly off, in that you shouldn't need to go around and gather any data from inputs when submitting (whether inside or outside the <form>). Your state should always be in one central, consolidated place.

Given you provide a Cancel option, keeping the Post data updated during an Edit in a separate part of the state is more elegant (IMHO) than directly modifying the "source" Post data.

You could create a reducer for the Edit Post form that keeps key/value pairs for the input fields.

When a user starts editing, you could clone the original Post data into this new part of the state specific to the Edit form. As the user changes the inputs, you could dispatch actions saying "hey, form field X was changed to value Y", which could be reduced onto the state without modifying the original Post data. Pseudo-code example of the state object in this flow:

{
    // The actual Post data
    post: {
        media: {...},
        title: "My First Post",
        publicationDate: 1455768160589,
        description: "Lorem ipsum dolor sit amet"                  
    },

    // The temporary Post data in the Edit form
    postForm: {
        media: {...},
        title: "My Turbo-Charged First Post",
        publicationDate: 1455769951276,
        description: "Updated description yadda blah"                  
    }
}

Then, in your presentation component, you could drive the input values off postForm instead of post.

Each input would be given a change handler function so updates are immediately reflected in the state (or not reflected, depending on your validation logic/reducers). i.e.:

// In your actions associated with `Post`
// ------------------------------------------
function updateForm(field, value) {
    return {
        type: UPDATE_FORM,
        field,
        value
    }
}

// In your container
// ------------------------------------------
handleOnEdit(event) {
    postActions.updateForm(event.target.name, event.target.value)
}

// In your reducer
// ------------------------------------------
switch (action.type) {
    case UPDATE_FORM:
        return {
            ...state,
            [action.field]: action.value
        }
}

// In your presentational component's render() method
// ------------------------------------------
const {postForm, handleOnEdit} = this.props
const descriptionMarkup = (
    isEditing
    ? <input type='text' name='description' value={postForm.description} onChange={handleOnEdit} />
    : (post.description || 'Lorem ipsum dolor sit amet, consectetur adipisici elit...')
)
// ...
<p className='text-left'>
    {descriptionMarkup}
</p>

If you follow this pattern (and again, not sure it's "right"!), submitting the form becomes as simple as doing something with your state's postForm object. That object should always reflect the latest-and-greatest form input values.

Canceling the form becomes as simple as setting the postForm part of the state tree to {}. The original Post data remains the same.

Hope this helps jog some ideas...

Some other examples/approaches you could try:

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download