diff --git a/cli.js b/cli.js index 7998d11..d6609d5 100644 --- a/cli.js +++ b/cli.js @@ -51,6 +51,7 @@ let wait = time => new Promise(resolve => setTimeout(resolve, time)) --as target platform react-dom (default) react-native + react-pdf --clean clean the autogenerated .view.js files --local default local language, defaults to English (en) --tools use with Views Tools, defauls to true when diff --git a/ensure-data.js b/ensure-data.js index 451d2d5..28badb2 100644 --- a/ensure-data.js +++ b/ensure-data.js @@ -5,98 +5,130 @@ let USE_DATA = `// This file is automatically generated by Views and will be ove // when the morpher runs. If you want to contribute to how it's generated, eg, // improving the algorithms inside, etc, see this: // https://github.com/viewstools/morph/blob/master/ensure-data.js -import get from 'lodash/get' -import produce from 'immer' -import set from 'lodash/set' -import React, { useContext, useEffect, useMemo, useReducer } from 'react' -import parseDate from 'date-fns/parse' -import parseISO from 'date-fns/parseISO' -import formatDate from 'date-fns/format' -import isValidDate from 'date-fns/isValid' - -let identity = { in: i => i, out: i => i } +import * as fromValidate from './Data/validators.js'; +import get from 'lodash/get'; +import produce from 'immer'; +import set from 'lodash/set'; +import React, { useContext, useEffect, useMemo, useReducer, useRef } from 'react'; +import parseDate from 'date-fns/parse'; +import parseISO from 'date-fns/parseISO'; +import formatDate from 'date-fns/format'; +import isValidDate from 'date-fns/isValid'; + +let identity = { in: i => i, out: i => i }; // show -let ItemContext = React.createContext({}) -export let ItemProvider = ItemContext.Provider -export let useItem = (path = null, format = identity) => { - let item = useContext(ItemContext) - - return useMemo(() => ( - path? format.in(get(item, path)) : item - ), [item, path, format]) // eslint-ignore-line +let ItemContext = React.createContext({}); +export let ItemProvider = ItemContext.Provider; +export let useItem = ({ path = null, format = identity } = {}) => { + let item = useContext(ItemContext); + + return useMemo(() => (path ? { value: format.in(get(item, path)) } : item), [ + item, + path, + format, + ]); // eslint-ignore-line // ignore get -} +}; // capture let captureItemReducer = produce((draft, action) => { switch (action.type) { case CAPTURE_SET_FIELD: { - set(draft, action.key, action.value) - break + set(draft, action.key, action.value); + break; } case CAPTURE_RESET: { - return action.state + return action.state; } default: { throw new Error( \`Unknown action type "\${action.type}" in update item reducer.\` - ) + ); } } -}) -let CAPTURE_SET_FIELD = 'capture/SET_FIELD' +}); +let CAPTURE_SET_FIELD = 'capture/SET_FIELD'; export let setField = (key, value) => ({ type: CAPTURE_SET_FIELD, key, value, -}) -let CAPTURE_RESET = 'capture/RESET' -export let reset = state => ({ type: CAPTURE_RESET, state }) - -let CaptureItemContext = React.createContext({}) -export let CaptureItemProvider = CaptureItemContext.Provider -export let useCaptureItem = (path = null, format = identity) => { - let captureItem = useContext(CaptureItemContext) +}); +let CAPTURE_RESET = 'capture/RESET'; +export let reset = state => ({ type: CAPTURE_RESET, state }); + +let CaptureItemContext = React.createContext({}); +export let CaptureItemProvider = CaptureItemContext.Provider; +export let useCaptureItem = ({ + path = null, format = identity, validate = null +} = {}) => { + let captureItem = useContext(CaptureItemContext); + let touched = useRef(false); + + if (process.env.NODE_ENV === 'development') { + if (validate && !(validate in fromValidate)) { + throw new Error( + \`"\${validate}" function doesn't exist or is not exported in Data/validators.js\` + ); + } + } return useMemo(() => { - if (!path) return captureItem + if (!path) return captureItem; let [item, dispatch, onSubmit] = captureItem; + if (!item) return {}; + + let value = format.in(get(item, path)); + let isValid = + touched.current && validate ? fromValidate[validate](value) : true; + let onChange = value => { + touched.current = true; + dispatch(setField(path, format.out(value))); + } return { - onChange: value => dispatch(setField(path, format.out(value))), + onChange, onSubmit, - value: format.in(get(item, path)), - } - }, [captureItem, path, format]) -} + value, + isValid, + isInvalid: !isValid + }; + }, [captureItem, path, format, validate]); +}; export let useCaptureItemProvider = (item, onSubmit) => { - let [state, dispatch] = useReducer(captureItemReducer, item) + let [state, dispatch] = useReducer(captureItemReducer, item); useEffect(() => { - dispatch(reset(item)) - }, [item]) + dispatch(reset(item)); + }, [item]); - return useMemo(() => [state, dispatch, onSubmit], [state, dispatch, onSubmit]) -} + return useMemo(() => [state, dispatch, onSubmit], [ + state, + dispatch, + onSubmit, + ]); +}; function formatDateInOut(rvalue, formatIn, formatOut, whenInvalid = '') { let value = formatIn === 'iso' ? parseISO(rvalue) - : parseDate(rvalue, formatIn, new Date()) - return isValidDate(value) ? formatDate(value, formatOut) : whenInvalid + : parseDate(rvalue, formatIn, new Date()); + return isValidDate(value) ? formatDate(value, formatOut) : whenInvalid; } export let useMakeFormatDate = (formatIn, formatOut, whenInvalid) => - useMemo(() => ({ - in: value => formatDateInOut(value, formatIn, formatOut, whenInvalid), - out: value => formatDateInOut(value, formatOut, formatIn, whenInvalid), - }), []) // eslint-disable-line - // ignore formatIn, formatouOut, whenInvalid + useMemo( + () => ({ + in: value => formatDateInOut(value, formatIn, formatOut, whenInvalid), + out: value => formatDateInOut(value, formatOut, formatIn, whenInvalid), + }), + [] + ); // eslint-disable-line +// ignore formatIn, formatouOut, whenInvalid ` export default function ensureIsBefore({ src }) { diff --git a/ensure-flow.js b/ensure-flow.js index ec8d33a..ef32f53 100644 --- a/ensure-flow.js +++ b/ensure-flow.js @@ -8,7 +8,7 @@ function ensureFirstStoryIsOn(flow, key, stories) { if (!stories.has(key)) return let story = flow.get(key) - if (story.stories.size > 0) { + if (story && story.stories.size > 0) { let index = 0 for (let id of story.stories) { if (index === 0 || !story.isSeparate) { diff --git a/fonts.js b/fonts.js index 920be0e..6f78a71 100644 --- a/fonts.js +++ b/fonts.js @@ -9,6 +9,7 @@ import relativise from './relativise.js' let morphFont = { 'react-dom': morphFontAsReactDom, 'react-native': morphFontAsReactNative, + 'react-pdf': morphFontAsReactNative, } export async function ensureFontsDirectory(src) { diff --git a/morph/react-dom.js b/morph/react-dom.js index aef9e27..1925105 100644 --- a/morph/react-dom.js +++ b/morph/react-dom.js @@ -44,6 +44,7 @@ export default ({ cssStatic: false, data: view.parsed.view.data, dataFormat: view.parsed.view.dataFormat, + dataValidate: view.parsed.view.dataValidate, dependencies: new Set(), flow: null, setFlow: false, diff --git a/morph/react-dom/get-value-for-property.js b/morph/react-dom/get-value-for-property.js index 7bc7ebb..ff209e2 100644 --- a/morph/react-dom/get-value-for-property.js +++ b/morph/react-dom/get-value-for-property.js @@ -60,10 +60,12 @@ export default (node, parent, state) => { state.data && (node.value === 'props.value' || node.value === 'props.onSubmit' || - node.value === 'props.onChange') + node.value === 'props.onChange' || + node.value === 'props.isInvalid' || + node.value === 'props.isValid') ) { return { - [node.name]: `{${node.value.replace('props.', '')} || ${node.value}}`, + [node.name]: `{${node.value.replace('props.', 'data.')}}`, } } else if (isValidImgSrc(node, parent)) { return { diff --git a/morph/react-native.js b/morph/react-native.js index fa4a4f3..34b824d 100644 --- a/morph/react-native.js +++ b/morph/react-native.js @@ -23,6 +23,7 @@ export default ({ getSystemImport, local, localSupported, + reactNativeLibraryImport = 'react-native', track, view, viewsById, @@ -44,6 +45,7 @@ export default ({ images: [], data: view.parsed.view.data, dataFormat: view.parsed.view.dataFormat, + dataValidate: view.parsed.view.dataValidate, dependencies: new Set(), flow: null, setFlow: false, @@ -74,6 +76,7 @@ export default ({ testIdKey: 'testID', testIds: {}, track, + reactNativeLibraryImport, usedBlockNames: { [finalName]: 1, AutoSizer: 1, Column: 1, Table: 1 }, uses: [], use(block, isLazy = false) { diff --git a/morph/react-native/get-value-for-property.js b/morph/react-native/get-value-for-property.js index c192ecf..2d42852 100644 --- a/morph/react-native/get-value-for-property.js +++ b/morph/react-native/get-value-for-property.js @@ -56,7 +56,18 @@ let getImageSource = (node, state, parent) => { } export default (node, parent, state) => { - if (isValidImgSrc(node, parent)) { + if ( + state.data && + (node.value === 'props.value' || + node.value === 'props.onSubmit' || + node.value === 'props.onChange' || + node.value === 'props.isValid' || + node.value === 'props.isInvalid') + ) { + return { + [node.name]: `{${node.value.replace('props.', 'data.')}}`, + } + } else if (isValidImgSrc(node, parent)) { return ( !parent.isSvg && { source: getImageSource(node, state, parent), diff --git a/morph/react-pdf.js b/morph/react-pdf.js new file mode 100644 index 0000000..e258c8f --- /dev/null +++ b/morph/react-pdf.js @@ -0,0 +1,7 @@ +import reactNativeMorph from './react-native.js' + +export default options => + reactNativeMorph({ + ...options, + reactNativeLibraryImport: '@react-pdf/renderer', + }) diff --git a/morph/react/block-off-when.js b/morph/react/block-off-when.js index f1b3a05..3b6fc89 100644 --- a/morph/react/block-off-when.js +++ b/morph/react/block-off-when.js @@ -10,8 +10,13 @@ export function enter(node, parent, state) { node.onWhen = true if (parent && !isList(parent)) state.render.push('{') - - state.render.push(`${onWhen.value} ? `) + let value = onWhen.value + if (state.data && value === 'props.isInvalid') { + value = 'data.isInvalid' + } else if (state.data && value === 'props.isValid') { + value = 'data.isValid' + } + state.render.push(`${value} ? `) } else if (isStory(node, state)) { node.onWhen = true state.render.push(`{flow.has("${state.pathToStory}/${node.name}") ? `) diff --git a/morph/react/get-body.js b/morph/react/get-body.js index a4010d7..32e5e20 100644 --- a/morph/react/get-body.js +++ b/morph/react/get-body.js @@ -75,18 +75,19 @@ export default ({ state, name }) => { if (state.data) { switch (state.data.type) { case 'show': { - data.push(`let value = fromData.useItem('${state.data.path}'`) + data.push(`let data = fromData.useItem({ path: '${state.data.path}', `) maybeDataFormat(state.dataFormat, data) - data.push(')') + data.push('})') break } case 'capture': { data.push( - `let { value, onChange, onSubmit }= fromData.useCaptureItem('${state.data.path}'` + `let data = fromData.useCaptureItem({ path: '${state.data.path}', ` ) maybeDataFormat(state.dataFormat, data) - data.push(')') + maybeDataValidate(state.dataValidate, data) + data.push('})') break } @@ -126,10 +127,14 @@ function maybeDataFormat(format, data) { if (format.type !== 'date') return data.push( - `, fromData.useMakeFormatDate('${format.formatIn}', '${format.formatOut}'` + `format: fromData.useMakeFormatDate('${format.formatIn}', '${format.formatOut}'` ) if (format.whenInvalid) { data.push(`, '${format.whenInvalid}'`) } - data.push(`)`) + data.push(`),`) +} +function maybeDataValidate(validate, data) { + if (!validate || validate.type !== 'js') return + data.push(`validate: '${validate.value}',`) } diff --git a/morph/react/get-dependencies.js b/morph/react/get-dependencies.js index 8f464d6..1e6ef99 100644 --- a/morph/react/get-dependencies.js +++ b/morph/react/get-dependencies.js @@ -126,7 +126,11 @@ export default (state, getImport) => { } if (usesNative.length > 0) { - dependencies.push(`import { ${usesNative.join(', ')} } from 'react-native'`) + dependencies.push( + `import { ${usesNative.join(', ')} } from '${ + state.reactNativeLibraryImport + }'` + ) } if (state.track) { diff --git a/morph/react/property-text.js b/morph/react/property-text.js index 9c4cd75..ee5c2db 100644 --- a/morph/react/property-text.js +++ b/morph/react/property-text.js @@ -26,7 +26,9 @@ let parseFormatValue = (value, type) => { export function enter(node, parent, state) { if (node.name === 'text' && parent.name === 'Text') { - if (hasCustomScopes(node, parent)) { + if (state.data && node.value === 'props.value') { + parent.explicitChildren = '{data.value}' + } else if (hasCustomScopes(node, parent)) { parent.explicitChildren = wrap(getScopedCondition(node, parent)) } else if (isSlot(node)) { parent.explicitChildren = wrap(node.value) diff --git a/morphers.js b/morphers.js index 4e1b4a4..b72186c 100644 --- a/morphers.js +++ b/morphers.js @@ -1,7 +1,9 @@ import reactDom from './morph/react-dom.js' import reactNative from './morph/react-native.js' +import reactPdf from './morph/react-pdf.js' export default { 'react-dom': reactDom, 'react-native': reactNative, + 'react-pdf': reactPdf, } diff --git a/package.json b/package.json index 88c18db..6ff8ffc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@viewstools/morph", - "version": "19.7.2", + "version": "19.9.0", "description": "Views language morpher", "main": "index.js", "type": "module", diff --git a/parse/helpers.js b/parse/helpers.js index 8068d34..dfc69f3 100644 --- a/parse/helpers.js +++ b/parse/helpers.js @@ -261,6 +261,14 @@ export let getDataFormat = maybeProp => { } } +export let getDataValidate = maybeProp => { + if (!maybeProp || !maybeProp.value) return null + return { + type: 'js', + value: maybeProp.value, + } +} + export let getFormat = line => { let properties = {} let values = line.split(' ') diff --git a/parse/index.js b/parse/index.js index 38af66e..44b9b16 100644 --- a/parse/index.js +++ b/parse/index.js @@ -7,6 +7,7 @@ import { getComment, getData, getDataFormat, + getDataValidate, getFormat, getProp, getPropType, @@ -806,6 +807,9 @@ That would mean that SomeView in ${block.name} will be replaced by ${block.name} view.dataFormat = getDataFormat( view.properties.find(p => p.name === 'dataFormat') ) + view.dataValidate = getDataValidate( + view.properties.find(p => p.name === 'dataValidate') + ) } else { view.isStory = false }