Skip to content

Commit 59ba974

Browse files
committed
onChange should not return a promise (more flexible);
Timeout on save to cancel if new value not returned; saved and error states; cleaned up machine context; new props: savedClass, errorClass, saveTimeout, savedDuration, errorDuration
1 parent 38c8926 commit 59ba974

File tree

3 files changed

+127
-34
lines changed

3 files changed

+127
-34
lines changed

example/index.tsx

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import 'react-app-polyfill/ie11'
22
import * as React from 'react'
3+
import { useState } from 'react'
34
import * as ReactDOM from 'react-dom'
45
import InlineEdit, { InputType } from '../.'
56

67
const App = () => {
7-
const validate = input => input.length > 3
8-
const onChange = value => {
9-
return fetch(value).then(_ => value)
8+
const validate = (input: string) => input.length > 3
9+
const onChange = (value: string) => {
10+
setTimeout(() => {
11+
setValue(value)
12+
}, 500)
1013
}
1114

1215
const options = [
@@ -15,37 +18,47 @@ const App = () => {
1518
{ id: 3, name: 'Albicocca' },
1619
]
1720

21+
const [value, setValue] = useState('pizza')
22+
1823
return (
1924
<div>
2025
<style
2126
dangerouslySetInnerHTML={{
2227
__html: `
2328
.styled { display: block; padding: 5px 10px; border-radius: 3px; }
24-
.styled:hover { background-color: #f0f0f0 }
25-
`,
29+
.styled:hover { background-color: #f0f0f0; }
30+
.disabled { color: #ccc; }
31+
.invalid { border: 1px solid #ff0000; }
32+
.loading { color: #fc0; }
33+
.saved { color: #4caf50; }
34+
.error { color: #c00; }
35+
`,
2636
}}
2737
/>
2838

2939
<InlineEdit
30-
value="pizza"
40+
value={value}
3141
validate={validate}
3242
onChange={onChange}
3343
viewClass="styled"
3444
allowEditWhileLoading
35-
optimisticUpdate={false}
3645
/>
3746
<InlineEdit
3847
value="disabled"
3948
validate={validate}
4049
onChange={onChange}
41-
editProps={{ style: { padding: 10 } }}
4250
isDisabled
4351
/>
4452
<InlineEdit
45-
value="koala"
53+
value={value}
4654
validate={validate}
4755
onChange={onChange}
48-
editProps={{ style: { padding: 10 } }}
56+
invalidClass="invalid"
57+
loadingClass="loading"
58+
savedClass="saved"
59+
errorClass="error"
60+
errorDuration={1200}
61+
savedDuration={1200}
4962
/>
5063
<InlineEdit
5164
type={InputType.Number}
@@ -54,6 +67,8 @@ const App = () => {
5467
onChange={onChange}
5568
editProps={{ min: 10, max: 20, step: 2 }}
5669
format={value => '€ ' + value}
70+
loadingClass="loading"
71+
saveTimeout={1200}
5772
/>
5873
<InlineEdit
5974
type={InputType.Date}

src/InlineEdit.tsx

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import * as React from 'react'
2+
import { useEffect, useRef } from 'react'
23
import { useMachine } from '@xstate/react'
34
import getInlineEditMachine from './machine'
45
import Input from './Input'
56
import InputType from './inputType'
67

78
interface InlineEditProps {
89
value: string
9-
onChange: (value: string) => Promise<any>
10+
onChange: (value: string) => void
1011
type?: InputType
1112
validate?: (value: string) => boolean
1213
isDisabled?: boolean
@@ -17,6 +18,8 @@ interface InlineEditProps {
1718
disabledClass?: string
1819
loadingClass?: string
1920
invalidClass?: string
21+
savedClass?: string
22+
errorClass?: string
2023
editProps?: {
2124
[key: string]: any
2225
}
@@ -25,6 +28,9 @@ interface InlineEditProps {
2528
options?: any[]
2629
valueKey?: string
2730
labelKey?: string
31+
saveTimeout?: number
32+
savedDuration?: number
33+
errorDuration?: number
2834
}
2935

3036
const InlineEdit: React.FC<InlineEditProps> = ({
@@ -41,11 +47,16 @@ const InlineEdit: React.FC<InlineEditProps> = ({
4147
disabledClass,
4248
loadingClass,
4349
invalidClass,
50+
savedClass,
51+
errorClass,
4452
format,
4553
showNewLines = true,
4654
options = [],
4755
valueKey = 'value',
4856
labelKey = 'label',
57+
saveTimeout = 2000,
58+
savedDuration = 700,
59+
errorDuration = 1000,
4960
}) => {
5061
//==========================
5162
// XState Machine
@@ -58,9 +69,29 @@ const InlineEdit: React.FC<InlineEditProps> = ({
5869
optimisticUpdate,
5970
validate,
6071
onChange,
72+
saveTimeout,
73+
savedDuration,
74+
errorDuration,
6175
})
6276
)
6377

78+
//==========================
79+
// Send SAVED event when a
80+
// new value is received
81+
// =========================
82+
const isFirstRun = useRef(true)
83+
84+
useEffect(() => {
85+
// Prevent triggering SAVED
86+
// on first render
87+
if (isFirstRun.current) {
88+
isFirstRun.current = false
89+
return
90+
}
91+
// Trigger it on value changes
92+
send({ type: 'SAVED', value })
93+
}, [value])
94+
6495
//==========================
6596
// Event Handlers
6697
// =========================
@@ -94,6 +125,12 @@ const InlineEdit: React.FC<InlineEditProps> = ({
94125
if (loadingClass && current.value === 'loading') {
95126
viewClassNames.push(loadingClass)
96127
}
128+
if (savedClass && current.value === 'saved') {
129+
viewClassNames.push(savedClass)
130+
}
131+
if (errorClass && current.value === 'error') {
132+
viewClassNames.push(errorClass)
133+
}
97134
if (disabledClass && isDisabled) {
98135
viewClassNames.push(disabledClass)
99136
}
@@ -151,7 +188,10 @@ const InlineEdit: React.FC<InlineEditProps> = ({
151188
// =========================
152189
return (
153190
<div>
154-
{(current.value === 'view' || current.value === 'loading') && (
191+
{(current.value === 'view' ||
192+
current.value === 'loading' ||
193+
current.value === 'saved' ||
194+
current.value === 'error') && (
155195
<span
156196
{...viewClassProp}
157197
onClick={() => send('CLICK')}

src/machine.ts

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ interface InlineEditState {
55
view: {}
66
edit: {}
77
loading: {}
8+
saved: {}
9+
error: {}
810
}
911
}
1012

@@ -15,22 +17,25 @@ type InlineEditEvent =
1517
| { type: 'ESC'; value: string }
1618
| { type: 'ENTER'; value: string }
1719
| { type: 'BLUR'; value: string }
20+
| { type: 'SAVED'; value: string }
1821

1922
interface InlineEditContext {
2023
value: string
2124
newValue: string
22-
isDisabled: boolean
23-
allowEditWhileLoading: boolean
25+
oldValue: string
2426
isValid: boolean
2527
}
2628

2729
interface InlineEditMachineProps {
2830
value: string
31+
onChange: (value: string) => void
2932
isDisabled: boolean
3033
allowEditWhileLoading: boolean
3134
optimisticUpdate: boolean
3235
validate?: (value: string) => boolean
33-
onChange: (value: string) => Promise<any>
36+
saveTimeout: number
37+
savedDuration: number
38+
errorDuration: number
3439
}
3540

3641
const getInlineEditMachine = ({
@@ -40,6 +45,9 @@ const getInlineEditMachine = ({
4045
optimisticUpdate,
4146
validate,
4247
onChange,
48+
saveTimeout,
49+
savedDuration,
50+
errorDuration
4351
}: InlineEditMachineProps) =>
4452
Machine<InlineEditContext, InlineEditState, InlineEditEvent>(
4553
{
@@ -48,8 +56,7 @@ const getInlineEditMachine = ({
4856
context: {
4957
value,
5058
newValue: '',
51-
isDisabled,
52-
allowEditWhileLoading,
59+
oldValue: '',
5360
isValid:
5461
validate && typeof validate === 'function' ? validate(value) : true,
5562
},
@@ -59,6 +66,7 @@ const getInlineEditMachine = ({
5966
on: {
6067
CLICK: { target: 'edit', cond: 'isEnabled' },
6168
FOCUS: { target: 'edit', cond: 'isEnabled' },
69+
SAVED: { target: 'saved', actions: 'commitChange' }
6270
},
6371
},
6472
edit: {
@@ -67,27 +75,46 @@ const getInlineEditMachine = ({
6775
CHANGE: { target: 'edit', actions: 'change' },
6876
ESC: 'view',
6977
ENTER: [
70-
{ target: 'loading', cond: 'shouldCommit' },
78+
{ target: 'loading', cond: 'shouldSend' },
7179
{ target: 'view' },
7280
],
7381
BLUR: [
74-
{ target: 'loading', cond: 'shouldCommit' },
82+
{ target: 'loading', cond: 'shouldSend' },
7583
{ target: 'view' },
7684
],
7785
},
7886
},
7987
loading: {
80-
invoke: {
81-
id: 'commitChange',
82-
src: 'commitChange',
83-
onDone: { target: 'view', actions: optimisticUpdate ? 'optimisticUpdate' : '' },
84-
onError: { target: 'view' },
85-
},
88+
entry: [optimisticUpdate ? 'optimisticUpdate': 'noAction', 'sendChange'],
8689
on: {
8790
CLICK: { target: 'edit', cond: 'canEditWhileLoading' },
8891
FOCUS: { target: 'edit', cond: 'canEditWhileLoading' },
92+
SAVED: { target: 'saved', actions: 'commitChange' }
8993
},
94+
after: {
95+
SAVE_TIMEOUT: { target: 'error', actions: 'cancelChange' }
96+
}
9097
},
98+
saved: {
99+
on: {
100+
CLICK: { target: 'edit', cond: 'isEnabled' },
101+
FOCUS: { target: 'edit', cond: 'isEnabled' },
102+
SAVED: { target: 'saved', actions: 'commitChange' }
103+
},
104+
after: {
105+
SAVED_DURATION: { target: 'view' }
106+
}
107+
},
108+
error: {
109+
on: {
110+
CLICK: { target: 'edit', cond: 'isEnabled' },
111+
FOCUS: { target: 'edit', cond: 'isEnabled' },
112+
SAVED: { target: 'saved', actions: 'commitChange' }
113+
},
114+
after: {
115+
ERROR_DURATION: { target: 'view' }
116+
}
117+
}
91118
},
92119
},
93120
{
@@ -99,8 +126,19 @@ const getInlineEditMachine = ({
99126
newValue: context => context.value,
100127
}),
101128
optimisticUpdate: assign({
129+
oldValue: context => context.value,
102130
value: context => context.newValue,
103131
}),
132+
noAction: () => { },
133+
sendChange: (context: InlineEditContext) => {
134+
onChange(context.newValue)
135+
},
136+
commitChange: assign({
137+
value: (_, event) => event.value,
138+
}),
139+
cancelChange: assign({
140+
value: context => context.oldValue,
141+
}),
104142
validate:
105143
validate && typeof validate === 'function'
106144
? assign({
@@ -109,17 +147,17 @@ const getInlineEditMachine = ({
109147
: () => {},
110148
},
111149
guards: {
112-
shouldCommit: context =>
150+
shouldSend: context =>
113151
context.isValid && context.newValue !== context.value,
114-
isEnabled: context => !context.isDisabled,
115-
canEditWhileLoading: context =>
116-
!context.isDisabled && context.allowEditWhileLoading,
117-
},
118-
services: {
119-
commitChange: context => {
120-
return onChange(context.newValue)
121-
},
152+
isEnabled: () => !isDisabled,
153+
canEditWhileLoading: () =>
154+
!isDisabled && allowEditWhileLoading,
122155
},
156+
delays: {
157+
SAVE_TIMEOUT: saveTimeout,
158+
SAVED_DURATION: savedDuration,
159+
ERROR_DURATION: errorDuration
160+
}
123161
}
124162
)
125163

0 commit comments

Comments
 (0)