Fully remount update forms when initial values change

This commit is contained in:
Andrew Kingston 2021-06-10 11:17:14 +01:00
parent 20e568baea
commit f4cb901f33
2 changed files with 200 additions and 172 deletions

View File

@ -1,8 +1,6 @@
<script>
import { setContext, getContext, onMount } from "svelte"
import { writable, get } from "svelte/store"
import { createValidatorFromConstraints } from "./validation"
import { generateID } from "../helpers"
import { getContext } from "svelte"
import InnerForm from "./InnerForm.svelte"
export let dataSource
export let theme
@ -10,14 +8,7 @@
export let disabled = false
export let actionType = "Create"
const component = getContext("component")
const context = getContext("context")
const { styleable, API, Provider, ActionTypes } = getContext("sdk")
let loaded = false
let schema
let table
let fieldMap = {}
// Returns the closes data context which isn't a built in context
const getInitialValues = (type, dataSource, context) => {
@ -41,167 +32,19 @@
return closestContext
}
// Use the closest data context as the initial form values
const initialValues = getInitialValues(actionType, dataSource, $context)
// Form state contains observable data about the form
const formState = writable({ values: initialValues, errors: {}, valid: true })
// Form API contains functions to control the form
const formApi = {
registerField: (field, defaultValue = null, fieldDisabled = false) => {
if (!field) {
return
}
// Auto columns are always disabled
const isAutoColumn = !!schema?.[field]?.autocolumn
// Create validation function based on field schema
const constraints = schema?.[field]?.constraints
const validate = createValidatorFromConstraints(constraints, field, table)
// Construct field object
fieldMap[field] = {
fieldState: makeFieldState(
field,
defaultValue,
disabled || fieldDisabled || isAutoColumn
),
fieldApi: makeFieldApi(field, defaultValue, validate),
fieldSchema: schema?.[field] ?? {},
}
// Set initial value
const initialValue = get(fieldMap[field].fieldState).value
formState.update(state => ({
...state,
values: {
...state.values,
[field]: initialValue,
},
}))
return fieldMap[field]
},
validate: () => {
const fields = Object.keys(fieldMap)
fields.forEach(field => {
const { fieldApi } = fieldMap[field]
fieldApi.validate()
})
return get(formState).valid
},
}
// Provide both form API and state to children
setContext("form", { formApi, formState, dataSource })
// Action context to pass to children
$: actions = [{ type: ActionTypes.ValidateForm, callback: formApi.validate }]
// Creates an API for a specific field
const makeFieldApi = (field, defaultValue, validate) => {
const setValue = (value, skipCheck = false) => {
const { fieldState } = fieldMap[field]
// Skip if the value is the same
if (!skipCheck && get(fieldState).value === value) {
return
}
const newValue = value == null ? defaultValue : value
const newError = validate ? validate(newValue) : null
// Update field state
fieldState.update(state => {
state.value = newValue
state.error = newError
return state
})
// Update form state
formState.update(state => {
state.values = { ...state.values, [field]: newValue }
if (newError) {
state.errors = { ...state.errors, [field]: newError }
} else {
delete state.errors[field]
}
state.valid = Object.keys(state.errors).length === 0
return state
})
return !newError
}
return {
setValue,
validate: () => {
const { fieldState } = fieldMap[field]
setValue(get(fieldState).value, true)
},
}
}
// Creates observable state data about a specific field
const makeFieldState = (field, defaultValue, fieldDisabled) => {
return writable({
field,
fieldId: `id-${generateID()}`,
value: initialValues[field] ?? defaultValue,
error: null,
disabled: fieldDisabled,
})
}
// Fetches the form schema from this form's dataSource, if one exists
const fetchSchema = async () => {
if (!dataSource?.tableId) {
schema = {}
table = null
} else {
table = await API.fetchTableDefinition(dataSource?.tableId)
if (table) {
if (dataSource?.type === "query") {
schema = {}
const params = table.parameters || []
params.forEach(param => {
schema[param.name] = { ...param, type: "string" }
})
} else {
schema = table.schema || {}
}
}
}
loaded = true
}
// Load the form schema on mount
onMount(fetchSchema)
$: initialValues = getInitialValues(actionType, dataSource, $context)
$: resetKey = JSON.stringify(initialValues)
</script>
<Provider
{actions}
data={{ ...$formState.values, tableId: dataSource?.tableId }}
>
<div
lang="en"
dir="ltr"
use:styleable={$component.styles}
class={`spectrum ${size || "spectrum--medium"} ${
theme || "spectrum--light"
}`}
{#key resetKey}
<InnerForm
{dataSource}
{theme}
{size}
{disabled}
{actionType}
{initialValues}
>
{#if loaded}
<slot />
{/if}
</div>
</Provider>
<style>
div {
padding: 20px;
position: relative;
background-color: var(--spectrum-alias-background-color-secondary);
}
</style>
<slot />
</InnerForm>
{/key}

View File

@ -0,0 +1,185 @@
<script>
import { setContext, getContext, onMount } from "svelte"
import { writable, get } from "svelte/store"
import { createValidatorFromConstraints } from "./validation"
import { generateID } from "../helpers"
export let dataSource
export let theme
export let size
export let disabled = false
export let actionType = "Create"
export let initialValues
const component = getContext("component")
const context = getContext("context")
const { styleable, API, Provider, ActionTypes } = getContext("sdk")
let loaded = false
let schema
let table
let fieldMap = {}
// Form state contains observable data about the form
const formState = writable({ values: initialValues, errors: {}, valid: true })
// Form API contains functions to control the form
const formApi = {
registerField: (field, defaultValue = null, fieldDisabled = false) => {
if (!field) {
return
}
// Auto columns are always disabled
const isAutoColumn = !!schema?.[field]?.autocolumn
// Create validation function based on field schema
const constraints = schema?.[field]?.constraints
const validate = createValidatorFromConstraints(constraints, field, table)
// Construct field object
fieldMap[field] = {
fieldState: makeFieldState(
field,
defaultValue,
disabled || fieldDisabled || isAutoColumn
),
fieldApi: makeFieldApi(field, defaultValue, validate),
fieldSchema: schema?.[field] ?? {},
}
// Set initial value
const initialValue = get(fieldMap[field].fieldState).value
formState.update(state => ({
...state,
values: {
...state.values,
[field]: initialValue,
},
}))
return fieldMap[field]
},
validate: () => {
const fields = Object.keys(fieldMap)
fields.forEach(field => {
const { fieldApi } = fieldMap[field]
fieldApi.validate()
})
return get(formState).valid
},
}
// Provide both form API and state to children
setContext("form", { formApi, formState, dataSource })
// Action context to pass to children
const actions = [
{ type: ActionTypes.ValidateForm, callback: formApi.validate },
]
// Creates an API for a specific field
const makeFieldApi = (field, defaultValue, validate) => {
const setValue = (value, skipCheck = false) => {
const { fieldState } = fieldMap[field]
// Skip if the value is the same
if (!skipCheck && get(fieldState).value === value) {
return
}
const newValue = value == null ? defaultValue : value
const newError = validate ? validate(newValue) : null
// Update field state
fieldState.update(state => {
state.value = newValue
state.error = newError
return state
})
// Update form state
formState.update(state => {
state.values = { ...state.values, [field]: newValue }
if (newError) {
state.errors = { ...state.errors, [field]: newError }
} else {
delete state.errors[field]
}
state.valid = Object.keys(state.errors).length === 0
return state
})
return !newError
}
return {
setValue,
validate: () => {
const { fieldState } = fieldMap[field]
setValue(get(fieldState).value, true)
},
}
}
// Creates observable state data about a specific field
const makeFieldState = (field, defaultValue, fieldDisabled) => {
return writable({
field,
fieldId: `id-${generateID()}`,
value: initialValues[field] ?? defaultValue,
error: null,
disabled: fieldDisabled,
})
}
// Fetches the form schema from this form's dataSource, if one exists
const fetchSchema = async () => {
if (!dataSource?.tableId) {
schema = {}
table = null
} else {
table = await API.fetchTableDefinition(dataSource?.tableId)
if (table) {
if (dataSource?.type === "query") {
schema = {}
const params = table.parameters || []
params.forEach(param => {
schema[param.name] = { ...param, type: "string" }
})
} else {
schema = table.schema || {}
}
}
}
loaded = true
}
// Load the form schema on mount
onMount(fetchSchema)
</script>
<Provider
{actions}
data={{ ...$formState.values, tableId: dataSource?.tableId }}
>
<div
lang="en"
dir="ltr"
use:styleable={$component.styles}
class={`spectrum ${size || "spectrum--medium"} ${
theme || "spectrum--light"
}`}
>
{#if loaded}
<slot />
{/if}
</div>
</Provider>
<style>
div {
padding: 20px;
position: relative;
background-color: var(--spectrum-alias-background-color-secondary);
}
</style>