Svelte-Specma
Svelte-Specma is a Svelte store used to do client-side validation using the very powerful Specma library.
- Collection specs defined with the exact same shape as the value!
- Based on predicate functions of type
true || "reason"
-
Composable specs with intuitive
and
/or
operators - Easy cross-validation between multiple fields
- User defined customized invalid reasons
- Async validation out of the box
- Very small footprint
Design considerations
Spec design
You should first get familiar with the Specma way of defining a spec. All valid Specma specs will work fine with Svelte-Specma, including async validation, spec composition and cross-fields validation with getFrom
function as second predicate argument. Specs can hence be reused in a variety of contexts without redefining them all the time.
Simple composition with possibily nested and
and or
allows adding advanced constraints on a base spec, such as calling a remote endpoint asynchronously to check a value availability.
Requiredness
By design, as in Specma, the requiredness of certain fields is not defined in the spec itself. It is rather is defined at store creation time by passing in a required
object of the same shape as the value. This allows reusing the same spec in different forms, where only the required fields change, not the predicates themselves.
Values that are undefined
won't be validated against their spec unless they are specified as required. Also, because Svelte-Specma is mainly used in forms, null
and ""
are considered as missing when a value is required.
Svelte integration
Svelte-Specma uses Svelte stores to manage state and validation. It is not tied to any particular component, although it might make sense for you to eventually design components to reduce boilerplate code (updating store value, activating on input blur, displaying loading or error messages, etc.).
configure
- Before using!
Since Specma relies on some Javascript Symbols to define specs, Svelte-Specma must use the same instance of the Spec library. It must hence be configured once prior to usage.
import * as specma from "specma";
import { configure } from "svelte-specma";
configure(specma);
specable
Main entry point and preferred way for defining a specable store. Will dispatch to collSpecable
or predSpecable
based on its arguments (based on spec's shape first, then initial value's shape) to eventually produce a deeply nested store.
Inputs
const store = specable(initialValue, { spec, ...rest });
-
initialValue
: Any. The initial value to validate against the spec. -
spec
: Any. The Specma spec to validate against. -
...rest
: Object. SeecollSpecable
andpredSpecable
details below.
Output
Either a collSpecable
or predSpecable
store (see below).
predSpecable
A Svelte store used to validate a value against the predicate function part of a Specma spec.
/* Will create a `predSpecable` store, since initial value is not a collection */
const store = specable(
30, // Initial value
{
spec: (v) => v === 42 || "is not the answer",
required: false,
id: "someId",
}
);
/* Static properties and methods */
const { id, isRequired, spec, activate, reset, set, subscribe } = store;
/* Subscription state values */
$: ({ value, active, changed, valid, validating, error, promise, id } = $store);
Inputs
-
initialValue
: Any. The initial value to validate, saved in internal state -
spec
: Any. The Specma spec. Only the predicate function part of the spec is used in apredSpecable
, so a compound value (object, array, etc.) won't validate its children spec. To do so, use acollSpecable
instead. -
required
: Boolean. If a value is required, must be different thanundefined
,null
or""
; will return the SpecmaisRequired
message otherwise. False by default. -
changePred
: Function.(a, b) => boolean
. Function to evaluate if value is different from initial value. By default, will do a deep object value equality and compare dates by value. -
id
: Any. Used to uniquely identify the store. Mainly useful for values part of a list.
Outputs
-
id
: Any. A pass-through of the store's creationid
. -
isRequired
: Boolean. Is the store value required? Based on therequired
creation argument. -
spec
: The predicate function used to derive a validation result. -
activate
: Async function.(Boolean = true) => Promise
. Method to de/activate the store validation. If set to true, will immediately trigger validation and return a promise that resolves to thevalid
result property. -
reset
: Function.(Any = initialValue) => undefined
. Reset the store to a new initial value (or the initial one if no argument) and deactivate the store. -
set
: Function.(Any, shouldActivate = false) => undefined
. Set the store's internal value to a new one. Will trigger validation if store is active. IfshouldActivate = true
, activates the store after setting the value. -
submit
: Function.(value) => {}
. Method to first activate then handle the current value.submitting
state is set totrue
during submit. -
subscribe
: Function.(fn) => unsubscribe
. The Svelte store's subscribe method. Usually used in Svelte components with a reactive autosubscription declaration ($: storeState = $store
). The function argument will be called with the updated internal state (see below) each time it changes. Returns an unsubscribe function that can be called when component unmounts to prevent memory leaks.-
value
: Any. The internal state value, which can be modified with the store's methods. -
active
: Boolean. Is the store currently active? Default tofalse
. -
changed
: Boolean. Is the value different (value based deep-equality) from the initial value? -
valid
: Boolean. Is the value valid when checked against spec? Alwaystrue
when store is not active.false
if the store is validating asynchronously until a firm result is available. -
validating
: Boolean. Is the store currently validating asynchronously? -
submitting
: Boolean. Is the store currently submitting asynchronously? -
error
: Any. Description of the error. Usually a string, but any value different thantrue
returned from a predicate function validation. -
promise
: A promise of the validation result that resolves when asynchronous validation is completed. Property is always set even if result is synchronously available to allow waiting for resolution in any case. -
id
: Any. A pass-through of the store's creationid
.
-
collSpecable
A Svelte store used to validate a collection value (array, object, Map) against a Specma spec, including its children.
The initial value is only used to generate all initial children specable stores, but the store's value is then derived from the values of those children stores. The set
method actually sets the values of the underlying children stores.
A collSpecable
offers methods to modify its children specable stores : add
, remove
, update
.
Spec definition
import { and, spread } from "specma";
/* The spec could be shared between client and server */
export const baseSpec = {
name: (v = "") => v.length > 5 || "must be longer than 5 characters",
list: spread({
x: and(
(v) => typeof v === "number" || "must be a number",
(v) => v < 100 || "must be less than 100"
),
y: (v) => ["foo", "bar"].includes(v) || "is not an acceptable choice",
}),
};
Usage with Svelte-Specma
import { and, spread } from "specma";
import { baseSpec } from "/some/shared/file";
/* Client could enhance the base spec to add further validation
* (async in this case) */
const enhancedSpec = and(baseSpec, {
name: async (v = "") => await checkNameIsUnique(v),
});
/* Will create a `collSpecable` store, since initial value is a collection */
const store = specable(
{ name: "Foo", list: [{ id: "1234", x: 20, y: "abc", z: null }] },
{
spec: enhancedSpec,
required: { name: 1, list: spread({ x: 1 }) },
fields: { name: 1, list: spread({ x: 1, y: 1, z: 1 }) },
getId: { list: ({ id }) => id },
id: "myColl",
}
);
/* Store static properties and methods */
const {
id,
isRequired,
spec,
activate,
add,
getChild,
remove,
reset,
set,
update,
children,
stores,
subscribe,
} = store;
/* Store subscription internal state values */
$: ({
value,
active,
changed,
valid,
validating,
error,
errors,
collErrors,
promise,
id,
} = $store);
Inputs
-
initialValue
: Collection. The initial value that will generate all initial specable stores. -
spec
: Collection. The Specma spec. -
required
: Collection. A deeply nested collection description of the required fields. Requiredness is defined at the root store to keep specs more reusable. -
fields
: Collection. A deeply nested collection description of which fields to expect, that will ensure that those fields are defined as children specable stores. Undeclared field values will be treated as constant and won't be displayed as children stores. -
getId
: Collection. A deeply nested collection description of how a subcollection should define theid
of its children. Can be defined the same way as a spec (withand
,spread
, etc.), where the predicate function of a level has the shape(value, key) => id
. If no function is provided, array items' store will be assigned a unique random id, while objects will use their key as id. -
changePred
: Collection. A deeply nested collection description of change predicates (seepredCheckable
inputs). Can be defined the same way asgetId
. -
id
: Used to uniquely identify the store. Mainly useful for values part of a list.
Outputs
-
id
: Any. A pass-through of the store's creationid
. -
isRequired
: Boolean. Is the store value required? Based on therequired
creation argument. -
spec
: Any. A pass-through of the store's creation spec. -
activate
: Async function.(Boolean = true) => Promise
. Method to de/activate the store validation, including all its children stores. If set to true, will immediately trigger validation and return a promise that resolves to thevalid
result property. -
add
: Function.(coll) => store
. Method to add new children specable stores. Argument should be declared as a collection of the same type as the store's value. Returns the store for chaining. -
getChild
: Function.(path = []) => store
. Method to retrieve a deeply nested child store from a path of keys (not ids) such as["students", 0, "name"]
. Returnsnull
if no store found. -
getChildren
: Function.() => storesCollection
. Method to retrieve the current collection of children stores without having to subscribe to thechildren
substore. Unlike thestores
property, the return value of this method will be current each time it is called. -
remove
: Function.(idsToRemove) => store
. Method to remove children specable stores. Will remove stores by their id. Returns the store for chaining. -
set
: Function.(coll, partial = false, shouldActivate = false) => store
. Method to recursively set the values of the collection's underlying stores. Ifpartial = true
, sets only the values that are notundefined
. IfshouldActivate = true
, activates the store after setting the value. Returns the store for chaining. -
update
: Function.(fn) => store
. Method to modify the children specable stores collection by applying a function(storesCollection) => storesCollection
to it that returns a modified children stores collection. It operates on the collection of children stores, NOT the underlying value. To update the value itself, useset
with a modified$store.value
. Useful for instance to reorder the children based on their id. Returns the store for chaining. -
children
: Svelte readable store. A store that holds a reactive collection of the children specable stores that compose the collection. Subscribe to it to watch changes to a list of children, where some could be added, removed, reordered, etc. -
stores
: Collection. Same aschildren
, but non-reactive. Useful to destructure children stores that won't change over time, such as the fixed fields in a flat form, without having to subscribe first. -
submit
: Function.(value) => {}
. Method to first activate then handle the current value.submitting
state is set totrue
during submit. -
subscribe
: Function. Same aspredSpecable
, but with added state properties, listed below.-
error
: Any. The collection's own predicate spec error result. -
errors
: Array. Array of[{ error, path, which, isColl }]
containing all error descriptions. Useful to display all errors in a centralized location on a form, for instance.-
error
: Any. Description of the error. -
path
: Array. List of the complete path from root to error node. -
which
: String. Same aspath
, but joined with dots to form a string. Useful to lookup predefined captions in a dictionnary. -
isColl
: Boolean. Indicates if the error is on a collection value.
-
-
collErrors
: Array. Same aserrors
, but containing only the errors with theisColl
flag. Useful to display collection errors in a central location, while primitive field errors are displayed near their input in a form, for instance.
-
register
The register
function facilitates usage of the predSpecable
store in conjunction to a form input. It is designed to be used with the use:register
directive:
<input use:register="{predSpecableStore}" />
Doing so will:
- update the store's value on input;
- update the input on store value change;
- activate the store on input blur;
- remove all listeners and subscriptions when input is unmounted.
If the expected value's type is not the same as the HTML input's one (if validation expects an number or a JS Date instance, for example), use:register
can be used with a list of arguments where the second one is an object with keys { toInput, toValue }
:
-
toInput
: Function.(value) => htmlInputValue
; -
toValue
: Function.(htmlInputValue) => value
;
For example, if the expected value is a number, it could be used like so:
<input
use:register="{[predSpecableStore, { toValue: (x) => x && +x }]}"
/>
In any other case, the store should be used explicitely:
<input
value={$age.value}
on:input={(e) => age.set(+e.target.value)}
on:blur={() => age.activate()}
/>