2021-01-26 09:55:44 +01:00
|
|
|
<script>
|
|
|
|
import { setContext, getContext, onMount } from "svelte"
|
|
|
|
import { writable, get } from "svelte/store"
|
2021-01-26 15:40:44 +01:00
|
|
|
import { createValidatorFromConstraints } from "./validation"
|
2021-02-01 15:10:38 +01:00
|
|
|
import { generateID } from "../helpers"
|
2021-01-26 09:55:44 +01:00
|
|
|
|
2021-03-19 14:09:22 +01:00
|
|
|
export let dataSource
|
2021-01-26 09:55:44 +01:00
|
|
|
export let theme
|
|
|
|
export let size
|
2021-02-17 16:16:44 +01:00
|
|
|
export let disabled = false
|
2021-01-26 09:55:44 +01:00
|
|
|
|
|
|
|
const component = getContext("component")
|
2021-02-01 19:51:22 +01:00
|
|
|
const context = getContext("context")
|
|
|
|
const { styleable, API, Provider, ActionTypes } = getContext("sdk")
|
2021-01-26 09:55:44 +01:00
|
|
|
|
|
|
|
let loaded = false
|
2021-01-26 15:40:44 +01:00
|
|
|
let schema
|
|
|
|
let table
|
2021-01-26 09:55:44 +01:00
|
|
|
let fieldMap = {}
|
|
|
|
|
2021-02-16 16:30:20 +01:00
|
|
|
// Returns the closes data context which isn't a built in context
|
2021-05-04 12:32:22 +02:00
|
|
|
const getInitialValues = context => {
|
2021-02-16 16:30:20 +01:00
|
|
|
if (["user", "url"].includes(context.closestComponentId)) {
|
|
|
|
return {}
|
|
|
|
}
|
|
|
|
return context[`${context.closestComponentId}`] || {}
|
|
|
|
}
|
|
|
|
|
2021-02-16 14:27:38 +01:00
|
|
|
// Use the closest data context as the initial form values
|
2021-02-16 16:30:20 +01:00
|
|
|
const initialValues = getInitialValues($context)
|
2021-01-29 17:40:19 +01:00
|
|
|
|
2021-01-26 09:55:44 +01:00
|
|
|
// Form state contains observable data about the form
|
2021-01-29 17:40:19 +01:00
|
|
|
const formState = writable({ values: initialValues, errors: {}, valid: true })
|
2021-01-26 09:55:44 +01:00
|
|
|
|
|
|
|
// Form API contains functions to control the form
|
|
|
|
const formApi = {
|
2021-02-17 16:16:44 +01:00
|
|
|
registerField: (field, defaultValue = null, fieldDisabled = false) => {
|
2021-01-26 09:55:44 +01:00
|
|
|
if (!field) {
|
|
|
|
return
|
|
|
|
}
|
2021-02-17 16:16:44 +01:00
|
|
|
|
|
|
|
// Auto columns are always disabled
|
|
|
|
const isAutoColumn = !!schema?.[field]?.autocolumn
|
|
|
|
|
2021-01-26 15:40:44 +01:00
|
|
|
// Create validation function based on field schema
|
|
|
|
const constraints = schema?.[field]?.constraints
|
|
|
|
const validate = createValidatorFromConstraints(constraints, field, table)
|
|
|
|
|
2021-01-26 09:55:44 +01:00
|
|
|
fieldMap[field] = {
|
2021-02-17 16:16:44 +01:00
|
|
|
fieldState: makeFieldState(
|
|
|
|
field,
|
|
|
|
defaultValue,
|
|
|
|
disabled || fieldDisabled || isAutoColumn
|
|
|
|
),
|
2021-02-01 12:14:24 +01:00
|
|
|
fieldApi: makeFieldApi(field, defaultValue, validate),
|
2021-01-27 11:59:05 +01:00
|
|
|
fieldSchema: schema?.[field] ?? {},
|
2021-01-26 09:55:44 +01:00
|
|
|
}
|
2021-06-09 13:29:27 +02:00
|
|
|
|
|
|
|
// Set initial value
|
|
|
|
fieldMap[field].fieldApi.setValue(defaultValue, true)
|
|
|
|
|
2021-01-26 09:55:44 +01:00
|
|
|
return fieldMap[field]
|
|
|
|
},
|
2021-02-01 19:51:22 +01:00
|
|
|
validate: () => {
|
|
|
|
const fields = Object.keys(fieldMap)
|
2021-05-04 12:32:22 +02:00
|
|
|
fields.forEach(field => {
|
2021-02-01 19:51:22 +01:00
|
|
|
const { fieldApi } = fieldMap[field]
|
|
|
|
fieldApi.validate()
|
|
|
|
})
|
|
|
|
return get(formState).valid
|
|
|
|
},
|
2021-01-26 09:55:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Provide both form API and state to children
|
2021-06-08 13:50:58 +02:00
|
|
|
setContext("form", { formApi, formState, dataSource })
|
2021-01-26 09:55:44 +01:00
|
|
|
|
2021-02-01 19:51:22 +01:00
|
|
|
// Action context to pass to children
|
|
|
|
$: actions = [{ type: ActionTypes.ValidateForm, callback: formApi.validate }]
|
|
|
|
|
2021-01-26 09:55:44 +01:00
|
|
|
// Creates an API for a specific field
|
2021-02-01 12:14:24 +01:00
|
|
|
const makeFieldApi = (field, defaultValue, validate) => {
|
2021-02-01 19:51:22 +01:00
|
|
|
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
|
2021-05-04 12:32:22 +02:00
|
|
|
fieldState.update(state => {
|
2021-02-01 19:51:22 +01:00
|
|
|
state.value = newValue
|
|
|
|
state.error = newError
|
|
|
|
return state
|
|
|
|
})
|
|
|
|
|
|
|
|
// Update form state
|
2021-05-04 12:32:22 +02:00
|
|
|
formState.update(state => {
|
2021-02-01 19:51:22 +01:00
|
|
|
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
|
|
|
|
})
|
|
|
|
|
2021-04-15 12:51:18 +02:00
|
|
|
return !newError
|
2021-02-01 19:51:22 +01:00
|
|
|
}
|
2021-01-26 09:55:44 +01:00
|
|
|
return {
|
2021-02-01 19:51:22 +01:00
|
|
|
setValue,
|
|
|
|
validate: () => {
|
2021-01-26 09:55:44 +01:00
|
|
|
const { fieldState } = fieldMap[field]
|
2021-02-01 19:51:22 +01:00
|
|
|
setValue(get(fieldState).value, true)
|
2021-01-26 09:55:44 +01:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Creates observable state data about a specific field
|
2021-02-17 16:16:44 +01:00
|
|
|
const makeFieldState = (field, defaultValue, fieldDisabled) => {
|
2021-01-26 09:55:44 +01:00
|
|
|
return writable({
|
|
|
|
field,
|
2021-02-03 11:32:21 +01:00
|
|
|
fieldId: `id-${generateID()}`,
|
2021-02-01 12:14:24 +01:00
|
|
|
value: initialValues[field] ?? defaultValue,
|
2021-01-26 09:55:44 +01:00
|
|
|
error: null,
|
2021-02-17 16:16:44 +01:00
|
|
|
disabled: fieldDisabled,
|
2021-01-26 09:55:44 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-03-19 14:09:22 +01:00
|
|
|
// Fetches the form schema from this form's dataSource, if one exists
|
2021-01-26 09:55:44 +01:00
|
|
|
const fetchSchema = async () => {
|
2021-03-19 14:09:22 +01:00
|
|
|
if (!dataSource?.tableId) {
|
2021-01-26 09:55:44 +01:00
|
|
|
schema = {}
|
2021-01-26 15:40:44 +01:00
|
|
|
table = null
|
2021-01-26 09:55:44 +01:00
|
|
|
} else {
|
2021-03-19 14:09:22 +01:00
|
|
|
table = await API.fetchTableDefinition(dataSource?.tableId)
|
2021-01-26 09:55:44 +01:00
|
|
|
if (table) {
|
2021-03-19 14:09:22 +01:00
|
|
|
if (dataSource?.type === "query") {
|
2021-02-04 14:01:49 +01:00
|
|
|
schema = {}
|
|
|
|
const params = table.parameters || []
|
2021-05-04 12:32:22 +02:00
|
|
|
params.forEach(param => {
|
2021-02-04 14:01:49 +01:00
|
|
|
schema[param.name] = { ...param, type: "string" }
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
schema = table.schema || {}
|
|
|
|
}
|
2021-01-26 09:55:44 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
loaded = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Load the form schema on mount
|
|
|
|
onMount(fetchSchema)
|
|
|
|
</script>
|
|
|
|
|
2021-02-01 19:51:22 +01:00
|
|
|
<Provider
|
|
|
|
{actions}
|
2021-05-04 12:04:42 +02:00
|
|
|
data={{ ...$formState.values, tableId: dataSource?.tableId }}
|
|
|
|
>
|
2021-01-29 14:22:38 +01:00
|
|
|
<div
|
|
|
|
lang="en"
|
|
|
|
dir="ltr"
|
|
|
|
use:styleable={$component.styles}
|
2021-05-04 12:04:42 +02:00
|
|
|
class={`spectrum ${size || "spectrum--medium"} ${
|
|
|
|
theme || "spectrum--light"
|
|
|
|
}`}
|
|
|
|
>
|
2021-01-29 14:22:38 +01:00
|
|
|
{#if loaded}
|
|
|
|
<slot />
|
|
|
|
{/if}
|
|
|
|
</div>
|
2021-02-01 19:51:22 +01:00
|
|
|
</Provider>
|
2021-01-29 17:40:19 +01:00
|
|
|
|
|
|
|
<style>
|
|
|
|
div {
|
|
|
|
padding: 20px;
|
2021-02-10 20:23:53 +01:00
|
|
|
position: relative;
|
2021-05-12 15:52:43 +02:00
|
|
|
background-color: var(--spectrum-alias-background-color-secondary);
|
2021-01-29 17:40:19 +01:00
|
|
|
}
|
|
|
|
</style>
|