Add more functionality to core SpectrumField component to simplify other form components

This commit is contained in:
Andrew Kingston 2021-01-28 08:47:44 +00:00
parent b8fb2ba43c
commit 8c90edf462
5 changed files with 102 additions and 92 deletions

View File

@ -22,7 +22,7 @@
$: options = getOptions(schema, type) $: options = getOptions(schema, type)
const getOptions = (schema, fieldType) => { const getOptions = (schema, fieldType) => {
let entries = Object.entries(schema) let entries = Object.entries(schema ?? {})
if (fieldType) { if (fieldType) {
entries = entries.filter(entry => entry[1].type === fieldType) entries = entries.filter(entry => entry[1].type === fieldType)
} }

View File

@ -8,7 +8,7 @@
export let theme export let theme
export let size export let size
const { styleable, API } = getContext("sdk") const { styleable, API, setBindableValue } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
let loaded = false let loaded = false
@ -24,7 +24,7 @@
// Form API contains functions to control the form // Form API contains functions to control the form
const formApi = { const formApi = {
registerField: field => { registerField: (field, componentId) => {
if (!field) { if (!field) {
return return
} }
@ -38,11 +38,8 @@
fieldMap[field] = { fieldMap[field] = {
fieldState: makeFieldState(field), fieldState: makeFieldState(field),
fieldApi: makeFieldApi(field, validate), fieldApi: makeFieldApi(field, componentId, validate),
fieldSchema: schema?.[field] ?? {}, fieldSchema: schema?.[field] ?? {},
fieldId: `${Math.random()
.toString(32)
.substr(2)}/${field}`,
} }
fieldMap = fieldMap fieldMap = fieldMap
return fieldMap[field] return fieldMap[field]
@ -53,9 +50,11 @@
setContext("form", { formApi, formState }) setContext("form", { formApi, formState })
// Creates an API for a specific field // Creates an API for a specific field
const makeFieldApi = (field, validate) => { const makeFieldApi = (field, componentId, validate) => {
return { return {
setValue: value => { setValue: value => {
console.log("setting " + componentId + " to " + value)
setBindableValue(value, componentId)
const { fieldState } = fieldMap[field] const { fieldState } = fieldMap[field]
fieldState.update(state => { fieldState.update(state => {
state.value = value state.value = value
@ -72,6 +71,9 @@
const makeFieldState = field => { const makeFieldState = field => {
return writable({ return writable({
field, field,
fieldId: `${Math.random()
.toString(32)
.substr(2)}/${field}`,
value: null, value: null,
error: null, error: null,
valid: true, valid: true,

View File

@ -2,17 +2,15 @@
import "@spectrum-css/picker/dist/index-vars.css" import "@spectrum-css/picker/dist/index-vars.css"
import "@spectrum-css/popover/dist/index-vars.css" import "@spectrum-css/popover/dist/index-vars.css"
import "@spectrum-css/menu/dist/index-vars.css" import "@spectrum-css/menu/dist/index-vars.css"
import { getContext } from "svelte"
import SpectrumField from "./SpectrumField.svelte" import SpectrumField from "./SpectrumField.svelte"
export let field export let field
export let label export let label
export let placeholder export let placeholder
// Register this field with its form let fieldState
const { formApi } = getContext("form") ?? {} let fieldApi
const formField = formApi?.registerField(field) ?? {} let fieldSchema
const { fieldApi, fieldState, fieldSchema } = formField
// Picker state // Picker state
let open = false let open = false
@ -20,68 +18,52 @@
$: placeholderText = placeholder || "Choose an option" $: placeholderText = placeholder || "Choose an option"
$: isNull = $fieldState?.value == null || $fieldState?.value === "" $: isNull = $fieldState?.value == null || $fieldState?.value === ""
// Update value on blur only
const selectOption = value => { const selectOption = value => {
fieldApi.setValue(value) fieldApi.setValue(value)
open = false open = false
} }
</script> </script>
<SpectrumField {field} {label}> <SpectrumField {field} {label} bind:fieldState bind:fieldApi bind:fieldSchema>
<button {#if fieldState}
id={$fieldState.fieldId} <button
class="spectrum-Picker" id={$fieldState.fieldId}
class:is-invalid={!$fieldState.valid} class="spectrum-Picker"
class:is-open={open} class:is-invalid={!$fieldState.valid}
aria-haspopup="listbox" class:is-open={open}
on:click={() => (open = true)}> aria-haspopup="listbox"
<span class="spectrum-Picker-label" class:is-placeholder={isNull}> on:click={() => (open = true)}>
{isNull ? placeholderText : $fieldState.value} <span class="spectrum-Picker-label" class:is-placeholder={isNull}>
</span> {isNull ? placeholderText : $fieldState.value}
{#if !$fieldState.valid} </span>
<svg {#if !$fieldState.valid}
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Picker-validationIcon"
focusable="false"
aria-hidden="true"
aria-label="Folder">
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<svg
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
focusable="false"
aria-hidden="true">
<use xlink:href="#spectrum-css-icon-Chevron100" />
</svg>
</button>
<div
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover"
class:is-open={open}>
<ul class="spectrum-Menu" role="listbox">
<li
class="spectrum-Menu-item"
class:is-selected={isNull}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => selectOption(null)}>
<span class="spectrum-Menu-itemLabel">{placeholderText}</span>
<svg <svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon" class="spectrum-Icon spectrum-Icon--sizeM spectrum-Picker-validationIcon"
focusable="false" focusable="false"
aria-hidden="true"> aria-hidden="true"
<use xlink:href="#spectrum-css-icon-Checkmark100" /> aria-label="Folder">
<use xlink:href="#spectrum-icon-18-Alert" />
</svg> </svg>
</li> {/if}
{#each options as option} <svg
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
focusable="false"
aria-hidden="true">
<use xlink:href="#spectrum-css-icon-Chevron100" />
</svg>
</button>
<div
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover"
class:is-open={open}>
<ul class="spectrum-Menu" role="listbox">
<li <li
class="spectrum-Menu-item" class="spectrum-Menu-item"
class:is-selected={option === $fieldState.value} class:is-selected={isNull}
role="option" role="option"
aria-selected="true" aria-selected="true"
tabindex="0" tabindex="0"
on:click={() => selectOption(option)}> on:click={() => selectOption(null)}>
<span class="spectrum-Menu-itemLabel">{option}</span> <span class="spectrum-Menu-itemLabel">{placeholderText}</span>
<svg <svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon" class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false" focusable="false"
@ -89,7 +71,24 @@
<use xlink:href="#spectrum-css-icon-Checkmark100" /> <use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg> </svg>
</li> </li>
{/each} {#each options as option}
</ul> <li
</div> class="spectrum-Menu-item"
class:is-selected={option === $fieldState.value}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => selectOption(option)}>
<span class="spectrum-Menu-itemLabel">{option}</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true">
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
{/each}
</ul>
</div>
{/if}
</SpectrumField> </SpectrumField>

View File

@ -5,21 +5,32 @@
export let label export let label
export let field export let field
export let fieldState
export let fieldApi
export let fieldSchema
// Get contexts
const formContext = getContext("form") const formContext = getContext("form")
const fieldGroupContext = getContext("fieldGroup") const fieldGroupContext = getContext("fieldGroup")
const { styleable } = getContext("sdk") const { styleable } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
// Register field with form
const { formApi } = formContext || {} const { formApi } = formContext || {}
const labelPosition = fieldGroupContext?.labelPosition || "above" const labelPosition = fieldGroupContext?.labelPosition || "above"
const formField = formApi?.registerField(field) ?? {} const formField = formApi?.registerField(field, $component.id)
const { fieldId, fieldState } = formField
// Expose field properties to parent component
fieldState = formField?.fieldState
fieldApi = formField?.fieldApi
fieldSchema = formField?.fieldSchema
// Extract label position from field group context
$: labelPositionClass = $: labelPositionClass =
labelPosition === "above" ? "" : `spectrum-FieldLabel--${labelPosition}` labelPosition === "above" ? "" : `spectrum-FieldLabel--${labelPosition}`
</script> </script>
{#if !fieldId} {#if !fieldState}
<Placeholder>Add the Field setting to start using your component</Placeholder> <Placeholder>Add the Field setting to start using your component</Placeholder>
{:else if !formContext} {:else if !formContext}
<Placeholder>Form components need to be wrapped in a Form</Placeholder> <Placeholder>Form components need to be wrapped in a Form</Placeholder>
@ -28,7 +39,7 @@
<div class="spectrum-Form-item" use:styleable={$component.styles}> <div class="spectrum-Form-item" use:styleable={$component.styles}>
{#if label} {#if label}
<label <label
for={fieldId} for={$fieldState.fieldId}
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelPositionClass}`}> class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${labelPositionClass}`}>
{label} {label}
</label> </label>

View File

@ -1,6 +1,5 @@
<script> <script>
import "@spectrum-css/textfield/dist/index-vars.css" import "@spectrum-css/textfield/dist/index-vars.css"
import { getContext } from "svelte"
import SpectrumField from "./SpectrumField.svelte" import SpectrumField from "./SpectrumField.svelte"
export let field export let field
@ -8,33 +7,32 @@
export let placeholder export let placeholder
export let type = "text" export let type = "text"
// Register this field with its form let fieldState
const { formApi } = getContext("form") ?? {} let fieldApi
const formField = formApi?.registerField(field) ?? {}
const { fieldApi, fieldState } = formField
// Update value on blur only
const onBlur = event => { const onBlur = event => {
fieldApi.setValue(event.target.value) fieldApi.setValue(event.target.value)
} }
</script> </script>
<SpectrumField {label} {field}> <SpectrumField {label} {field} bind:fieldState bind:fieldApi>
<div class="spectrum-Textfield" class:is-invalid={!$fieldState.valid}> {#if fieldState}
{#if !$fieldState.valid} <div class="spectrum-Textfield" class:is-invalid={!$fieldState.valid}>
<svg {#if !$fieldState.valid}
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon" <svg
focusable="false" class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
aria-hidden="true"> focusable="false"
<use xlink:href="#spectrum-icon-18-Alert" /> aria-hidden="true">
</svg> <use xlink:href="#spectrum-icon-18-Alert" />
{/if} </svg>
<input {/if}
id={$fieldState.fieldId} <input
value={$fieldState.value || ''} id={$fieldState.fieldId}
placeholder={placeholder || ''} value={$fieldState.value || ''}
on:blur={onBlur} placeholder={placeholder || ''}
{type} on:blur={onBlur}
class="spectrum-Textfield-input" /> {type}
</div> class="spectrum-Textfield-input" />
</div>
{/if}
</SpectrumField> </SpectrumField>