Next, let's create a simple for updating note content. This form will autosave content that is typed into it.
imports/components/forms/autosave_input.jsx
import React from 'react'
import debounce from 'lodash.debounce'
export class AutoSaveInput extends React.Component {
constructor(props){
super(props)
this.state = {
contentValue: this.props.contentValue
}
}
handleUpdates(updatedValue){
const
updateInterval = 250,
options = { 'maxWait': 2000 },
submitUpdates = (collection, field, value) => {
this.props.handleUpdates(collection, field, value)
},
autoSaveChanges = debounce(submitUpdates, updateInterval, options)
autoSaveChanges(this.props.note, this.props.field, updatedValue)
}
handleOnChange(e) {
const updatedValue = e.target.value
this.setState({contentValue: updatedValue})
this.handleUpdates(updatedValue)
}
handleOnBlur() {
this.props.doneEditing()
}
render() {
return <form>
<div className="form-group">
<textarea
className="form-control"
placeholder={this.props.placeholder}
value={this.state.contentValue}
onChange={this.handleOnChange.bind(this)}
onBlur={this.handleOnBlur.bind(this)}
/>
</div>
<button className="btn btn-default">Done</button>
</form>
}
}
AutoSaveInput.propTypes = {
handleUpdates: React.PropTypes.func.isRequired,
field: React.PropTypes.string.isRequired,
contentValue: React.PropTypes.string,
placeholder: React.PropTypes.string
}
AutoSaveInput.defaultProps = {
contentValue: "" ,
placeholder: "Write something..."
}
TODO: discuss everything going on in this component.
- adding the debounce utility
- handle changes to the textarea
- handle blur and relationship to autofocus
Note that we need to add autoFocus. In order for a blur event to be triggered, the input needs to be in focus. (Blur is the opposite of focus. If the field is not in focus, blur will not do anything.)
Here, we are "wrapping" the ContentEditor
component in a EditableContent
component that has a editMode
state. Clicking on the content block will switch the state to edit mode. Then, we are passing a callback prop to the ContentEditor that will toggle edit mode again when the form input is blurred.
Also, note that the Done button in the ContentEditor is really just a dummy and serves no actual function. It doesn't matter if we click on it or not. However, it can improve the user experience to provide something to click on to exit edit mode.
/imports/components/pages/note_details_page.jsx
...
import { ContentEditor } from '../forms/content_editor'
export const NoteDetailsPage = (props) => {
...
<div id="main-content" className="container">
<ContentEditor content={props.note.content} />
</div>
...
}
Here, we are inserting the content editor and passing in the new content field we just created.
You should now see the (still not yet functional) content editor component when viewing a note details page.
...
Meteor.methods({
...
,
'note.update': (note) => {
note.set({
updatedAt: new Date()
})
note.save()
return note
}
...
})
export default createContainer(
() => {
const
...
,
handleUpdateNote = (collection, field, value) => {
const doc = {}
doc[field] = value
collection.set(doc)
Meteor.call('note.update', collection, (err, result) => {
if (err) {
console.log('error: ' + err.reason)
}
})
}
return {
note,
handleUpdateNote,
subsReady: sub.ready()
}
},
App
)
Now, let's actually update note content when typing into this component
We first need to add a server side operation for handling note updates.
/imports/collections/notes.js
...
Meteor.methods({
...
'/note/update': (note) => {
note.set({
updatedAt: new Date()
})
note.save()
return note
},
...
})
Now we can use this handler in our data container, and add a function that components can call and pass in content updates.
/imports/components/containers/note_details_container.js
...
export default createContainer(
() => {
const
...
,
handleUpdateNote = (note, field, value) => {
const doc = {}
doc[field] = value
note.set(doc)
Meteor.call('/note/update', note, (err, result) => {
if (err) {
console.log('error: ' + err.reason)
}
})
}
return {
...
handleUpdates: handleUpdateNote
}
},
App
)
This function will allow us to use the same handler both for note content and the note title, as well as any future note fields we might add.
We now need to pass along the necessary props to the content editor component.
/imports/components/pages/note_details_page.jsx
...
export const NoteDetailsPage = (props) => {
...
,
noteContent = props.subsReady? <ContentEditor contentValue={props.note.content} field={"content"} {...props} /> : <Loader />
...
/imports/components/forms/content_editor.jsx
...
import React from 'react'
import debounce from 'lodash.debounce'
export class ContentEditor extends React.Component {
...
handleUpdates(updatedValue){
const
updateInterval = 250,
options = { 'maxWait': 1000 },
submitUpdates = (collection, field, value) => {
this.props.handleUpdates(collection, field, value)
},
debouncedUpdates = debounce(submitUpdates, updateInterval, options)
debouncedUpdates(this.props.note, this.props.field, updatedValue)
}
handleOnChange(e) {
const updatedValue = e.target.value
this.setState({contentValue: updatedValue})
this.handleUpdates(updatedValue)
}
render() {
return <form>
<div className="form-group">
<textarea
...
onChange={this.handleOnChange.bind(this)}
/>
</div>
...
</form>
}
}
ContentEditor.propTypes = {
contentValue: React.PropTypes.string.isRequired,
field: React.PropTypes.string.isRequired,
handleUpdates: React.PropTypes.func.isRequired
}
...
We made quite a few updates to this component. You'll need to install the lodash.debounce utility for this to work.
npm i lodash.debounce --save
Debounce is great for handling events that are fired in rapid succession. In our case, these are the keypress events that are fired every time the user types a key when updating note content. We are using this utility to only submit a keypress event every 250ms, and to wait up to 1000ms (one second) to submit the keypress event if they are fired continuously.