2021-06-10 12:17:14 +02:00
|
|
|
<script>
|
2021-08-19 13:53:31 +02:00
|
|
|
import { setContext, getContext } from "svelte"
|
|
|
|
import { derived, get, writable } from "svelte/store"
|
2021-06-10 12:17:14 +02:00
|
|
|
import { createValidatorFromConstraints } from "./validation"
|
|
|
|
import { generateID } from "../helpers"
|
|
|
|
|
|
|
|
export let dataSource
|
|
|
|
export let disabled = false
|
|
|
|
export let initialValues
|
2021-08-19 13:53:31 +02:00
|
|
|
export let schema
|
|
|
|
export let table
|
2021-06-10 12:17:14 +02:00
|
|
|
|
|
|
|
const component = getContext("component")
|
2021-08-19 13:53:31 +02:00
|
|
|
const { styleable, Provider, ActionTypes } = getContext("sdk")
|
2021-06-10 12:17:14 +02:00
|
|
|
|
2021-08-19 13:53:31 +02:00
|
|
|
let fields = []
|
|
|
|
const currentStep = writable(1)
|
2021-08-18 15:58:58 +02:00
|
|
|
const formState = writable({
|
2021-08-19 13:53:31 +02:00
|
|
|
values: {},
|
2021-08-18 15:58:58 +02:00
|
|
|
errors: {},
|
|
|
|
valid: true,
|
2021-08-19 13:53:31 +02:00
|
|
|
currentStep: 1,
|
2021-08-18 15:58:58 +02:00
|
|
|
})
|
2021-06-10 12:17:14 +02:00
|
|
|
|
2021-08-19 13:53:31 +02:00
|
|
|
// Reactive derived stores to derive form state from field array
|
|
|
|
$: values = deriveFieldProperty(fields, f => f.fieldState.value)
|
|
|
|
$: errors = deriveFieldProperty(fields, f => f.fieldState.error)
|
|
|
|
$: valid = !Object.values($errors).some(error => error != null)
|
|
|
|
|
|
|
|
// Derive which fields belong in which steps
|
|
|
|
$: currentStepValid = derived(
|
|
|
|
[currentStep, ...fields],
|
|
|
|
([currentStepValue, ...fieldsValue]) => {
|
|
|
|
return !fieldsValue
|
|
|
|
.filter(f => f.step === currentStepValue)
|
|
|
|
.some(f => f.fieldState.error != null)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
// Update form state store from derived stores
|
|
|
|
$: {
|
|
|
|
formState.set({
|
|
|
|
values: $values,
|
|
|
|
errors: $errors,
|
|
|
|
valid,
|
|
|
|
currentStep: $currentStep,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generates a derived store from an array of fields, comprised of a map of
|
|
|
|
// extracted values from the field array
|
|
|
|
const deriveFieldProperty = (fieldStores, getProp) => {
|
|
|
|
return derived(fieldStores, fieldValues => {
|
|
|
|
const reducer = (map, field) => ({ ...map, [field.name]: getProp(field) })
|
|
|
|
return fieldValues.reduce(reducer, {})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Searches the field array for a certain field
|
|
|
|
const getField = name => {
|
|
|
|
return fields.find(field => get(field).name === name)
|
|
|
|
}
|
|
|
|
|
2021-06-10 12:17:14 +02:00
|
|
|
const formApi = {
|
2021-08-10 15:36:47 +02:00
|
|
|
registerField: (
|
|
|
|
field,
|
|
|
|
defaultValue = null,
|
|
|
|
fieldDisabled = false,
|
2021-08-19 13:53:31 +02:00
|
|
|
validationRules,
|
|
|
|
step = 1
|
2021-08-10 15:36:47 +02:00
|
|
|
) => {
|
2021-06-10 12:17:14 +02:00
|
|
|
if (!field) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-08-19 13:53:31 +02:00
|
|
|
// Skip if we've already registered this field
|
|
|
|
const existingField = getField(field)
|
|
|
|
if (existingField) {
|
|
|
|
return existingField
|
|
|
|
}
|
|
|
|
|
2021-06-10 12:17:14 +02:00
|
|
|
// Auto columns are always disabled
|
|
|
|
const isAutoColumn = !!schema?.[field]?.autocolumn
|
|
|
|
|
|
|
|
// Create validation function based on field schema
|
2021-08-10 15:36:47 +02:00
|
|
|
const schemaConstraints = schema?.[field]?.constraints
|
2021-08-11 16:07:06 +02:00
|
|
|
const validator = createValidatorFromConstraints(
|
2021-08-10 15:36:47 +02:00
|
|
|
schemaConstraints,
|
|
|
|
validationRules,
|
|
|
|
field,
|
|
|
|
table
|
|
|
|
)
|
2021-06-10 12:17:14 +02:00
|
|
|
|
2021-08-19 13:53:31 +02:00
|
|
|
// Construct field info
|
|
|
|
const fieldInfo = writable({
|
|
|
|
name: field,
|
|
|
|
step: step || 1,
|
|
|
|
fieldState: {
|
|
|
|
fieldId: `id-${generateID()}`,
|
|
|
|
value: initialValues[field] ?? defaultValue,
|
|
|
|
error: null,
|
|
|
|
disabled: disabled || fieldDisabled || isAutoColumn,
|
2021-06-10 12:17:14 +02:00
|
|
|
defaultValue,
|
2021-08-19 13:53:31 +02:00
|
|
|
validator,
|
|
|
|
},
|
2021-08-11 16:07:06 +02:00
|
|
|
fieldApi: makeFieldApi(field, defaultValue),
|
2021-06-10 12:17:14 +02:00
|
|
|
fieldSchema: schema?.[field] ?? {},
|
2021-08-19 13:53:31 +02:00
|
|
|
})
|
2021-06-10 12:17:14 +02:00
|
|
|
|
2021-08-19 13:53:31 +02:00
|
|
|
// Add this field
|
|
|
|
fields = [...fields, fieldInfo]
|
2021-06-10 12:17:14 +02:00
|
|
|
|
2021-08-19 13:53:31 +02:00
|
|
|
return fieldInfo
|
2021-06-10 12:17:14 +02:00
|
|
|
},
|
2021-08-19 13:53:31 +02:00
|
|
|
validate: (onlyCurrentStep = false) => {
|
|
|
|
// Validate only the current step if required
|
|
|
|
if (onlyCurrentStep) {
|
|
|
|
const stepFields = fields.filter(f => get(f).step === get(currentStep))
|
|
|
|
for (let field of stepFields) {
|
|
|
|
if (!get(field).fieldApi.validate()) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise validate all fields
|
|
|
|
for (let field of fields) {
|
|
|
|
if (!get(field).fieldApi.validate()) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
2021-06-10 12:17:14 +02:00
|
|
|
},
|
2021-07-26 13:58:18 +02:00
|
|
|
clear: () => {
|
2021-08-19 13:53:31 +02:00
|
|
|
// Clear the form by clearing each individual field
|
2021-07-26 13:58:18 +02:00
|
|
|
fields.forEach(field => {
|
2021-08-19 13:53:31 +02:00
|
|
|
get(field).fieldApi.clearValue()
|
2021-07-26 13:58:18 +02:00
|
|
|
})
|
2021-07-26 15:22:14 +02:00
|
|
|
},
|
2021-08-18 15:58:58 +02:00
|
|
|
nextStep: () => {
|
2021-08-19 13:53:31 +02:00
|
|
|
currentStep.update(step => step + 1)
|
2021-08-18 15:58:58 +02:00
|
|
|
},
|
|
|
|
prevStep: () => {
|
2021-08-19 13:53:31 +02:00
|
|
|
currentStep.update(step => Math.max(1, step - 1))
|
2021-08-18 15:58:58 +02:00
|
|
|
},
|
2021-06-10 12:17:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Creates an API for a specific field
|
2021-08-11 16:07:06 +02:00
|
|
|
const makeFieldApi = field => {
|
|
|
|
// Sets the value for a certain field and invokes validation
|
2021-06-10 12:17:14 +02:00
|
|
|
const setValue = (value, skipCheck = false) => {
|
2021-08-19 13:53:31 +02:00
|
|
|
const fieldInfo = getField(field)
|
|
|
|
const { fieldState } = get(fieldInfo)
|
|
|
|
const { validator } = fieldState
|
2021-06-10 12:17:14 +02:00
|
|
|
|
|
|
|
// Skip if the value is the same
|
2021-08-19 13:53:31 +02:00
|
|
|
if (!skipCheck && fieldState.value === value) {
|
2021-06-10 12:17:14 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update field state
|
2021-08-11 16:51:45 +02:00
|
|
|
const error = validator ? validator(value) : null
|
2021-08-19 13:53:31 +02:00
|
|
|
fieldInfo.update(state => {
|
|
|
|
state.fieldState.value = value
|
|
|
|
state.fieldState.error = error
|
2021-06-10 12:17:14 +02:00
|
|
|
return state
|
|
|
|
})
|
|
|
|
|
2021-08-04 15:32:46 +02:00
|
|
|
return !error
|
2021-06-10 12:17:14 +02:00
|
|
|
}
|
2021-07-26 13:58:18 +02:00
|
|
|
|
2021-08-11 16:07:06 +02:00
|
|
|
// Clears the value of a certain field back to the initial value
|
2021-07-26 13:58:18 +02:00
|
|
|
const clearValue = () => {
|
2021-08-19 13:53:31 +02:00
|
|
|
const fieldInfo = getField(field)
|
|
|
|
const { fieldState } = get(fieldInfo)
|
|
|
|
const newValue = initialValues[field] ?? fieldState.defaultValue
|
2021-08-11 16:07:06 +02:00
|
|
|
|
|
|
|
// Update field state
|
2021-08-19 13:53:31 +02:00
|
|
|
fieldInfo.update(state => {
|
|
|
|
state.fieldState.value = newValue
|
|
|
|
state.fieldState.error = null
|
2021-07-26 13:58:18 +02:00
|
|
|
return state
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-08-11 16:07:06 +02:00
|
|
|
// Updates the validator rules for a certain field
|
|
|
|
const updateValidation = validationRules => {
|
2021-08-19 13:53:31 +02:00
|
|
|
const fieldInfo = getField(field)
|
|
|
|
const { fieldState } = get(fieldInfo)
|
|
|
|
const { value, error } = fieldState
|
2021-08-11 16:07:06 +02:00
|
|
|
|
|
|
|
// Create new validator
|
|
|
|
const schemaConstraints = schema?.[field]?.constraints
|
|
|
|
const validator = createValidatorFromConstraints(
|
|
|
|
schemaConstraints,
|
|
|
|
validationRules,
|
|
|
|
field,
|
|
|
|
table
|
|
|
|
)
|
|
|
|
|
|
|
|
// Update validator
|
2021-08-19 13:53:31 +02:00
|
|
|
fieldInfo.update(state => {
|
|
|
|
state.fieldState.validator = validator
|
2021-08-11 16:07:06 +02:00
|
|
|
return state
|
|
|
|
})
|
|
|
|
|
|
|
|
// If there is currently an error, run the validator again in case
|
|
|
|
// the error should be cleared by the new validation rules
|
|
|
|
if (error) {
|
|
|
|
setValue(value, true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-10 12:17:14 +02:00
|
|
|
return {
|
|
|
|
setValue,
|
2021-07-26 13:58:18 +02:00
|
|
|
clearValue,
|
2021-08-11 16:07:06 +02:00
|
|
|
updateValidation,
|
2021-06-10 12:17:14 +02:00
|
|
|
validate: () => {
|
2021-08-19 13:53:31 +02:00
|
|
|
// Validate the field by force setting the same value again
|
|
|
|
const { fieldState } = get(getField(field))
|
|
|
|
return setValue(fieldState.value, true)
|
2021-06-10 12:17:14 +02:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-19 13:53:31 +02:00
|
|
|
// Provide form state and api for full control by children
|
|
|
|
setContext("form", {
|
|
|
|
formState,
|
|
|
|
formApi,
|
2021-06-10 12:17:14 +02:00
|
|
|
|
2021-08-19 13:53:31 +02:00
|
|
|
// Data source is needed by attachment fields to be able to upload files
|
|
|
|
// to the correct table ID
|
|
|
|
dataSource,
|
|
|
|
})
|
|
|
|
|
|
|
|
// Provide form step context so that forms without any step components
|
|
|
|
// register their fields to step 1
|
|
|
|
setContext("form-step", 1)
|
2021-06-10 12:17:14 +02:00
|
|
|
|
2021-08-19 13:53:31 +02:00
|
|
|
// Action context to pass to children
|
|
|
|
const actions = [
|
|
|
|
{ type: ActionTypes.ValidateForm, callback: formApi.validate },
|
|
|
|
{ type: ActionTypes.ClearForm, callback: formApi.clear },
|
|
|
|
{ type: ActionTypes.NextFormStep, callback: formApi.nextStep },
|
|
|
|
{ type: ActionTypes.PrevFormStep, callback: formApi.prevStep },
|
|
|
|
]
|
2021-06-10 12:17:14 +02:00
|
|
|
</script>
|
|
|
|
|
|
|
|
<Provider
|
|
|
|
{actions}
|
2021-08-18 15:58:58 +02:00
|
|
|
data={{
|
2021-08-19 13:53:31 +02:00
|
|
|
...$values,
|
|
|
|
valid,
|
|
|
|
currentStep: $currentStep,
|
|
|
|
currentStepValid: $currentStepValid,
|
2021-08-18 15:58:58 +02:00
|
|
|
}}
|
2021-06-10 12:17:14 +02:00
|
|
|
>
|
2021-06-28 13:55:11 +02:00
|
|
|
<div use:styleable={$component.styles}>
|
2021-08-19 13:53:31 +02:00
|
|
|
<slot />
|
2021-06-10 12:17:14 +02:00
|
|
|
</div>
|
|
|
|
</Provider>
|