diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a398c8f..b9c8e33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,8 +57,7 @@ jobs: - ember-lts-4.4 - ember-release - embroider-safe - # Disabled pending a shift away from string-based `implementationMap` lookups - # - embroider-optimized + - embroider-optimized steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index b9226f3..bfc433c 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ The entry point to a UI powered by ember-exclaim is the `` component. - `@implementationMap`: a mapping of names in the `ui` config to information about their backing implementations - `@onChange(envPathOfChangedValue)`: an optional function that will be invoked when a value in the `env` changes - `@wrapper`: an optional component that will wrap every rendered component in your UI configuration. The `wrapper` component will receive the `ComponentSpec` as `@spec` ([more on `ComponentSpec` here](ember-exclaim/src/-private/GLOSSARY.md)), the `Environment` as `@env` and the component's resolved `@config`. + - `@useClassicReactivity`: an optional flag that, if set, will cause any environment bindings Exclaim constructs to use classic Ember `computed` machinery rather than native getters and setters that assume data is appropriately `@tracked`. Each of these things is described in further detail below. @@ -161,7 +162,7 @@ Note that `$bind` works with paths, too, so `{ $bind: 'foo.bar' }` would access ### The Implementation Map The `@implementationMap` given to `` dictates what components it can render. It should be a hash whose keys are the component and helper names available for use in the UI config. The value for each key should itself be a hash describing the component or helper with that name. - - `componentPath` (for components): the name to the Ember component to be invoked when this exclaim-ui component is used in the config, as you'd give it to the `{{component}}` helper + - `component` (for components): the name to the Ember component to be invoked when this exclaim-ui component is used in the config, as you'd give it to the `{{component}}` helper - `helper` (for helper functions): a function that receives a `config` hash and `env` information and should return the output value for the helper - `shorthandProperty` (optional for both helpers and components): the name of a property that should be populated when shorthand notation is used for this component or helper (see above) @@ -169,11 +170,11 @@ The `@implementationMap` given to `` dictates what components it can The [demo app](https://salsify.github.io/ember-exclaim) for this repo contains [a variety of simple component implementations](tests/dummy/app/components/exclaim-components) that you can use as a starting point for building your own. -An ember-exclaim component implementation will receive two properties when rendered: `config` and `env`. +An ember-exclaim component implementation will receive two arguments when rendered: `@config` and `@env`. ### `@config` -The `@config` argument of the implementing component will contain all other information supplied in the `$component` hash representing it in the UI config. Any `$bind` directives in that config will be automatically be resolved when they are `get` or `set`. As an example, consider a lightweight implementation of the `input` component mentioned above. +The `@config` argument of the implementing component will contain all other information supplied in the `$component` hash representing it in the UI config. Any `$bind` directives in that config will be automatically be resolved when they are read or written. As an example, consider a lightweight implementation of the `input` component mentioned above. ```hbs @@ -197,10 +198,27 @@ For example, the [`vbox`](tests/dummy/app/components/exclaim-components/vbox) co {{/each}} ``` -By default, children will inherit the environment of their parent. This environment can be extended by passing a POJO with additional key/value pairs as a second parameter to `{{yield}}`. Check the implementation of [`each`](playground-app/app/components/exclaim-components/each) and [`let`](playground-app/app/components/exclaim-components/let) in the demo app for examples of how this can be used. +By default, children will inherit the environment of their parent. This environment can be extended by passing a POJO with additional key/value pairs as a second parameter to `{{yield}}`. Check [the implementation of `each` and `let`](playground-app/app/components/exclaim-components/) in the demo app for examples of how this can be used. ## Implementing Helpers The [demo app](https://salsify.github.io/ember-exclaim) for this repo contains [a handful of helper implementations](tests/dummy/app/utils/exclaim-helpers) that you can use as a starting point for building your own. An ember-exclaim helper implementation is simply a function that takes two arguments, `config` and `env`, which are the same two values described for components above. The value returned when this function is called will be the ultimate value of the `{ $helper: ... }` hash in the UI configuration. + +## Migrating from v1 to v2 + +The v2 release of `ember-exclaim` simplified and modernized the internals of the addon to enable clean operation against `@tracked` data, while also eliminating the need for using `get` and `unwrap` when working with data using the classic `computed` reactivity model. In addition, some inconsistencies and overly-complex APIs that had organically evolved over the course of v1 were cleaned up, resulting in a handful of breaking changes: + + - Exclaim now requires Ember 3.28+ and has dropped support for Internet Explorer. + - By default, `ExclaimUi` now uses native getters and setters for helpers and bindings in UI config, assuming data in the environment is appropriately `@tracked`. + - Support for the "classic" `computed` reactivity model is now opt-in via the `@useClassicReactivity` flag on `ExclaimUi`. + - Calling `.get()` or `.set()` on an object retrieved from a component's config or environment is now deprecated with the classic reactivity model, and fully unavailable under the tracked model. Fields on config or the environment may be read via direct access, and should use Ember's importable `set` if they require classic reactivity semantics. + - The `@env` passed into `ExclaimUi` is no longer wrapped in an `Environment` object + - It no longer automatically has `EmberObject` methods such as `get` and `set`. + - There is no longer an `.extend()` method; instead of yielding `this.args.env.extend(someExtraData)` to expose extra data to children, components should just yield `someExtraData`. + - The rarely-used `wrap` export has been removed, and the less-rarely-used `unwrap` export is now a deprecated no-op. + - The `@resolveFieldMeta` arg and `metaForField` env method have been removed. + - The shape of the implementation map has been adjusted: + - `componentPath` is now `component`, and expects a `ComponentLike` value rather than a string + - `componentMeta` and `helperMeta` have been renamed simply `meta` diff --git a/ember-exclaim/.gitignore b/ember-exclaim/.gitignore index 66c9c22..13bce18 100644 --- a/ember-exclaim/.gitignore +++ b/ember-exclaim/.gitignore @@ -5,6 +5,7 @@ /LICENSE.md # Build output /dist/ -/declarations/ +# We don't ignore declarations as they're currently hand-written. +# /declarations/ # npm/pnpm/yarn pack output *.tgz diff --git a/ember-exclaim/declarations/components/exclaim-ui.d.ts b/ember-exclaim/declarations/components/exclaim-ui.d.ts new file mode 100644 index 0000000..d25131a --- /dev/null +++ b/ember-exclaim/declarations/components/exclaim-ui.d.ts @@ -0,0 +1,67 @@ +import { ComponentLike } from '@glint/template'; +import { ComponentSpec, ImplementationMap } from '../index'; + +declare const ExclaimUi: ComponentLike<{ + Args: { + /** + * A spec for an Exclaim UI. + */ + ui: unknown; + + /** + * A mapping of names to implementations for use by `$component` + * and `$helper` in the given UI spec. + */ + implementationMap: ImplementationMap; + + /** + * The backing data that any `$bind` in the UI spec will be bound + * to. This value will also be directly available as `@env` to all + * components rendered in this UI. + */ + env?: unknown; + + /** + * A callback that will be invoked when the given path in the + * environment has changed because a `$bind` value was written. + */ + onChange?: (envPath: string) => void; + + /** + * Set this flag `true` to use `computed`-based reactivity for managings + * bindings and helpers within this UI. When `false` or unset, native + * getters and setters will be used instead. + */ + useClassicReactivity?: boolean; + + /** + * An optional component that, if provided, will be invoked around each + * component in this UI. + */ + wrapper?: ComponentLike<{ + Args: { + /** The {@link ComponentSpec} being wrapped. */ + componentSpec: ComponentSpec; + + /** The resolved config object the wrapped component will receive. */ + config: unknown; + + /** The env object the wrapped component will receive. */ + env: unknown; + }; + Blocks: { + default: []; + }; + }>; + }; + + Blocks: { + /** + * If an error is encountered when processing the given UI spec, it + * will be yielded to the default block. + */ + default: [error: unknown]; + }; +}>; + +export default ExclaimUi; diff --git a/ember-exclaim/declarations/index.d.ts b/ember-exclaim/declarations/index.d.ts new file mode 100644 index 0000000..83ee657 --- /dev/null +++ b/ember-exclaim/declarations/index.d.ts @@ -0,0 +1,63 @@ +import { ComponentLike } from '@glint/template'; + +declare const Env: unique symbol; + +/** A marker type for objects that are operating as an Exclaim environment. */ +export type Environment = T & Record; + +/** + * Returns the environment and source path within it where the field at + * the path on the given object originates, if known. + */ +export function resolveEnvPath( + object: unknown, + path: string, +): string | undefined; + +/** The data defining a helper in an {@link ImplementationMap} */ +export type HelperImplementation = { + helper: (...args: Array) => unknown; + shorthandProperty?: string; + meta?: unknown; +}; + +/** The data defining a component in an {@link ImplementationMap} */ +export type ComponentImplementation = { + component: ComponentLike; + shorthandProperty?: string; + meta?: unknown; +}; + +/** + * A mapping of names that can be used in a UI spec with `$helper` + * or `$component` to the runtime implementation that should be invoked + * when that name is encountered. + */ +export type ImplementationMap = Record< + Names, + HelperImplementation | ComponentImplementation +>; + +/** Represents a `$component` object in a UI spec. */ +export class ComponentSpec { + readonly component: ComponentLike; + readonly config: unknown; + readonly meta: unknown; + + resolveConfig(env: Environment): unknown; +} + +/** Represents a `$helper` object in a UI spec. */ +export class HelperSpec { + readonly helper: (...args: Array) => unknown; + readonly config: unknown; + readonly meta: unknown; + readonly bindings: Array; + + invoke(env: Environment): unknown; +} + +/** + * @deprecated This function is now a no-op and should no longer be used. + */ +export function unwrap(value: T): T; diff --git a/ember-exclaim/declarations/template-registry.d.ts b/ember-exclaim/declarations/template-registry.d.ts new file mode 100644 index 0000000..6385a78 --- /dev/null +++ b/ember-exclaim/declarations/template-registry.d.ts @@ -0,0 +1,5 @@ +import { ExclaimUi } from './components/exclaim-ui'; + +export default interface ExclaimTemplateRegistry { + ExclaimUi: typeof ExclaimUi; +} diff --git a/ember-exclaim/package.json b/ember-exclaim/package.json index b024f34..c91bc02 100644 --- a/ember-exclaim/package.json +++ b/ember-exclaim/package.json @@ -31,15 +31,21 @@ }, "dependencies": { "@embroider/addon-shim": "^1.8.7", + "@glimmer/component": "^1.1.2", + "@glimmer/tracking": "^1.1.2", "botanist": "^1.3.0", "decorator-transforms": "^1.0.1", "tracked-built-ins": "^3.3.0" }, + "peerDependencies": { + "@glint/template": "^1.3.0" + }, "devDependencies": { "@babel/core": "^7.23.6", "@babel/eslint-parser": "^7.23.3", "@babel/runtime": "^7.17.0", "@embroider/addon-dev": "^4.1.0", + "@glint/template": "^1.3.0", "@rollup/plugin-babel": "^6.0.4", "babel-plugin-ember-template-compilation": "^2.2.1", "concurrently": "^8.2.2", @@ -69,13 +75,25 @@ "main": "addon-main.cjs", "app-js": { "./components/exclaim-component.js": "./dist/_app_/components/exclaim-component.js", - "./components/exclaim-default-component-wrapper.js": "./dist/_app_/components/exclaim-default-component-wrapper.js", "./components/exclaim-ui.js": "./dist/_app_/components/exclaim-ui.js" } }, + "typesVersions": { + "*": { + "*": [ + "declarations/*" + ] + } + }, "exports": { - ".": "./dist/index.js", - "./*": "./dist/*.js", + ".": { + "types": "./declarations/index.d.ts", + "default": "./dist/index.js" + }, + "./*": { + "types": "./declarations/*.d.ts", + "default": "./dist/*.js" + }, "./addon-main.js": "./addon-main.cjs" } } diff --git a/ember-exclaim/src/-private/GLOSSARY.md b/ember-exclaim/src/-private/GLOSSARY.md index a5ec85f..2e5bf1d 100644 --- a/ember-exclaim/src/-private/GLOSSARY.md +++ b/ember-exclaim/src/-private/GLOSSARY.md @@ -12,12 +12,8 @@ A **`HelperSpec`** is the information necessary to compute the value of a helper Any hash in the UI config with a `$helper` key will be transformed into a `HelperSpec` instance, following the same resolution rules as `ComponentSpec`s do, but looking for a `helper` function in the `implementationMap` rather than a `componentPath`. -A **`Binding`** is a reference to some value available in the salient `Environment` (see below), much like a variable reference in a programming language. A `Binding` is meaningless on its own, and must always be evaluated in the context of some `Environment`. Note that the `config` for a `ComponentSpec` may contain `Binding`s, which won't be resolved until they're actually used. This allows components to evaluate parts of their config in varying contexts, such as an `each` component rendering the same subcomponent config with varying values for its iteration variable. +A **`Binding`** is a reference to some value available in the salient _environment_ (see below), much like a variable reference in a programming language. A `Binding` is meaningless on its own, and must always be evaluated in the context of some `Environment`. Note that the `config` for a `ComponentSpec` may contain `Binding`s, which won't be resolved until they're actually used. This allows components to evaluate parts of their config in varying contexts, such as an `each` component rendering the same subcomponent config with varying values for its iteration variable. ## Runtime Elements -At runtime, exclaim UIs are evaluated relative to an **`Environment`**, which is analogous to scope in a programming language. An `Environment` contains all the bound values that are available to `Binding`s, and may itself contain `Binding` instances that point at other data within itself. When the time comes to resolve the `config` for a component to actual values, `ComponentSpec` instances expose a `resolveConfig(environment)` method, which returns an `EnvironmentData` instance for the configuration. - -An **`EnvironmentData`** object is a proxy for some arbitrary hash or array that resolves any `Binding`s it contains relative to some `Environment`. You can think of `EnvironmentData` as a piece of data that remembers where it came from. For instance, given an `EnvironmentData` instance `data` wrapping the hash `{ hi: 'hello', bye: new Binding('farewell') }`, calling `data.get('hi')` would return the string `'hello'`, and calling `data.get('bye')` would return whatever the associated `Environment` contains for the key `farewell`. - -Implementation note: when an `Environment` or `EnvironmentData` instance is asked to `get` a property, it first inspects whether the underlying value for that property is a `Binding`, and if so, resolves it. Once this resolution has occurred the first time, a computed property is generated so that subsequent lookups don't have to re-resolve, and changes to the underlying bound property will be reflected on the host `EnvironmentData` or `Environment`. Any non-primitive result of a `get` on an `Environment` or `EnvironmentData` instance will itself be a `EnvironmentData`, so that `Binding`s nested arbitrarily deep will always be resolved. There is also an `EnvironmentData` variant called `EnvironmentArray` which functions similarly but wraps arrays rather than objects. +At runtime, exclaim UIs are evaluated relative to an _environment_, which is analogous to scope in a programming language. An environment contains all the bound values that are available to `Binding`s. When the time comes to resolve the `config` for a component to actual values, `ComponentSpec` instances expose a `resolveConfig(environment)` method, which creates a copy of its configuration with any `Binding` or `HelperSpec` instances replaced with appropriate getters and setters that will read data from the given environment. diff --git a/ember-exclaim/src/-private/build-spec-processor.js b/ember-exclaim/src/-private/build-spec-processor.js index 12b28c2..268022c 100644 --- a/ember-exclaim/src/-private/build-spec-processor.js +++ b/ember-exclaim/src/-private/build-spec-processor.js @@ -30,7 +30,7 @@ function buildBaseRules(implementationMap) { return new HelperSpec( implementationMap[name].helper, config, - implementationMap[name].helperMeta, + implementationMap[name].meta, ); } else { throw new Error(`Unable to resolve helper ${name}`); @@ -42,12 +42,12 @@ function buildBaseRules(implementationMap) { ({ name, config }) => { if ( hasOwnProperty(implementationMap, name) && - implementationMap[name].componentPath + implementationMap[name].component ) { return new ComponentSpec( - implementationMap[name].componentPath, + implementationMap[name].component, config, - implementationMap[name].componentMeta, + implementationMap[name].meta, ); } else { throw new Error(`Unable to resolve component ${name}`); @@ -63,7 +63,7 @@ function buildShorthandRules(implementationMap) { Object.keys(implementationMap).forEach((name) => { let details = implementationMap[name]; if (details.shorthandProperty) { - if (details.componentPath) { + if (details.component) { rules.push(buildComponentRule(name, details)); } else if (details.helper) { rules.push(buildHelperRule(name, details)); @@ -74,25 +74,22 @@ function buildShorthandRules(implementationMap) { return rules; } -function buildComponentRule( - name, - { shorthandProperty, componentPath, componentMeta }, -) { +function buildComponentRule(name, { shorthandProperty, component, meta }) { return rule( { [`$${name}`]: subtree('shorthandValue'), ...rest('config') }, ({ shorthandValue, config }) => { let fullConfig = { [shorthandProperty]: shorthandValue, ...config }; - return new ComponentSpec(componentPath, fullConfig, componentMeta); + return new ComponentSpec(component, fullConfig, meta); }, ); } -function buildHelperRule(name, { shorthandProperty, helper, helperMeta }) { +function buildHelperRule(name, { shorthandProperty, helper, meta }) { return rule( { [`$${name}`]: subtree('shorthandValue'), ...rest('config') }, ({ shorthandValue, config }) => { let fullConfig = { [shorthandProperty]: shorthandValue, ...config }; - return new HelperSpec(helper, fullConfig, helperMeta); + return new HelperSpec(helper, fullConfig, meta); }, ); } diff --git a/ember-exclaim/src/-private/env/index.js b/ember-exclaim/src/-private/env/index.js index 5a6796c..e1c9573 100644 --- a/ember-exclaim/src/-private/env/index.js +++ b/ember-exclaim/src/-private/env/index.js @@ -12,6 +12,10 @@ export function isEnv(data) { } export function extendEnv(env, newBindings) { + if (!newBindings || !Object.keys(newBindings).length) { + return env; + } + const internals = envInternals.get(env); const newEnv = internals.extend(env, newBindings); const onChange = (key) => { diff --git a/ember-exclaim/src/components/exclaim-component.hbs b/ember-exclaim/src/components/exclaim-component.hbs index a514eb3..edef3de 100644 --- a/ember-exclaim/src/components/exclaim-component.hbs +++ b/ember-exclaim/src/components/exclaim-component.hbs @@ -1,20 +1,22 @@ -{{#component - @wrapper - componentSpec=@componentSpec - env=this.effectiveEnv - config=this.resolvedConfig -}} - {{#component - @componentSpec.component - config=this.resolvedConfig - env=this.effectiveEnv - as |componentSpec overrideEnv| - ~}} - + {{~null~}} + <@componentSpec.component + @config={{this.componentData.config}} + @env={{this.componentData.env}} + as |componentSpec additionalEnvData| + > + {{~null~}} + - {{~/component}} -{{/component}} \ No newline at end of file + {{~null~}} + + {{~null~}} + \ No newline at end of file diff --git a/ember-exclaim/src/components/exclaim-component.js b/ember-exclaim/src/components/exclaim-component.js index 59d63ab..86e42a4 100644 --- a/ember-exclaim/src/components/exclaim-component.js +++ b/ember-exclaim/src/components/exclaim-component.js @@ -1,22 +1,10 @@ -import { computed } from '@ember/object'; -import Component from '@ember/component'; +import Component from '@glimmer/component'; import { extendEnv } from '../-private/env/index.js'; -export default Component.extend({ - tagName: '', - - componentSpec: null, - env: null, - - effectiveEnv: computed('env', 'overrideEnv', function () { - if (this.overrideEnv) { - return extendEnv(this.env, this.overrideEnv); - } else { - return this.env; - } - }), - - resolvedConfig: computed('componentSpec', 'effectiveEnv', function () { - return this.componentSpec?.resolveConfig?.(this.effectiveEnv); - }), -}); +export default class ExclaimComponent extends Component { + get componentData() { + let env = extendEnv(this.args.env, this.args.additionalEnvData); + let config = this.args.componentSpec?.resolveConfig?.(env); + return { env, config }; + } +} diff --git a/ember-exclaim/src/components/exclaim-default-component-wrapper.js b/ember-exclaim/src/components/exclaim-default-component-wrapper.js deleted file mode 100644 index 4798652..0000000 --- a/ember-exclaim/src/components/exclaim-default-component-wrapper.js +++ /dev/null @@ -1,5 +0,0 @@ -import Component from '@ember/component'; - -export default Component.extend({ - tagName: '', -}); diff --git a/ember-exclaim/src/components/exclaim-ui.hbs b/ember-exclaim/src/components/exclaim-ui.hbs index 325e68d..57b7d2e 100644 --- a/ember-exclaim/src/components/exclaim-ui.hbs +++ b/ember-exclaim/src/components/exclaim-ui.hbs @@ -1,9 +1,11 @@ -{{#if this.content.componentSpec~}} - -{{~else if this.content.error~}} - {{yield this.content.error}} -{{~/if}} +
+ {{~#if this.content.componentSpec~}} + + {{~else if this.content.error~}} + {{yield this.content.error}} + {{~/if~}} +
\ No newline at end of file diff --git a/ember-exclaim/src/components/exclaim-ui.js b/ember-exclaim/src/components/exclaim-ui.js index ec3ddc6..7923183 100644 --- a/ember-exclaim/src/components/exclaim-ui.js +++ b/ember-exclaim/src/components/exclaim-ui.js @@ -1,37 +1,29 @@ -import { computed, getProperties, get } from '@ember/object'; -import Component from '@ember/component'; -import buildSpecProcessor from '../-private/build-spec-processor'; +import Component from '@glimmer/component'; +import templateOnlyComponent from '@ember/component/template-only'; import { makeEnv } from '../-private/env/index.js'; +import buildSpecProcessor from '../-private/build-spec-processor'; import * as computedEnv from '../-private/env/computed.js'; import * as trackedEnv from '../-private/env/tracked.js'; -export default Component.extend({ - ui: null, - env: null, - implementationMap: null, - useClassicReactivity: false, +const DefaultWrapper = templateOnlyComponent(); - baseEnv: computed('env', 'onChange', 'useClassicReactivity', function () { - const envImpl = this.useClassicReactivity ? computedEnv : trackedEnv; - return makeEnv(this.env ?? {}, this.onChange, envImpl); - }), - - content: computed('specProcessor', 'ui', function () { - const processor = get(this, 'specProcessor'); - const ui = get(this, 'ui'); +export default class ExclaimUi extends Component { + get env() { + const envImpl = this.args.useClassicReactivity ? computedEnv : trackedEnv; + return makeEnv(this.args.env ?? {}, this.args.onChange, envImpl); + } + get content() { try { + const { ui, implementationMap } = this.args; + const processor = buildSpecProcessor({ implementationMap }); return { componentSpec: processor(ui) }; } catch (error) { return { error }; } - }), - - specProcessor: computed('implementationMap', function () { - return buildSpecProcessor(getProperties(this, 'implementationMap')); - }), + } - wrapperComponentName: computed('wrapper', function () { - return get(this, 'wrapper') || 'exclaim-default-component-wrapper'; - }), -}); + get wrapper() { + return this.args.wrapper ?? DefaultWrapper; + } +} diff --git a/ember-exclaim/src/index.js b/ember-exclaim/src/index.js index f2b42bd..315be11 100644 --- a/ember-exclaim/src/index.js +++ b/ember-exclaim/src/index.js @@ -1,6 +1,14 @@ +import { deprecate } from '@ember/debug'; + export { ComponentSpec, HelperSpec } from './-private/ui-spec.js'; export { resolveEnvPath } from './-private/paths.js'; export function unwrap(value) { + deprecate('`unwrap` is a no-op in Exclaim v2', true, { + for: 'ember-exclaim', + id: 'ember-exclaim.unwrap', + since: { available: '2.0.0', enabled: '2.0.0' }, + until: '3.0.0', + }); return value; } diff --git a/playground-app/app/components/exclaim-components/box.hbs b/playground-app/app/components/exclaim-components/box.hbs new file mode 100644 index 0000000..dcc1185 --- /dev/null +++ b/playground-app/app/components/exclaim-components/box.hbs @@ -0,0 +1,5 @@ +
+ {{~#each @config.children as |child|~}} + {{yield child}} + {{~/each~}} +
\ No newline at end of file diff --git a/playground-app/app/components/exclaim-components/box.js b/playground-app/app/components/exclaim-components/box.js new file mode 100644 index 0000000..d2dbc6b --- /dev/null +++ b/playground-app/app/components/exclaim-components/box.js @@ -0,0 +1,18 @@ +import templateOnlyComponent from '@ember/component/template-only'; + +export const Box = { + component: templateOnlyComponent(), + shorthandProperty: 'children', + meta: { + description: + 'A container whose children flow naturally, whatever that means', + properties: [ + { + name: 'children', + description: 'An array items in the container', + }, + ], + }, +}; + +export default Box.component; diff --git a/playground-app/app/components/exclaim-components/box/component.js b/playground-app/app/components/exclaim-components/box/component.js deleted file mode 100644 index be08ad9..0000000 --- a/playground-app/app/components/exclaim-components/box/component.js +++ /dev/null @@ -1,14 +0,0 @@ -import Component from '@ember/component'; - -export const NAME = 'box'; -export const DESCRIPTION = - 'A container whose children flow naturally, whatever that means'; -export const SHORTHAND_PROPERTY = 'children'; -export const PROPERTIES = [ - { - name: 'children', - description: 'An array items in the container', - }, -]; - -export default Component.extend({}); diff --git a/playground-app/app/components/exclaim-components/box/template.hbs b/playground-app/app/components/exclaim-components/box/template.hbs deleted file mode 100644 index bc83dc4..0000000 --- a/playground-app/app/components/exclaim-components/box/template.hbs +++ /dev/null @@ -1,3 +0,0 @@ -{{#each @config.children as |child|}} - {{~yield child~}} -{{/each}} diff --git a/playground-app/app/components/exclaim-components/checkbox.hbs b/playground-app/app/components/exclaim-components/checkbox.hbs new file mode 100644 index 0000000..e12c7ab --- /dev/null +++ b/playground-app/app/components/exclaim-components/checkbox.hbs @@ -0,0 +1,7 @@ +{{! template-lint-disable require-input-label }} + + \ No newline at end of file diff --git a/playground-app/app/components/exclaim-components/checkbox.js b/playground-app/app/components/exclaim-components/checkbox.js new file mode 100644 index 0000000..16ffa57 --- /dev/null +++ b/playground-app/app/components/exclaim-components/checkbox.js @@ -0,0 +1,25 @@ +import Component from '@glimmer/component'; + +class ExclaimCheckbox extends Component { + onChange = (event) => { + this.args.config.checked = event.target.checked; + }; +} + +export const Checkbox = { + component: ExclaimCheckbox, + shorthandProperty: 'checked', + meta: { + isUserInput: true, + description: 'A checkbox', + properties: [ + { + name: 'checked', + description: + 'Whether or not this box is checked. If bound to a field in the environment, checking or unchecking the box will update that field.', + }, + ], + }, +}; + +export default Checkbox.component; diff --git a/playground-app/app/components/exclaim-components/checkbox/component.js b/playground-app/app/components/exclaim-components/checkbox/component.js deleted file mode 100644 index 080203a..0000000 --- a/playground-app/app/components/exclaim-components/checkbox/component.js +++ /dev/null @@ -1,21 +0,0 @@ -import Component from '@ember/component'; - -export const NAME = 'checkbox'; -export const DESCRIPTION = 'A checkbox'; -export const SHORTHAND_PROPERTY = 'checked'; -export const PROPERTIES = [ - { - name: 'checked', - description: - 'Whether or not this box is checked. If bound to a field in the environment, checking or unchecking the box will update that field.', - }, -]; - -export const COMPONENT_META = { - isUserInput: true, - writesKeys: [SHORTHAND_PROPERTY], -}; - -export default Component.extend({ - tagName: '', -}); diff --git a/playground-app/app/components/exclaim-components/checkbox/template.hbs b/playground-app/app/components/exclaim-components/checkbox/template.hbs deleted file mode 100644 index dfce894..0000000 --- a/playground-app/app/components/exclaim-components/checkbox/template.hbs +++ /dev/null @@ -1,7 +0,0 @@ -{{! template-lint-disable no-action require-input-label }} - - diff --git a/playground-app/app/components/exclaim-components/each.hbs b/playground-app/app/components/exclaim-components/each.hbs new file mode 100644 index 0000000..b283033 --- /dev/null +++ b/playground-app/app/components/exclaim-components/each.hbs @@ -0,0 +1,3 @@ +{{#each @config.items as |item|}} + {{yield @config.do (singleton-hash @config.yield item)}} +{{/each}} \ No newline at end of file diff --git a/playground-app/app/components/exclaim-components/each.js b/playground-app/app/components/exclaim-components/each.js new file mode 100644 index 0000000..a645e72 --- /dev/null +++ b/playground-app/app/components/exclaim-components/each.js @@ -0,0 +1,28 @@ +import templateOnlyComponent from '@ember/component/template-only'; + +export const Each = { + component: templateOnlyComponent(), + shorthandProperty: 'items', + meta: { + description: + 'A construct for rendering a component for each element of an array', + properties: [ + { + name: 'items', + description: 'The array of items to render components for', + }, + { + name: 'yield', + description: + 'A name to use to make the current item available to "$bind"', + }, + { + name: 'do', + description: + 'A component spec that will be rendered for each item in the "items" array', + }, + ], + }, +}; + +export default Each.component; diff --git a/playground-app/app/components/exclaim-components/each/component.js b/playground-app/app/components/exclaim-components/each/component.js deleted file mode 100644 index 25d604a..0000000 --- a/playground-app/app/components/exclaim-components/each/component.js +++ /dev/null @@ -1,29 +0,0 @@ -import Component from '@ember/component'; -import { helper } from '@ember/component/helper'; - -export const NAME = 'each'; -export const DESCRIPTION = - 'A construct for rendering a component for each element of an array'; -export const SHORTHAND_PROPERTY = 'items'; -export const PROPERTIES = [ - { - name: 'items', - description: 'The array of items to render components for', - }, - { - name: 'yield', - description: 'A name to use to make the current item available to "$bind"', - }, - { - name: 'do', - description: - 'A component spec that will be rendered for each item in the "items" array', - }, -]; - -const singletonHash = helper(([key, value]) => ({ [key]: value })); - -export default Component.extend({ - tagName: '', - singletonHash, -}); diff --git a/playground-app/app/components/exclaim-components/each/template.hbs b/playground-app/app/components/exclaim-components/each/template.hbs deleted file mode 100644 index 6efa028..0000000 --- a/playground-app/app/components/exclaim-components/each/template.hbs +++ /dev/null @@ -1,3 +0,0 @@ -{{#each @config.items as |item|}} - {{yield @config.do (this.singletonHash @config.yield item)}} -{{/each}} \ No newline at end of file diff --git a/playground-app/app/components/exclaim-components/hbox/styles.css b/playground-app/app/components/exclaim-components/hbox.css similarity index 100% rename from playground-app/app/components/exclaim-components/hbox/styles.css rename to playground-app/app/components/exclaim-components/hbox.css diff --git a/playground-app/app/components/exclaim-components/hbox.hbs b/playground-app/app/components/exclaim-components/hbox.hbs new file mode 100644 index 0000000..4c0454e --- /dev/null +++ b/playground-app/app/components/exclaim-components/hbox.hbs @@ -0,0 +1,5 @@ +
+ {{~#each @config.children as |child|~}} + {{yield child}} + {{~/each~}} +
\ No newline at end of file diff --git a/playground-app/app/components/exclaim-components/hbox.js b/playground-app/app/components/exclaim-components/hbox.js new file mode 100644 index 0000000..cea94a3 --- /dev/null +++ b/playground-app/app/components/exclaim-components/hbox.js @@ -0,0 +1,17 @@ +import templateOnlyComponent from '@ember/component/template-only'; + +export const Hbox = { + component: templateOnlyComponent(), + shorthandProperty: 'children', + meta: { + description: 'A container that lays out its contents horizontally', + properties: [ + { + name: 'children', + description: 'An array items in the container', + }, + ], + }, +}; + +export default Hbox.component; diff --git a/playground-app/app/components/exclaim-components/hbox/component.js b/playground-app/app/components/exclaim-components/hbox/component.js deleted file mode 100644 index cd3f36b..0000000 --- a/playground-app/app/components/exclaim-components/hbox/component.js +++ /dev/null @@ -1,16 +0,0 @@ -import Component from '@ember/component'; - -export const NAME = 'hbox'; -export const DESCRIPTION = - 'A container that lays out its contents horizontally'; -export const SHORTHAND_PROPERTY = 'children'; -export const PROPERTIES = [ - { - name: 'children', - description: 'An array items in the container', - }, -]; - -export default Component.extend({ - tagName: '', -}); diff --git a/playground-app/app/components/exclaim-components/hbox/template.hbs b/playground-app/app/components/exclaim-components/hbox/template.hbs deleted file mode 100644 index 248dd88..0000000 --- a/playground-app/app/components/exclaim-components/hbox/template.hbs +++ /dev/null @@ -1,5 +0,0 @@ -
- {{#each @config.children as |child|}} - {{yield child}} - {{/each}} -
\ No newline at end of file diff --git a/playground-app/app/components/exclaim-components/header.hbs b/playground-app/app/components/exclaim-components/header.hbs new file mode 100644 index 0000000..7dbb39e --- /dev/null +++ b/playground-app/app/components/exclaim-components/header.hbs @@ -0,0 +1 @@ +

{{@config.content}}

\ No newline at end of file diff --git a/playground-app/app/components/exclaim-components/header.js b/playground-app/app/components/exclaim-components/header.js new file mode 100644 index 0000000..a2e0115 --- /dev/null +++ b/playground-app/app/components/exclaim-components/header.js @@ -0,0 +1,18 @@ +import templateOnlyComponent from '@ember/component/template-only'; + +export const Header = { + component: templateOnlyComponent(), + shorthandProperty: 'content', + meta: { + description: 'A header', + properties: [ + { + name: 'content', + description: + 'A value or array of values that will be concatenated together and displayed', + }, + ], + }, +}; + +export default Header.component; diff --git a/playground-app/app/components/exclaim-components/header/component.js b/playground-app/app/components/exclaim-components/header/component.js deleted file mode 100644 index 889dbf5..0000000 --- a/playground-app/app/components/exclaim-components/header/component.js +++ /dev/null @@ -1,16 +0,0 @@ -import Component from '@ember/component'; - -export const NAME = 'header'; -export const DESCRIPTION = 'A header'; -export const SHORTHAND_PROPERTY = 'content'; -export const PROPERTIES = [ - { - name: 'content', - description: - 'A value or array of values that will be concatenated together and displayed', - }, -]; - -export default Component.extend({ - tagName: 'h3', -}); diff --git a/playground-app/app/components/exclaim-components/header/template.hbs b/playground-app/app/components/exclaim-components/header/template.hbs deleted file mode 100644 index 6d6a8be..0000000 --- a/playground-app/app/components/exclaim-components/header/template.hbs +++ /dev/null @@ -1 +0,0 @@ -{{@config.content}} diff --git a/playground-app/app/components/exclaim-components/input.hbs b/playground-app/app/components/exclaim-components/input.hbs new file mode 100644 index 0000000..6130a43 --- /dev/null +++ b/playground-app/app/components/exclaim-components/input.hbs @@ -0,0 +1,7 @@ +{{! template-lint-disable require-input-label }} + \ No newline at end of file diff --git a/playground-app/app/components/exclaim-components/input.js b/playground-app/app/components/exclaim-components/input.js new file mode 100644 index 0000000..f53c670 --- /dev/null +++ b/playground-app/app/components/exclaim-components/input.js @@ -0,0 +1,39 @@ +import Component from '@glimmer/component'; + +class ExclaimInput extends Component { + get type() { + return this.args.config.type ?? 'text'; + } + + onInput = (event) => { + this.args.config.value = event.target.value; + }; +} + +export const Input = { + component: ExclaimInput, + shorthandProperty: 'value', + meta: { + description: 'A basic form field', + isUserInput: true, + properties: [ + { + name: 'type', + description: + 'The type of input (e.g. text, number, etc); defaults to "text"', + }, + { + name: 'placeholder', + description: + 'An optional placeholder to display in the field when it has no value', + }, + { + name: 'value', + description: + 'The value of this input. If bound to a field in the environment, changes to the input will update that field.', + }, + ], + }, +}; + +export default Input.component; diff --git a/playground-app/app/components/exclaim-components/input/component.js b/playground-app/app/components/exclaim-components/input/component.js deleted file mode 100644 index 9714ca5..0000000 --- a/playground-app/app/components/exclaim-components/input/component.js +++ /dev/null @@ -1,36 +0,0 @@ -import { computed } from '@ember/object'; -import Component from '@ember/component'; - -export const NAME = 'input'; -export const DESCRIPTION = 'A basic form field'; -export const SHORTHAND_PROPERTY = 'value'; -export const PROPERTIES = [ - { - name: 'type', - description: - 'The type of input (e.g. text, number, etc); defaults to "text"', - }, - { - name: 'placeholder', - description: - 'An optional placeholder to display in the field when it has no value', - }, - { - name: 'value', - description: - 'The value of this input. If bound to a field in the environment, changes to the input will update that field.', - }, -]; - -export const COMPONENT_META = { - isUserInput: true, - writesKeys: [SHORTHAND_PROPERTY], -}; - -export default Component.extend({ - tagName: '', - - type: computed('config.type', function () { - return this.config.type ?? 'text'; - }), -}); diff --git a/playground-app/app/components/exclaim-components/input/template.hbs b/playground-app/app/components/exclaim-components/input/template.hbs deleted file mode 100644 index b7d8945..0000000 --- a/playground-app/app/components/exclaim-components/input/template.hbs +++ /dev/null @@ -1,8 +0,0 @@ -{{! template-lint-disable require-input-label no-action }} - - diff --git a/playground-app/app/components/exclaim-components/let/template.hbs b/playground-app/app/components/exclaim-components/let.hbs similarity index 100% rename from playground-app/app/components/exclaim-components/let/template.hbs rename to playground-app/app/components/exclaim-components/let.hbs diff --git a/playground-app/app/components/exclaim-components/let.js b/playground-app/app/components/exclaim-components/let.js new file mode 100644 index 0000000..30f0aea --- /dev/null +++ b/playground-app/app/components/exclaim-components/let.js @@ -0,0 +1,23 @@ +import templateOnlyComponent from '@ember/component/template-only'; + +export const Let = { + component: templateOnlyComponent(), + shorthandProperty: 'bindings', + meta: { + description: 'A construct for binding values in child components', + properties: [ + { + name: 'bindings', + description: + 'A hash of values, each of which will be available to "$bind" in child components', + }, + { + name: 'do', + description: + 'A child component, in which all the values in "bindings" will be available', + }, + ], + }, +}; + +export default Let.component; diff --git a/playground-app/app/components/exclaim-components/let/component.js b/playground-app/app/components/exclaim-components/let/component.js deleted file mode 100644 index cb57ec7..0000000 --- a/playground-app/app/components/exclaim-components/let/component.js +++ /dev/null @@ -1,21 +0,0 @@ -import Component from '@ember/component'; - -export const NAME = 'let'; -export const DESCRIPTION = 'A construct for binding values in child components'; -export const SHORTHAND_PROPERTY = 'bindings'; -export const PROPERTIES = [ - { - name: 'bindings', - description: - 'A hash of values, each of which will be available to "$bind" in child components', - }, - { - name: 'do', - description: - 'A child component, in which all the values in "bindings" will be available', - }, -]; - -export default Component.extend({ - tagName: '', -}); diff --git a/playground-app/app/components/exclaim-components/table/template.hbs b/playground-app/app/components/exclaim-components/table.hbs similarity index 82% rename from playground-app/app/components/exclaim-components/table/template.hbs rename to playground-app/app/components/exclaim-components/table.hbs index 5520eb2..db060e1 100644 --- a/playground-app/app/components/exclaim-components/table/template.hbs +++ b/playground-app/app/components/exclaim-components/table.hbs @@ -13,7 +13,7 @@ {{#each @config.items as |item|}} {{#each @config.row as |cell|}} - {{yield cell (this.singletonHash @config.yield item)}} + {{yield cell (singleton-hash @config.yield item)}} {{/each}} {{/each}} diff --git a/playground-app/app/components/exclaim-components/table.js b/playground-app/app/components/exclaim-components/table.js new file mode 100644 index 0000000..9082d3a --- /dev/null +++ b/playground-app/app/components/exclaim-components/table.js @@ -0,0 +1,33 @@ +import templateOnlyComponent from '@ember/component/template-only'; + +export const Table = { + component: templateOnlyComponent(), + shorthandProperty: 'items', + meta: { + description: + 'A table rendered with one row for each element of the given array', + properties: [ + { + name: 'items', + description: 'An array of items representing the table rows', + }, + { + name: 'yield', + description: + 'A name to use to make the item for a given row available to "$bind"', + }, + { + name: 'header', + description: + 'An optional array of component specs defining what content should go in the header cells of the table', + }, + { + name: 'row', + description: + 'An array of component specs defining the content should be rendered in each cell of a row', + }, + ], + }, +}; + +export default Table.component; diff --git a/playground-app/app/components/exclaim-components/table/component.js b/playground-app/app/components/exclaim-components/table/component.js deleted file mode 100644 index 56ab92f..0000000 --- a/playground-app/app/components/exclaim-components/table/component.js +++ /dev/null @@ -1,35 +0,0 @@ -import Component from '@ember/component'; -import { helper } from '@ember/component/helper'; - -export const NAME = 'table'; -export const DESCRIPTION = - 'A table rendered with one row for each element of the given array'; -export const SHORTHAND_PROPERTY = 'items'; -export const PROPERTIES = [ - { - name: 'items', - description: 'An array of items representing the table rows', - }, - { - name: 'yield', - description: - 'A name to use to make the item for a given row available to "$bind"', - }, - { - name: 'header', - description: - 'An optional array of component specs defining what content should go in the header cells of the table', - }, - { - name: 'row', - description: - 'An array of component specs defining the content should be rendered in each cell of a row', - }, -]; - -const singletonHash = helper(([key, value]) => ({ [key]: value })); - -export default Component.extend({ - tagName: '', - singletonHash, -}); diff --git a/playground-app/app/components/exclaim-components/text.hbs b/playground-app/app/components/exclaim-components/text.hbs new file mode 100644 index 0000000..ae5b772 --- /dev/null +++ b/playground-app/app/components/exclaim-components/text.hbs @@ -0,0 +1 @@ +{{@config.content}} \ No newline at end of file diff --git a/playground-app/app/components/exclaim-components/text.js b/playground-app/app/components/exclaim-components/text.js new file mode 100644 index 0000000..814afda --- /dev/null +++ b/playground-app/app/components/exclaim-components/text.js @@ -0,0 +1,18 @@ +import templateOnlyComponent from '@ember/component/template-only'; + +export const Text = { + component: templateOnlyComponent(), + shorthandProperty: 'content', + meta: { + description: 'Some text', + properties: [ + { + name: 'content', + description: + 'A value or array of values that will be concatenated together and displayed', + }, + ], + }, +}; + +export default Text.component; diff --git a/playground-app/app/components/exclaim-components/text/component.js b/playground-app/app/components/exclaim-components/text/component.js deleted file mode 100644 index b098d02..0000000 --- a/playground-app/app/components/exclaim-components/text/component.js +++ /dev/null @@ -1,16 +0,0 @@ -import Component from '@ember/component'; - -export const NAME = 'text'; -export const DESCRIPTION = 'Some text'; -export const SHORTHAND_PROPERTY = 'content'; -export const PROPERTIES = [ - { - name: 'content', - description: - 'A value or array of values that will be concatenated together and displayed', - }, -]; - -export default Component.extend({ - tagName: 'span', -}); diff --git a/playground-app/app/components/exclaim-components/text/template.hbs b/playground-app/app/components/exclaim-components/text/template.hbs deleted file mode 100644 index 6d6a8be..0000000 --- a/playground-app/app/components/exclaim-components/text/template.hbs +++ /dev/null @@ -1 +0,0 @@ -{{@config.content}} diff --git a/playground-app/app/components/exclaim-components/vbox/styles.css b/playground-app/app/components/exclaim-components/vbox.css similarity index 100% rename from playground-app/app/components/exclaim-components/vbox/styles.css rename to playground-app/app/components/exclaim-components/vbox.css diff --git a/playground-app/app/components/exclaim-components/vbox.hbs b/playground-app/app/components/exclaim-components/vbox.hbs new file mode 100644 index 0000000..dc7fad7 --- /dev/null +++ b/playground-app/app/components/exclaim-components/vbox.hbs @@ -0,0 +1,5 @@ +
+ {{~#each @config.children as |child|~}} + {{yield child}} + {{~/each~}} +
\ No newline at end of file diff --git a/playground-app/app/components/exclaim-components/vbox.js b/playground-app/app/components/exclaim-components/vbox.js new file mode 100644 index 0000000..5aeab79 --- /dev/null +++ b/playground-app/app/components/exclaim-components/vbox.js @@ -0,0 +1,17 @@ +import templateOnlyComponent from '@ember/component/template-only'; + +export const VBox = { + component: templateOnlyComponent(), + shorthandProperty: 'children', + meta: { + descriptioN: 'A container that lays out its contents vertically', + properties: [ + { + name: 'children', + description: 'An array items in the container', + }, + ], + }, +}; + +export default VBox.component; diff --git a/playground-app/app/components/exclaim-components/vbox/component.js b/playground-app/app/components/exclaim-components/vbox/component.js deleted file mode 100644 index d8fb5f8..0000000 --- a/playground-app/app/components/exclaim-components/vbox/component.js +++ /dev/null @@ -1,15 +0,0 @@ -import Component from '@ember/component'; - -export const NAME = 'vbox'; -export const DESCRIPTION = 'A container that lays out its contents vertically'; -export const SHORTHAND_PROPERTY = 'children'; -export const PROPERTIES = [ - { - name: 'children', - description: 'An array items in the container', - }, -]; - -export default Component.extend({ - tagName: '', -}); diff --git a/playground-app/app/components/exclaim-components/vbox/template.hbs b/playground-app/app/components/exclaim-components/vbox/template.hbs deleted file mode 100644 index cc57b25..0000000 --- a/playground-app/app/components/exclaim-components/vbox/template.hbs +++ /dev/null @@ -1,5 +0,0 @@ -
- {{#each @config.children as |child|}} - {{yield child}} - {{/each}} -
\ No newline at end of file diff --git a/playground-app/app/components/json-editor/styles.css b/playground-app/app/components/json-editor.css similarity index 100% rename from playground-app/app/components/json-editor/styles.css rename to playground-app/app/components/json-editor.css diff --git a/playground-app/app/components/json-editor/template.hbs b/playground-app/app/components/json-editor.hbs similarity index 88% rename from playground-app/app/components/json-editor/template.hbs rename to playground-app/app/components/json-editor.hbs index 1a40040..f68d68e 100644 --- a/playground-app/app/components/json-editor/template.hbs +++ b/playground-app/app/components/json-editor.hbs @@ -1,7 +1,7 @@ { try { JSON.parse(newString); - get(this, 'onChange')(newString); - } catch (error) { + this.args.onChange(newString); + } catch { // Ignore, just don't update } - }), -}); + }; +} diff --git a/playground-app/app/components/sample-wrapper/template.hbs b/playground-app/app/components/sample-wrapper.hbs similarity index 100% rename from playground-app/app/components/sample-wrapper/template.hbs rename to playground-app/app/components/sample-wrapper.hbs diff --git a/playground-app/app/components/sample-wrapper/component.js b/playground-app/app/components/sample-wrapper/component.js deleted file mode 100644 index 4798652..0000000 --- a/playground-app/app/components/sample-wrapper/component.js +++ /dev/null @@ -1,5 +0,0 @@ -import Component from '@ember/component'; - -export default Component.extend({ - tagName: '', -}); diff --git a/playground-app/app/example/controller.js b/playground-app/app/example/controller.js index c464a66..22deada 100644 --- a/playground-app/app/example/controller.js +++ b/playground-app/app/example/controller.js @@ -1,16 +1,12 @@ import Controller from '@ember/controller'; import { tracked } from '@glimmer/tracking'; -import config from 'playground-app/config/environment'; -import discoverImplementations from 'playground-app/utils/discover-implementations'; +import implementationMap from 'playground-app/implementation-map'; export default class ExampleController extends Controller { @tracked ui; @tracked env; - implementationMap = discoverImplementations(config, { - componentPrefix: 'components/exclaim-components', - helperPrefix: 'utils/exclaim-helpers', - }); + implementationMap = implementationMap; handleSave = async (event) => { if (event.ctrlKey && event.key === 's') { diff --git a/playground-app/app/helpers/singleton-hash.js b/playground-app/app/helpers/singleton-hash.js new file mode 100644 index 0000000..5cab8bf --- /dev/null +++ b/playground-app/app/helpers/singleton-hash.js @@ -0,0 +1,3 @@ +import { helper } from '@ember/component/helper'; + +export default helper(([key, value]) => ({ [key]: value })); diff --git a/playground-app/app/implementation-map.js b/playground-app/app/implementation-map.js new file mode 100644 index 0000000..34dc183 --- /dev/null +++ b/playground-app/app/implementation-map.js @@ -0,0 +1,27 @@ +import { Box } from 'playground-app/components/exclaim-components/box'; +import { Checkbox } from 'playground-app/components/exclaim-components/checkbox'; +import { Each } from 'playground-app/components/exclaim-components/each'; +import { Hbox } from 'playground-app/components/exclaim-components/hbox'; +import { Header } from 'playground-app/components/exclaim-components/header'; +import { If } from 'playground-app/utils/exclaim-helpers/if'; +import { Input } from 'playground-app/components/exclaim-components/input'; +import { Join } from 'playground-app/utils/exclaim-helpers/join'; +import { Let } from 'playground-app/components/exclaim-components/let'; +import { Table } from 'playground-app/components/exclaim-components/table'; +import { Text } from 'playground-app/components/exclaim-components/text'; +import { VBox } from 'playground-app/components/exclaim-components/vbox'; + +export default { + box: Box, + checkbox: Checkbox, + each: Each, + hbox: Hbox, + header: Header, + if: If, + input: Input, + join: Join, + let: Let, + table: Table, + text: Text, + vbox: VBox, +}; diff --git a/playground-app/app/index/controller.js b/playground-app/app/index/controller.js index 2243c08..146bad7 100644 --- a/playground-app/app/index/controller.js +++ b/playground-app/app/index/controller.js @@ -1,39 +1,34 @@ -import { computed, get } from '@ember/object'; import Controller from '@ember/controller'; -import config from 'playground-app/config/environment'; -import discoverImplementations from 'playground-app/utils/discover-implementations'; +import { tracked } from '@glimmer/tracking'; +import implementationMap from 'playground-app/implementation-map'; import samples from './samples'; +import { TrackedObject } from 'tracked-built-ins/.'; -const implementationMap = discoverImplementations(config, { - componentPrefix: 'components/exclaim-components', - helperPrefix: 'utils/exclaim-helpers', -}); +export default class IndexController extends Controller { + queryParams = ['active']; -const docs = Object.values(implementationMap); -const componentDocs = docs.filter((doc) => doc.componentPath); -const helperDocs = docs.filter((doc) => doc.helper); + @tracked active = -1; + @tracked uiString = this.samples[this.active]?.interface ?? ''; + @tracked envString = this.samples[this.active]?.environment ?? ''; -export default Controller.extend({ - queryParams: ['active'], - active: -1, + samples = samples; + implementationMap = implementationMap; - samples, - componentDocs, - helperDocs, - implementationMap, + onSampleSelect = (event) => { + this.active = Number(event.target.value); - uiString: sampleValue('interface'), - envString: sampleValue('environment'), -}); + let sample = this.samples[this.active]; + if (sample) { + this.uiString = sample.interface ?? ''; + this.envString = sample.environment ?? ''; + } + }; -function sampleValue(key) { - return computed('active', { - get() { - return get(this, `samples.${get(this, 'active')}.${key}`) || ''; - }, + get ui() { + return JSON.parse(this.uiString || '{}'); + } - set(key, value) { - return value; - }, - }); + get env() { + return new TrackedObject(JSON.parse(this.envString || '{}')); + } } diff --git a/playground-app/app/index/template.hbs b/playground-app/app/index/template.hbs index edb5f37..df5cd59 100644 --- a/playground-app/app/index/template.hbs +++ b/playground-app/app/index/template.hbs @@ -1,11 +1,9 @@ -{{! template-lint-disable no-action require-input-label }} - -
-
-

+
+
+

Environment - {{#each this.samples as |sample index|}}

- {{json-editor - local-class='editor' - string=this.envString - onChange=(action (mut this.envString)) - }} +
-
-

Interface

- {{json-editor - local-class='editor' - string=this.uiString - onChange=(action (mut this.uiString)) - }} +
+

Interface

+
-
-
-

Result

- {{#exclaim-ui - local-class='rendered' - ui=(json this.uiString) - env=(json this.envString) - implementationMap=this.implementationMap - wrapper=(component 'sample-wrapper') - useClassicReactivity=true +
+
+

Result

+ Error: {{error.message}} - {{/exclaim-ui}} +
-
-

Demo Component Documentation

-
- {{#each this.componentDocs as |component|}} -
- {{component.name}}: - {{component.description}} -
    - {{#each component.properties as |property|}} -
  • - - {{~property.name~}} - : - {{property.description}} -
  • - {{/each}} -
-
- {{/each}} +
+

Demo Component Documentation

+
+ {{#each-in this.implementationMap as |name impl|}} + {{#if impl.component}} +
+ {{name}}: + {{impl.meta.description}} +
    + {{#each impl.meta.properties as |property|}} +
  • + + {{~property.name~}} + : + {{property.description}} +
  • + {{/each}} +
+
+ {{/if}} + {{/each-in}}
- -

Demo Helper Documentation

-
- {{#each this.helperDocs as |helperInfo|}} -
- {{helperInfo.name}}: - {{helperInfo.description}} -
    - {{#each helperInfo.properties as |property|}} -
  • - - {{~property.name~}} - : - {{property.description}} -
  • - {{/each}} -
-
- {{/each}} +

Demo Helper Documentation

+
+ {{#each-in this.implementationMap as |name impl|}} + {{#if impl.helper}} +
+ {{name}}: + {{impl.meta.description}} +
    + {{#each impl.meta.properties as |property|}} +
  • + + {{~property.name~}} + : + {{property.description}} +
  • + {{/each}} +
+
+ {{/if}} + {{/each-in}}
-
+
\ No newline at end of file diff --git a/playground-app/app/utils/discover-implementations.js b/playground-app/app/utils/discover-implementations.js deleted file mode 100644 index b306115..0000000 --- a/playground-app/app/utils/discover-implementations.js +++ /dev/null @@ -1,64 +0,0 @@ -/* global require */ -export default function discoverImplementations( - { modulePrefix, podModulePrefix }, - { componentPrefix, helperPrefix } -) { - const fullComponentPrefix = new RegExp( - `^(${modulePrefix}|${podModulePrefix})/${componentPrefix}/` - ); - const fullHelperPrefix = new RegExp( - `^(${modulePrefix}|${podModulePrefix})/${helperPrefix}/` - ); - const modulePrefixRegex = new RegExp( - `^(${modulePrefix}|${podModulePrefix})/` - ); - - const map = {}; - for (const key of Object.keys(require.entries)) { - if (componentPrefix && fullComponentPrefix.test(key)) { - const { - NAME, - DESCRIPTION, - SHORTHAND_PROPERTY, - PROPERTIES, - COMPONENT_META, - } = require(key); - if (!NAME) continue; - - const shortName = key - .replace(fullComponentPrefix, '') - .replace(/\/component$/, ''); - - map[NAME] = { - componentPath: `${componentPrefix - .replace(modulePrefixRegex, '') - .replace('components/', '')}/${shortName}`, - name: NAME, - description: DESCRIPTION, - shorthandProperty: SHORTHAND_PROPERTY, - properties: PROPERTIES, - componentMeta: COMPONENT_META, - }; - } else if (helperPrefix && fullHelperPrefix.test(key)) { - const { - NAME, - DESCRIPTION, - SHORTHAND_PROPERTY, - PROPERTIES, - HELPER_META, - default: helper, - } = require(key); - if (!NAME) continue; - - map[NAME] = { - helper, - name: NAME, - description: DESCRIPTION, - shorthandProperty: SHORTHAND_PROPERTY, - properties: PROPERTIES, - helperMeta: HELPER_META, - }; - } - } - return map; -} diff --git a/playground-app/app/utils/exclaim-helpers/if.js b/playground-app/app/utils/exclaim-helpers/if.js index ab84584..c54f290 100644 --- a/playground-app/app/utils/exclaim-helpers/if.js +++ b/playground-app/app/utils/exclaim-helpers/if.js @@ -1,27 +1,23 @@ -export const NAME = 'if'; -export const DESCRIPTION = 'A construct for rendering one thing or another'; -export const SHORTHAND_PROPERTY = 'condition'; -export const PROPERTIES = [ - { - name: 'condition', - description: 'A value that will determine which case is rendered', +export const If = { + helper: (config) => (config.condition ? config.then : config.else), + shorthandProperty: 'condition', + meta: { + description: 'A construct for rendering one thing or another', + properties: [ + { + name: 'condition', + description: 'A value that will determine which case is rendered', + }, + { + name: 'then', + description: + 'An optional component that will be rendered if the condition is true', + }, + { + name: 'else', + description: + 'An optional component that will be rendered if the condition is false', + }, + ], }, - { - name: 'then', - description: - 'An optional component that will be rendered if the condition is true', - }, - { - name: 'else', - description: - 'An optional component that will be rendered if the condition is false', - }, -]; - -export default (config) => { - if (config.condition) { - return config.then; - } else { - return config.else; - } }; diff --git a/playground-app/app/utils/exclaim-helpers/join.js b/playground-app/app/utils/exclaim-helpers/join.js index f44050c..50b8417 100644 --- a/playground-app/app/utils/exclaim-helpers/join.js +++ b/playground-app/app/utils/exclaim-helpers/join.js @@ -1,18 +1,18 @@ -export const NAME = 'join'; -export const DESCRIPTION = 'Joins an array of values into a single string'; -export const SHORTHAND_PROPERTY = 'items'; -export const PROPERTIES = [ - { - name: 'items', - description: 'An array of values to be joined together', +export const Join = { + helper: (config) => config.items?.join(config.separator ?? ''), + shorthandProperty: 'items', + meta: { + description: 'Joins an array of values into a single string', + properties: [ + { + name: 'items', + description: 'An array of values to be joined together', + }, + { + name: 'separator', + description: + 'An optional string that should be used between each joined element', + }, + ], }, - { - name: 'separator', - description: - 'An optional string that should be used between each joined element', - }, -]; - -export default (config) => { - return config.items?.join(config.separator ?? ''); }; diff --git a/test-app/tests/integration/environment-computed-test.js b/test-app/tests/integration/environment-computed-test.js index c3263fe..1d87ce4 100644 --- a/test-app/tests/integration/environment-computed-test.js +++ b/test-app/tests/integration/environment-computed-test.js @@ -26,14 +26,14 @@ module('Integration | environment | computed', function (hooks) { this.implementationMap = { let: { shorthandProperty: 'bindings', - componentPath: setComponentTemplate( + component: setComponentTemplate( hbs`{{yield @config.in @config.bindings}}`, class extends Component {}, ), }, validate: { shorthandProperty: 'callback', - componentPath: class extends Component { + component: class extends Component { constructor(owner, args) { super(owner, args); setTimeout(() => diff --git a/test-app/tests/integration/environment-tracked-test.js b/test-app/tests/integration/environment-tracked-test.js index aa01638..64c2aaa 100644 --- a/test-app/tests/integration/environment-tracked-test.js +++ b/test-app/tests/integration/environment-tracked-test.js @@ -25,14 +25,14 @@ module('Integration | environment | tracked', function (hooks) { this.implementationMap = { let: { shorthandProperty: 'bindings', - componentPath: setComponentTemplate( + component: setComponentTemplate( hbs`{{yield @config.in @config.bindings}}`, class extends Component {}, ), }, validate: { shorthandProperty: 'callback', - componentPath: class extends Component { + component: class extends Component { constructor(owner, args) { super(owner, args); setTimeout(() => diff --git a/test-app/tests/integration/exclaim-ui-computed-test.js b/test-app/tests/integration/exclaim-ui-computed-test.js index 0b609d9..5c02462 100644 --- a/test-app/tests/integration/exclaim-ui-computed-test.js +++ b/test-app/tests/integration/exclaim-ui-computed-test.js @@ -43,9 +43,7 @@ module('Integration | Component | ExclaimUi | computed env', function (hooks) { test('it invokes helpers', async function (assert) { const implementationMap = { 'simple-component': { - componentPath: makeComponent( - hbs`
{{@config.value}}
`, - ), + component: makeComponent(hbs`
{{@config.value}}
`), }, join: { shorthandProperty: 'items', @@ -72,9 +70,7 @@ module('Integration | Component | ExclaimUi | computed env', function (hooks) { test('it renders a basic component', async function (assert) { const implementationMap = { 'simple-component': { - componentPath: makeComponent( - hbs`
{{@config.value}}
`, - ), + component: makeComponent(hbs`
{{@config.value}}
`), }, }; @@ -94,12 +90,12 @@ module('Integration | Component | ExclaimUi | computed env', function (hooks) { test('it renders subcomponents', async function (assert) { const implementationMap = { 'parent-component': { - componentPath: makeComponent( + component: makeComponent( hbs`{{yield @config.childA}}{{yield @config.childB}}`, ), }, 'child-component': { - componentPath: makeComponent( + component: makeComponent( hbs`
{{@config.name}}
`, ), }, @@ -120,9 +116,7 @@ module('Integration | Component | ExclaimUi | computed env', function (hooks) { test('it renders data bound to the env', async function (assert) { const implementationMap = { 'simple-component': { - componentPath: makeComponent( - hbs`
{{@config.value}}
`, - ), + component: makeComponent(hbs`
{{@config.value}}
`), }, }; @@ -150,7 +144,7 @@ module('Integration | Component | ExclaimUi | computed env', function (hooks) { test('it writes bound data back to the env', async function (assert) { const implementationMap = { 'simple-component': { - componentPath: makeComponent( + component: makeComponent( hbs``, ), }, @@ -179,7 +173,7 @@ module('Integration | Component | ExclaimUi | computed env', function (hooks) { const implementationMap = { root: { - componentPath: makeComponent( + component: makeComponent( hbs`
{{@config.global}}
{{yield @config.child (hash self='first')}}
@@ -189,7 +183,7 @@ module('Integration | Component | ExclaimUi | computed env', function (hooks) { ), }, child: { - componentPath: makeComponent( + component: makeComponent( hbs`
{{@config.global}}
{{@config.self}}
@@ -295,13 +289,13 @@ module('Integration | Component | ExclaimUi | computed env', function (hooks) { const implementationMap = { root: { - componentPath: makeComponent( + component: makeComponent( hbs`{{yield @config.child (hash local='yes' base='no')}}`, (instance) => (components.root = instance), ), }, child: { - componentPath: makeComponent( + component: makeComponent( hbs``, (instance) => (components.child = instance), ), @@ -367,13 +361,13 @@ module('Integration | Component | ExclaimUi | computed env', function (hooks) { const implementationMap = { root: { - componentPath: makeComponent( + component: makeComponent( hbs`{{yield @config.child (hash local='local')}}`, (instance) => (components.root = instance), ), }, child: { - componentPath: makeComponent( + component: makeComponent( hbs``, (instance) => (components.child = instance), ), diff --git a/test-app/tests/integration/exclaim-ui-tracked-test.js b/test-app/tests/integration/exclaim-ui-tracked-test.js index 2152c84..7dc190e 100644 --- a/test-app/tests/integration/exclaim-ui-tracked-test.js +++ b/test-app/tests/integration/exclaim-ui-tracked-test.js @@ -42,9 +42,7 @@ module('Integration | Component | ExclaimUi | tracked env', function (hooks) { test('it invokes helpers', async function (assert) { const implementationMap = { 'simple-component': { - componentPath: makeComponent( - hbs`
{{@config.value}}
`, - ), + component: makeComponent(hbs`
{{@config.value}}
`), }, join: { shorthandProperty: 'items', @@ -71,9 +69,7 @@ module('Integration | Component | ExclaimUi | tracked env', function (hooks) { test('it renders a basic component', async function (assert) { const implementationMap = { 'simple-component': { - componentPath: makeComponent( - hbs`
{{@config.value}}
`, - ), + component: makeComponent(hbs`
{{@config.value}}
`), }, }; @@ -93,12 +89,12 @@ module('Integration | Component | ExclaimUi | tracked env', function (hooks) { test('it renders subcomponents', async function (assert) { const implementationMap = { 'parent-component': { - componentPath: makeComponent( + component: makeComponent( hbs`{{yield @config.childA}}{{yield @config.childB}}`, ), }, 'child-component': { - componentPath: makeComponent( + component: makeComponent( hbs`
{{@config.name}}
`, ), }, @@ -119,9 +115,7 @@ module('Integration | Component | ExclaimUi | tracked env', function (hooks) { test('it renders data bound to the env', async function (assert) { const implementationMap = { 'simple-component': { - componentPath: makeComponent( - hbs`
{{@config.value}}
`, - ), + component: makeComponent(hbs`
{{@config.value}}
`), }, }; @@ -149,7 +143,7 @@ module('Integration | Component | ExclaimUi | tracked env', function (hooks) { test('it writes bound data back to the env', async function (assert) { const implementationMap = { 'simple-component': { - componentPath: makeComponent( + component: makeComponent( hbs``, ), }, @@ -178,7 +172,7 @@ module('Integration | Component | ExclaimUi | tracked env', function (hooks) { const implementationMap = { root: { - componentPath: makeComponent( + component: makeComponent( hbs`
{{@config.global}}
{{yield @config.child (hash self='first')}}
@@ -188,7 +182,7 @@ module('Integration | Component | ExclaimUi | tracked env', function (hooks) { ), }, child: { - componentPath: makeComponent( + component: makeComponent( hbs`
{{@config.global}}
{{@config.self}}
@@ -294,13 +288,13 @@ module('Integration | Component | ExclaimUi | tracked env', function (hooks) { const implementationMap = { root: { - componentPath: makeComponent( + component: makeComponent( hbs`{{yield @config.child (hash local='yes' base='no')}}`, (instance) => (components.root = instance), ), }, child: { - componentPath: makeComponent( + component: makeComponent( hbs``, (instance) => (components.child = instance), ), @@ -366,13 +360,13 @@ module('Integration | Component | ExclaimUi | tracked env', function (hooks) { const implementationMap = { root: { - componentPath: makeComponent( + component: makeComponent( hbs`{{yield @config.child (hash local='local')}}`, (instance) => (components.root = instance), ), }, child: { - componentPath: makeComponent( + component: makeComponent( hbs``, (instance) => (components.child = instance), ), diff --git a/yarn.lock b/yarn.lock index 2a393e0..aa16467 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1824,6 +1824,11 @@ "@glimmer/interfaces" "^0.88.1" "@glimmer/util" "^0.88.1" +"@glint/template@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@glint/template/-/template-1.3.0.tgz#8fa1e0c466d84cc8665e2253bbe1992af7c89542" + integrity sha512-FUfbXSyh+KnwUaMTG4skESPPYL6trwAIKOp9yMwDo+Uw4LxCJjQ9/RCAJTTXZ0/kiTHLr7S2P4vsIbHeorOvaA== + "@gwhitney/detect-indent@7.0.1": version "7.0.1" resolved "https://registry.yarnpkg.com/@gwhitney/detect-indent/-/detect-indent-7.0.1.tgz#db16d7fe6d13b26dc792442e5156677b44cc428e"