Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: morph/react make use of granular functions when morphing data block #241

Merged
merged 1 commit into from
May 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 131 additions & 53 deletions morph/react/block-add-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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('_')
)
}
49 changes: 43 additions & 6 deletions morph/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
5 changes: 3 additions & 2 deletions parse/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
47 changes: 31 additions & 16 deletions parse/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}

Expand All @@ -423,34 +434,38 @@ 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 &&
currentDataGroup.data.length > 1
) {
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++
}
Expand Down