diff --git a/docs-app/.docfy-config.js b/docs-app/.docfy-config.js deleted file mode 100644 index efc9f3b..0000000 --- a/docs-app/.docfy-config.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -const path = require('path'); - -const monorepoRoot = path.resolve(__dirname, '..'); - -module.exports = { - repository: { - url: 'https://github.com/CrowdStrike/@universal-ember/form', - editBranch: 'main', - }, - sources: [ - { - root: path.resolve(monorepoRoot, 'docs'), - pattern: '**/*.md', - // if set to "manual", the URL will need to be specified in each markdown file - urlSchema: 'auto', - urlPrefix: 'docs', - }, - ], -}; diff --git a/docs-app/.editorconfig b/docs-app/.editorconfig deleted file mode 100644 index c35a002..0000000 --- a/docs-app/.editorconfig +++ /dev/null @@ -1,19 +0,0 @@ -# EditorConfig helps developers define and maintain consistent -# coding styles between different editors and IDEs -# editorconfig.org - -root = true - -[*] -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true -indent_style = space -indent_size = 2 - -[*.hbs] -insert_final_newline = false - -[*.{diff,md}] -trim_trailing_whitespace = false diff --git a/docs-app/.ember-cli b/docs-app/.ember-cli deleted file mode 100644 index 3e90db4..0000000 --- a/docs-app/.ember-cli +++ /dev/null @@ -1,15 +0,0 @@ -{ - /** - Ember CLI sends analytics information by default. The data is completely - anonymous, but there are times when you might want to disable this behavior. - - Setting `disableAnalytics` to true will prevent any data from being sent. - */ - "disableAnalytics": false, - /** - Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript - rather than JavaScript by default, when a TypeScript version of a given blueprint is available. - */ - "isTypeScriptProject": false, - "port": 4201 -} \ No newline at end of file diff --git a/docs-app/.gitignore b/docs-app/.gitignore index f1e859b..8b8a365 100644 --- a/docs-app/.gitignore +++ b/docs-app/.gitignore @@ -1,32 +1,6 @@ -# See https://help.github.com/ignore-files/ for more about ignoring files. - -# compiled output -/dist/ -/tmp/ - -# dependencies -/bower_components/ -/node_modules/ - -# misc -/.env* -/.pnp* -/.sass-cache -/.eslintcache -/connect.lock -/coverage/ -/libpeerconnection.log -/npm-debug.log* -/testem.log -/yarn-error.log - -# ember-try -/.node_modules.ember-try/ -/bower.json.ember-try -/npm-shrinkwrap.json.ember-try -/package.json.ember-try -/package-lock.json.ember-try -/yarn.lock.ember-try - -# broccoli-debug -/DEBUG/ +dist/ +node_modules/ +.eslintcache +.env* +!.env.example +tmp/ diff --git a/docs-app/.prettierignore b/docs-app/.prettierignore index 4178fd5..29c69b2 100644 --- a/docs-app/.prettierignore +++ b/docs-app/.prettierignore @@ -1,25 +1,3 @@ -# unconventional js -/blueprints/*/files/ -/vendor/ - -# compiled output -/dist/ -/tmp/ - -# dependencies -/bower_components/ -/node_modules/ - -# misc -/coverage/ -!.* -.eslintcache -.lint-todo/ - -# ember-try -/.node_modules.ember-try/ -/bower.json.ember-try -/npm-shrinkwrap.json.ember-try -/package.json.ember-try -/package-lock.json.ember-try -/yarn.lock.ember-try +dist/ +node_modules/ +pnpm-lock.yaml diff --git a/docs-app/.prettierrc.js b/docs-app/.prettierrc.js deleted file mode 100644 index 5ee0a9c..0000000 --- a/docs-app/.prettierrc.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict'; - -module.exports = { - plugins: ['prettier-plugin-ember-template-tag'], - singleQuote: true, - templateSingleQuote: false, - // this was required to make the VSCode + Prettier work correctly with satisfies TOC<{ Element: HTMLAnchorElement }>; diff --git a/docs-app/src/config.ts b/docs-app/src/config.ts new file mode 100644 index 0000000..5933244 --- /dev/null +++ b/docs-app/src/config.ts @@ -0,0 +1,10 @@ +const ENV = { + modulePrefix: 'docs-app', + environment: import.meta.env.DEV ? 'development' : 'production', + rootURL: '/', + locationType: 'history', + EmberENV: {}, + APP: {} as Record, +}; + +export default ENV; diff --git a/docs-app/src/router.ts b/docs-app/src/router.ts new file mode 100644 index 0000000..684f9ab --- /dev/null +++ b/docs-app/src/router.ts @@ -0,0 +1,31 @@ +import { registerDestructor } from '@ember/destroyable'; +import EmbroiderRouter from '@embroider/router'; + +import { properLinks } from 'ember-primitives/proper-links'; +import { addRoutes } from 'kolay'; + +import config from '#config'; + +@properLinks({ + ignore: ['/tests'], +}) +export default class Router extends EmbroiderRouter { + location = config.locationType; + rootURL = config.rootURL; + + constructor(...args: unknown[]) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + super(...(args as any[])); + + const scroll = () => window.scrollTo(0, 0); + + this.on('routeDidChange', scroll); + registerDestructor(this, () => { + this.off('routeDidChange', scroll); + }); + } +} + +Router.map(function () { + addRoutes(this); +}); diff --git a/docs-app/src/routes/api-docs.gts b/docs-app/src/routes/api-docs.gts new file mode 100644 index 0000000..3706e73 --- /dev/null +++ b/docs-app/src/routes/api-docs.gts @@ -0,0 +1,26 @@ +import { + APIDocs as KolayAPIDocs, + ComponentSignature as KolayComponentSignature, +} from 'kolay'; + +import type { TOC } from '@ember/component/template-only'; + +export const APIDocs: TOC<{ + Args: { declaration: string; name: string }; +}> = ; + +export const ComponentSignature: TOC<{ + Args: { declaration: string; name: string }; +}> = ; diff --git a/docs-app/src/routes/application.ts b/docs-app/src/routes/application.ts new file mode 100644 index 0000000..addcbd0 --- /dev/null +++ b/docs-app/src/routes/application.ts @@ -0,0 +1,81 @@ +import Route from '@ember/routing/route'; + +import rehypeShikiFromHighlighter from '@shikijs/rehype/core'; +import { setupTabster } from 'ember-primitives/tabster'; +import { setupKolay } from 'kolay/setup'; +import { createHighlighterCore } from 'shiki/core'; +import { createOnigurumaEngine } from 'shiki/engine/oniguruma'; + +import { Callout } from '@universal-ember/docs-support'; + +import { APIDocs, ComponentSignature } from './api-docs.gts'; + +export default class Application extends Route { + async model() { + const highlighter = await createHighlighterCore({ + themes: [ + import('shiki/themes/github-dark.mjs'), + import('shiki/themes/github-light.mjs'), + ], + langs: [ + import('shiki/langs/javascript.mjs'), + import('shiki/langs/typescript.mjs'), + import('shiki/langs/bash.mjs'), + import('shiki/langs/css.mjs'), + import('shiki/langs/diff.mjs'), + import('shiki/langs/html.mjs'), + import('shiki/langs/glimmer-js.mjs'), + import('shiki/langs/glimmer-ts.mjs'), + import('shiki/langs/handlebars.mjs'), + import('shiki/langs/jsonc.mjs'), + import('shiki/langs/markdown.mjs'), + ], + engine: createOnigurumaEngine(import('shiki/wasm')), + }); + + const [manifest] = await Promise.all([ + setupTabster(this), + setupKolay(this, { + topLevelScope: { + Callout, + APIDocs, + ComponentSignature, + }, + modules: { + // this repo's libraries + '@universal-ember/form': () => import('@universal-ember/form'), + '@universal-ember/form-yup': () => + import('@universal-ember/form-yup'), + '@universal-ember/form-changeset': () => + import('@universal-ember/form-changeset'), + + // demo dependencies + yup: () => import('yup'), + 'ember-changeset': () => import('ember-changeset'), + 'validated-changeset': () => import('validated-changeset'), + + // community libraries + 'ember-resources': () => import('ember-resources'), + 'tracked-built-ins': () => import('tracked-built-ins'), + 'ember-primitives': () => import('ember-primitives'), + kolay: () => import('kolay'), + }, + rehypePlugins: [ + [ + rehypeShikiFromHighlighter, + highlighter, + { + defaultColor: false, + themes: { + light: 'github-light', + dark: 'github-dark', + }, + }, + ], + ], + }), + ]); + + return { manifest }; + } +} diff --git a/docs-app/src/routes/page.ts b/docs-app/src/routes/page.ts new file mode 100644 index 0000000..dd70555 --- /dev/null +++ b/docs-app/src/routes/page.ts @@ -0,0 +1,13 @@ +import Route from '@ember/routing/route'; + +import { handlePotentialIndexVisit } from 'kolay'; + +import type RouterService from '@ember/routing/router-service'; + +type Transition = ReturnType; + +export default class PageRoute extends Route { + beforeModel(transition: Transition) { + handlePotentialIndexVisit(this, transition); + } +} diff --git a/docs-app/src/styles/app.css b/docs-app/src/styles/app.css new file mode 100644 index 0000000..1eabe50 --- /dev/null +++ b/docs-app/src/styles/app.css @@ -0,0 +1,96 @@ +@import './docs-support.css'; + +/* + * Small utility set used by the doc pages' demos + * (kept intentionally tiny -- this app does not use tailwind) + */ +.flex { + display: flex; +} +.flex-col { + flex-direction: column; +} +.flex-row { + flex-direction: row; +} +.flex-1 { + flex: 1 1 0%; +} +.flex-initial { + flex: 0 1 auto; +} +.flex-wrap { + flex-wrap: wrap; +} +.my-2 { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} +.mx-auto { + margin-left: auto; + margin-right: auto; +} +.px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} +.p-8 { + padding: 2rem; +} +.gap-8 { + gap: 2rem; +} +.gap-12 { + gap: 3rem; +} +.space-x-2 > * + * { + margin-left: 0.5rem; +} +.border { + border: 1px solid #94a3b8; +} +.border-2 { + border-width: 2px; +} +.border-purple-500 { + border-color: #a855f7; +} +.rounded { + border-radius: 0.25rem; +} +.cursor-pointer { + cursor: pointer; +} +.justify-around { + justify-content: space-around; +} + +/* demo container tweaks */ +.featured-demo { + button { + color: black; + background: white; + border: 1px solid; + padding: 0 0.25rem; + } + + &[data-demo-fit] { + .glimdown-render { + max-height: unset; + } + } + + &[data-demo-tight] { + .glimdown-render { + padding: 2rem; + } + } +} + +.glimdown-render { + input, + select, + textarea { + color: black; + } +} diff --git a/docs-app/src/styles/docs-support.css b/docs-app/src/styles/docs-support.css new file mode 100644 index 0000000..2b71f03 --- /dev/null +++ b/docs-app/src/styles/docs-support.css @@ -0,0 +1,230 @@ +.typedoc__type-link, +.typedoc__unknown__yield, +.typedoc-intrinsic { + border: 1px solid #222; + display: inline-block; + font-style: italic; + font-family: monospace; + margin: 0; + font-size: 0.75rem; + padding: 0 0.5rem; +} + +.typedoc__type-link { + padding: 0 0.5rem; +} + +.typedoc-declaration-name { + margin: 0; + display: inline-block; + line-height: 1.5rem; +} +.typedoc-declaration { + .typedoc-declaration-name { + font-weight: bold; + } +} +section { + > .typedoc-declaration { + > .typedoc-declaration-name { + font-size: 1.5rem; + } + } +} + +.typedoc-heading { + display: block; +} + +.typedoc-property, +.typedoc-component-arg { + padding: 0 1rem; +} + +.typedoc-declaration-signatures { + list-style: none; +} + +.typedoc-named-tuple, +.typedoc-component-arg-info { + display: flex; + gap: 0.25rem; + align-items: baseline; + justify-content: space-between; +} +.typedoc-component-arg-info > .typedoc-name { + display: inline-block; + margin: 0; + font-size: 1rem; + font-weight: bold; +} + +.typedoc-declaration-children { + list-style: none; + padding: 0 0.5rem; +} + +/** + * + * Component Signatures + * + */ +.typedoc__component-signature__element, +.typedoc__component-signature__block { + display: block; + padding: 0 1rem; +} +.typedoc__component-signature__element-type { + display: flex; + gap: 0.25rem; + align-items: baseline; + justify-content: space-between; +} + +.typedoc__component-signature__element-type > .typedoc__name { + display: inline-block; + margin: 0; + line-height: 1.5rem; +} + +.typedoc__component-signature__block + .typedoc-declaration-name + + .typedoc-reference { + margin-bottom: -0.25rem; +} +.typedoc__component-signature__block + > .typedoc-property + .typedoc-declaration-children { + display: grid; + gap: 0.5rem; +} +.typedoc__component-signature__block + > .typedoc-property + > .typedoc-declaration + > ul.typedoc-declaration-children + > li + > .typedoc-declaration { + @apply p-6 rounded-xl bg-sky-50 dark:bg-slate-800/60 dark:ring-1 dark:ring-slate-300/10; +} + +.typedoc-component-arg { + margin-bottom: 0.5rem; + display: grid; + gap: 0.25rem; +} +.typedoc-component-arg > .typedoc-name, +.typedoc__component-signature__block > .typedoc__name { + font-size: 1.2rem; + overflow-y: hidden; + overflow-x: hidden; + overflow: hidden; + max-height: unset; +} + +.typedoc-component-arg .typedoc-rendered-comment p { + margin-top: 0; + margin-bottom: 0.25rem; +} + +/** + * References + */ +.typedoc__reference { +} +.typedoc__reference__name { + display: inline; + border: 1px solid #222; + font-style: italic; + font-family: monospace; + margin: 0; + font-size: 0.75rem; + padding: 0 0.5rem; +} +.typedoc__reference__typeArguments { + display: inline-grid; + width: fit-content; + grid-auto-flow: column; + gap: 0.5rem; +} +.typedoc__reference__typeArgument { + border: 1px solid; +} + +/** + * Array formatting + */ +.typedoc__array > div.typedoc-declaration > ul.typedoc-declaration-children { + border: 1px solid; + margin: 0; +} + +/** + * Signature / Function Formatting + */ +.typedoc__function__type { + display: flex; + gap: 0.25rem; +} + +.typedoc__function__type:has(.typedoc__function__parameter) { + display: grid; +} + +.typedoc__function__parameters { + margin-left: 1rem; + display: grid; + gap: 0.25rem; +} +.typedoc__function__parameter__container { + border: 1px solid; + padding: 0.5rem; +} +.typedoc__function__parameter { + width: fit-content; + display: grid; + grid-auto-flow: column; + gap: 0.5rem; +} +.typedoc__function__parameter__comment { + /* font-style: italic; */ + /* font-size: 0.75rem; */ +} +.typedoc__function__return_type, +.typedoc__function__close { + display: inline-block; +} +.typedoc__function__return_type { + margin-left: 1rem; +} +.typedoc__function__parameter__name { + font-style: italic; + font-family: monospace; + font-size: 1rem; +} + +/** + * Literal + */ +.typedoc__literal { + font-family: monospace; + font-size: 0.75rem; +} + +/** + * Union + */ +.typedoc__union { + display: inline-flex; + flex-wrap: wrap; + gap: 0.25rem; +} +.typedoc__union__type { + display: inline-flex; + gap: 0.25rem; +} +.typedoc__union__type::before { + content: '|'; +} +.typedoc__union .typedoc__union__type:first-child::before { + display: none; +} diff --git a/docs-app/src/templates/1-get-started/index.gjs.md b/docs-app/src/templates/1-get-started/index.gjs.md new file mode 100644 index 0000000..12f43d6 --- /dev/null +++ b/docs-app/src/templates/1-get-started/index.gjs.md @@ -0,0 +1,26 @@ +# Getting started + +## Compatibility + +- Ember.js 4.4 or above (CI-verified on 5.8+) +- a modern (vite) app, or a classic build via ember-auto-import v2 / Embroider +- optionally: TypeScript and Glint (see [TypeScript support](/4-typescript/index.md)) + +## Installation + +To use `@universal-ember/form` in your project, run one of the following commands in your terminal: + +```bash +pnpm add @universal-ember/form +# or +yarn add @universal-ember/form +# or +npm install @universal-ember/form +``` + +There are additional integration packages for validation libraries: + +- [`@universal-ember/form-yup`](/3-validation/yup.md) for [yup](https://github.com/jquense/yup) +- [`@universal-ember/form-changeset`](/3-validation/ember-changeset.md) for [ember-changeset](https://github.com/poteto/ember-changeset) + +Continue with [basic usage](/2-usage/index.md) to build your first form. diff --git a/docs-app/src/templates/2-usage/async.gjs.md b/docs-app/src/templates/2-usage/async.gjs.md new file mode 100644 index 0000000..62b15ba --- /dev/null +++ b/docs-app/src/templates/2-usage/async.gjs.md @@ -0,0 +1,80 @@ +# Managing asynchronous state + +`@universal-ember/form` knows about two events that can be asynchronous: + +- **validation** will often be synchronous, but you can also define [asynchronous validations](/3-validation/custom-validation.md#asynchronous-validation) for e.g. validating data on the server +- **submission** is most often asynchronous when e.g. sending a `POST` request with your form data to the server + +To make the form aware of the asynchronous submission process, you just need to return a Promise from the submit callback passed to [`@onSubmit`](/2-usage/data.md#getting-data-out). + +`@universal-ember/form` will then make the async state of both these events available to you in the template. This allows for use cases like + +- disabling the submit button while a submission is ongoing +- showing a loading indicator while submission or validation is pending +- rendering the results of the (either successful or failed) submission, after it is resolved/rejected + +To enable these, the form component is yielding `validationState` and `submissionState` objects with these properties: + +- `isPending` +- `isResolved` +- `isRejected` +- `value` (when resolved) +- `error` (when rejected) + +These derived properties are fully reactive and typed, as these are provided by the excellent [ember-async-data](https://github.com/tracked-tools/ember-async-data) library. Refer to their documentation for additional details! + +Submit this form with a valid email, and with the same email again, to see how it disables the submit button, changes its label, and shows error messages coming from the "backend": + + diff --git a/docs-app/src/templates/2-usage/controls.gjs.md b/docs-app/src/templates/2-usage/controls.gjs.md new file mode 100644 index 0000000..d5f73a5 --- /dev/null +++ b/docs-app/src/templates/2-usage/controls.gjs.md @@ -0,0 +1,180 @@ +# Form controls + +Controls as we use the term here refer to the UI widgets that allow a user to enter data. In its most basic form that would be an ``. + +`@universal-ember/form` comes with support for the following controls built-in (but you can also use [custom controls](/2-usage/custom-controls.md)), all yielded from the `Field` component: + +## Input + +Renders a basic `` element. Set `@type` to any of the supported [input types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types) other than the default `text`. + +Note that `checkbox` and `radio` should not be used, as they have dedicated control components (see below). +Also these types are not useful to use as input controls: `button`,`file`,`image`,`reset`,`submit`. + +```gjs live preview no-shadow +import { HeadlessForm } from '@universal-ember/form'; + + +``` + +## Textarea + +Renders a `