Skip to content
Permalink
Browse files

feat: show/capture data with context

Show:
```
CreatedAt View
flow together
data show createdAt
dataFormat date iso MM/dd/yyyy
  Text
  text <value
```

Capture:
```
Name View
flow together
data user.profile.name
  Input
  onChange <
  onSubmit <
  value <
```

What's next?
- Making the GraphQL queries. These changes still require you to make
  the query in a logic file.
- Validations could be integrated with it I think.
- Formatters and validations can be extended to use external functions
  too. Eg `dataFormat myCustomFormatter.js#someFn`
- Maybe rename `text <` for `value <` in `Text` block to make it the
  same as `Capture`.
- Maybe change `onChange` to send text in `Capture` (we never had to use
  the event, not even once!) and add an `onSubmit` by default to take in
  the `Enter` key.
  • Loading branch information...
dariocravero committed Aug 17, 2019
1 parent 67d4372 commit 7a25a61dd3ed30b2a499b20d84272efa57f920cd
@@ -0,0 +1,105 @@
import { promises as fs } from 'fs'
import path from 'path'

let USE_DATA = `// This file is automatically generated by Views and will be overwritten
// 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 }
// 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
// ignore get
}
// capture
let captureItemReducer = produce((draft, action) => {
switch (action.type) {
case CAPTURE_SET_FIELD: {
set(draft, action.key, action.value)
break
}
case CAPTURE_RESET: {
return action.state
}
default: {
throw new Error(
\`Unknown action type "\${action.type}" in update item reducer.\`
)
}
}
})
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)
return useMemo(() => {
if (!path) return captureItem
let [item, dispatch, onSubmit] = captureItem;
return {
onChange: value => dispatch(setField(path, format.out(value))),
onSubmit,
value: format.in(get(item, path)),
}
}, [captureItem, path, format])
}
export let useCaptureItemProvider = (item, onSubmit) => {
let [state, dispatch] = useReducer(captureItemReducer, item)
useEffect(() => {
dispatch(reset(item))
}, [item])
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
}
export let useMakeFormatDate = (formatIn, formatOut, whenInvalid) =>
useMemo(() => ({
in: value => formatDateInOut(value, formatIn, formatOut, whenInvalid),
out: value => formatDateInOut(value, formatOut, formatIn, whenInvalid),
}), [])
`

export default function ensureIsBefore({ src }) {
return fs.writeFile(path.join(src, 'useData.js'), USE_DATA, {
encoding: 'utf8',
})
}
@@ -3,6 +3,7 @@ import path from 'path'

let FILE_USE_IS_BEFORE = 'useIsBefore.js'
let FILE_USE_IS_MEDIA = 'useIsMedia.js'
let FILE_USE_DATA = 'useData.js'
let FILE_USE_FLOW = 'useFlow.js'
let FILE_LOCAL_CONTAINER = 'LocalContainer.js'
let FILE_TRACK_CONTEXT = 'TrackContext.js'
@@ -26,6 +27,12 @@ export default function makeGetSystemImport(src) {
path.join(src, FILE_USE_IS_BEFORE)
)}'`

case 'ViewsUseData':
return `import * as fromData from '${relativise(
file,
path.join(src, FILE_USE_DATA)
)}'`

case 'ViewsUseFlow':
return `import * as fromFlow from '${relativise(
file,
@@ -3,6 +3,7 @@ import {
morphAllFonts,
processCustomFonts,
} from './fonts.js'
import ensureData from './ensure-data.js'
import ensureFlow from './ensure-flow.js'
import ensureIsBefore from './ensure-is-before.js'
import ensureIsMedia from './ensure-is-media.js'
@@ -93,6 +94,7 @@ export default function makeMorpher({
})

await Promise.all([
ensureData(state),
ensureFlow(state),
ensureIsBefore(state),
ensureIsMedia(state),
@@ -42,6 +42,8 @@ export default ({
animations: {},
cssDynamic: false,
cssStatic: false,
data: view.parsed.view.data,
dataFormat: view.parsed.view.dataFormat,
dependencies: new Set(),
flow: null,
setFlow: false,
@@ -97,6 +99,10 @@ export default ({
useIsMedia: view.parsed.view.useIsMedia,
}

if (state.data) {
state.use('ViewsUseData')
}

if (name !== finalName) {
console.warn(
`// ${name} is a Views reserved name. To fix this, change its file name to something else.`
@@ -56,7 +56,16 @@ 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')
) {
return {
[node.name]: `{${node.value.replace('props.', '')} || ${node.value}}`,
}
} else if (isValidImgSrc(node, parent)) {
return {
src: getImageSource(node, state, parent),
}
@@ -42,6 +42,8 @@ export default ({
animations: {},
animated: new Set(),
images: [],
data: view.parsed.view.data,
dataFormat: view.parsed.view.dataFormat,
dependencies: new Set(),
flow: null,
setFlow: false,
@@ -103,6 +105,10 @@ export default ({
maybeUsesRouter(state)
maybeUsesStyleSheet(state)

if (state.data) {
state.use('ViewsUseData')
}

return {
code: toComponent({
getImport: makeGetImport({
@@ -1,4 +1,4 @@
import { getPropValueOrDefault, isStory } from '../utils.js'
import { getProp, getPropValueOrDefault, isStory } from '../utils.js'
import { leave } from '../react/block-name.js'
import handleTable from '../react/block-name-handle-table.js'
import getBlockName from './get-block-name.js'
@@ -71,6 +71,30 @@ export default ({ state, name }) => {
if (state.setFlow) {
flow.push(`let setFlow = fromFlow.useSetFlow()`)
}
let data = []
if (state.data) {
switch (state.data.type) {
case 'show': {
data.push(`let value = fromData.useItem('${state.data.path}'`)
maybeDataFormat(state.dataFormat, data)
data.push(')')
break
}

case 'capture': {
data.push(
`let { value, onChange, onSubmit }= fromData.useCaptureItem('${state.data.path}'`
)
maybeDataFormat(state.dataFormat, data)
data.push(')')
break
}

default: {
break
}
}
}

if (state.hasRefs) {
let trackOpen = state.track ? '<TrackContext.Consumer>{track => (' : ''
@@ -90,8 +114,22 @@ export default ({ state, name }) => {
${state.useIsMedia ? 'let isMedia = useIsMedia()' : ''}
${animated.join('\n')}
${flow.join('\n')}
${data.join('\n')}
return ${ret}
}`
}
}

function maybeDataFormat(format, data) {
if (!format) return
if (format.type !== 'date') return

data.push(
`, fromData.useMakeFormatDate('${format.formatIn}', '${format.formatOut}'`
)
if (format.whenInvalid) {
data.push(`, '${format.whenInvalid}'`)
}
data.push(`)`)
}
@@ -1,6 +1,6 @@
{
"name": "@viewstools/morph",
"version": "19.6.11",
"version": "19.7.0",
"description": "Views language morpher",
"main": "index.js",
"type": "module",
@@ -15,6 +15,8 @@ let dymPropMatcher = new DidYouMeanMatcher([
'cy',
'd',
'data',
'xdata',
'dataFormat',
'defaultValue',
'fill',
'flow',
@@ -236,6 +238,29 @@ export let getComment = line => {
return ''
}
}

export let getData = maybeProp => {
if (!maybeProp) return null

let match = maybeProp.value.match(/^(show|capture)\s+(.+)$/)
if (!match) return null

return { type: match[1], path: match[2] }
}
export let getDataFormat = maybeProp => {
if (!maybeProp) return null

let match = maybeProp.value.match(/^(date)\s+(.+?)\s+(.+?)(\s+(.+))?$/)
if (!match) return null

return {
type: match[1],
formatIn: match[2],
formatOut: match[3],
whenInvalid: match[5] || null,
}
}

export let getFormat = line => {
let properties = {}
let values = line.split(' ')

0 comments on commit 7a25a61

Please sign in to comment.
You can’t perform that action at this time.