Fully remount update forms when initial values change
This commit is contained in:
parent
20e568baea
commit
f4cb901f33
|
@ -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}
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue