1 | import ensureFile from './ensure-file.js'
|
2 | import path from 'path'
|
3 |
|
4 | let 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
|
8 | import * as fromValidate from './validate.js'
|
9 | import * as fromFormat from './format.js'
|
10 | // import get from 'dlv';
|
11 | import get from 'lodash/get'
|
12 | import produce from 'immer'
|
13 | // import set from 'dset';
|
14 | import set from 'lodash/set'
|
15 | import React, {
|
16 | useContext,
|
17 | useEffect,
|
18 | useMemo,
|
19 | useReducer,
|
20 | useRef,
|
21 | } from 'react'
|
22 |
|
23 | let SET = 'data/SET'
|
24 | let SET_FN = 'data/SET_FN'
|
25 | let RESET = 'data/RESET'
|
26 | let FORCE_REQUIRED = 'data/FORCE_REQUIRED'
|
27 | let 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 |
|
56 | let DataContexts = {
|
57 | default: React.createContext([]),
|
58 | }
|
59 | export 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 | }
|
121 | DataProvider.defaultProps = {
|
122 | context: 'default',
|
123 | onChange: () => {},
|
124 | onSubmit: () => {},
|
125 | }
|
126 |
|
127 | export 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 |
|
225 | export default function ensureData({ src }) {
|
226 | return ensureFile({
|
227 | file: path.join(src, 'Data', 'ViewsData.js'),
|
228 | content: DATA,
|
229 | })
|
230 | }
|