UNPKG

5.97 kBJavaScriptView Raw
1import ensureFile from './ensure-file.js'
2import path from 'path'
3
4let DATA = `// This file is automatically generated by Views and will be overwritten
5// when the morpher runs. If you want to contribute to how it's generated, eg,
6// improving the algorithms inside, etc, see this:
7// https://github.com/viewstools/morph/blob/master/ensure-data.js
8import * as fromValidate from './validate.js'
9import * as fromFormat from './format.js'
10// import get from 'dlv';
11import get from 'lodash/get'
12import produce from 'immer'
13// import set from 'dset';
14import set from 'lodash/set'
15import React, {
16 useContext,
17 useEffect,
18 useMemo,
19 useReducer,
20 useRef,
21} from 'react'
22
23let SET = 'data/SET'
24let SET_FN = 'data/SET_FN'
25let RESET = 'data/RESET'
26let FORCE_REQUIRED = 'data/FORCE_REQUIRED'
27let reducer = produce((draft, action) => {
28 switch (action.type) {
29 case SET: {
30 set(draft, action.path, action.value)
31 break
32 }
33
34 case SET_FN: {
35 action.fn(draft, set, get)
36 break
37 }
38
39 case RESET: {
40 return action.value
41 }
42
43 case FORCE_REQUIRED: {
44 draft._forceRequired = true
45 break
46 }
47
48 default: {
49 throw new Error(
50 \`Unknown action type "\${action.type}" in useData reducer.\`
51 )
52 }
53 }
54})
55
56let DataContexts = {
57 default: React.createContext([]),
58}
59export function DataProvider(props) {
60 if (!props.context) {
61 throw new Error(
62 \`You're missing the context value in DataProvider. Eg: <DataProvider context="namespace" ...\`
63 )
64 }
65 if (!(props.context in DataContexts)) {
66 DataContexts[props.context] = React.createContext([])
67 }
68 let Context = DataContexts[props.context]
69
70 let [state, dispatch] = useReducer(reducer, props.value)
71 let isSubmitting = useRef(false)
72 let shouldCallOnChange = useRef(false)
73
74 useEffect(() => {
75 if (isSubmitting.current) return
76
77 shouldCallOnChange.current = false
78 dispatch({ type: RESET, value: props.value })
79 }, [props.value]) // eslint-disable-line
80 // ignore dispatch
81
82 let value = useMemo(() => {
83 async function onSubmit(args) {
84 if (isSubmitting.current) return
85 isSubmitting.current = true
86
87 try {
88 let res = await props.onSubmit(state, args)
89 isSubmitting.current = false
90
91 if (!res) return
92 } catch (error) {
93 isSubmitting.current = false
94 }
95
96 dispatch({ type: FORCE_REQUIRED })
97 }
98
99 return [state, dispatch, onSubmit]
100 }, [state, props.onSubmit]) // eslint-disable-line
101 // the linter says we need props when props.onSubmit is already there
102
103 // keep track of props.onChange outside of the following effect to
104 // prevent loops. Making the function useCallback didn't work
105 let onChange = useRef(props.onChange)
106 useEffect(() => {
107 onChange.current = props.onChange
108 }, [props.onChange])
109
110 useEffect(() => {
111 if (!shouldCallOnChange.current) {
112 shouldCallOnChange.current = true
113 return
114 }
115
116 onChange.current(state, fn => dispatch({ type: SET_FN, fn }))
117 }, [state])
118
119 return <Context.Provider value={value}>{props.children}</Context.Provider>
120}
121DataProvider.defaultProps = {
122 context: 'default',
123 onChange: () => {},
124 onSubmit: () => {},
125}
126
127export function useData({
128 path = null,
129 context = 'default',
130 formatIn = null,
131 formatOut = null,
132 validate = null,
133 validateRequired = false,
134} = {}) {
135 if (process.env.NODE_ENV === 'development') {
136 if (!(context in DataContexts)) {
137 throw new Error(
138 \`"\${context}" isn't a valid Data context. Check that you have <DataProvider context="\${context}" value={data}> in the component that defines the context for this story.\`
139 )
140 }
141
142 if (formatIn && !(formatIn in fromFormat)) {
143 throw new Error(
144 \`"\${formatIn}" function doesn't exist or is not exported in Data/format.js\`
145 )
146 }
147
148 if (formatOut && !(formatOut in fromFormat)) {
149 throw new Error(
150 \`"\${formatOut}" function doesn't exist or is not exported in Data/format.js\`
151 )
152 }
153
154 if (validate && !(validate in fromValidate)) {
155 throw new Error(
156 \`"\${validate}" function doesn't exist or is not exported in Data/validators.js\`
157 )
158 }
159 }
160
161 let contextValue = useContext(DataContexts[context])
162 let touched = useRef(false)
163
164 return useMemo(() => {
165 let [data, dispatch, onSubmit] = contextValue
166
167 if (!data) {
168 if (process.env.NODE_ENV === 'development') {
169 console.error(
170 'Check that you have <DataProvider value={data}> in the component that defines the data for this story.',
171 {
172 path,
173 formatIn,
174 formatOut,
175 validate,
176 validateRequired,
177 data,
178 }
179 )
180 }
181 return {}
182 }
183
184 let rawValue = path ? get(data, path) : data
185 let value = rawValue
186 if (path && formatIn) {
187 value = fromFormat[formatIn](rawValue, data)
188 }
189
190 let isValidInitial = true
191 if (validate) {
192 isValidInitial = fromValidate[validate](rawValue, value, data)
193 }
194 let isValid = touched.current || (validateRequired && data._forceRequired)? isValidInitial : true
195
196 function onChange(value, changePath = path) {
197 touched.current = !!value
198
199 if (typeof value === 'function') {
200 dispatch({ type: SET_FN, fn: value })
201 } else if (!changePath) {
202 dispatch({ type: RESET, value })
203 } else {
204 dispatch({
205 type: SET,
206 path: changePath,
207 value: formatOut ? fromFormat[formatOut](value, data) : value,
208 })
209 }
210 }
211
212 return {
213 onChange,
214 onSubmit,
215 value,
216 isValid,
217 isValidInitial,
218 isInvalid: !isValid,
219 isInvalidInitial: !isValidInitial,
220 }
221 }, [contextValue, path, formatIn, formatOut, validateRequired, validate])
222}
223`
224
225export default function ensureData({ src }) {
226 return ensureFile({
227 file: path.join(src, 'Data', 'ViewsData.js'),
228 content: DATA,
229 })
230}