├── .editorconfig
├── .eslintrc.json
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .vscode
└── extensions.json
├── CHANGELOG.md
├── CONTRIBUTION.md
├── README.md
├── apps
└── formula-app
│ ├── .eslintrc.json
│ ├── jest.config.js
│ ├── package.json
│ ├── public
│ ├── favicon.png
│ ├── global.css
│ └── index.html
│ ├── rollup.config.js
│ ├── src
│ ├── App.svelte
│ ├── main.ts
│ └── pages
│ │ ├── App2.svelte
│ │ ├── Home.svelte
│ │ ├── SignupForm.svelte
│ │ ├── Table.svelte
│ │ └── WeePage.svelte
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ └── tsconfig.spec.json
├── assets
└── logo.png
├── babel.config.json
├── docs
├── 1.23afa72b.js
├── 17896441.f9a33f88.js
├── 2.215e07fe.js
├── 2.215e07fe.js.LICENSE.txt
├── 27.e7fa62ba.js
├── 28.1be0f6ce.js
├── 40370821.3f878b17.js
├── 404.html
├── 41b8b04a.1fa08bb7.js
├── 5b8e8dbb.7997005f.js
├── 71eaf432.28e1d60c.js
├── 935f2afb.19677f49.js
├── 94d81bc1.9b5ae040.js
├── 95487a17.76aea661.js
├── CNAME
├── a3b85b22.dc69a3a0.js
├── abd305bb.2462f6f3.js
├── aebba8f7.37199675.js
├── b5f6d7ef.27541933.js
├── b8066252.66c747a8.js
├── bad4e641.93d08170.js
├── blog
│ ├── atom.xml
│ └── rss.xml
├── c4f5d8e4.f1743761.js
├── c9862f47.46757e4a.js
├── d0cdf717.ec9f3058.js
├── db915915.90407040.js
├── docs
│ ├── attributes
│ │ └── index.html
│ ├── examples
│ │ ├── custom-event
│ │ │ └── index.html
│ │ ├── customer-rows
│ │ │ └── index.html
│ │ └── signup
│ │ │ └── index.html
│ ├── formula
│ │ └── index.html
│ ├── groups
│ │ ├── beaker
│ │ │ └── index.html
│ │ └── data
│ │ │ └── index.html
│ ├── lifecycle
│ │ └── index.html
│ ├── options
│ │ └── index.html
│ └── stores
│ │ ├── stores-dirty
│ │ └── index.html
│ │ ├── stores-enrichment
│ │ └── index.html
│ │ ├── stores-form-valid
│ │ └── index.html
│ │ ├── stores-form-validity
│ │ └── index.html
│ │ ├── stores-form-values
│ │ └── index.html
│ │ ├── stores-initial-values
│ │ └── index.html
│ │ ├── stores-submit-values
│ │ └── index.html
│ │ ├── stores-touched
│ │ └── index.html
│ │ ├── stores-validity
│ │ └── index.html
│ │ └── stores
│ │ └── index.html
├── f09413f6.7f430def.js
├── f3437f8c.45755d72.js
├── f5547967.fe5f509f.js
├── fb696b7f.c01b6e9a.js
├── img
│ ├── atom.png
│ ├── atom_256.png
│ ├── beaker-large.png
│ ├── beaker-small.png
│ ├── beaker_256.png
│ ├── favicon.ico
│ ├── formula-small.png
│ ├── logo-small.png
│ ├── logo.png
│ ├── logo.svg
│ ├── logo_256.png
│ ├── molecular-structure.png
│ ├── molecular-structure_256.png
│ ├── svelte-logo.svg
│ ├── svelte-vertical.svg
│ ├── undraw_apps.svg
│ ├── undraw_docusaurus_mountain.svg
│ ├── undraw_docusaurus_react.svg
│ ├── undraw_docusaurus_tree.svg
│ └── undraw_form.svg
├── index.html
├── main.64739da4.js
├── main.64739da4.js.LICENSE.txt
├── runtime~main.0404ab80.js
├── sitemap.xml
├── styles.92ad7215.js
└── styles.c39ff6cc.css
├── jest.config.js
├── jest.preset.js
├── nx.json
├── package-lock.json
├── package.json
├── packages
├── .gitkeep
├── docs-site
│ ├── babel.config.js
│ ├── docs
│ │ ├── attributes.md
│ │ ├── examples
│ │ │ ├── custom-event.md
│ │ │ ├── customer-rows.md
│ │ │ └── signup.md
│ │ ├── formula.md
│ │ ├── groups
│ │ │ ├── data.md
│ │ │ └── groups.md
│ │ ├── lifecycle.md
│ │ ├── options.md
│ │ └── stores
│ │ │ ├── dirty.mdx
│ │ │ ├── enrichment.mdx
│ │ │ ├── form-validity.mdx
│ │ │ ├── form-values.mdx
│ │ │ ├── initial-values.mdx
│ │ │ ├── is-form-valid.mdx
│ │ │ ├── stores.md
│ │ │ ├── submit-values.md
│ │ │ ├── touched.mdx
│ │ │ └── validity.mdx
│ ├── docusaurus.config.js
│ ├── sidebars.js
│ ├── src
│ │ ├── css
│ │ │ └── custom.css
│ │ ├── pages
│ │ │ ├── index.js
│ │ │ └── styles.module.css
│ │ └── theme
│ │ │ └── prism-include-languages.js
│ └── static
│ │ ├── CNAME
│ │ └── img
│ │ ├── atom.png
│ │ ├── atom_256.png
│ │ ├── beaker-large.png
│ │ ├── beaker-small.png
│ │ ├── beaker_256.png
│ │ ├── favicon.ico
│ │ ├── formula-small.png
│ │ ├── logo-small.png
│ │ ├── logo.png
│ │ ├── logo.svg
│ │ ├── logo_256.png
│ │ ├── molecular-structure.png
│ │ ├── molecular-structure_256.png
│ │ ├── svelte-logo.svg
│ │ ├── svelte-vertical.svg
│ │ ├── undraw_apps.svg
│ │ ├── undraw_docusaurus_mountain.svg
│ │ ├── undraw_docusaurus_react.svg
│ │ ├── undraw_docusaurus_tree.svg
│ │ └── undraw_form.svg
├── formula-app-e2e
│ ├── .eslintrc.json
│ ├── cypress.json
│ ├── src
│ │ ├── fixtures
│ │ │ └── example.json
│ │ ├── integration
│ │ │ └── app.spec.ts
│ │ ├── plugins
│ │ │ └── index.js
│ │ └── support
│ │ │ ├── app.po.ts
│ │ │ ├── commands.ts
│ │ │ └── index.ts
│ ├── tsconfig.e2e.json
│ └── tsconfig.json
└── svelte
│ └── formula
│ ├── .babelrc
│ ├── .eslintrc.json
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ ├── index.ts
│ ├── lib
│ │ ├── form
│ │ │ ├── aria.spec.ts
│ │ │ ├── aria.ts
│ │ │ ├── dirty.spec.ts
│ │ │ ├── dirty.ts
│ │ │ ├── enrichment.spec.ts
│ │ │ ├── enrichment.ts
│ │ │ ├── errors.spec.ts
│ │ │ ├── errors.ts
│ │ │ ├── event.spec.ts
│ │ │ ├── event.ts
│ │ │ ├── extract.ts
│ │ │ ├── form.ts
│ │ │ ├── init.ts
│ │ │ ├── touch.spec.ts
│ │ │ └── touch.ts
│ │ ├── group
│ │ │ └── group.ts
│ │ └── shared
│ │ │ ├── fields.spec.ts
│ │ │ ├── fields.ts
│ │ │ ├── stores.spec.ts
│ │ │ └── stores.ts
│ └── types
│ │ ├── enrich.ts
│ │ ├── forms.ts
│ │ ├── formula.ts
│ │ ├── groups.ts
│ │ ├── index.ts
│ │ ├── options.ts
│ │ └── validation.ts
│ ├── tsconfig.json
│ ├── tsconfig.lib.json
│ └── tsconfig.spec.json
├── tools
├── generators
│ └── .gitkeep
└── tsconfig.tools.json
├── tsconfig.base.json
├── workspace.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = 120
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "ignorePatterns": ["**/*"],
4 | "plugins": ["@nrwl/nx"],
5 | "overrides": [
6 | {
7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
8 | "rules": {
9 | "@nrwl/nx/enforce-module-boundaries": [
10 | "error",
11 | {
12 | "enforceBuildableLibDependency": true,
13 | "allow": [],
14 | "depConstraints": [
15 | {
16 | "sourceTag": "*",
17 | "onlyDependOnLibsWithTags": ["*"]
18 | }
19 | ]
20 | }
21 | ]
22 | }
23 | },
24 | {
25 | "files": ["*.ts", "*.tsx"],
26 | "extends": ["plugin:@nrwl/nx/typescript"],
27 | "rules": {}
28 | },
29 | {
30 | "files": ["*.js", "*.jsx"],
31 | "extends": ["plugin:@nrwl/nx/javascript"],
32 | "rules": {}
33 | }
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 |
8 | # dependencies
9 | /node_modules
10 |
11 | # IDEs and editors
12 | /.idea
13 | .project
14 | .classpath
15 | .c9/
16 | *.launch
17 | .settings/
18 | *.sublime-workspace
19 |
20 | # IDE - VSCode
21 | .vscode/*
22 | !.vscode/settings.json
23 | !.vscode/tasks.json
24 | !.vscode/launch.json
25 | !.vscode/extensions.json
26 |
27 | # misc
28 | /.sass-cache
29 | /connect.lock
30 | /coverage
31 | /libpeerconnection.log
32 | npm-debug.log
33 | yarn-error.log
34 | testem.log
35 | /typings
36 |
37 | # System Files
38 | .DS_Store
39 | Thumbs.db
40 | /docs/
41 |
42 | # Generated Docusaurus files
43 | .docusaurus/
44 | .cache-loader/
45 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Add files here to ignore them from prettier formatting
2 |
3 | /dist
4 | /coverage
5 | .docusaurus/
6 | docs/
7 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "printWidth": 120,
4 | "semi": true,
5 | "trailingComma": "all"
6 | }
7 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "ms-vscode.vscode-typescript-tslint-plugin",
4 | "esbenp.prettier-vscode",
5 | "firsttris.vscode-jest-runner"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/CONTRIBUTION.md:
--------------------------------------------------------------------------------
1 | # This is an ad-hoc contribution guide.
2 |
3 | The purpose of this document is to describe steps required to get the repo working, as well as the common command used for testing before submitting a pull request.
4 |
5 | ## Setup
6 |
7 | - The repo uses NX and Yarn. You can install Yarn with `npm install --global yarn`
8 | - You can install NX and other packages with `yarn`
9 |
10 | ## Once set up, there's a couple of commands:
11 |
12 | - `npm run start formula-app --rollupConfig=apps/formula-app/rollup.config.js` - this starts the small testing app I use running, it's not very pretty but it lets me test thing (I already have a branch investigating using this app for Cypress e2e testing as well but there is an issue with the internal lib resolving)
13 |
14 | - `npm run start svelte-formula --skip-nx-cache --watch` If you run that with the above, you get hot-reloading when you change the lib`
15 |
16 | - `npm run build svelte-formula --skip-nx-cache` Runs build
17 |
18 | - `npm run lint svelte-formula` Runs linter
19 |
20 | - `npm run start docs-site` Runs the docosaurus site under packages/doc-site
21 |
22 | - `npm run nx test svelte-formula` - Runs the unit test suite
23 |
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Formula + Beaker Δ→
4 |
5 | **Reactive Forms for Svelte**
6 |
7 | 
8 |
9 |
10 |
11 | [](https://www.npmjs.com/package/svelte-formula)
12 |
13 | - [Documentation](https://tanepiper.github.io/svelte-formula)
14 | - [Changelog](https://github.com/tanepiper/svelte-formula/blob/main/CHANGELOG.md)
15 |
16 | `svelte-formula` is a Library for use with [Svelte](https://svelte.dev) that super-charges your ability to create rich
17 | data-driven for applications.
18 |
19 | ## Install Instructions
20 |
21 | `npm i svelte-formula`
22 |
23 | ## Usage
24 |
25 | All you need is an element container with the Svelte [use](https://svelte.dev/docs#use_action) directive and form input
26 | fields with their `name` property set.
27 |
28 | Visit the [documentation](https://tanepiper.github.io/svelte-formula) for more details API instructions.
29 |
30 | ## Formula
31 |
32 | [Demo](https://svelte.dev/repl/dda29ae516284147871b58a4f1966315)
33 |
34 |
35 |
36 | 
37 |
38 |
39 |
40 | **Formula** is a library for creating _Zero Configuration_ reactive form components, and fully data-driven applications.
41 |
42 | **Zero-Configuration** means you need nothing more than a well-defined HTML5 form element to have fully reactive stores
43 | of data and form states.
44 |
45 | Accessing the input requires only setting the `name` property, and for validation providing attributes like `require`
46 | or `minlength`. Formula supports single and multi-value inputs across all widely supported HTML inputs and extends them
47 | with checkbox groups and radio groups, and composite fields of values like text or number.
48 |
49 | Formula creates a form instance that contains Svelte [stores](https://tanepiper.github.io/svelte-formula/docs/stores/stores) that
50 | contain value and validation information, and some
51 | additional [lifecycle methods](https://tanepiper.github.io/svelte-formula/docs/lifecycle) that allow your to dynamically add and
52 | remove customisations, and reset or destroy the form. It also attempts to apply ARIA attributes to help with
53 | accessibility.
54 |
55 | ### Extending Formula
56 |
57 | Formula also supports a bunch of [powerful options](https://tanepiper.github.io/svelte-formula/docs/options) that provide additional
58 | validation, enrichment and custom messages.
59 |
60 | For example with the `enrich` [option](https://tanepiper.github.io/svelte-formula/docs/options#enrich)
61 | and `enrichment` [store](https://tanepiper.github.io/svelte-formula/docs/stores/stores-enrichment) you can provide functions that
62 | calculate additional computed values based on user input - for example calculating a password strength, or the length of
63 | text a user has entered. These are useful.
64 |
65 | Validations can be provided at the form and field level, and integrate with in-built browser validations to provide
66 | native messages, which can be customised for localisation.
67 |
68 | ### Beaker
69 |
70 | [Demo](https://svelte.dev/repl/c146c7976360405cba9a696e3fee853b)
71 |
72 |
73 |
74 | 
75 |
76 |
77 |
78 | **Beaker** take Formula and adds another layer for working with collections of data.
79 |
80 | Using row-based input you can create full form instances per row that are also fully reactive and feed into Beaker's
81 | collection store.
82 |
83 | Beaker also provides methods for setting, adding and removing items from the in-built stores, when can be used with
84 | Svelte's `{#each}{/each}` blocks to create a re-usable template in the component
85 |
86 | With this you can build applications such as multi-row editable tables or lists. See
87 | the [documentation](https://tanepiper.github.io/svelte-formula/docs/groups/beaker) for more details and examples.
88 |
--------------------------------------------------------------------------------
/apps/formula-app/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "env": {
5 | "browser": true,
6 | "commonjs": true,
7 | "es6": true,
8 | "jest": true,
9 | "node": true
10 | },
11 | "plugins": ["import", "jsx-a11y", "react", "react-hooks"],
12 | "overrides": [
13 | {
14 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
15 | "parserOptions": {
16 | "project": ["apps/formula-app/tsconfig.*?.json"]
17 | },
18 | "rules": {}
19 | },
20 | {
21 | "files": ["*.ts", "*.tsx"],
22 | "rules": {}
23 | },
24 | {
25 | "files": ["*.js", "*.jsx"],
26 | "rules": {}
27 | }
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/apps/formula-app/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | displayName: 'formula-app',
3 | preset: '../../jest.preset.js',
4 | globals: {
5 | 'ts-jest': {
6 | tsconfig: '/tsconfig.spec.json',
7 | },
8 | },
9 | transform: {
10 | '^.+\\.svelte$': 'svelte-jester',
11 | '^.+\\.[tj]s$': 'ts-jest',
12 | },
13 | moduleFileExtensions: ['ts', 'js', 'html', 'svelte'],
14 | coverageDirectory: '../../coverage/apps/formula-app',
15 | };
16 |
--------------------------------------------------------------------------------
/apps/formula-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "formula-app",
3 | "version": "0.0.1",
4 | "peerDependencies": {
5 | "svelte": ">= 3.0.0",
6 | "svelte-formula": "*"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/apps/formula-app/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tanepiper/svelte-formula/bb09898fb2c852914c70208d2d0a992434ac857f/apps/formula-app/public/favicon.png
--------------------------------------------------------------------------------
/apps/formula-app/public/global.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | position: relative;
4 | width: 100%;
5 | height: 100%;
6 | }
7 |
8 | body {
9 | color: #333;
10 | margin: 0;
11 | padding: 8px;
12 | box-sizing: border-box;
13 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue',
14 | sans-serif;
15 | }
16 |
17 | a {
18 | color: rgb(0, 100, 200);
19 | text-decoration: none;
20 | }
21 |
22 | a:hover {
23 | text-decoration: underline;
24 | }
25 |
26 | a:visited {
27 | color: rgb(0, 80, 160);
28 | }
29 |
30 | label {
31 | display: block;
32 | }
33 |
34 | input,
35 | button,
36 | select,
37 | textarea {
38 | font-family: inherit;
39 | font-size: inherit;
40 | -webkit-padding: 0.4em 0;
41 | padding: 0.4em;
42 | margin: 0 0 0.5em 0;
43 | box-sizing: border-box;
44 | border: 1px solid #ccc;
45 | border-radius: 2px;
46 | }
47 |
48 | input:disabled {
49 | color: #ccc;
50 | }
51 |
52 | button {
53 | color: #333;
54 | background-color: #f4f4f4;
55 | outline: none;
56 | }
57 |
58 | button:disabled {
59 | color: #999;
60 | }
61 |
62 | button:not(:disabled):active {
63 | background-color: #ddd;
64 | }
65 |
66 | button:focus {
67 | border-color: #666;
68 | }
69 |
--------------------------------------------------------------------------------
/apps/formula-app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Svelte app
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/apps/formula-app/rollup.config.js:
--------------------------------------------------------------------------------
1 | module.exports = (config) => {
2 | const { output, external, ...rest } = config;
3 |
4 | const updatedOutput = {
5 | ...output,
6 | globals: [
7 | {
8 | moduleId: 'svelte-formula',
9 | global: 'SvelteFormula',
10 | },
11 | ],
12 | };
13 | const updatedExternal = [...[external || []], 'svelte-formula'];
14 | const result = {
15 | ...rest,
16 | output: { ...updatedOutput },
17 | external: [...updatedExternal],
18 | };
19 | return result;
20 | };
21 |
--------------------------------------------------------------------------------
/apps/formula-app/src/App.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/apps/formula-app/src/main.ts:
--------------------------------------------------------------------------------
1 | import App from './App.svelte';
2 |
3 | const app = new App({
4 | target: document.body,
5 | props: {
6 | name: 'formula-app',
7 | },
8 | });
9 |
10 | export default app;
11 |
--------------------------------------------------------------------------------
/apps/formula-app/src/pages/Home.svelte:
--------------------------------------------------------------------------------
1 |
5 |
19 |
--------------------------------------------------------------------------------
/apps/formula-app/src/pages/WeePage.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
23 |
24 |
27 |
--------------------------------------------------------------------------------
/apps/formula-app/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["src/**/*"],
4 | "exclude": ["__sapper__/*", "public/*"]
5 | }
6 |
--------------------------------------------------------------------------------
/apps/formula-app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 |
4 | "compilerOptions": {
5 | "moduleResolution": "node",
6 | "target": "es2017",
7 | /**
8 | Svelte Preprocess cannot figure out whether you have a value or a type, so tell TypeScript
9 | to enforce using `import type` instead of `import` for Types.
10 | */
11 | "importsNotUsedAsValues": "error",
12 | "isolatedModules": true,
13 | /**
14 | To have warnings/errors of the Svelte compiler at the correct position,
15 | enable source maps by default.
16 | */
17 | "sourceMap": true,
18 | /** Requests the runtime types from the svelte modules by default. Needed for TS files or else you get errors. */
19 | "types": ["svelte"],
20 |
21 | "strict": false,
22 | "esModuleInterop": true,
23 | "skipLibCheck": true,
24 | "forceConsistentCasingInFileNames": true
25 | },
26 | "files": [],
27 | "include": [],
28 | "references": [
29 | {
30 | "path": "./tsconfig.app.json"
31 | },
32 | {
33 | "path": "./tsconfig.spec.json"
34 | }
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/apps/formula-app/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "include": ["**/*.spec.ts", "**/*.spec.tsx", "**/*.spec.js", "**/*.spec.jsx", "**/*.d.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tanepiper/svelte-formula/bb09898fb2c852914c70208d2d0a992434ac857f/assets/logo.png
--------------------------------------------------------------------------------
/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@nrwl/web/babel"],
3 | "babelrcRoots": ["*"]
4 | }
5 |
--------------------------------------------------------------------------------
/docs/2.215e07fe.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*!
2 | Copyright (c) 2017 Jed Watson.
3 | Licensed under the MIT License (MIT), see
4 | http://jedwatson.github.io/classnames
5 | */
6 |
--------------------------------------------------------------------------------
/docs/28.1be0f6ce.js:
--------------------------------------------------------------------------------
1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[28],{108:function(e,t,a){"use strict";a.r(t);var n=a(0),o=a.n(n),l=a(106);t.default=function(){return o.a.createElement(l.a,{title:"Page Not Found"},o.a.createElement("main",{className:"container margin-vert--xl"},o.a.createElement("div",{className:"row"},o.a.createElement("div",{className:"col col--6 col--offset-3"},o.a.createElement("h1",{className:"hero__title"},"Page Not Found"),o.a.createElement("p",null,"We could not find what you were looking for."),o.a.createElement("p",null,"Please contact the owner of the site that linked you to the original URL and let them know their link is broken.")))))}}}]);
--------------------------------------------------------------------------------
/docs/40370821.3f878b17.js:
--------------------------------------------------------------------------------
1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[4],{73:function(e,t,n){"use strict";n.r(t),n.d(t,"frontMatter",(function(){return l})),n.d(t,"metadata",(function(){return o})),n.d(t,"toc",(function(){return u})),n.d(t,"default",(function(){return c}));var r=n(3),a=n(7),i=(n(0),n(97)),l={id:"stores-initial-values",title:"initialValues",sidebar_label:"initialValues"},o={unversionedId:"stores/stores-initial-values",id:"stores/stores-initial-values",isDocsHomePage:!1,title:"initialValues",description:"Description",source:"@site/docs/stores/initial-values.mdx",slug:"/stores/stores-initial-values",permalink:"/svelte-formula/docs/stores/stores-initial-values",version:"current",sidebar_label:"initialValues",sidebar:"someSidebar",previous:{title:"formValues",permalink:"/svelte-formula/docs/stores/stores-form-values"},next:{title:"isFormValid",permalink:"/svelte-formula/docs/stores/stores-form-valid"}},u=[{value:"Description",id:"description",children:[]},{value:"Example",id:"example",children:[]}],s={toc:u};function c(e){var t=e.components,n=Object(a.a)(e,["components"]);return Object(i.b)("wrapper",Object(r.a)({},s,n,{components:t,mdxType:"MDXLayout"}),Object(i.b)("h2",{id:"description"},"Description"),Object(i.b)("p",null,"This store contains all the form values at the time the form was initialised - this may include ",Object(i.b)("inlineCode",{parentName:"p"},"defaultValues")," and any values\nbound on the form at configuration. This store will never change during the lifecycle of the form The values are an ",Object(i.b)("inlineCode",{parentName:"p"},"Object")," with the\nkey per group ",Object(i.b)("inlineCode",{parentName:"p"},"name")," and it's value."),Object(i.b)("p",null,"The value can be a single value, or an array or values depending on there being fields with the same ",Object(i.b)("inlineCode",{parentName:"p"},"name")," (e.g. multiple checkboxes), or a ",Object(i.b)("inlineCode",{parentName:"p"},"")," element\nwith the ",Object(i.b)("inlineCode",{parentName:"p"},"multiple")," attribute."),Object(i.b)("h2",{id:"example"},"Example"),Object(i.b)("pre",null,Object(i.b)("code",{parentName:"pre",className:"language-svelte"},"
22 |
23 | {
24 | value--;
25 | el.dispatchEvent(new Event('customEvent'))
26 | }}>
27 | -
28 |
29 |
30 |
32 |
33 | {
34 | value++;
35 | el.dispatchEvent(new Event('customEvent'))
36 | }}>
37 | +
38 |
39 |
40 | ```
41 |
42 | ## Component Use
43 |
44 | ```svelte
45 |
46 |
74 |
75 |
82 |
83 |
88 |
89 | ```
90 |
--------------------------------------------------------------------------------
/packages/docs-site/docs/examples/customer-rows.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: customer-rows
3 |
4 | title: Example - Customer Rows
5 |
6 | sidebar_label: Row Data
7 | ---
8 |
9 | ```svelte
10 |
65 |
66 |
67 |
140 | ```
141 |
--------------------------------------------------------------------------------
/packages/docs-site/docs/examples/signup.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: signup
3 |
4 | title: Example - Signup Form
5 |
6 | sidebar_label: Complex Form
7 | ---
8 |
9 | ```svelte
10 |
54 | {passwordStrength}
55 |
86 |
87 |
92 |
93 | ```
94 |
--------------------------------------------------------------------------------
/packages/docs-site/docs/formula.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: formula
3 |
4 | title: Formula API
5 |
6 | sidebar_label: Formula API
7 | ---
8 |
9 | [](https://www.npmjs.com/package/svelte-formula)
10 |
11 | ## What is Formula?
12 |
13 | > **Formula is still in active development - as such there may still be API changes**
14 |
15 | Formula is a library for [Svelte](https://svelte.dev) with features for creating **Zero Configuration** reactive forms
16 | and fully data-driven applications.
17 |
18 | Out-of-the box it's designed to work with HTML5 forms. Using the `name` attribute of your HTML elements, Formula builds
19 | a set of state objects using Svelte's subscribable [stores](stores/stores.md), making them available for you subscribe
20 | to in your application. These stores contain values and validation states, which are configured as easily as setting
21 | supported attributes, and doesn't get in the way of things like Accessibility.
22 |
23 | > Want to make a field required? Just add the ` ` attribute, or add ` ` to set a minimum length on the fields.
24 |
25 | Validation is enhanced via custom field and form level validation functions passed in the [options](options.md) - you
26 | can also pass default values, or override default HTML5 validation text with your own versions (such as localised text).
27 |
28 | You can also enrich you fields with computed values (such as a password strength derived from the users input).
29 |
30 | ### Working with Data Collections
31 |
32 | Formula also provides an API for working with collections of data - [beaker](groups/groups.md) allows you to use Formula to
33 | create rich row-level forms for applications such as data grids.
34 |
35 | ## Installation
36 |
37 | Formula is available on NPM, with the source available on GitHub. To install in your project type:
38 |
39 | > `npm i svelte-formula`
40 |
41 | ## Basic Form Example
42 |
43 | To use in your project all you need is an element container binding the form with
44 | Svelte [use](https://svelte.dev/docs#use_action)
45 | directive, and form input fields with their `name` property set.
46 |
47 | ```svelte
48 |
58 |
59 |
60 | Project Name
61 |
62 | {validity?.projectName?.message}
63 | Update Project Name
64 |
65 | ```
66 |
--------------------------------------------------------------------------------
/packages/docs-site/docs/groups/data.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: data
3 |
4 | title: Data API
5 |
6 | sidebar_label: Data API
7 | ---
8 |
9 | Like Formula, `beaker` returns an instance of the Beaker stores which are have the same name as
10 | the [Formula store](../stores/stores.md), but container Array of form data as rows. The exceptions are
11 | `isFormReady` and `isFormValid` which are for the entire group and still a single value.
12 |
13 | It also contains some additional properties and methods. The methods listed below allow for data to be added or removed
14 | from the groups `formValue` store - this store can also be used with a `{#each}` block to render the rows in the
15 | template.
16 |
17 | ## `init`
18 |
19 | Pass initial data into the form group, or reset the form to initial data - this will the form store data with the
20 | current items to render. Each key and value should match the fields in the group template (the exception
21 | is [radio fields](./groups.md) which should be based on the `data-formula-name` attribute passed).
22 |
23 | ```svelte
24 |
25 |
35 |
36 | {#each items as item, i}
37 |
38 | {/each}
39 |
40 | ```
41 |
42 | ## `add`
43 |
44 | Add a row item to the store - this item should be a single object with the same key/value type for the form.
45 |
46 | ```svelte
47 |
48 |
58 |
59 | contacts.add({...item})}>Add Item
60 |
61 | {#each items as item, i}
62 |
63 | {/each}
64 |
65 | ```
66 |
67 | ## `set`
68 |
69 | Update an existing row in the store at the passed index.
70 |
71 | ```svelte
72 |
73 |
83 |
84 |
85 | {#each items as item, i}
86 | ...
87 | contacts.set(i, {...newData})}>Save Item
88 |
89 | ...
90 | {/each}
91 |
92 | ```
93 |
94 | ## `delete`
95 |
96 | Deletes a row from the form - this method takes the index of the row to remove.
97 |
98 | ```svelte
99 |
100 |
110 |
111 |
112 | {#each items as item, i}
113 | contacts.delete(i)}>Delete Item
114 | {/each}
115 |
116 | ```
117 |
118 | ## `clear`
119 |
120 | Calling this will empty the group of all rows of data.
121 |
122 | ## `forms`
123 |
124 | An `Map` of all the underlying [Formula](../formula.md) instances that allows for finer control, or access to the form
125 | stores
126 |
--------------------------------------------------------------------------------
/packages/docs-site/docs/lifecycle.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: lifecycle
3 |
4 | title: Formula Lifecycle
5 |
6 | sidebar_label: Formula Lifecycle
7 | ---
8 |
9 | ## Create
10 |
11 | To create your form instance call the `formula()` method, this creates a `form` instance that can be attached to any
12 | element with the [use](https://svelte.dev/docs#use_action) directive.
13 |
14 | When using like this, on component destruction the form will automatically unbind
15 |
16 | ```svelte
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | ```
29 |
30 | ## Update
31 |
32 | Any Formula instance can be updated using the `updateForm` method, which accepts a new `FormulaOptions` object. When
33 | using `update` all existing handlers will be removed and rebound.
34 |
35 | ```svelte
36 |
48 |
49 |
50 | switchLanguage('en')}>English
51 | switchLanguage('nl')}>Nederlands
52 | switchLanguage('fr')}>Français
53 |
54 |
55 |
58 | ```
59 |
60 | ## Reset
61 |
62 | The `resetForm` can be called at any time during the life of the form, it will reset the form to it's initial state after
63 | `defaultValues` and element values have been applied, also `touched` and `dirty` stores are reset.
64 |
65 | ```svelte
66 |
74 |
85 | ```
86 |
87 | ## Destroy
88 |
89 | The `destroyForm` method allows the form to be destroyed early, which removes all handlers and removes the stores from the
90 | global store.
91 |
--------------------------------------------------------------------------------
/packages/docs-site/docs/options.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: options
3 |
4 | title: Formula Options
5 |
6 | sidebar_label: Formula Options
7 | ---
8 |
9 | Formula is zero-configuration - Out-of-the-box - using standard HTML5 validation properties to build up its validation
10 | rules - however it is also possible to pass custom validation rules via the `formula()` options object.
11 |
12 | ## `defaultValues`
13 |
14 | The `defaultValues` option allows an initial set of values to be passed to the form.
15 |
16 | Form values can also be set with defaults using ` `, if no value it set it will fall back to
17 | this value. If there is no default value, then it will be an empty string or array value.
18 |
19 | ```svelte
20 |
29 |
30 |
31 |
32 |
33 | ```
34 |
35 | ## `enrich`
36 |
37 | The `enrich` object is used to pass methods to the Formula instance that allow the generation of computed values for
38 | current form values - these are added at the field level, and each field can have multiple. All the calculated values
39 | are available via the [enrichment store](stores/enrichment.mdx).
40 |
41 | ```svelte
42 |
53 |
54 |
55 |
56 | Length ${$enrichment?.content?.contentLength}
57 |
58 | ```
59 |
60 | ## `messages`
61 |
62 | Used for localisation and custom messages, this is a `Object` containing a key that is the field `name` to apply the
63 | messages to. The value is another `Object` that contains the key for each error (e.g. `valueMissing`) and the value is
64 | the replacement string.
65 |
66 | ```svelte
67 |
68 |
79 | ```
80 |
81 | ## `validators`
82 |
83 | An `Object` containing a key that is the field `name` to apply the validation to, the value is another object that
84 | contains each named validation function. The result are made available in the `validity` store.
85 |
86 | ```svelte
87 |
88 |
99 | ```
100 |
101 | ## `formValidators`
102 |
103 | An `Object` containing a key that is the name of the validation rule, and the function that returns the validation
104 | result. The results are available in the `formValidity` store
105 |
106 | ```svelte
107 |
108 |
119 |
120 |
121 | {$formValidity?.passwordsMatch}
122 |
123 | ```
124 |
125 | ## `preChanges`
126 |
127 | A `Function` that is called before any values are read from the DOM changes and the store updates. This can be used to
128 | carry out additional changes to the form.
129 |
130 | ```svelte
131 |
132 |
141 |
142 |
143 | ```
144 |
145 | ## `postChanges`
146 |
147 | A `Function` that is called after all the values have been read and stores updated. This function receives the latest
148 | values and is functionaly the same as subscribing to the `form.formValues` store.
149 |
150 | ```svelte
151 |
152 |
159 | ```
160 |
--------------------------------------------------------------------------------
/packages/docs-site/docs/stores/dirty.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | id: stores-dirty
3 |
4 | title: dirty
5 |
6 | sidebar_label: dirty
7 | ---
8 |
9 | ## Description
10 |
11 | This store provides the dirty status of a field, or group of fields under a single `name` property.
12 |
13 | On form creation every form field is assigned a `blur` handler, and it reads the current value of the field. The dirty status for the group `name` is also set to `false`.
14 |
15 | When the user exits the field with a `blur` event the value is checked, and if changed the store is updated to reflect the `name` as `true`.
16 | All of the `blur` handlers for the group and then immediately removed, so this group will no longer update the status.
17 |
18 | ## Example
19 |
20 | ```svelte
21 |
25 |
30 | ```
31 |
--------------------------------------------------------------------------------
/packages/docs-site/docs/stores/enrichment.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | id: stores-enrichment
3 |
4 | title: enrichment
5 |
6 | sidebar_label: enrichment
7 | ---
8 |
9 | ## Description
10 |
11 | This store provides the results of any method passed to the `enrich` object for a group `name`. Each group contains a key and value based on the available methods.
12 |
13 | It emits on every value change, where the value group has available methods
14 |
15 | ## Example
16 |
17 | ```svelte
18 |
30 |
31 | Password
32 |
33 |
34 | ```
35 |
--------------------------------------------------------------------------------
/packages/docs-site/docs/stores/form-validity.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | id: stores-form-validity
3 |
4 | title: formValidity
5 |
6 | sidebar_label: formValidity
7 | ---
8 |
9 | ## Description
10 |
11 | This store provides the overall form status when using custom form validations, provided in the `formValidation` options object. If none are provided this store will not update.
12 |
13 | The store contains a `Object` that can contains the key of any validations that have failed, and a string value of the error message.
14 |
15 | Every time there is a value update the store is reset and any custom validations called, they should return `null` for a valid result,
16 | or a string for an invalid result, which can be used to display error messages.
17 |
18 | ## Example
19 |
20 | ```svelte
21 |
29 |
37 | ```
38 |
--------------------------------------------------------------------------------
/packages/docs-site/docs/stores/form-values.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | id: stores-form-values
3 |
4 | title: formValues
5 |
6 | sidebar_label: formValues
7 | ---
8 |
9 | ## Description
10 |
11 | This store contains all the current form values at the time any value changes on a bound element. The values are an `Object` with the
12 | key per group `name` and it's value.
13 |
14 | The value can be a single value, or an array or values depending on there being fields with the same `name` (e.g. multiple checkboxes), or a `` element
15 | with the `multiple` attribute.
16 |
17 | ## Example
18 |
19 | ```svelte
20 |
27 |
35 | ```
36 |
--------------------------------------------------------------------------------
/packages/docs-site/docs/stores/initial-values.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | id: stores-initial-values
3 |
4 | title: initialValues
5 |
6 | sidebar_label: initialValues
7 | ---
8 |
9 | ## Description
10 |
11 | This store contains all the form values at the time the form was initialised - this may include `defaultValues` and any values
12 | bound on the form at configuration. This store will never change during the lifecycle of the form The values are an `Object` with the
13 | key per group `name` and it's value.
14 |
15 | The value can be a single value, or an array or values depending on there being fields with the same `name` (e.g. multiple checkboxes), or a `` element
16 | with the `multiple` attribute.
17 |
18 | ## Example
19 |
20 | ```svelte
21 |
29 |
40 | ```
41 |
--------------------------------------------------------------------------------
/packages/docs-site/docs/stores/is-form-valid.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | id: stores-form-valid
3 |
4 | title: isFormValid
5 |
6 | sidebar_label: isFormValid
7 | ---
8 |
9 | ## Description
10 |
11 | This store emits a single boolean value any time the form validity changes. The state is checked on every value change,
12 | and if there are no invalid fields it's set to `true`, otherwise `false` (which is the default).
13 |
14 | ## Example
15 |
16 | ```svelte
17 |
21 |
29 | ```
30 |
--------------------------------------------------------------------------------
/packages/docs-site/docs/stores/stores.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: stores
3 |
4 | title: Formula Stores
5 |
6 | sidebar_label: Formula Stores
7 | ---
8 |
9 | ## Accessing Stores
10 |
11 | Formula and Beaker provides a set of Svelte [stores](https://svelte.dev/docs#svelte_store) as part of each instance
12 | created. These stores provide different types of values at different lifecycles of your application.
13 |
14 | All the stores are listed in the sidebar.
15 |
16 | * When using Formula, the stores contain a single `Object` instance of the form
17 | * When using Beaker, the stores contain an `Array` of `Object` values for each row instance
18 |
19 | ```javascript
20 | const { form, enrichement, formValdity, formValues, isFormValid, submitValues, touched, validity } = formula();
21 | ```
22 |
23 | If you have multiple forms on the page you can also access stores via `form.stores`
24 |
25 | ```svelte
26 |
27 |
35 |
38 |
39 | ```
40 |
41 | ## Global Store
42 |
43 | When attaching a form to an element, if you provide an `id` property the stores will be added to a global `Map` that can
44 | be accessed from anywhere else in your application from via `formulaStores` or `beakerStores`
45 |
46 | ```svelte
47 |
48 |
60 |
61 |
64 | ```
65 |
--------------------------------------------------------------------------------
/packages/docs-site/docs/stores/submit-values.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: stores-submit-values
3 |
4 | title: submitValues
5 |
6 | sidebar_label: submitValues
7 | ---
8 |
9 | ## Description
10 |
11 | This store contains all the form values at submit time, only if the bound element is a `form` element. The values are
12 | an `Object` with the key per group `name` and it's value.
13 |
14 | The value can be a single value, or an array or values depending on there being fields with the same `name` (e.g.
15 | multiple checkboxes), or a `` element with the `multiple` attribute.
16 |
17 | ## Example
18 |
19 | ```svelte
20 |
31 |
39 | ```
40 |
--------------------------------------------------------------------------------
/packages/docs-site/docs/stores/touched.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | id: stores-touched
3 |
4 | title: touched
5 |
6 | sidebar_label: touched
7 | ---
8 |
9 | ## Description
10 |
11 | This store provides the touched status of a field, or group of fields under a single `name` property.
12 |
13 | On form creation every form field is assigned a `focus` handler. The touched status for the group `name` is also set to `false`.
14 |
15 | When the user clicks or tabs on to the field the store is updated to reflect the `name` as `true`.
16 | All of the `focus` handlers for the group and then immediately removed, so this group will no longer update the status.
17 |
18 | ## Example
19 |
20 | ```svelte
21 |
25 |
30 | ```
31 |
--------------------------------------------------------------------------------
/packages/docs-site/docs/stores/validity.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | id: stores-validity
3 |
4 | title: validity
5 |
6 | sidebar_label: validity
7 | ---
8 |
9 | ## Description
10 |
11 | This store emits on every value change the validity of every `name` group. The value is an object that contains the `name` and
12 | an `Object` that contains the following properties:
13 |
14 | - `valid` - If the field is valid
15 | - `invalid` - If the field is invalid
16 | - `message` - If the form is invalid, the message to display.
17 | - `errors` - An object with the keys of the current errors applied to the field (the value is `true`)
18 |
19 | The `message` will always be the first one to match, HTML5 errors first (e.g HTML required comes before HTML minlength)
20 | then custom errors from `validators`. The `errors` object may contain more than one key for errors that apply.
21 |
22 | ## Example
23 |
24 | ```svelte
25 |
29 |
34 | ```
35 |
--------------------------------------------------------------------------------
/packages/docs-site/docusaurus.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | title: 'Formula',
3 | tagline: 'Zero Configuration Reactive Forms for Svelte',
4 | url: 'https://tanepiper.github.io/',
5 | baseUrl: '/svelte-formula/',
6 | onBrokenLinks: 'throw',
7 | favicon: 'img/favicon.ico',
8 | organizationName: 'tanepiper', // Usually your GitHub org/user name.
9 | projectName: 'svelte-formula', // Usually your repo name.
10 | plugins: [require.resolve('docusaurus-plugin-fathom')],
11 | themeConfig: {
12 | fathomAnalytics: {
13 | siteId: 'NOVPWZMR',
14 | },
15 | navbar: {
16 | title: 'Formula',
17 | logo: {
18 | alt: 'The Formula logo containing a molecule and two science beakers',
19 | src: 'img/logo-small.png',
20 | },
21 | items: [
22 | {
23 | to: 'docs/formula',
24 | activeBasePath: 'docs',
25 | label: 'Docs',
26 | position: 'left',
27 | },
28 | {
29 | href: 'https://github.com/tanepiper/svelte-formula/blob/main/CHANGELOG.md',
30 | label: 'Changelog',
31 | position: 'left',
32 | },
33 |
34 | {
35 | href: 'https://www.npmjs.com/package/svelte-formula',
36 | label: 'NPM',
37 | position: 'right',
38 | },
39 | {
40 | href: 'https://github.com/tanepiper/svelte-formula',
41 | label: 'GitHub',
42 | position: 'right',
43 | },
44 | ],
45 | },
46 | footer: {
47 | style: 'dark',
48 | links: [],
49 | copyright: `Copyright © ${new Date().getFullYear()} Tane Piper. Built with Docusaurus.`,
50 | },
51 | },
52 | presets: [
53 | [
54 | '@docusaurus/preset-classic',
55 | {
56 | docs: {
57 | sidebarPath: require.resolve('./sidebars.js'),
58 | // Please change this to your repo.
59 | //editUrl: 'https://github.com/tanepiper/svelte-formula/edit/master/packages/',
60 | },
61 | blog: {
62 | showReadingTime: true,
63 | // Please change this to your repo.
64 | //editUrl: 'https://github.com/facebook/docusaurus/edit/master/website/blog/',
65 | },
66 | theme: {
67 | customCss: require.resolve('./src/css/custom.css'),
68 | },
69 | },
70 | ],
71 | ],
72 | };
73 |
--------------------------------------------------------------------------------
/packages/docs-site/sidebars.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | someSidebar: {
3 | Formula: ['formula', 'options', 'lifecycle', 'attributes'],
4 | Beaker: ['groups/beaker', 'groups/data'],
5 | Stores: [
6 | 'stores/stores',
7 | 'stores/stores-dirty',
8 | 'stores/stores-enrichment',
9 | 'stores/stores-form-validity',
10 | 'stores/stores-form-values',
11 | 'stores/stores-initial-values',
12 | 'stores/stores-form-valid',
13 | 'stores/stores-submit-values',
14 | 'stores/stores-touched',
15 | 'stores/stores-validity',
16 | ],
17 | Examples: ['examples/signup', 'examples/customer-rows', 'examples/custom-event'],
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/packages/docs-site/src/css/custom.css:
--------------------------------------------------------------------------------
1 | /* stylelint-disable docusaurus/copyright-header */
2 | /**
3 | * Any CSS included here will be global. The classic template
4 | * bundles Infima by default. Infima is a CSS framework designed to
5 | * work well for content-centric websites.
6 | */
7 |
8 | /* You can override the default Infima variables here. */
9 | :root {
10 | --ifm-color-primary: rgba(255, 62, 0, 0.7);
11 | --ifm-color-primary-dark: rgb(33, 175, 144);
12 | --ifm-color-primary-darker: rgb(31, 165, 136);
13 | --ifm-color-primary-darkest: rgb(26, 136, 112);
14 | --ifm-color-primary-light: rgb(70, 203, 174);
15 | --ifm-color-primary-lighter: rgb(102, 212, 189);
16 | --ifm-color-primary-lightest: rgb(146, 224, 208);
17 | --ifm-code-font-size: 95%;
18 | }
19 |
20 | .docusaurus-highlight-code-line {
21 | background-color: rgb(72, 77, 91);
22 | display: block;
23 | margin: 0 calc(-1 * var(--ifm-pre-padding));
24 | padding: 0 var(--ifm-pre-padding);
25 | }
26 |
27 | .hero h1, .hero p, .features_packages-docs-site-src-pages- h3, .navbar__title {
28 | font-family: monospace;
29 | text-align: center;
30 | }
31 |
32 | .navbar__title, .features_packages-docs-site-src-pages- h3 {
33 | font-size: 1.6em;
34 | }
35 |
--------------------------------------------------------------------------------
/packages/docs-site/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import clsx from 'clsx';
3 | import Layout from '@theme/Layout';
4 | import Link from '@docusaurus/Link';
5 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
6 | import useBaseUrl from '@docusaurus/useBaseUrl';
7 | import styles from './styles.module.css';
8 |
9 | const features = [
10 | {
11 | title: <>Zero Configuration>,
12 | alt: 'An image of an atom',
13 | imageUrl: 'img/atom_256.png',
14 | description: (
15 | <>
16 | Native HTML5 forms and validation support without the need for any additional JavaScript configuration, but with
17 | powerful options to enhance forms.
18 | >
19 | ),
20 | },
21 | {
22 | title: <>Fully Reactive>,
23 | alt: 'An image of a chemical reaction',
24 | imageUrl: 'img/beaker_256.png',
25 | description: (
26 | <>
27 | Build powerful reactive data-driven applications with Formula and data groups with Beaker - enrich with custom
28 | validation, computed values and custom messages messages.
29 | >
30 | ),
31 | },
32 | {
33 | title: <>Built for Svelte>,
34 | alt: 'An image of a molecular structure',
35 | imageUrl: 'img/molecular-structure_256.png',
36 | description: (
37 | <>
38 | Easy to install Action and Subscriptions that just work with your Svelte application without getting in the way.
39 | >
40 | ),
41 | },
42 | ];
43 |
44 | function Feature({ imageUrl, title, description, alt }) {
45 | const imgUrl = useBaseUrl(imageUrl);
46 | return (
47 |
48 | {imgUrl && (
49 |
50 |
51 |
{title}
52 |
{description}
53 |
54 | )}
55 |
56 | );
57 | }
58 |
59 | function Home() {
60 | const context = useDocusaurusContext();
61 | const { siteConfig = {} } = context;
62 | return (
63 |
64 |
82 |
83 | {features && features.length > 0 && (
84 |
85 |
86 |
87 | {features.map((props, idx) => (
88 |
89 | ))}
90 |
91 |
92 |
93 | )}
94 |
95 |
96 | );
97 | }
98 |
99 | export default Home;
100 |
--------------------------------------------------------------------------------
/packages/docs-site/src/pages/styles.module.css:
--------------------------------------------------------------------------------
1 | /* stylelint-disable docusaurus/copyright-header */
2 | /**
3 | * CSS files with the .module.css suffix will be treated as CSS modules
4 | * and scoped locally.
5 | */
6 |
7 | .heroBanner {
8 | padding: 4rem 0;
9 | text-align: center;
10 | position: relative;
11 | overflow: hidden;
12 | }
13 |
14 | @media screen and (max-width: 966px) {
15 | .heroBanner {
16 | padding: 2rem;
17 | }
18 | }
19 |
20 | .buttons {
21 | display: flex;
22 | align-items: center;
23 | justify-content: center;
24 | }
25 |
26 | .features {
27 | display: flex;
28 | align-items: center;
29 | padding: 2rem 0;
30 | width: 100%;
31 | }
32 |
33 | .featureImage {
34 | height: 200px;
35 | width: 200px;
36 | }
37 |
--------------------------------------------------------------------------------
/packages/docs-site/src/theme/prism-include-languages.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Facebook, Inc. and its affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 | import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
8 | import siteConfig from '@generated/docusaurus.config';
9 |
10 | const prismIncludeLanguages = PrismObject => {
11 | if (ExecutionEnvironment.canUseDOM) {
12 | const {
13 | themeConfig: {
14 | prism: {
15 | additionalLanguages = []
16 | } = {}
17 | }
18 | } = siteConfig;
19 | window.Prism = PrismObject;
20 | additionalLanguages.forEach(lang => {
21 | require(`prismjs/components/prism-${lang}`); // eslint-disable-line
22 |
23 | });
24 | require('prism-svelte')
25 | delete window.Prism;
26 | }
27 | };
28 |
29 | export default prismIncludeLanguages;
30 |
--------------------------------------------------------------------------------
/packages/docs-site/static/CNAME:
--------------------------------------------------------------------------------
1 | tanepiper.github.io/svelte-formula
2 |
--------------------------------------------------------------------------------
/packages/docs-site/static/img/atom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tanepiper/svelte-formula/bb09898fb2c852914c70208d2d0a992434ac857f/packages/docs-site/static/img/atom.png
--------------------------------------------------------------------------------
/packages/docs-site/static/img/atom_256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tanepiper/svelte-formula/bb09898fb2c852914c70208d2d0a992434ac857f/packages/docs-site/static/img/atom_256.png
--------------------------------------------------------------------------------
/packages/docs-site/static/img/beaker-large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tanepiper/svelte-formula/bb09898fb2c852914c70208d2d0a992434ac857f/packages/docs-site/static/img/beaker-large.png
--------------------------------------------------------------------------------
/packages/docs-site/static/img/beaker-small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tanepiper/svelte-formula/bb09898fb2c852914c70208d2d0a992434ac857f/packages/docs-site/static/img/beaker-small.png
--------------------------------------------------------------------------------
/packages/docs-site/static/img/beaker_256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tanepiper/svelte-formula/bb09898fb2c852914c70208d2d0a992434ac857f/packages/docs-site/static/img/beaker_256.png
--------------------------------------------------------------------------------
/packages/docs-site/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tanepiper/svelte-formula/bb09898fb2c852914c70208d2d0a992434ac857f/packages/docs-site/static/img/favicon.ico
--------------------------------------------------------------------------------
/packages/docs-site/static/img/formula-small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tanepiper/svelte-formula/bb09898fb2c852914c70208d2d0a992434ac857f/packages/docs-site/static/img/formula-small.png
--------------------------------------------------------------------------------
/packages/docs-site/static/img/logo-small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tanepiper/svelte-formula/bb09898fb2c852914c70208d2d0a992434ac857f/packages/docs-site/static/img/logo-small.png
--------------------------------------------------------------------------------
/packages/docs-site/static/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tanepiper/svelte-formula/bb09898fb2c852914c70208d2d0a992434ac857f/packages/docs-site/static/img/logo.png
--------------------------------------------------------------------------------
/packages/docs-site/static/img/logo_256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tanepiper/svelte-formula/bb09898fb2c852914c70208d2d0a992434ac857f/packages/docs-site/static/img/logo_256.png
--------------------------------------------------------------------------------
/packages/docs-site/static/img/molecular-structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tanepiper/svelte-formula/bb09898fb2c852914c70208d2d0a992434ac857f/packages/docs-site/static/img/molecular-structure.png
--------------------------------------------------------------------------------
/packages/docs-site/static/img/molecular-structure_256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tanepiper/svelte-formula/bb09898fb2c852914c70208d2d0a992434ac857f/packages/docs-site/static/img/molecular-structure_256.png
--------------------------------------------------------------------------------
/packages/docs-site/static/img/svelte-logo.svg:
--------------------------------------------------------------------------------
1 | svelte-logo
--------------------------------------------------------------------------------
/packages/docs-site/static/img/svelte-vertical.svg:
--------------------------------------------------------------------------------
1 | svelte-vertical
--------------------------------------------------------------------------------
/packages/docs-site/static/img/undraw_form.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/formula-app-e2e/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["plugin:cypress/recommended", "../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "rules": {},
5 | "overrides": [
6 | {
7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
8 | "parserOptions": {
9 | "project": ["packages/formula-app-e2e/tsconfig.*?.json"]
10 | },
11 | "rules": {}
12 | },
13 | {
14 | "files": ["src/plugins/index.js"],
15 | "rules": {
16 | "@typescript-eslint/no-var-requires": "off",
17 | "no-undef": "off"
18 | }
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/packages/formula-app-e2e/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "fileServerFolder": ".",
3 | "fixturesFolder": "./src/fixtures",
4 | "integrationFolder": "./src/integration",
5 | "modifyObstructiveCode": false,
6 | "pluginsFile": "./src/plugins/index",
7 | "supportFile": "./src/support/index.ts",
8 | "video": true,
9 | "videosFolder": "../../dist/cypress/packages/formula-app-e2e/videos",
10 | "screenshotsFolder": "../../dist/cypress/packages/formula-app-e2e/screenshots",
11 | "chromeWebSecurity": false
12 | }
13 |
--------------------------------------------------------------------------------
/packages/formula-app-e2e/src/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io"
4 | }
5 |
--------------------------------------------------------------------------------
/packages/formula-app-e2e/src/integration/app.spec.ts:
--------------------------------------------------------------------------------
1 | import { getLoginErrors } from '../support/app.po';
2 |
3 | describe('formula-app', () => {
4 | beforeEach(() => cy.visit('/#/login-form'));
5 |
6 | it('should have a form visible', () => {
7 | getLoginErrors().contains('Your passwords do not match');
8 | });
9 |
10 | it('should toggle touched on field', () => {
11 | const input = cy.get('input[name=username]');
12 | input.click();
13 |
14 | input.should('have.attr', 'data-formula-touched', 'true');
15 | });
16 |
17 | it('should show an error when not a valid email', () => {
18 | cy.get('input[name=username]').type('foo').find('~ span').contains("Please include an '@' in the email address");
19 | });
20 |
21 | it('should show an error when not a valid domain', () => {
22 | cy.get('input[name=username]').type('foo@bar.com').find('~ span').contains('You in the svelte codes?');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/packages/formula-app-e2e/src/plugins/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example plugins/index.js can be used to load plugins
3 | //
4 | // You can change the location of this file or turn off loading
5 | // the plugins file with the 'pluginsFile' configuration option.
6 | //
7 | // You can read more here:
8 | // https://on.cypress.io/plugins-guide
9 | // ***********************************************************
10 |
11 | // This function is called when a project is opened or re-opened (e.g. due to
12 | // the project's config changing)
13 |
14 | const { preprocessTypescript } = require('@nrwl/cypress/plugins/preprocessor');
15 |
16 | module.exports = (on, config) => {
17 | // `on` is used to hook into various events Cypress emits
18 | // `config` is the resolved Cypress config
19 |
20 | // Preprocess Typescript file using Nx helper
21 | on('file:preprocessor', preprocessTypescript(config));
22 | };
23 |
--------------------------------------------------------------------------------
/packages/formula-app-e2e/src/support/app.po.ts:
--------------------------------------------------------------------------------
1 | export const getGreeting = () => cy.get('h1');
2 |
3 | export const getLoginErrors = () => cy.get('.errors');
4 |
--------------------------------------------------------------------------------
/packages/formula-app-e2e/src/support/commands.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 |
11 | // eslint-disable-next-line @typescript-eslint/no-namespace
12 | declare namespace Cypress {
13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
14 | interface Chainable {
15 | login(email: string, password: string): void;
16 | }
17 | }
18 | //
19 | // -- This is a parent command --
20 | Cypress.Commands.add('login', (email, password) => {
21 | console.log('Custom command example: Login', email, password);
22 | });
23 | //
24 | // -- This is a child command --
25 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
26 | //
27 | //
28 | // -- This is a dual command --
29 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
30 | //
31 | //
32 | // -- This will overwrite an existing command --
33 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
34 |
--------------------------------------------------------------------------------
/packages/formula-app-e2e/src/support/index.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands';
18 |
--------------------------------------------------------------------------------
/packages/formula-app-e2e/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "sourceMap": false,
5 | "outDir": "../../dist/out-tsc",
6 | "allowJs": true,
7 | "types": ["cypress", "node"]
8 | },
9 | "include": ["src/**/*.ts", "src/**/*.js"]
10 | }
11 |
--------------------------------------------------------------------------------
/packages/formula-app-e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "files": [],
4 | "include": [],
5 | "references": [
6 | {
7 | "path": "./tsconfig.e2e.json"
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/packages/svelte/formula/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@nrwl/web/babel"],
3 | "plugins": []
4 | }
5 |
--------------------------------------------------------------------------------
/packages/svelte/formula/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["../../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "rules": {},
5 | "overrides": [
6 | {
7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
8 | "parserOptions": {
9 | "project": ["packages/svelte/formula/tsconfig.*?.json"]
10 | },
11 | "rules": {}
12 | },
13 | {
14 | "files": ["*.ts", "*.tsx"],
15 | "rules": {}
16 | },
17 | {
18 | "files": ["*.js", "*.jsx"],
19 | "rules": {}
20 | }
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/packages/svelte/formula/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | displayName: 'svelte-formula',
3 | preset: '../../../jest.preset.js',
4 | globals: {
5 | 'ts-jest': {
6 | tsConfig: '/tsconfig.spec.json',
7 | },
8 | },
9 | transform: {
10 | '^.+\\.[tj]sx?$': 'ts-jest',
11 | },
12 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
13 | coverageDirectory: '../../../coverage/packages/svelte/formula',
14 | };
15 |
--------------------------------------------------------------------------------
/packages/svelte/formula/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svelte-formula",
3 | "description": "Reactive Forms for Svelte",
4 | "version": "0.12.0",
5 | "keywords": [
6 | "svelte",
7 | "sveltejs",
8 | "forms",
9 | "reactive",
10 | "data",
11 | "validation"
12 | ],
13 | "license": "MIT",
14 | "author": {
15 | "name": "Tane Piper",
16 | "url": "https://tane.dev"
17 | },
18 | "homepage": "https://tanepiper.github.io/svelte-formula",
19 | "bugs": {
20 | "url": "https://github.com/tanepiper/svelte-formula/issues"
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "https://github.com/tanepiper/svelte-formula.git"
25 | },
26 | "peerDependencies": {
27 | "svelte": ">=3.0.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/packages/svelte/formula/src/index.ts:
--------------------------------------------------------------------------------
1 | import { createForm } from './lib/form/form';
2 | import { createGroup } from './lib/group/group';
3 | import {
4 | Beaker,
5 | BeakerOptions,
6 | BeakerStores,
7 | Formula,
8 | FormulaError,
9 | FormulaOptions,
10 | FormulaStores,
11 | FormulaValue,
12 | FormulaValueDefault,
13 | } from './types';
14 |
15 | export { Beaker, BeakerStores, Formula, FormulaError, FormulaOptions, FormulaStores };
16 |
17 | /**
18 | * A global map of stores for elements with an `id` property and the `use` directive,
19 | * if no ID is used the store is not added
20 | * @type Map
21 | */
22 | export const formulaStores = new Map();
23 | export const beakerStores = new Map();
24 |
25 | /**
26 | * The `formula` function returns a form object that can be bound to any HTML
27 | * element that contains form inputs. Once bound you can get the current values
28 | *
29 | * @param options Optional options that the library supports, none of these options are
30 | * required to use Formula
31 | *
32 | * @returns Formula object containing the current form, function to update or destroy
33 | * the form and all the stores available for the form
34 | */
35 | export function formula(options?: FormulaOptions): Formula {
36 | return createForm(options, formulaStores);
37 | }
38 |
39 | /**
40 | * The beaker function returns an instance of a group of elements and their stores, it also provides methods
41 | * to set the group value store
42 | *
43 | * @param options
44 | *
45 | * @returns Beaker object containing the form group and it's associated methods
46 | */
47 | export function beaker(options?: BeakerOptions): Beaker {
48 | return createGroup(options, beakerStores);
49 | }
50 |
--------------------------------------------------------------------------------
/packages/svelte/formula/src/lib/form/aria.spec.ts:
--------------------------------------------------------------------------------
1 | import { setAriaRole, setAriaStates } from './aria';
2 |
3 | describe('Formula ARIA', () => {
4 | describe('Set Role', () => {
5 | let element: HTMLElement;
6 |
7 | afterEach(() => {
8 | document.body.removeChild(element);
9 | });
10 |
11 | it('should set radio and radiogroup', () => {
12 | element = document.createElement('div');
13 | const group = [];
14 | for (let i = 0; i < 4; i++) {
15 | const input = document.createElement('input');
16 | input.id = `testing-${i}`;
17 | input.setAttribute('type', 'radio');
18 | input.setAttribute('name', 'testing');
19 | input.value = `${i}`;
20 | group.push(input);
21 | element.appendChild(input);
22 | }
23 | document.body.appendChild(element);
24 | setAriaRole(group[0], group);
25 | expect(group[0].getAttribute('aria-role')).toBe('radio');
26 | expect(element.getAttribute('aria-role')).toBe('radiogroup');
27 | });
28 |
29 | it('should set checkbox', () => {
30 | element = document.createElement('input');
31 | element.setAttribute('type', 'checkbox');
32 | element.setAttribute('name', 'testing');
33 | document.body.appendChild(element);
34 | setAriaRole(element as any, [element] as any[]);
35 | expect(element.getAttribute('aria-role')).toBe('checkbox');
36 | });
37 |
38 | it('should set text', () => {
39 | element = document.createElement('input');
40 | element.setAttribute('type', 'text');
41 | element.setAttribute('name', 'testing');
42 | document.body.appendChild(element);
43 | setAriaRole(element as any, [element] as any[]);
44 | expect(element.getAttribute('aria-role')).toBe('input-text');
45 | });
46 |
47 | it('should set select-one', () => {
48 | element = document.createElement('select');
49 | element.setAttribute('name', 'testing');
50 | document.body.appendChild(element);
51 | setAriaRole(element as any, [element] as any[]);
52 | expect(element.getAttribute('aria-role')).toBe('select-one');
53 | });
54 |
55 | it('should set select-multiple', () => {
56 | element = document.createElement('select');
57 | element.setAttribute('multiple', 'multiple');
58 | element.setAttribute('name', 'testing');
59 | document.body.appendChild(element);
60 | setAriaRole(element as any, [element] as any[]);
61 | expect(element.getAttribute('aria-role')).toBe('select-multiple');
62 | });
63 |
64 | it('should set textarea', () => {
65 | element = document.createElement('textarea');
66 | element.setAttribute('name', 'testing');
67 | document.body.appendChild(element);
68 | setAriaRole(element as any, [element] as any[]);
69 | expect(element.getAttribute('aria-role')).toBe('textbox');
70 | });
71 |
72 | it('should set file', () => {
73 | element = document.createElement('input');
74 | element.setAttribute('type', 'file');
75 | element.setAttribute('name', 'testing');
76 | document.body.appendChild(element);
77 | setAriaRole(element as any, [element] as any[]);
78 | expect(element.getAttribute('aria-role')).toBe('file-upload');
79 | });
80 | });
81 |
82 | describe('Set State', () => {
83 | let element: HTMLElement;
84 |
85 | afterEach(() => {
86 | document.body.removeChild(element);
87 | });
88 |
89 | it('should set required', () => {
90 | element = document.createElement('input');
91 | element.setAttribute('type', 'text');
92 | element.setAttribute('name', 'testing');
93 | element.setAttribute('required', 'required');
94 | document.body.appendChild(element);
95 | setAriaStates(element as any);
96 | expect(element.getAttribute('aria-required')).toBe('required');
97 | });
98 | });
99 | });
100 |
--------------------------------------------------------------------------------
/packages/svelte/formula/src/lib/form/aria.ts:
--------------------------------------------------------------------------------
1 | import { FormEl } from '../../types';
2 |
3 | /**
4 | * Go up from the current element to the parent to find the group container, but never go further than the current
5 | * group or form
6 | * @param el
7 | */
8 | function getRadioGroupParent(el?: HTMLElement) {
9 | if (!el) {
10 | return undefined;
11 | }
12 | const isContainer = Array.from(el.querySelectorAll(':scope input[type=radio]')).length > 1;
13 | if (!isContainer) {
14 | if (!el.parentElement) {
15 | return undefined;
16 | }
17 | if (el.parentElement.dataset?.beakerGroup || el.parentElement.dataset?.formulaForm) {
18 | return undefined;
19 | }
20 | return getRadioGroupParent(el?.parentElement);
21 | }
22 | return el;
23 | }
24 |
25 | /**
26 | * Sets the ARIA role based on the input type
27 | * @param el
28 | * @param elements
29 | */
30 | export function setAriaRole(el: FormEl, elements: FormEl[]) {
31 | if (el.hasAttribute('aria-role')) {
32 | return;
33 | }
34 | if (el.type === 'radio') {
35 | if (elements.length < 2) {
36 | el.parentElement.setAttribute('aria-role', 'radiogroup');
37 | } else {
38 | const radioGroup = getRadioGroupParent(el.parentElement);
39 | if (radioGroup) radioGroup.setAttribute('aria-role', 'radiogroup');
40 | }
41 | el.setAttribute('aria-role', 'radio');
42 | } else {
43 | el.setAttribute(
44 | 'aria-role',
45 | (() => {
46 | switch (el.type) {
47 | case 'select-one':
48 | case 'select-multiple':
49 | case 'checkbox': {
50 | return el.type;
51 | }
52 | case 'file': {
53 | return 'file-upload';
54 | }
55 | case 'textarea': {
56 | return 'textbox';
57 | }
58 | default:
59 | return `input-${el.type}`;
60 | }
61 | })(),
62 | );
63 | }
64 | }
65 |
66 | /**
67 | * Sets ARIA states based on attributes on the form
68 | * @param el
69 | */
70 | export function setAriaStates(el: FormEl) {
71 | if (el.hasAttribute('required')) {
72 | el.setAttribute('aria-required', 'required');
73 | }
74 | }
75 |
76 | /**
77 | * Sets ARIA attributes based on the current value
78 | * @param element
79 | * @param elGroup
80 | */
81 | export function setAriaValue(element: FormEl, elGroup: FormEl[]): void {
82 | if (element.type === 'radio') {
83 | for (const el of elGroup) {
84 | el.removeAttribute('aria-checked');
85 | }
86 | }
87 | if ((element as HTMLInputElement).checked) {
88 | element.setAttribute('aria-checked', 'checked');
89 | } else {
90 | element.removeAttribute('aria-checked');
91 | }
92 | }
93 |
94 | /**
95 | * Set the container
96 | * @param container
97 | * @param isGroup
98 | */
99 | export function setAriaContainer(container: HTMLElement, isGroup: boolean) {
100 | if (!container.hasAttribute('aria-role')) {
101 | container.setAttribute('aria-role', isGroup ? 'row' : 'form');
102 | }
103 | }
104 |
105 | /**
106 | * Add the ARIA button role to all buttons contained in the form that don't already have an ARIA role
107 | * @param container
108 | */
109 | export function setAriaButtons(container: HTMLElement) {
110 | const nonAriaButtons = Array.from(container.querySelectorAll('button:not([aria-role])'));
111 | for (const el of nonAriaButtons) {
112 | el.setAttribute('aria-role', 'button');
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/packages/svelte/formula/src/lib/form/dirty.spec.ts:
--------------------------------------------------------------------------------
1 | import { writable } from 'svelte/store';
2 | import { createDirtyHandler } from './dirty';
3 |
4 | describe('Formula Dirty Check', () => {
5 | const storeMock: any = {
6 | dirty: writable({}),
7 | formValues: writable({}),
8 | };
9 |
10 | let element;
11 | let elements;
12 | let destroyHandler;
13 |
14 | beforeEach(() => {
15 | element = document.createElement('input');
16 | element.type = 'text';
17 | element.setAttribute('name', 'testing');
18 | elements = [element];
19 |
20 | document.body.appendChild(element);
21 | destroyHandler = createDirtyHandler('testing', elements, storeMock);
22 | });
23 |
24 | afterEach(() => {
25 | document.body.removeChild(element);
26 | destroyHandler();
27 | });
28 |
29 | it('should create the handler function', () => {
30 | expect(destroyHandler).toBeInstanceOf(Function);
31 | });
32 |
33 | it('should set the default value to false', () => {
34 | storeMock.dirty.subscribe((v) => {
35 | expect(v).toStrictEqual({ testing: false });
36 | })();
37 | });
38 |
39 | it('should not update if there is no change in value', () => {
40 | element.focus();
41 | element.blur();
42 | storeMock.dirty.subscribe((v) => {
43 | expect(v).toStrictEqual({ testing: false });
44 | })();
45 | });
46 |
47 | it('should update if there is a change in value', () => {
48 | element.focus();
49 |
50 | // Mock writing to the store
51 | storeMock.formValues.set({ testing: 'testing' });
52 |
53 | element.blur();
54 |
55 | storeMock.dirty.subscribe((v) => {
56 | expect(v).toStrictEqual({ testing: true });
57 | })();
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/packages/svelte/formula/src/lib/form/dirty.ts:
--------------------------------------------------------------------------------
1 | import { FormEl, FormulaStores } from '../../types';
2 | import { get } from 'svelte/store';
3 |
4 | /**
5 | * Check if two arrays match
6 | * @param array1
7 | * @param array2
8 | */
9 | const matchingArrays = (array1: unknown[], array2: unknown[]) => array1.every((e) => array2.includes(e));
10 |
11 | /**
12 | * Creates the handler for a group of elements for the dirty event on the group name, once an
13 | * element in the group has been changed, all element blur handlers will be removed
14 | *
15 | * @private
16 | *
17 | * @param name The name of the group to create the blur handlers for
18 | * @param elements The elements that belong to the named group
19 | * @param stores The stores for the form instance
20 | *
21 | * @returns Function that when called will remove all blur handlers from the elements, if not removed by user action
22 | */
23 | export function createDirtyHandler(name: string, elements: FormEl[], stores: FormulaStores): () => void {
24 | /**
25 | * Internal map of element blur handlers
26 | */
27 | const elementHandlers = new Map void>();
28 |
29 | /**
30 | * Internal map of initial values
31 | */
32 | const initialValues = new Map();
33 |
34 | const setDirtyAndStopListening = () => {
35 | for (const [el, handler] of elementHandlers) {
36 | el.setAttribute('data-formula-dirty', 'true');
37 | el.removeEventListener('blur', handler);
38 | }
39 | elementHandlers.clear();
40 | };
41 |
42 | // Set initial dirty state and initial value
43 | stores.dirty.update((state) => ({ ...state, [name]: false }));
44 | stores.formValues.subscribe((v) => initialValues.set(name, v[name]))();
45 |
46 | function createElementHandler(groupName: string) {
47 | return () => {
48 | const startValue = initialValues.get(groupName);
49 | const currentValues = get(stores.formValues);
50 | if (Array.isArray(currentValues[groupName])) {
51 | if (!matchingArrays(currentValues[groupName] as unknown[], startValue as unknown[])) {
52 | stores.dirty.update((state) => ({ ...state, [groupName]: true }));
53 | setDirtyAndStopListening();
54 | }
55 | } else if (currentValues[groupName] !== startValue) {
56 | stores.dirty.update((state) => ({ ...state, [groupName]: true }));
57 | setDirtyAndStopListening();
58 | }
59 | };
60 | }
61 |
62 | for (const el of elements) {
63 | const handler = createElementHandler(name);
64 | el.addEventListener('blur', handler);
65 | elementHandlers.set(el, handler);
66 | }
67 |
68 | return setDirtyAndStopListening;
69 | }
70 |
--------------------------------------------------------------------------------
/packages/svelte/formula/src/lib/form/enrichment.spec.ts:
--------------------------------------------------------------------------------
1 | import { createEnrichField } from 'packages/svelte/formula/src/lib/form/enrichment';
2 |
3 | describe('Formula Enrichment', () => {
4 | let enrich;
5 |
6 | beforeEach(() => {
7 | enrich = createEnrichField('testing', {
8 | enrich: {
9 | testing: {
10 | getLength: (value: string) => value.length,
11 | },
12 | },
13 | });
14 | });
15 |
16 | it('should update the enrich store', () => {
17 | const result = enrich('hello');
18 | expect(result).toStrictEqual({ getLength: 5 });
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/packages/svelte/formula/src/lib/form/enrichment.ts:
--------------------------------------------------------------------------------
1 | import { FormulaOptions, FormulaValue, FormulaValueDefault } from '../../types';
2 |
3 | /**
4 | * Creates an enrichment object for the named group,
5 | * @param name
6 | * @param options
7 | */
8 | export function createEnrichField(
9 | name: string,
10 | options: FormulaOptions,
11 | ): (value: unknown) => T {
12 | return (value: unknown): T =>
13 | Object.entries(options.enrich[name]).reduce((a, [key, fn]) => {
14 | a[key] = fn(value);
15 | return a;
16 | }, {}) as T;
17 | }
18 |
--------------------------------------------------------------------------------
/packages/svelte/formula/src/lib/form/errors.spec.ts:
--------------------------------------------------------------------------------
1 | import { createValidationChecker } from './errors';
2 |
3 | describe('Formula Field Validation', () => {
4 | let validationChecker;
5 | let element;
6 | let elGroup;
7 |
8 | beforeEach(() => {
9 | element = document.createElement('input');
10 | element.type = 'text';
11 | element.setAttribute('data-value-missing', 'You must provide a value');
12 | element.setAttribute('name', 'testing');
13 | element.setAttribute('required', 'required');
14 | element.setAttribute('pattern', '.{5,}');
15 |
16 | elGroup = [element];
17 |
18 | document.body.appendChild(element);
19 |
20 | validationChecker = createValidationChecker('testing', elGroup, {
21 | messages: {
22 | testing: {
23 | patternMismatch: 'You have not matched the pattern',
24 | },
25 | },
26 | validators: {
27 | testing: {
28 | startsWithCapital: (value: string) => {
29 | const firstLetterCode = value.charCodeAt(0);
30 | return firstLetterCode >= 65 && firstLetterCode <= 90
31 | ? null
32 | : 'The first character must be a capital letter';
33 | },
34 | },
35 | },
36 | });
37 | });
38 |
39 | afterEach(() => {
40 | document.body.removeChild(element);
41 | });
42 |
43 | it('should return an error if there is no value', () => {
44 | element.value = '';
45 | const result = validationChecker(element, '');
46 | expect(result.valid).toBeFalsy();
47 | });
48 |
49 | it('should return a message defined as on element data', () => {
50 | element.value = '';
51 | const result = validationChecker(element, '');
52 | expect(result.message).toBe('You must provide a value');
53 | });
54 |
55 | it('should return invalid if it does not match the pattern', () => {
56 | element.value = 'test';
57 | const result = validationChecker(element, 'test');
58 | expect(result.valid).toBeFalsy();
59 | });
60 |
61 | it('should return a custom message', () => {
62 | element.value = 'test';
63 | const result = validationChecker(element, 'test');
64 | expect(result.message).toBe('You have not matched the pattern');
65 | });
66 |
67 | it('should return custom validation', () => {
68 | element.value = 'testing';
69 | const result = validationChecker(element, 'testing');
70 | expect(result.message).toBe('The first character must be a capital letter');
71 | });
72 |
73 | it('should return valid if all conditions pass', () => {
74 | element.value = 'Testing';
75 | const result = validationChecker(element, 'Testing');
76 | expect(result.valid).toBeTruthy();
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/packages/svelte/formula/src/lib/form/errors.ts:
--------------------------------------------------------------------------------
1 | import { FormEl, FormulaError, FormulaOptions, ValidationFn } from '../../types';
2 |
3 | /**
4 | * The object returned by the {@link https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation|Contraints Validation API} cannot
5 | * be enumerated, so we need to loop over the keys to extract them
6 | *
7 | * The key object is merged with any custom errors
8 | *
9 | * @private
10 | *
11 | * @param el The elements to read the constraints from
12 | * @param custom Custom error keys
13 | *
14 | * @returns An object containing keys for validity errors, set to true
15 | */
16 | function extractErrors(el: FormEl, custom?: Record): Record {
17 | const output: Record = {};
18 | for (const key in el.validity) {
19 | if (key !== 'valid' && el.validity[key]) {
20 | output[key] = el.validity[key];
21 | }
22 | }
23 | return { ...output, ...custom };
24 | }
25 |
26 | /**
27 | * Get the result of any custom validations available on the fields.
28 | * @param value
29 | * @param validations
30 | */
31 | function getCustomValidations(
32 | value: unknown | unknown[],
33 | validations: Record = {},
34 | ): [Record, Record] {
35 | const messages: Record = {};
36 | const errors: Record = {};
37 |
38 | Object.entries(validations).forEach(([key, validation]) => {
39 | const message = validation(value);
40 | if (message !== null) {
41 | messages[key] = message;
42 | errors[key] = true;
43 | }
44 | });
45 | return [messages, errors];
46 | }
47 |
48 | /**
49 | * Create a validation checker for an element group - when an element has been updated. If there are no options
50 | * just return the element validity. If there are options, check with custom validators if thet exist,
51 | * and also check for custom messages
52 | *
53 | * @private
54 | *
55 | * @param inputGroup The name of the group of elements that this validation message will update
56 | * @param elementGroup The element group containing the elment siblings
57 | * @param options The passed formula options
58 | *
59 | * @returns Function that is called each time an element is updated which returns field validity state
60 | */
61 | export function createValidationChecker(inputGroup: string, elementGroup: FormEl[], options?: FormulaOptions) {
62 | /**
63 | * Method called each time a field is updated
64 | *
65 | * @private
66 | *
67 | * @param el The element to validate against
68 | * @param elValue The value for the element
69 | *
70 | * @returns A Formula Error object
71 | */
72 | return (el: FormEl, elValue: unknown | unknown[]): FormulaError => {
73 | // Reset the validity
74 | elementGroup.forEach((gel) => {
75 | gel.setCustomValidity('');
76 | gel.removeAttribute('data-formula-invalid');
77 | });
78 |
79 | // If there's no options, just return the current error
80 | if (!options) {
81 | const valid = el.checkValidity();
82 | if (!valid) {
83 | el.setAttribute('data-formula-invalid', 'true');
84 | }
85 | return {
86 | valid,
87 | invalid: !valid,
88 | message: el.validationMessage,
89 | errors: extractErrors(el),
90 | };
91 | }
92 |
93 | // Check for any custom messages in the options or dataset
94 | const customMessages = { ...options?.messages?.[inputGroup], ...el.dataset };
95 | // Check for any custom validations
96 | const [messages, customErrors] = getCustomValidations(elValue, options?.validators?.[inputGroup]);
97 |
98 | const errors = extractErrors(el, customErrors);
99 | const errorKeys = Object.keys(errors);
100 | // If there is no field validity issues, set custom ones
101 | if (el.checkValidity()) {
102 | if (errorKeys.length > 0) {
103 | el.setCustomValidity(messages[errorKeys[0]]);
104 | }
105 | // Check for custom messages
106 | } else {
107 | if (customMessages[errorKeys[0]]) {
108 | el.setCustomValidity(customMessages[errorKeys[0]]);
109 | }
110 | }
111 | // Recheck validity and show any messages
112 | const valid = el.checkValidity();
113 | if (!valid) {
114 | el.setAttribute('data-formula-invalid', 'true');
115 | }
116 |
117 | return {
118 | valid,
119 | invalid: !valid,
120 | message: el.validationMessage,
121 | errors,
122 | };
123 | };
124 | }
125 |
--------------------------------------------------------------------------------
/packages/svelte/formula/src/lib/form/event.ts:
--------------------------------------------------------------------------------
1 | import {
2 | FormEl,
3 | FormulaError,
4 | FormulaField,
5 | FormulaOptions,
6 | FormulaStores,
7 | FormulaValue,
8 | FormulaValueDefault,
9 | ValidationFn,
10 | } from '../../types';
11 | import { createFieldExtract } from './extract';
12 | import { createEnrichField } from './enrichment';
13 | import { get } from 'svelte/store';
14 |
15 | /**
16 | * Do validation on the form and set the form validity state and set the form to invalid if there
17 | * are any form validations
18 | * @param formValidators
19 | * @param stores
20 | */
21 | function formValidation(formValidators: Record, stores: FormulaStores) {
22 | const currentValues = get(stores.formValues);
23 | stores.formValidity.set({});
24 | const validators = Object.entries(formValidators);
25 |
26 | const invalidStates = {};
27 | for (let i = 0; i < validators.length; i++) {
28 | const [name, validator] = validators[i];
29 | const invalid = validator(currentValues);
30 | if (invalid !== null) {
31 | invalidStates[name] = invalid;
32 | }
33 | }
34 |
35 | if (Object.keys(invalidStates).length > 0) {
36 | stores.formValidity.set(invalidStates);
37 | stores.isFormValid.set(false);
38 | }
39 | }
40 |
41 | /**
42 | * Update the value and error stores, also update form validity
43 | * @param details
44 | * @param stores
45 | * @param options
46 | * @param hiddenFields
47 | * @param enrich
48 | */
49 | export function valueUpdate(
50 | details: FormulaField,
51 | stores: FormulaStores,
52 | options: FormulaOptions,
53 | hiddenFields: Map,
54 | enrich?: (value: unknown | unknown[]) => Record,
55 | ): void {
56 | const { name, value, ...validity } = details;
57 |
58 | stores.formValues.update((state) => ({ ...state, [name]: value }));
59 | if (hiddenFields.size) {
60 | stores.formValues.update((state) => {
61 | hiddenFields.forEach(
62 | (group, name) => (state[name] = group.length > 1 ? group.map((e) => e.value) : group[0].value),
63 | );
64 | return state;
65 | });
66 | }
67 | // Update validity and form validity
68 | stores.validity.update((state) => ({ ...state, [name]: validity }));
69 | stores.isFormValid.set(Object.values(get(stores.validity)).every((v: FormulaError) => v.valid));
70 | if (options?.formValidators) {
71 | formValidation(options.formValidators, stores);
72 | }
73 | if (enrich) {
74 | stores.enrichment.set({ [name]: enrich(value) });
75 | }
76 | if (typeof options?.postChanges === 'function') {
77 | options?.postChanges(get(stores.formValues));
78 | }
79 | }
80 |
81 | /**
82 | * Creates an event handler for the passed element with it's data handler
83 | * @param extractor
84 | * @param stores
85 | * @param options
86 | * @param hiddenFields
87 | * @param enrich
88 | */
89 | function createHandlerForData(
90 | extractor: (el: FormEl) => FormulaField,
91 | stores: FormulaStores,
92 | options: FormulaOptions,
93 | hiddenFields: Map,
94 | enrich?: (value: unknown | unknown[]) => Record,
95 | ) {
96 | return (event: Event) => {
97 | if (typeof options?.preChanges === 'function') options.preChanges();
98 | // Allow elements to update by letting the browser do a tick
99 | setTimeout(() => {
100 | const el = (event.currentTarget || event.target) as FormEl;
101 | valueUpdate(extractor(el), stores, options, hiddenFields, enrich);
102 | }, 0);
103 | };
104 | }
105 |
106 | /**
107 | * Create a handler for the passed element
108 | * @param name
109 | * @param eventName
110 | * @param element
111 | * @param groupElements
112 | * @param stores
113 | * @param options
114 | * @param hiddenGroups
115 | */
116 | export function createHandler(
117 | name: string,
118 | eventName: string,
119 | element: FormEl,
120 | groupElements: FormEl[],
121 | stores: FormulaStores,
122 | options: FormulaOptions,
123 | hiddenGroups: Map,
124 | ): () => void {
125 | const extract = createFieldExtract(name, groupElements, options, stores);
126 | let enrich;
127 | if (options?.enrich?.[name]) enrich = createEnrichField(name, options);
128 | const handler = createHandlerForData(extract, stores, options, hiddenGroups, enrich);
129 | element.addEventListener(eventName, handler);
130 | return () => element.removeEventListener(eventName, handler);
131 | }
132 |
133 | /**
134 | * Create a handler for a form element submission, when called it copies the contents
135 | * of the current value store to the submit store and then unsubscribes
136 | * @param stores
137 | * @param form
138 | */
139 | export function createSubmitHandler(
140 | stores: FormulaStores,
141 | form: HTMLFormElement,
142 | ): (event: Event) => void {
143 | return (): void => {
144 | if (!form.noValidate) form.reportValidity();
145 | stores.formValues.subscribe((v) => stores.submitValues.set(v))();
146 | };
147 | }
148 |
--------------------------------------------------------------------------------
/packages/svelte/formula/src/lib/form/init.ts:
--------------------------------------------------------------------------------
1 | import { FormEl, FormulaError, FormulaOptions, FormulaStores } from '../../types';
2 | import { createFieldExtract } from './extract';
3 | import { createEnrichField } from './enrichment';
4 |
5 | /**
6 | * Initialise the stores with data from the form, it will also use any default values provided
7 | * @param node
8 | * @param allGroups
9 | * @param stores
10 | * @param options
11 | */
12 | function getInitialFormValues(
13 | node: HTMLElement,
14 | allGroups: [string, FormEl[]][],
15 | stores: FormulaStores,
16 | options: FormulaOptions,
17 | ): [Record, Record, Record>] {
18 | const formValues: Record = {};
19 | const validityValues: Record = {};
20 | const enrichmentValues: Record> = {};
21 | for (const [key, elements] of allGroups) {
22 | const extract = createFieldExtract(key, elements, options, stores);
23 | const { name, value, ...validity } = extract(elements[0], true);
24 | formValues[name] = value;
25 | validityValues[name] = validity;
26 | if (options?.enrich?.[name]) {
27 | const enrich = createEnrichField(name, options);
28 | enrichmentValues[name] = enrich(value);
29 | }
30 | }
31 | stores.formValues.set({ ...formValues });
32 | stores.initialValues.set({ ...formValues });
33 | stores.validity.set({ ...validityValues });
34 | stores.isFormValid.set(Object.values({ ...validityValues }).every((v: FormulaError) => v.valid));
35 | stores.enrichment.set({ ...enrichmentValues });
36 |
37 | return [formValues, validityValues, enrichmentValues];
38 | }
39 |
40 | /**
41 | * Create the form reset method
42 |
43 | */
44 | export function createReset(
45 | node: HTMLElement,
46 | allGroups: [string, FormEl[]][],
47 | stores: FormulaStores,
48 | options: FormulaOptions,
49 | ) {
50 | const [formValues, validityValues, enrichmentValues] = getInitialFormValues(node, allGroups, stores, options);
51 | /**
52 | * Resets the form to the initial values
53 | */
54 | return () => {
55 | stores.formValues.set(formValues);
56 | stores.validity.set(validityValues);
57 | stores.isFormValid.set(Object.values(validityValues).every((v: FormulaError) => v.valid));
58 | stores.enrichment.set(enrichmentValues);
59 | // Also override touched and dirty
60 | stores.touched.set(Object.keys(formValues).reduce((val, key) => ({ ...val, [key]: false }), {}));
61 | stores.dirty.set(Object.keys(formValues).reduce((val, key) => ({ ...val, [key]: false }), {}));
62 |
63 | // Update the elements
64 | for (const [key, elements] of allGroups) {
65 | const extract = createFieldExtract(key, elements, options, stores);
66 | extract(elements[0], false, true);
67 | }
68 | };
69 | }
70 |
--------------------------------------------------------------------------------
/packages/svelte/formula/src/lib/form/touch.spec.ts:
--------------------------------------------------------------------------------
1 | import { writable } from 'svelte/store';
2 | import { createTouchHandlers } from 'packages/svelte/formula/src/lib/form/touch';
3 |
4 | describe('Formula Touch Handler', () => {
5 | const storeMock: any = {
6 | touched: writable({}),
7 | };
8 |
9 | let element;
10 | let elements;
11 | let destroyHandler;
12 |
13 | beforeEach(() => {
14 | element = document.createElement('input');
15 | element.type = 'text';
16 | element.setAttribute('name', 'testing');
17 | elements = [element];
18 |
19 | document.body.appendChild(element);
20 |
21 | destroyHandler = createTouchHandlers('testing', elements, storeMock);
22 | });
23 |
24 | afterEach(() => {
25 | document.body.removeChild(element);
26 | destroyHandler();
27 | });
28 |
29 | it('should create the handler function', () => {
30 | expect(destroyHandler).toBeInstanceOf(Function);
31 | });
32 |
33 | it('should set the default value to false', () => {
34 | storeMock.touched.subscribe((v) => {
35 | expect(v).toStrictEqual({ testing: false });
36 | })();
37 | });
38 |
39 | it('should update the store on focus', () => {
40 | element.focus();
41 | storeMock.touched.subscribe((v) => {
42 | expect(v).toStrictEqual({ testing: true });
43 | })();
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/packages/svelte/formula/src/lib/form/touch.ts:
--------------------------------------------------------------------------------
1 | import { FormEl, FormulaStores } from '../../types';
2 |
3 | /**
4 | * Creates the handler for a group of elements for the touch event on the group name, once an
5 | * element in the group has been touched, all element focus handlers will be removed
6 | *
7 | * @private
8 | *
9 | * @param name The name of the group to create the touch handlers for
10 | * @param elements The elements that belong to the named group
11 | * @param stores The stores for the form instance
12 | *
13 | * @returns Function that when called will remove all focus handlers from the elements, if not removed by user action
14 | */
15 | export function createTouchHandlers(name: string, elements: FormEl[], stores: FormulaStores): () => void {
16 | /**
17 | * Internal map of element focus handlers
18 | */
19 | const elementHandlers = new Map void>();
20 |
21 | /**
22 | * Method to call when handlers should be destroyed, clean up handlers then remove the handlers
23 | */
24 | const destroy = () => {
25 | for (const [el, handler] of elementHandlers) {
26 | el.setAttribute('data-formula-touched', 'true');
27 | el.removeEventListener('focus', handler);
28 | }
29 | elementHandlers.clear();
30 | };
31 |
32 | // Set the current touched state to false
33 | stores.touched.update((state) => ({ ...state, [name]: false }));
34 |
35 | /**
36 | * Creates the instance of the handler for each element
37 | */
38 | function createElementHandler() {
39 | return () => {
40 | stores.touched.update((state) => ({ ...state, [name]: true }));
41 | destroy();
42 | };
43 | }
44 |
45 | for (const el of elements) {
46 | const handler = createElementHandler();
47 | el.addEventListener('focus', handler);
48 | elementHandlers.set(el, handler);
49 | }
50 |
51 | return destroy;
52 | }
53 |
--------------------------------------------------------------------------------
/packages/svelte/formula/src/lib/shared/fields.spec.ts:
--------------------------------------------------------------------------------
1 | import { getFormFields, getGroupFields } from 'packages/svelte/formula/src/lib/shared/fields';
2 |
3 | describe('Formula Fields Methods', () => {
4 | let root;
5 | let groupEl;
6 | beforeAll(() => {
7 | root = document.createElement('form');
8 | const inputWithName1 = document.createElement('input');
9 | inputWithName1.setAttribute('type', 'text');
10 | inputWithName1.setAttribute('name', 'testing1');
11 |
12 | const inputWithName2 = document.createElement('input');
13 | inputWithName2.setAttribute('type', 'text');
14 | inputWithName2.setAttribute('name', 'testing2');
15 |
16 | const inputWithoutName = document.createElement('input');
17 | inputWithoutName.setAttribute('type', 'text');
18 |
19 | const subEl = document.createElement('div');
20 | const inputInSub = document.createElement('input');
21 | inputInSub.setAttribute('type', 'text');
22 | inputInSub.setAttribute('name', 'testing3');
23 | subEl.appendChild(inputInSub);
24 |
25 | groupEl = document.createElement('div');
26 | groupEl.id = 'group-test-1';
27 | const inputInGroup1 = document.createElement('input');
28 | inputInGroup1.setAttribute('type', 'text');
29 | inputInGroup1.setAttribute('name', 'testing4');
30 | inputInGroup1.setAttribute('data-in-group', 'group-test-1');
31 | groupEl.appendChild(inputInGroup1);
32 |
33 | const inputInGroup2 = document.createElement('input');
34 | inputInGroup2.setAttribute('type', 'text');
35 | inputInGroup2.setAttribute('name', 'testing5');
36 | inputInGroup2.setAttribute('data-in-group', 'group-test-1');
37 | groupEl.appendChild(inputInGroup2);
38 |
39 | root.appendChild(inputWithName1);
40 | root.appendChild(inputWithoutName);
41 | root.appendChild(subEl);
42 | root.appendChild(groupEl);
43 | root.appendChild(inputWithName2);
44 |
45 | document.body.appendChild(root);
46 | });
47 |
48 | afterAll(() => {
49 | document.body.removeChild(root);
50 | });
51 |
52 | describe('getFormFields', () => {
53 | it('should find elements only with name attribute and not in groups', () => {
54 | const elements = getFormFields(root);
55 | expect(elements.length).toBe(3);
56 | });
57 | });
58 |
59 | describe('getGroupFields', () => {
60 | it('should find elements any elements with name', () => {
61 | const elements = getGroupFields(groupEl);
62 | expect(elements.length).toBe(2);
63 | });
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/packages/svelte/formula/src/lib/shared/fields.ts:
--------------------------------------------------------------------------------
1 | import { FormEl } from '../../types';
2 |
3 | /**
4 | * Extract all fields from the form that are valid inputs with `name` property that are not part of a form group
5 | *
6 | * @private
7 | * @internal
8 | * @param rootEl
9 | */
10 | export function getFormFields(rootEl: HTMLElement): FormEl[] {
11 | const nodeList = rootEl.querySelectorAll('*[name]:not([data-in-group])') as NodeListOf;
12 | return Array.from(nodeList).filter((el: FormEl) => el.checkValidity) as FormEl[];
13 | }
14 |
15 | /**
16 | * Extract all fields from a group that are valid inputs with `name` property
17 | * @param rootEl
18 | */
19 | export function getGroupFields(rootEl: HTMLElement): FormEl[] {
20 | const nodeList = rootEl.querySelectorAll('*[name]') as NodeListOf;
21 | return Array.from(nodeList).filter((el: FormEl) => el.checkValidity) as FormEl[];
22 | }
23 |
--------------------------------------------------------------------------------
/packages/svelte/formula/src/lib/shared/stores.spec.ts:
--------------------------------------------------------------------------------
1 | import { createFormStores } from 'packages/svelte/formula/src/lib/shared/stores';
2 | import { get } from 'svelte/store';
3 |
4 | describe('Formula Stores', () => {
5 | it('should create empty default stores', () => {
6 | const stores = createFormStores();
7 | expect(get(stores.formValues)).toStrictEqual({});
8 | });
9 |
10 | it('should create store with default values', () => {
11 | const stores = createFormStores({
12 | defaultValues: {
13 | foo: 'testing',
14 | bar: 'formula',
15 | },
16 | });
17 | expect(get(stores.formValues)).toStrictEqual({ foo: 'testing', bar: 'formula' });
18 | });
19 |
20 | it('should create store with default validity', () => {
21 | const stores = createFormStores({
22 | defaultValues: {
23 | foo: 'testing',
24 | },
25 | });
26 | expect(get(stores.validity)).toStrictEqual({
27 | foo: {
28 | valid: true,
29 | invalid: false,
30 | message: '',
31 | errors: {},
32 | },
33 | });
34 | });
35 |
36 | it('should create store with default touched', () => {
37 | const stores = createFormStores({
38 | defaultValues: {
39 | foo: 'testing',
40 | },
41 | });
42 | expect(get(stores.touched)).toStrictEqual({ foo: false });
43 | });
44 |
45 | it('should create store with default dirty', () => {
46 | const stores = createFormStores({
47 | defaultValues: {
48 | foo: 'testing',
49 | },
50 | });
51 | expect(get(stores.dirty)).toStrictEqual({ foo: false });
52 | });
53 |
54 | it('should create store with default enrichment', () => {
55 | const stores = createFormStores({
56 | defaultValues: {
57 | foo: 'testing',
58 | },
59 | enrich: {
60 | foo: {
61 | valueLength: (value: string) => value.length,
62 | },
63 | },
64 | });
65 | expect(get(stores.enrichment)).toStrictEqual({
66 | foo: {
67 | valueLength: 7,
68 | },
69 | });
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/packages/svelte/formula/src/types/enrich.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Enrich function is used with field data to generate an enrichment
3 | */
4 | export type EnrichFn = (value: unknown | unknown[]) => unknown;
5 |
6 | /**
7 | * A single validation rule with the name of the rule and validation function
8 | */
9 | export type EnrichValue = Record;
10 |
11 | /**
12 | * Custom validation rules for Formula
13 | */
14 | export type EnrichFields = Record;
15 |
--------------------------------------------------------------------------------
/packages/svelte/formula/src/types/forms.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Single type for use where we want any type of HTML form element, before we narrow
3 | * down to a single
4 | * @internal
5 | */
6 | export type FormEl = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
7 |
8 | /**
9 | * An error state for an form input
10 | */
11 | export interface FormulaError {
12 | /**
13 | * If the field is valid
14 | */
15 | valid: boolean;
16 | /**
17 | * If the field is invalid
18 | */
19 | invalid: boolean;
20 | /**
21 | * The message returned from the HTML element
22 | */
23 | message: string;
24 | /**
25 | * The errors from the {@link https://developer.mozilla.org/en-US/docs/Web/API/Constraint_validation|Contraint Validation API}
26 | */
27 | errors: Record;
28 | }
29 |
30 | /**
31 | * Internally extracted data from the form element, used to generate other store values
32 | * @internal
33 | */
34 | export interface FormulaField extends FormulaError {
35 | /**
36 | * The name of the field being handled
37 | */
38 | name: string;
39 | /**
40 | * The current value or values of the field
41 | */
42 | value: unknown | unknown[];
43 | }
44 |
45 | /**
46 | * Form Errors
47 | */
48 | export type FormErrors = Record;
49 |
--------------------------------------------------------------------------------
/packages/svelte/formula/src/types/formula.ts:
--------------------------------------------------------------------------------
1 | import { Writable } from 'svelte/store';
2 | import { FormulaError } from './forms';
3 | import { FormulaOptions } from './options';
4 |
5 | /**
6 | * Internal type for object
7 | */
8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
9 | export type FormulaValue = Record;
10 | export type FormulaValueDefault = Record;
11 |
12 | type PartialFormValue = Partial>;
13 |
14 | /**
15 | * The stores available in Formula
16 | */
17 | export interface FormulaStores {
18 | /**
19 | * A store containing the current form values
20 | */
21 | formValues: Writable;
22 | /**
23 | * A store containing the values at the time of `