add update on blur

This commit is contained in:
davidwells 2018-06-12 12:02:08 -07:00
parent 4640eb9494
commit 894293837f
5 changed files with 230 additions and 2 deletions

View file

@ -63,6 +63,7 @@
font-size: 17px;
font-weight: 500;
color: #5a5a5a;
flex-grow: 1;
}
@keyframes App-logo-spin {

View file

@ -1,5 +1,6 @@
import React, { Component } from 'react'
import logo from './logo.svg'
import ContentEditable from './components/ContentEditable'
import './App.css'
class App extends Component {
@ -30,7 +31,7 @@ class App extends Component {
return false
}
// reset input
// reset input to empty
this.inputElement.value = ''
// Optimistically add todo to UI
@ -109,6 +110,49 @@ class App extends Component {
})
})
}
updateTodo = (id, todoValue) => {
// Make API request to update todo
fetch(`/.netlify/functions/todos-update/${id}`, {
body: JSON.stringify({
title: todoValue
}),
method: 'POST',
// mode: 'cors', // no-cors, cors, *same-origin
})
.then(response => response.json())
.then((json) => {
console.log(json)
// fin
}).catch((e) => {
console.log('An API error occurred', e)
})
}
handleDataChange = (event, currentValue) => {
// save on change debounced?
}
handleBlur = (event, currentValue) => {
let isDifferent = false
const key = event.target.dataset.key
const updatedTodos = this.state.todos.map((todo, i) => {
const { data, ref } = todo
const id = getTodoId(todo)
if (id === key && todo.data.title !== currentValue) {
todo.data.title = currentValue
isDifferent = true
}
return todo
})
// only set state if input different
if (isDifferent) {
this.setState({
todos: updatedTodos
}, () => {
this.updateTodo(key, currentValue)
})
}
}
renderTodos() {
const { todos } = this.state
return todos.map((todo, i) => {
@ -124,7 +168,14 @@ class App extends Component {
return (
<div key={i} className='todo-item'>
<div className='todo-list-title'>
{data.title}
<ContentEditable
tagName='span'
editKey={id}
onChange={this.handleDataChange} // handle innerHTML change
onBlur={this.handleBlur} // handle innerHTML change
html={data.title}
>
</ContentEditable>
</div>
{deleteButton}
</div>

View file

@ -0,0 +1,9 @@
.editable {
cursor: text;
display: block;
padding: 10px;
width: 90%;
}
.editable[contenteditable="true"] {
outline: 3px solid #efefef;
}

View file

@ -0,0 +1,66 @@
/* fork of https://github.com/lovasoa/react-contenteditable */
import React from 'react'
export default class Editable extends React.Component {
shouldComponentUpdate(nextProps) {
// We need not rerender if the change of props simply reflects the user's
// edits. Rerendering in this case would make the cursor/caret jump.
return (
// Rerender if there is no element yet...
!this.htmlEl
// ...or if html really changed... (programmatically, not by user edit)
|| (nextProps.html !== this.htmlEl.innerHTML
&& nextProps.html !== this.props.html)
// ...or if editing is enabled or disabled.
|| this.props.disabled !== nextProps.disabled
)
}
componentDidUpdate() {
if (this.htmlEl && this.props.html !== this.htmlEl.innerHTML) {
// Perhaps React (whose VDOM gets outdated because we often prevent
// rerendering) did not update the DOM. So we update it manually now.
this.htmlEl.innerHTML = this.props.html
}
}
preventEnter = (evt) => {
if (evt.which === 13) {
evt.preventDefault()
if (!this.htmlEl) {
return false
}
this.htmlEl.blur()
return false
}
}
emitChange = (evt) => {
if (!this.htmlEl) {
return false
}
const html = this.htmlEl.innerHTML
if (this.props.onChange && html !== this.lastHtml) {
evt.target.value = html
this.props.onChange(evt, html)
}
this.lastHtml = html
}
render() {
const { tagName, html, onChange, ...props } = this.props
const domNodeType = tagName || 'div'
const elementProps = {
...props,
ref: (e) => this.htmlEl = e,
onKeyDown: this.preventEnter,
onInput: this.emitChange,
onBlur: this.props.onBlur || this.emitChange,
contentEditable: !this.props.disabled,
}
let children = this.props.children
if (html) {
elementProps.dangerouslySetInnerHTML = { __html: html }
children = null
}
return React.createElement(domNodeType, elementProps, children)
}
}

View file

@ -0,0 +1,101 @@
import React from 'react'
import Editable from './Editable'
import './ContentEditable.css'
export default class ContentEditable extends React.Component {
constructor(props) {
super(props)
this.state = {
disabled: true
}
this.hasFocused = false
}
handleClick = (e) => {
e.preventDefault()
const event = e || window.event
// hacks to give the contenteditable block a better UX
event.persist()
if (!this.hasFocused) {
const caretRange = getMouseEventCaretRange(event)
window.setTimeout(() => {
selectRange(caretRange)
this.hasFocused = true
}, 0)
}
// end hacks to give the contenteditable block a better UX
this.setState({
disabled: false
})
}
handleClickOutside = (evt) => {
const event = evt || window.event
// presist blur event for react
event.persist()
const value = evt.target.value || evt.target.innerText
this.setState({
disabled: true
}, () => {
this.hasFocused = false // reset single click functionality
if (this.props.onBlur) {
this.props.onBlur(evt, value)
}
})
}
render() {
const { onChange, children, html, editKey, tagName } = this.props
const content = html || children
return (
<Editable
tagName={tagName}
data-key={editKey}
className={'editable'}
onClick={this.handleClick}
onBlur={this.handleClickOutside}
html={content}
disabled={this.state.disabled}
onChange={onChange}
/>
)
}
}
function getMouseEventCaretRange(event) {
const x = event.clientX
const y = event.clientY
let range
if (document.body.createTextRange) {
// IE
range = document.body.createTextRange()
range.moveToPoint(x, y)
} else if (typeof document.createRange !== 'undefined') {
// Try Firefox rangeOffset + rangeParent properties
if (typeof event.rangeParent !== 'undefined') {
range = document.createRange()
range.setStart(event.rangeParent, event.rangeOffset)
range.collapse(true)
} else if (document.caretPositionFromPoint) {
// Try the standards-based way next
const pos = document.caretPositionFromPoint(x, y)
range = document.createRange()
range.setStart(pos.offsetNode, pos.offset)
range.collapse(true)
} else if (document.caretRangeFromPoint) {
// WebKit
range = document.caretRangeFromPoint(x, y)
}
}
return range
}
function selectRange(range) {
if (range) {
if (typeof range.select !== 'undefined') {
range.select()
} else if (typeof window.getSelection !== 'undefined') {
const sel = window.getSelection()
sel.removeAllRanges()
sel.addRange(range)
}
}
}