diff --git a/morph/react/block-add-data.js b/morph/react/block-add-data.js index 05414a9..ad32944 100644 --- a/morph/react/block-add-data.js +++ b/morph/react/block-add-data.js @@ -9,25 +9,26 @@ export function enter(node, parent, state) { if (data.isConstant) { addConstantData(data, state) } else { - addData(data, state) + addData(data, !!dataGroup.aggregate, state) } } if (dataGroup.aggregate) { - dataGroup.name = getVariableName('aggregateData', state) - dataGroup.valueName = dataGroup.name + dataGroup.name = getDataVariableName(['aggregate'], state) let importName = getAggregateImportName(dataGroup.aggregate.source, state) state.variables.push( `let ${dataGroup.name} = ${importName}.${ dataGroup.aggregate.value - }(${dataGroup.data.map((d) => d.name).join(', ')})` + }(${dataGroup.data + .map((d) => (d.isConstant ? d.name : d.variables.value)) + .join(', ')})` ) dataGroup.context = dataGroup.data[0].context dataGroup.path = dataGroup.data[0].path } else { // no aggregate function so will use the data directly dataGroup.name = dataGroup.data[0].name - dataGroup.valueName = dataGroup.data[0].valueName + dataGroup.variables = dataGroup.data[0].variables // the list item data provider functionality makes use of the path // so adding it to keep consistency with already generated data providers dataGroup.context = dataGroup.data[0].context @@ -37,8 +38,7 @@ export function enter(node, parent, state) { } function addConstantData(data, state) { - data.name = getVariableName('constantData', state) - data.valueName = data.name + data.name = getDataVariableName(['constant'], state) if (data.format?.formatIn) { let importName = getImportNameForSource(data.format.formatIn.source, state) @@ -50,38 +50,103 @@ function addConstantData(data, state) { } } -function addData(data, state) { - data.name = getDataVariableName(data, state) - data.valueName = `${data.name}.value` +function addData(data, isAggregate, state) { + data.variables = {} - // at the moment it will create multiple instances for the same data key - // an optimization will be implemented to reuse a variable if possible - state.variables.push(`let ${data.name} = fromData.useData({ viewPath,`) - maybeDataContext(data, state) - maybeDataPath(data, state) - maybeDataFormat(data.format, state) - maybeDataValidate(data.validate, state) - state.variables.push('})') + if (data.uses.has('useDataValue') || isAggregate) { + let dataValueName = getDataVariableName( + [data.context, data.path, 'value'], + state + ) + state.variables.push( + `let ${dataValueName} = fromData.${ + data.format?.formatIn ? 'useDataFormat' : 'useDataValue' + }({ viewPath,` + ) + maybeDataContext(data, state) + maybeDataPath(data, state) + maybeDataFormat(data, state) + state.variables.push('})') + data.variables.value = dataValueName + } - state.use('ViewsUseData') -} + if (data.validate && data.validate.type === 'js') { + if (data.uses.has('useDataIsValidInitial')) { + let dataIsValidInitialName = getDataVariableName( + [data.context, data.path, 'isValidInitial'], + state + ) + state.variables.push( + `let ${dataIsValidInitialName} = fromData.useDataIsValidInitial({ viewPath,` + ) + maybeDataContext(data, state) + maybeDataPath(data, state) + maybeDataValidate(data, state) + state.variables.push('})') + data.variables.isValidInitial = dataIsValidInitialName + } -function getDataVariableName(data, state) { - let name = `${toCamelCase( - [ - data.context, - data.path ? data.path.replace(/\./g, '_') : null, - data.format?.formatIn?.value, - data.format?.formatOut?.value, - data.validate?.value, - data.validate?.required ? 'required' : null, - 'data', - ] - .filter(Boolean) - .map(toSnakeCase) - .join('_') - )}` - return getVariableName(name, state) + if (data.uses.has('useDataIsValid')) { + let dataIsValidName = getDataVariableName( + [data.context, data.path, 'isValid'], + state + ) + state.variables.push( + `let ${dataIsValidName} = fromData.useDataIsValid({ viewPath,` + ) + maybeDataContext(data, state) + maybeDataPath(data, state) + maybeDataValidate(data, state) + if (data.validate.required) { + state.variables.push('required: true,') + } + state.variables.push('})') + data.variables.isValid = dataIsValidName + } + } + + if (data.uses.has('useDataChange')) { + let dataChangeName = getDataVariableName( + [data.context, data.path, 'change'], + state + ) + state.variables.push( + `let ${dataChangeName} = fromData.useDataChange({ viewPath,` + ) + maybeDataContext(data, state) + maybeDataPath(data, state) + maybeDataFormatOut(data, state) + state.variables.push('})') + data.variables.onChange = dataChangeName + } + + if (data.uses.has('useDataSubmit')) { + let dataSubmitName = getDataVariableName( + [data.context, data.path, 'submit'], + state + ) + state.variables.push( + `let ${dataSubmitName} = fromData.useDataSubmit({ viewPath,` + ) + maybeDataContext(data, state) + state.variables.push('})') + data.variables.onSubmit = dataSubmitName + } + + if (data.uses.has('useDataIsSubmitting')) { + let dataIsSubmittingName = getDataVariableName( + [data.context, data.path, 'isSubmitting'], + state + ) + state.variables.push( + `let ${dataIsSubmittingName} = fromData.useDataIsSubmitting({ viewPath,` + ) + maybeDataContext(data, state) + state.variables.push('})') + data.variables.isSubmitting = dataIsSubmittingName + } + + state.use('ViewsUseData') } function maybeDataContext(dataDefinition, state) { @@ -96,27 +161,26 @@ function maybeDataPath(dataDefinition, state) { state.variables.push(`path: '${dataDefinition.path}',`) } -function maybeDataFormat(format, state) { - if (!format) return +function maybeDataFormat(data, state) { + if (!data.format?.formatIn) return - if (format.formatIn) { - let importName = getFormatImportName(format.formatIn.source, state) - state.variables.push(`formatIn: ${importName}.${format.formatIn.value},`) - } + let importName = getFormatImportName(data.format.formatIn.source, state) + state.variables.push(`format: ${importName}.${data.format.formatIn.value},`) +} - if (format.formatOut) { - let importName = getFormatImportName(format.formatOut.source, state) - state.variables.push(`formatOut: ${importName}.${format.formatOut.value},`) - } +function maybeDataFormatOut(data, state) { + if (!data.format?.formatOut) return + + let importName = getFormatImportName(data.format.formatOut.source, state) + state.variables.push( + `formatOut: ${importName}.${data.format.formatOut.value},` + ) } -function maybeDataValidate(validate, state) { - if (!validate || validate.type !== 'js') return - let importName = getValidateImportName(validate.source, state) - state.variables.push(`validate: ${importName}.${validate.value},`) - if (validate.required) { - state.variables.push('validateRequired: true,') - } +function maybeDataValidate(data, state) { + if (!data.validate || data.validate.type !== 'js') return + let importName = getValidateImportName(data.validate.source, state) + state.variables.push(`validate: ${importName}.${data.validate.value},`) } function getAggregateImportName(source, state) { @@ -151,3 +215,17 @@ function getFormatImportName(source, state) { } return importName } + +function getDataVariableName(params, state) { + return getVariableName(transformToCamelCase([...params, 'data']), state) +} + +function transformToCamelCase(args) { + return toCamelCase( + args + .filter(Boolean) + .map((arg) => arg.replace(/\./g, '_')) + .map(toSnakeCase) + .join('_') + ) +} diff --git a/morph/utils.js b/morph/utils.js index 432ddd4..6ef0eff 100644 --- a/morph/utils.js +++ b/morph/utils.js @@ -118,7 +118,7 @@ function getScopedConditionPropValue(node, parent, state) { } let CHILD_VALUES = /!?props\.(isSelected|isHovered|isFocused|isSelectedHovered)/ -let DATA_VALUES = /!?props\.(isInvalid|isInvalidInitial|isValid|isValidInitial|value|isSubmitting)$/ +let DATA_VALUES = /!?props\.(isInvalid|isInvalidInitial|isValid|isValidInitial|isSubmitting|value|onSubmit|onChange)$/ let IS_HOVERED_OR_SELECTED_HOVER = /!?props\.(isHovered|isSelectedHovered)/ let IS_FLOW = /!?props\.(isFlow|flow)$/ export function isFlow(prop) { @@ -620,11 +620,48 @@ export function getDataForLoc(blockNode, loc) { } export function replacePropWithDataValue(value, dataGroup) { - return value - .replace('props.value', dataGroup.valueName) - .replace('props.onChange', 'props.change') - .replace('props.onSubmit', 'props.submit') - .replace('props', dataGroup.name) + let propValue = value.replace('props.', '') + if (dataGroup.aggregate) { + if (propValue === 'value') { + return dataGroup.name + } else { + throw new Error( + `Property ${propValue} is not available on aggregate data, only "value" is a valid option` + ) + } + } else if (dataGroup.data[0].isConstant) { + if (propValue === 'value') { + return dataGroup.data[0].name + } else { + throw new Error( + `Property ${propValue} is not available on constant data, only "value" is a valid option` + ) + } + } else { + if (propValue === 'isInvalid') return `!${dataGroup.variables['isValid']}` + if (propValue === 'isInvalidInitial') + return `!${dataGroup.variables['isValidInitial']}` + return dataGroup.variables[propValue] + } +} + +let PROP_TO_USE_DATA = { + isInvalid: 'useDataIsValid', + isInvalidInitial: 'useDataIsValidInitial', + isValid: 'useDataIsValid', + isValidInitial: 'useDataIsValidInitial', + isSubmitting: 'useDataIsSubmitting', + value: 'useDataValue', + onSubmit: 'useDataSubmit', + onChange: 'useDataChange', +} +export function maybeGetUseDataForValue(p) { + if (DATA_VALUES.test(p.value)) { + let [, value] = DATA_VALUES.exec(p.value) + return PROP_TO_USE_DATA[value] + } else { + return null + } } export function getImportNameForSource(source, state) { diff --git a/parse/helpers.js b/parse/helpers.js index fe1c609..f2a0e6a 100644 --- a/parse/helpers.js +++ b/parse/helpers.js @@ -249,15 +249,16 @@ export let getData = (maybeProp) => { return { value: fullPath, isConstant: true, + uses: new Set(), } } if (!isNaN(Number(fullPath))) { - return { value: Number(fullPath), isConstant: true } + return { value: Number(fullPath), isConstant: true, uses: new Set() } } let [context = null] = /\./.test(fullPath) ? fullPath.split('.') : [fullPath] let path = context === fullPath ? null : fullPath.replace(`${context}.`, '') - return { path, context } + return { path, context, uses: new Set() } } function maybeSourceAndValue(input) { diff --git a/parse/index.js b/parse/index.js index bef8c14..0d35c39 100644 --- a/parse/index.js +++ b/parse/index.js @@ -33,6 +33,7 @@ import { isGoogleFont } from '../morph/fonts.js' import getLoc from './get-loc.js' import getTags from './get-tags.js' import path from 'path' +import { maybeGetUseDataForValue } from '../morph/utils.js' export default ({ convertSlotToProps = true, @@ -399,6 +400,16 @@ export default ({ do { index++ if (index === block.properties.length) { + if ( + 'validate' in currentData && + !('type' in currentData.validate) + ) { + // required keyword without validate + delete currentData.validate + } + currentData.loc.end = block.properties[index - 1].loc.end + currentDataGroup.loc.end = block.properties[index - 1].loc.end + currentDataGroup = null break } @@ -423,6 +434,21 @@ export default ({ } else if (p.name === 'data') { // data section finished - setting end line currentData.loc.end = block.properties[index - 1].loc.end + + if ( + 'validate' in currentData && + !('type' in currentData.validate) + ) { + // required keyword without validate + delete currentData.validate + } + currentData.loc.end = block.properties[index - 1].loc.end + if (currentData.uses.size) { + // if current data has an assignment then set currentDataGroup to null + // else leave it as probably is part of aggregate data group + currentDataGroup.loc.end = block.properties[index - 1].loc.end + currentDataGroup = null + } } else { if ( !currentDataGroup.aggregate && @@ -430,27 +456,16 @@ export default ({ ) { warnings.push({ type: `No aggregate function was provided, but ${currentDataGroup.data.length} data keys were found. Did you forget to specify an aggregate function?`, - line, loc: block.loc, }) } - if ( - 'validate' in currentData && - !('type' in currentData.validate) - ) { - // required keyword without validate - delete currentData.validate + + let value = maybeGetUseDataForValue(p) + if (value) { + currentData.uses.add(value) } - currentData.loc.end = block.properties[index - 1].loc.end - currentDataGroup.loc.end = block.properties[index - 1].loc.end - currentDataGroup = null } - } while ( - index < block.properties.length && - ['format', 'formatOut', 'validate', 'required', 'aggregate'].includes( - p.name - ) - ) + } while (index < block.properties.length && p.name !== 'data') } else { index++ }