mirror of https://github.com/Budibase/budibase.git
2 changed files with 200 additions and 172 deletions
@ -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