From 41d1ff6aee3f28ad7dbcd674986021c72c162534 Mon Sep 17 00:00:00 2001 From: tpluscode Date: Wed, 11 Dec 2024 10:25:54 +0100 Subject: [PATCH 01/48] wip: simpler component model --- .changeset/tasty-hounds-hope.md | 6 + .../shapes/editors/dash/EnumSelect/iris.ttl | 22 ++ .../editors/dash/EnumSelect/strings.ttl | 16 ++ .../stories/DASH/EnumSelectEditor.stories.ts | 36 +++ package-lock.json | 9 +- packages/core/components.ts | 5 +- packages/core/index.ts | 2 +- packages/core/lib/components.ts | 3 - packages/core/lib/components/autoComplete.ts | 1 - .../lib/components/base/instancesSelect.ts | 11 - packages/core/lib/components/blankNode.ts | 18 +- packages/core/lib/components/booleanSelect.ts | 19 -- packages/core/lib/components/datePicker.ts | 18 +- .../core/lib/components/dateTimePicker.ts | 18 +- packages/core/lib/components/details.ts | 18 +- packages/core/lib/components/enumSelect.ts | 53 +---- .../core/lib/components/instancesSelect.ts | 24 +- packages/core/lib/components/richText.ts | 2 +- packages/core/lib/components/textArea.ts | 13 +- packages/core/lib/property.ts | 8 +- packages/core/models/components/index.ts | 150 ++----------- .../core/models/components/lib/decorate.ts | 41 ---- packages/core/models/components/reducers.ts | 68 +----- .../core/models/forms/effects/addObject.ts | 4 +- packages/core/models/forms/index.ts | 4 +- .../core/models/forms/lib/stateBuilder.ts | 1 - .../models/forms/reducers/addFormField.ts | 6 - packages/core/renderer.ts | 3 - packages/wc/NativeComponents.ts | 99 ++------- packages/wc/ShaperoneForm.ts | 57 +++-- .../wc/components/ShaperoneElementBase.ts | 21 ++ packages/wc/components/connect.ts | 7 +- packages/wc/components/editor.ts | 16 ++ packages/wc/components/index.ts | 78 ++----- packages/wc/components/readonly.ts | 6 +- packages/wc/components/sh1-button.ts | 23 ++ packages/wc/components/sh1-focus-node.ts | 14 ++ packages/wc/components/sh1-form.ts | 8 + packages/wc/components/sh1-group.ts | 13 ++ packages/wc/components/sh1-object.ts | 76 +++++++ packages/wc/components/sh1-property.ts | 74 +++++++ packages/wc/components/validity.ts | 25 ++- packages/wc/configure.ts | 6 +- packages/wc/elements/BooleanSelect.ts | 25 +++ packages/wc/elements/EnumSelect.ts | 50 +++++ packages/wc/elements/MultiEditorBase.ts | 10 + packages/wc/elements/SingleEditorBase.ts | 36 +++ packages/wc/elements/TextArea.ts | 15 ++ packages/wc/elements/TextField.ts | 19 ++ packages/wc/index.ts | 22 +- packages/wc/package.json | 2 + packages/wc/renderer/decorator.ts | 58 ----- packages/wc/renderer/editor.ts | 99 --------- packages/wc/renderer/group.ts | 51 ----- packages/wc/renderer/index.ts | 127 +++++++++-- packages/wc/renderer/model.ts | 71 +----- packages/wc/renderer/object.ts | 9 +- packages/wc/renderer/property.ts | 7 +- packages/wc/store.ts | 5 + packages/wc/templates.ts | 208 ------------------ 60 files changed, 771 insertions(+), 1145 deletions(-) create mode 100644 .changeset/tasty-hounds-hope.md create mode 100644 demos/storybook/shapes/editors/dash/EnumSelect/iris.ttl create mode 100644 demos/storybook/shapes/editors/dash/EnumSelect/strings.ttl create mode 100644 demos/storybook/stories/DASH/EnumSelectEditor.stories.ts delete mode 100644 packages/core/lib/components/booleanSelect.ts delete mode 100644 packages/core/models/components/lib/decorate.ts create mode 100644 packages/wc/components/ShaperoneElementBase.ts create mode 100644 packages/wc/components/editor.ts create mode 100644 packages/wc/components/sh1-button.ts create mode 100644 packages/wc/components/sh1-focus-node.ts create mode 100644 packages/wc/components/sh1-form.ts create mode 100644 packages/wc/components/sh1-group.ts create mode 100644 packages/wc/components/sh1-object.ts create mode 100644 packages/wc/components/sh1-property.ts create mode 100644 packages/wc/elements/BooleanSelect.ts create mode 100644 packages/wc/elements/EnumSelect.ts create mode 100644 packages/wc/elements/MultiEditorBase.ts create mode 100644 packages/wc/elements/SingleEditorBase.ts create mode 100644 packages/wc/elements/TextArea.ts create mode 100644 packages/wc/elements/TextField.ts delete mode 100644 packages/wc/renderer/decorator.ts delete mode 100644 packages/wc/renderer/group.ts delete mode 100644 packages/wc/templates.ts diff --git a/.changeset/tasty-hounds-hope.md b/.changeset/tasty-hounds-hope.md new file mode 100644 index 00000000..1b77765e --- /dev/null +++ b/.changeset/tasty-hounds-hope.md @@ -0,0 +1,6 @@ +--- +"@hydrofoil/shaperone-core": major +"@hydrofoil/shaperone-wc": major +--- + +Refactored the whole component model diff --git a/demos/storybook/shapes/editors/dash/EnumSelect/iris.ttl b/demos/storybook/shapes/editors/dash/EnumSelect/iris.ttl new file mode 100644 index 00000000..fea046dd --- /dev/null +++ b/demos/storybook/shapes/editors/dash/EnumSelect/iris.ttl @@ -0,0 +1,22 @@ +PREFIX rdfs: +PREFIX ex: +PREFIX dash: +PREFIX schema: +PREFIX sh: + +<> + a sh:NodeShape ; + sh:property + [ + sh:name "Category" ; + sh:path schema:category ; + dash:editor dash:EnumSelectEditor ; + sh:in ( ex:A ex:B ex:C ) ; + sh:minCount 1 ; + sh:maxCount 1 ; + ] +. + +ex:A a schema:Category ; rdfs:label "A" . +ex:B a schema:Category ; rdfs:label "B" . +ex:C a schema:Category ; rdfs:label "C" . diff --git a/demos/storybook/shapes/editors/dash/EnumSelect/strings.ttl b/demos/storybook/shapes/editors/dash/EnumSelect/strings.ttl new file mode 100644 index 00000000..87d0bf6f --- /dev/null +++ b/demos/storybook/shapes/editors/dash/EnumSelect/strings.ttl @@ -0,0 +1,16 @@ +PREFIX dash: +PREFIX schema: +PREFIX sh: + +<> + a sh:NodeShape ; + sh:property + [ + sh:name "Category" ; + sh:path schema:category ; + dash:editor dash:EnumSelectEditor ; + sh:in ( "A" "B" "C" ) ; + sh:minCount 1 ; + sh:maxCount 1 ; + ] +. diff --git a/demos/storybook/stories/DASH/EnumSelectEditor.stories.ts b/demos/storybook/stories/DASH/EnumSelectEditor.stories.ts new file mode 100644 index 00000000..ca5a936c --- /dev/null +++ b/demos/storybook/stories/DASH/EnumSelectEditor.stories.ts @@ -0,0 +1,36 @@ +import type { Meta, StoryObj as Story } from '@storybook/web-components' +import { render } from '../render.js' +import enumStrings from '../../shapes/editors/dash/EnumSelect/strings.ttl?raw' +import enumIris from '../../shapes/editors/dash/EnumSelect/iris.ttl?raw' + +/** + * Examples of using the dash:EnumSelectEditor + */ +const meta: Meta = { + component: 'shaperone-form', +} + +export default meta + +/** + * Values can be provided as literals using the `sh:in` property + */ +export const Literals: Story = { + name: 'sh:in choice of literals', + args: { + shapes: enumStrings, + }, + render, +} + +/** + * Values can also be provided as resources. + * They must be IRIs, present in the Shapes Graph, and `rdfs:label` property will be used as the option text. + */ +export const Resources: Story = { + name: 'sh:in choice of IRIs', + args: { + shapes: enumIris, + }, + render, +} diff --git a/package-lock.json b/package-lock.json index 9931fe37..9261607f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6926,6 +6926,12 @@ "node": ">=0.3.1" } }, + "node_modules/@webcomponents/scoped-custom-element-registry": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@webcomponents/scoped-custom-element-registry/-/scoped-custom-element-registry-0.0.9.tgz", + "integrity": "sha512-4RVHwN9oIp23mhrxeDbP9/0wvdcrB0d/MFMqIStShIgfzsqXZXf1OdU5kvqjgdMYavQ5Zm8C6BuBcAjzNwAetQ==", + "license": "BSD-3-Clause" + }, "node_modules/@webcomponents/shadycss": { "version": "1.10.2", "license": "BSD-3-Clause" @@ -23748,10 +23754,12 @@ "dependencies": { "@hydrofoil/shaperone-core": "^0.12.1", "@open-wc/lit-helpers": "^0.7.0", + "@open-wc/scoped-elements": "^3.0.5", "@rdfine/shacl": "^0.10.5", "@rdfjs-elements/lit-helpers": "^0.3.7", "@tpluscode/rdf-ns-builders": "^4.3.0", "@tpluscode/rdfine": "^0.7.9", + "@webcomponents/scoped-custom-element-registry": "^0.0.9", "@zazuko/env": "^2.1.0", "concat-merge": "^1.0.3", "lit": "^3", @@ -24148,7 +24156,6 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/@open-wc/scoped-elements/-/scoped-elements-3.0.5.tgz", "integrity": "sha512-q4U+hFTQQRyorJILOpmBm6PY2hgjCnQe214nXJNjbJMQ9EvT55oyZ7C8BY5aFYJkytUyBoawlMpZt4F2xjdzHw==", - "dev": true, "license": "MIT", "dependencies": { "@open-wc/dedupe-mixin": "^1.4.0", diff --git a/packages/core/components.ts b/packages/core/components.ts index 5de6373a..c2f31341 100644 --- a/packages/core/components.ts +++ b/packages/core/components.ts @@ -9,8 +9,7 @@ export type { AutoComplete, AutoCompleteEditor } from './lib/components/autoComp export { autoComplete } from './lib/components/autoComplete.js' export type { BlankNode, BlankNodeEditor } from './lib/components/blankNode.js' export { blankNode } from './lib/components/blankNode.js' -export type { BooleanSelectEditor, BooleanSelect } from './lib/components/booleanSelect.js' -export { booleanSelect } from './lib/components/booleanSelect.js' +export type { BooleanSelect } from './lib/components/booleanSelect.js' export type { DatePickerEditor, DatePicker } from './lib/components/datePicker.js' export { datePicker } from './lib/components/datePicker.js' export type { DateTimePicker, DateTimePickerEditor } from './lib/components/dateTimePicker.js' @@ -23,7 +22,7 @@ export type { InstancesSelect, InstancesSelectEditor } from './lib/components/in export { instancesSelect } from './lib/components/instancesSelect.js' export type { RichTextEditor, RichText } from './lib/components/richText.js' export { richText } from './lib/components/richText.js' -export type { TextAreaEditor, TextArea } from './lib/components/textArea.js' +export type { TextArea } from './lib/components/textArea.js' export { textArea } from './lib/components/textArea.js' export type { TextAreaWithLang, TextAreaWithLangEditor } from './lib/components/textAreaWithLang.js' export { textAreaWithLang } from './lib/components/textAreaWithLang.js' diff --git a/packages/core/index.ts b/packages/core/index.ts index 08ef0728..3cdcd2a5 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -11,5 +11,5 @@ import '@rdfine/dash/extensions/sh/PropertyShape' * A [focus node](https://www.w3.org/TR/shacl/#focusNodes) is a graph pointer to a Named Node or a Blank Node */ export type FocusNode = GraphPointer -export type { Component, SingleEditorComponent, MultiEditorComponent, RenderComponent, Lazy } from './models/components/index.js' +export type { Component, SingleEditorComponent, MultiEditorComponent } from './models/components/index.js' export type { Editor, SingleEditor, MultiEditor } from './models/editors/index.js' diff --git a/packages/core/lib/components.ts b/packages/core/lib/components.ts index 814cfb27..ff2ee2d8 100644 --- a/packages/core/lib/components.ts +++ b/packages/core/lib/components.ts @@ -2,11 +2,8 @@ import type { GraphPointer } from 'clownface' import { getLocalizedLabel } from '@rdfjs-elements/lit-helpers' import { rdfs } from '@tpluscode/rdf-ns-builders' import type { PropertyShape } from '@rdfine/shacl' -import type { SingleEditorComponent } from '../models/components/index.js' import env from '../env.js' -export type CoreComponent> = Omit - export function sort(shape: PropertyShape) { const orderByList = [...shape.pointer.out(env().ns.sh1.orderBy).list() || []] const orderByPredicates = orderByList.length ? orderByList.map(i => i.term) : [rdfs.label] diff --git a/packages/core/lib/components/autoComplete.ts b/packages/core/lib/components/autoComplete.ts index 7c9b5eae..0029f970 100644 --- a/packages/core/lib/components/autoComplete.ts +++ b/packages/core/lib/components/autoComplete.ts @@ -1,5 +1,4 @@ import { dash } from '@tpluscode/rdf-ns-builders' -import type { CoreComponent } from '../components.js' import { sort } from '../components.js' import * as select from './base/instancesSelect.js' import type { State, Editor } from './base/instancesSelect.js' diff --git a/packages/core/lib/components/base/instancesSelect.ts b/packages/core/lib/components/base/instancesSelect.ts index cf76b901..a1b3e3be 100644 --- a/packages/core/lib/components/base/instancesSelect.ts +++ b/packages/core/lib/components/base/instancesSelect.ts @@ -41,17 +41,6 @@ export interface Editor extends SingleEditorComponent { sort(shape: PropertyShape, env: ShaperoneEnvironment): (left: GraphPointer, right: GraphPointer) => number } -export function shouldLoad({ componentState }: SingleEditorRenderParams): boolean { - return !componentState.instances -} - -export async function loadInstance({ property, value }: { - property: PropertyShape - value: GraphPointer -}): Promise { - return property.pointer.node(value) -} - export async function loadChoices({ property }: SingleEditorRenderParams): Promise { if (property.shape.class) { return property.shape.pointer.any() diff --git a/packages/core/lib/components/blankNode.ts b/packages/core/lib/components/blankNode.ts index 8a30e443..393a171e 100644 --- a/packages/core/lib/components/blankNode.ts +++ b/packages/core/lib/components/blankNode.ts @@ -1,19 +1,7 @@ -import { dash } from '@tpluscode/rdf-ns-builders/loose' -import type { ComponentInstance, SingleEditorComponent } from '../../models/components/index.js' -import type { CoreComponent } from '../components.js' +import type { SingleEditorComponent } from '../../models/components/index.js' /** - * Instance state of blank node component + * An interface for implementing dash:BlankNodeEditor */ -export interface BlankNode extends ComponentInstance { -} - -export interface BlankNodeEditor extends SingleEditorComponent { -} - -/** - * Extend to implement [DASH blank node editor](http://datashapes.org/forms.html#BlankNodeEditor) - */ -export const blankNode: CoreComponent = { - editor: dash.BlankNodeEditor, +export interface BlankNodeEditor extends SingleEditorComponent { } diff --git a/packages/core/lib/components/booleanSelect.ts b/packages/core/lib/components/booleanSelect.ts deleted file mode 100644 index 1f8bc03e..00000000 --- a/packages/core/lib/components/booleanSelect.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { dash } from '@tpluscode/rdf-ns-builders' -import type { ComponentInstance, SingleEditorComponent } from '../../models/components/index.js' -import type { CoreComponent } from '../components.js' - -/** - * Instance state of boolean select component - */ -export interface BooleanSelect extends ComponentInstance { -} - -export interface BooleanSelectEditor extends SingleEditorComponent { -} - -/** - * Extend to implement [DASH boolean select editor](http://datashapes.org/forms.html#BooleanSelectEditor) - */ -export const booleanSelect: CoreComponent = { - editor: dash.BooleanSelectEditor, -} diff --git a/packages/core/lib/components/datePicker.ts b/packages/core/lib/components/datePicker.ts index 85c03c20..0a9c0da6 100644 --- a/packages/core/lib/components/datePicker.ts +++ b/packages/core/lib/components/datePicker.ts @@ -1,19 +1,7 @@ -import { dash } from '@tpluscode/rdf-ns-builders' -import type { ComponentInstance, SingleEditorComponent } from '../../models/components/index.js' -import type { CoreComponent } from '../components.js' +import type { SingleEditorComponent } from '../../models/components/index.js' /** - * Instance state of date picker component + * An interface for implementing dash:DatePickerEditor */ -export interface DatePicker extends ComponentInstance { -} - -export interface DatePickerEditor extends SingleEditorComponent { -} - -/** - * Extend to implement [DASH date picker editor](http://datashapes.org/forms.html#DatePickerEditor) - */ -export const datePicker: CoreComponent = { - editor: dash.DatePickerEditor, +export interface DatePickerEditor extends SingleEditorComponent { } diff --git a/packages/core/lib/components/dateTimePicker.ts b/packages/core/lib/components/dateTimePicker.ts index b09ae86d..4f0e9363 100644 --- a/packages/core/lib/components/dateTimePicker.ts +++ b/packages/core/lib/components/dateTimePicker.ts @@ -1,19 +1,7 @@ -import { dash } from '@tpluscode/rdf-ns-builders' -import type { ComponentInstance, SingleEditorComponent } from '../../models/components/index.js' -import type { CoreComponent } from '../components.js' +import type { SingleEditorComponent } from '../../models/components/index.js' /** - * Instance state of date-time picker component + * An interface for implementing dash:DateTimePickerEditor */ -export interface DateTimePicker extends ComponentInstance { -} - -export interface DateTimePickerEditor extends SingleEditorComponent { -} - -/** - * Extend to implement [DASH date-time picker editor](http://datashapes.org/forms.html#DateTimePickerEditor) - */ -export const dateTimePicker: CoreComponent = { - editor: dash.DateTimePickerEditor, +export interface DateTimePickerEditor extends SingleEditorComponent { } diff --git a/packages/core/lib/components/details.ts b/packages/core/lib/components/details.ts index c605b0f3..26e8360c 100644 --- a/packages/core/lib/components/details.ts +++ b/packages/core/lib/components/details.ts @@ -1,19 +1,7 @@ -import { dash } from '@tpluscode/rdf-ns-builders' -import type { ComponentInstance, SingleEditorComponent } from '../../models/components/index.js' -import type { CoreComponent } from '../components.js' +import type { SingleEditorComponent } from '../../models/components/index.js' /** - * Instance state of details component + * An interface for implementing dash:DetailsEditor */ -export interface Details extends ComponentInstance { -} - -export interface DetailsEditor extends SingleEditorComponent
{ -} - -/** - * Extend to implement [DASH details editor](http://datashapes.org/forms.html#DetailsEditor) - */ -export const details: CoreComponent = { - editor: dash.DetailsEditor, +export interface DetailsEditor extends SingleEditorComponent { } diff --git a/packages/core/lib/components/enumSelect.ts b/packages/core/lib/components/enumSelect.ts index 98fd631c..ebc2aaed 100644 --- a/packages/core/lib/components/enumSelect.ts +++ b/packages/core/lib/components/enumSelect.ts @@ -1,24 +1,14 @@ import type { PropertyShape } from '@rdfine/shacl' import type { GraphPointer } from 'clownface' -import { dash } from '@tpluscode/rdf-ns-builders' -import type { ComponentInstance, SingleEditorComponent } from '../../models/components/index.js' +import type { SingleEditorComponent } from '../../models/components/index.js' import type { FocusNode } from '../../index.js' -import type { CoreComponent } from '../components.js' -import { sort } from '../components.js' -import type { ShaperoneEnvironment } from '../../env.js' - -/** - * Represents the state of an enum select component - */ -export interface EnumSelect extends ComponentInstance { - choices?: GraphPointer[] - loading?: boolean -} /** * An interface for implementing dash:EnumSelectEditor */ -export interface EnumSelectEditor extends SingleEditorComponent { +export interface EnumSelectEditor extends SingleEditorComponent { + choices: GraphPointer[] + /** * Asynchronously load the values for the component * @param params @@ -27,39 +17,4 @@ export interface EnumSelectEditor extends SingleEditorComponent { focusNode: FocusNode property: PropertyShape }): Promise - - sort(shape: PropertyShape, env: ShaperoneEnvironment): (left: GraphPointer, right: GraphPointer) => number -} - -/** - * A base implementation of Enum Select component which sets {@link EnumSelect.ready} state flag the choices are first loaded - * - * The enums data (in case of Named Node values) will be loaded from the shapes graph - */ -export const enumSelect: CoreComponent = { - editor: dash.EnumSelectEditor, - init({ env, focusNode, property, componentState, updateComponentState }) { - if (!componentState.choices && !componentState.loading) { - updateComponentState({ - loading: true, - }); - (async () => { - const pointers = await this.loadChoices({ focusNode, property: property.shape }) - const choices = pointers.sort(this.sort(property.shape, env)) - - updateComponentState({ - choices, - ready: true, - loading: false, - }) - })() - - return false - } - return !componentState.loading - }, - async loadChoices({ property }) { - return property.pointer.node(property.in).toArray() - }, - sort, } diff --git a/packages/core/lib/components/instancesSelect.ts b/packages/core/lib/components/instancesSelect.ts index 1d129c4f..3ef808c2 100644 --- a/packages/core/lib/components/instancesSelect.ts +++ b/packages/core/lib/components/instancesSelect.ts @@ -1,25 +1,7 @@ -import { dash } from '@tpluscode/rdf-ns-builders' -import type { CoreComponent } from '../components.js' -import { sort } from '../components.js' -import * as select from './base/instancesSelect.js' -import type { State, Editor } from './base/instancesSelect.js' +import type { SingleEditorComponent } from '../../models/components/index.js' /** - * Represents the state of an instances select component + * An interface for implementing dash:InstancesSelectEditor */ -export interface InstancesSelect extends State { -} - -export interface InstancesSelectEditor extends Editor { -} - -/** - * A base implementation of Instances Select component which sets {@link InstancesSelect.ready} state flag the instances are first loaded. - * - * The instance data will be loaded from the shapes graph - */ -export const instancesSelect: CoreComponent = { - editor: dash.InstancesSelectEditor, - ...select, - sort, +export interface InstancesSelectEditor extends SingleEditorComponent { } diff --git a/packages/core/lib/components/richText.ts b/packages/core/lib/components/richText.ts index 5d468a23..a26b23bc 100644 --- a/packages/core/lib/components/richText.ts +++ b/packages/core/lib/components/richText.ts @@ -8,7 +8,7 @@ import type { CoreComponent } from '../components.js' export interface RichText extends ComponentInstance { } -export interface RichTextEditor extends SingleEditorComponent { +export interface RichTextEditor extends SingleEditorComponent { } /** diff --git a/packages/core/lib/components/textArea.ts b/packages/core/lib/components/textArea.ts index 9a8b2d6a..2ccd0f3b 100644 --- a/packages/core/lib/components/textArea.ts +++ b/packages/core/lib/components/textArea.ts @@ -1,19 +1,12 @@ import { dash } from '@tpluscode/rdf-ns-builders' -import type { ComponentInstance, SingleEditorComponent } from '../../models/components/index.js' -import type { CoreComponent } from '../components.js' +import type { Component } from '../../models/components/index.js' -/** - * Instance state of text area component - */ -export interface TextArea extends ComponentInstance { -} - -export interface TextAreaEditor extends SingleEditorComponent` -} - -export const enumSelect: Render = function ({ property, value, componentState }, { update }) { - const choices = componentState.choices || [] - - function updateHandler(e: any) { - const chosen = choices[(e.target).selectedIndex - 1] - if (chosen) update(chosen.term) - } - - return html`` -} - -export const datePicker = (type: 'date' | 'datetime-local'): Render => function ({ property, value }, { update }) { - return html`` -} - -export const instancesSelect: Render = function ({ property, value, componentState }, { update, clear }) { +export const instancesSelect: Render = function ({ property, value, componentState }, { + update, + clear, +}) { const choices = componentState.instances || [] function onInput(e: any) { @@ -60,33 +29,18 @@ export const instancesSelect: Render = function ({ proper } return html`` + + ${repeat(choices, pointer => html` + `)} + ` } export const uri: Render = function ({ env, property, value }, { update }) { return html`` -} - -export const booleanSelect: Render = function ({ env, value, property }, { clear, update }) { - function changed(e: any) { - if (e.target.value) { - update(env.literal(e.target.value, xsd.boolean)) - } else { - clear() - } - } - - return html`` + type="url" + ${validity(value)} + ${readOnly(property)} + @blur="${(e: any) => update(env.namedNode(e.target.value))}">` } diff --git a/packages/wc/components/readonly.ts b/packages/wc/components/readonly.ts index cd71d84c..8f6dbb92 100644 --- a/packages/wc/components/readonly.ts +++ b/packages/wc/components/readonly.ts @@ -12,12 +12,12 @@ class ReadonlyDirective extends Directive { } } - render(arg: PropertyState) { + render(arg: PropertyState | undefined) { return noChange } - update(part: ElementPart, [{ shape }]: Parameters) { - if (shape.readOnly) { + update(part: ElementPart, [state]: Parameters) { + if (state?.shape.readOnly) { part.element.setAttribute('readonly', 'readonly') part.element.setAttribute('disabled', 'disabled') } else { diff --git a/packages/wc/components/sh1-button.ts b/packages/wc/components/sh1-button.ts new file mode 100644 index 00000000..8fca9bbb --- /dev/null +++ b/packages/wc/components/sh1-button.ts @@ -0,0 +1,23 @@ +import { css, html, LitElement } from 'lit' + +export default class extends LitElement { + static get styles() { + return css` + button { + background: none; + border: 1px solid transparent; + border-radius: 1px; + padding: 2px 4px; + } + + button:hover { + border-color: black; + }` + } + + render() { + return html`` + } +} diff --git a/packages/wc/components/sh1-focus-node.ts b/packages/wc/components/sh1-focus-node.ts new file mode 100644 index 00000000..1bf12677 --- /dev/null +++ b/packages/wc/components/sh1-focus-node.ts @@ -0,0 +1,14 @@ +import { html, LitElement } from 'lit' +import type { FocusNodeState } from '@hydrofoil/shaperone-core/models/forms/index.js' +import { property } from 'lit/decorators.js' +import { repeat } from 'lit/directives/repeat.js' +import { ifDefined } from 'lit/directives/if-defined.js' + +export class Sh1FocusNode extends LitElement { + @property({ type: Object }) + private focusNode!: FocusNodeState + + render() { + return html`${repeat(this.focusNode.groups, group => html``)}` + } +} diff --git a/packages/wc/components/sh1-form.ts b/packages/wc/components/sh1-form.ts new file mode 100644 index 00000000..baff4100 --- /dev/null +++ b/packages/wc/components/sh1-form.ts @@ -0,0 +1,8 @@ +import { html, LitElement } from 'lit' + +export class Sh1Form extends LitElement { + render() { + return html` + ` + } +} diff --git a/packages/wc/components/sh1-group.ts b/packages/wc/components/sh1-group.ts new file mode 100644 index 00000000..3673c870 --- /dev/null +++ b/packages/wc/components/sh1-group.ts @@ -0,0 +1,13 @@ +import type { PropertyGroupState } from '@hydrofoil/shaperone-core/models/forms/index.js' +import { property } from 'lit/decorators.js' +import { html, LitElement } from 'lit' + +export default class Sh1Group extends LitElement { + @property({ type: Object }) + private group!: PropertyGroupState + + render() { + return html` + ` + } +} diff --git a/packages/wc/components/sh1-object.ts b/packages/wc/components/sh1-object.ts new file mode 100644 index 00000000..729bf9e2 --- /dev/null +++ b/packages/wc/components/sh1-object.ts @@ -0,0 +1,76 @@ +import { css, html } from 'lit' +import type { GraphPointer } from 'clownface' +import type { Term } from '@rdfjs/types' +import { createTerm } from '@hydrofoil/shaperone-core/lib/property.js' +import { property } from 'lit/decorators.js' +import type { FocusNodeState, PropertyObjectState, PropertyState } from '@hydrofoil/shaperone-core/models/forms/index.js' +import type { Dispatch } from '@hydrofoil/shaperone-core/state/index.js' +import ShaperoneElementBase from './ShaperoneElementBase.js' + +export class Sh1Object extends ShaperoneElementBase { + dispatch: Dispatch | undefined + + static get styles() { + return css` + :host { + display: block; + } + ` + } + + @property({ type: Object }) + private object!: PropertyObjectState + + @property({ type: Object }) + private focusNode!: FocusNodeState + + @property({ type: Object }) + private property!: PropertyState + + protected createRenderRoot(): HTMLElement | DocumentFragment { + const shadowRoot = super.createRenderRoot() + + shadowRoot.addEventListener('value-changed', (e: any) => { + const termOrString: GraphPointer | Term | string = e.detail.value + const value = typeof termOrString === 'string' + ? createTerm(this.property, termOrString) + : termOrString + + this.dispatch?.form.updateObject({ + focusNode: this.focusNode.focusNode, + property: this.property.shape, + object: this.object, + newValue: value, + }) + }) + + return shadowRoot + } + + render() { + if (!this.object || !this.property || !this.focusNode) { + return '' + } + + return html` + + + + + × + + ` + } + + removeObject() { + this.dispatch?.form.removeObject({ + focusNode: this.focusNode.focusNode, + property: this.property.shape, + object: this.object, + }) + } +} diff --git a/packages/wc/components/sh1-property.ts b/packages/wc/components/sh1-property.ts new file mode 100644 index 00000000..b202c89e --- /dev/null +++ b/packages/wc/components/sh1-property.ts @@ -0,0 +1,74 @@ +import type { FocusNodeState, PropertyState } from '@hydrofoil/shaperone-core/models/forms/index.js' +import { css, html } from 'lit' +import { property } from 'lit/decorators.js' +import { localizedLabel } from '@rdfjs-elements/lit-helpers/localizedLabel.js' +import { sh } from '@tpluscode/rdf-ns-builders' +import type { Dispatch } from '@hydrofoil/shaperone-core/state/index.js' +import ShaperoneElementBase from './ShaperoneElementBase.js' + +export class Sh1Property extends ShaperoneElementBase { + static get styles() { + return css` + :host { + display: flex; + align-content: space-between; + } + + .label { + flex: 1; + } + + .objects { + text-align: end; + } + + :host(:not([can-add])) slot[name=add-object] { + display: none; + } + ` + } + + private dispatch!: Dispatch | undefined + + @property({ type: Object }) + private focusNode!: FocusNodeState + + @property({ type: Object }) + private property!: PropertyState + + @property({ type: Boolean, reflect: true, attribute: 'can-add' }) + get canAdd() { + return this.property.canAdd + } + + render() { + return html` +
+ + + +
+
+ + + + + ${this.propertyLabel} + + +
+ ` + } + + addObject() { + this.dispatch?.form.addObject({ + focusNode: this.focusNode.focusNode, + property: this.property.shape, + }) + } + + get propertyLabel() { + return html`${localizedLabel(this.property.shape, { property: sh.name })}` + } +} diff --git a/packages/wc/components/validity.ts b/packages/wc/components/validity.ts index 2ad0921a..ab510a79 100644 --- a/packages/wc/components/validity.ts +++ b/packages/wc/components/validity.ts @@ -15,23 +15,26 @@ class ValidityDirective extends Directive { } } - render(arg: PropertyObjectState) { + render(arg: PropertyObjectState | undefined) { return noChange } - update(part: ElementPart, [{ object, validationResults }]: Parameters) { - const tb = part.element as HTMLInputElement + update(part: ElementPart, [state]: Parameters) { + if (state) { + const { object, validationResults } = state + const tb = part.element as HTMLInputElement - tb.setCustomValidity(validationResults.map(({ result }) => result.resultMessage || 'Value is not valid').join('; ')) - const isValid = validationResults.length === 0 + tb.setCustomValidity(validationResults.map(({ result }) => result.resultMessage || 'Value is not valid').join('; ')) + const isValid = validationResults.length === 0 - if (object?.value !== this.value || this.isValid !== isValid) { - this.value = object?.value + if (object?.value !== this.value || this.isValid !== isValid) { + this.value = object?.value - tb.reportValidity() - if (this.isValid !== isValid) { - this.isValid = isValid - tb.setAttribute('part', this.isValid ? 'component' : 'component invalid') + tb.reportValidity() + if (this.isValid !== isValid) { + this.isValid = isValid + tb.setAttribute('part', this.isValid ? 'component' : 'component invalid') + } } } diff --git a/packages/wc/configure.ts b/packages/wc/configure.ts index 724a2da8..cad9b034 100644 --- a/packages/wc/configure.ts +++ b/packages/wc/configure.ts @@ -5,12 +5,13 @@ * @module @hydrofoil/shaperone-wc/configure */ +import '@webcomponents/scoped-custom-element-registry' import CoreMetadata from '@hydrofoil/shaperone-core/metadata.js' import type { RequiredEnvironment } from '@hydrofoil/shaperone-core/env.js' import { setEnv } from '@hydrofoil/shaperone-core/env.js' import onetime from 'onetime' import store from './store.js' -import * as nativeComponents from './NativeComponents.js' +import * as native from './NativeComponents.js' const { components } = store.dispatch const { renderer } = store.dispatch @@ -50,7 +51,8 @@ export async function configure(first?: RequiredEnvironment | ConfigCallback, se } editors.addMetadata(CoreMetadata) - components.pushComponents(nativeComponents) + components.pushComponents(native.editors) + renderer.pushComponents(native.layoutComponents) await editors.loadDash() await userConfigure?.({ components, renderer, editors, validation }) diff --git a/packages/wc/elements/BooleanSelect.ts b/packages/wc/elements/BooleanSelect.ts new file mode 100644 index 00000000..59eceec2 --- /dev/null +++ b/packages/wc/elements/BooleanSelect.ts @@ -0,0 +1,25 @@ +import { html } from 'lit' +import { dash, xsd } from '@tpluscode/rdf-ns-builders' +import { validity } from '../components/validity.js' +import { readOnly } from '../components/readonly.js' +import { SingleEditorBase } from './SingleEditorBase.js' + +export default class extends SingleEditorBase { + static readonly editor = dash.BooleanSelectEditor + + protected render() { + return html`` + } + + private changed(e: any) { + if (e.target.value) { + this.setValue(this.env.literal(e.target.value, xsd.boolean)) + } else { + this.clear() + } + } +} diff --git a/packages/wc/elements/EnumSelect.ts b/packages/wc/elements/EnumSelect.ts new file mode 100644 index 00000000..a0d9edb2 --- /dev/null +++ b/packages/wc/elements/EnumSelect.ts @@ -0,0 +1,50 @@ +import { dash } from '@tpluscode/rdf-ns-builders' +import { html } from 'lit' +import { localizedLabel } from '@rdfjs-elements/lit-helpers/localizedLabel.js' +import { repeat } from 'lit/directives/repeat.js' +import { property } from 'lit/decorators.js' +import type { GraphPointer } from 'clownface' +import { sort } from '@hydrofoil/shaperone-core/lib/components.js' +import type { EnumSelectEditor } from '@hydrofoil/shaperone-core/lib/components/enumSelect.js' +import { SingleEditorBase } from './SingleEditorBase.js' +import { validity } from '../components/validity.js' +import { readOnly } from '../components/readonly.js' + +export default class extends SingleEditorBase implements EnumSelectEditor { + static readonly editor = dash.EnumSelectEditor + + @property({ type: Array }) + public choices: GraphPointer[] = [] + + connectedCallback() { + super.connectedCallback() + + this.setChoices() + } + + protected render(): unknown { + return html`` + } + + private selectionChanged(e: any) { + const chosen = this.choices[(e.target).selectedIndex - 1] + if (chosen) { + this.setValue(chosen.term) + } + } + + private async setChoices() { + const pointers: GraphPointer[] = await this.loadChoices() + this.choices = pointers.sort(sort(this.property.shape)) + } + + async loadChoices(): Promise { + return this.property.shape.pointer.node(this.property.shape.in).toArray() + } +} diff --git a/packages/wc/elements/MultiEditorBase.ts b/packages/wc/elements/MultiEditorBase.ts new file mode 100644 index 00000000..29022254 --- /dev/null +++ b/packages/wc/elements/MultiEditorBase.ts @@ -0,0 +1,10 @@ +import { html, LitElement } from 'lit' + +/** + * Inherit to implement a `dash:MultiEditor` component rendered as Web Components using [lit](https://lit.dev/) + */ +export class MultiEditorBase extends LitElement { + render() { + return html`` + } +} diff --git a/packages/wc/elements/SingleEditorBase.ts b/packages/wc/elements/SingleEditorBase.ts new file mode 100644 index 00000000..b65d939f --- /dev/null +++ b/packages/wc/elements/SingleEditorBase.ts @@ -0,0 +1,36 @@ +import type { FocusNodeState, PropertyObjectState, PropertyState } from '@hydrofoil/shaperone-core/models/forms/index.js' +import { LitElement } from 'lit' +import { property } from 'lit/decorators.js' +import type { SingleEditorComponent } from '@hydrofoil/shaperone-core/models/components/index.js' +import type { ShaperoneEnvironment } from '@hydrofoil/shaperone-core/env.js' +import type { Term } from '@rdfjs/types' + +/** + * Inherit to implement a `dash:SingleEditor` component rendered as Web Components using [lit](https://lit.dev/) + */ +export abstract class SingleEditorBase extends LitElement implements SingleEditorComponent { + @property({ type: Object }) + protected value!: PropertyObjectState + + @property({ type: Object }) + protected property!: PropertyState + + @property({ type: Object }) + protected focusNode!: FocusNodeState + + @property({ type: Object }) + public env!: ShaperoneEnvironment + + setValue(value: Term | string) { + this.dispatchEvent(new CustomEvent('value-changed', { + detail: { + value, + }, + bubbles: true, + })) + } + + clear() { + this.dispatchEvent(new CustomEvent('cleared')) + } +} diff --git a/packages/wc/elements/TextArea.ts b/packages/wc/elements/TextArea.ts new file mode 100644 index 00000000..67302e54 --- /dev/null +++ b/packages/wc/elements/TextArea.ts @@ -0,0 +1,15 @@ +import { html } from 'lit' +import { dash } from '@tpluscode/rdf-ns-builders' +import { SingleEditorBase } from './SingleEditorBase.js' +import { readOnly } from '../components/readonly.js' +import { validity } from '../components/validity.js' + +export default class extends SingleEditorBase { + static readonly editor = dash.TextAreaEditor + + render() { + return html`` + } +} diff --git a/packages/wc/elements/TextField.ts b/packages/wc/elements/TextField.ts new file mode 100644 index 00000000..7a6dc910 --- /dev/null +++ b/packages/wc/elements/TextField.ts @@ -0,0 +1,19 @@ +import { html } from 'lit' +import { spread } from '@open-wc/lit-helpers' +import { dash } from '@tpluscode/rdf-ns-builders' +import { SingleEditorBase } from './SingleEditorBase.js' +import { readOnly } from '../components/readonly.js' +import { validity } from '../components/validity.js' +import { getType } from '../components/lib/textFieldType.js' + +export default class extends SingleEditorBase { + static readonly editor = dash.TextFieldEditor + + render() { + return html`` + } +} diff --git a/packages/wc/index.ts b/packages/wc/index.ts index 2b6a00a2..d63c4ed4 100644 --- a/packages/wc/index.ts +++ b/packages/wc/index.ts @@ -3,10 +3,6 @@ * @module @hydrofoil/shaperone-wc */ -import type { TemplateResult } from 'lit' -import type * as Core from '@hydrofoil/shaperone-core' -import type { Component, ComponentInstance } from '@hydrofoil/shaperone-core/models/components' - export { configure } from './configure.js' export type { ConfigCallback } from './configure.js' @@ -14,19 +10,7 @@ export type { ShaperoneForm } from './ShaperoneForm.js' export { html, css, render } from 'lit' export type { TemplateResult } from 'lit' -export type { SingleEditor, MultiEditor, Lazy } from '@hydrofoil/shaperone-core' +export type { SingleEditor, MultiEditor } from '@hydrofoil/shaperone-core' -/** - * Inherit to implement a `dash:SingleEditor` component rendered as Web Components using [lit](https://lit.dev/) - */ -export type SingleEditorComponent = Core.SingleEditorComponent - -/** - * Inherit to implement a `dash:MultiEditor` component rendered as Web Components using [lit](https://lit.dev/) - */ -export type MultiEditorComponent = Core.MultiEditorComponent - -/** - * Function interface for declaring a component's `.render` function which returns a [lit](https://lit.dev/) template result - */ -export type Render = Core.RenderComponent +export { SingleEditorBase as SingleEditorComponent } from './elements/SingleEditorBase.js' +export { MultiEditorBase as MultiEditorComponent } from './elements/MultiEditorBase.js' diff --git a/packages/wc/package.json b/packages/wc/package.json index 578dff64..5c9a0eec 100644 --- a/packages/wc/package.json +++ b/packages/wc/package.json @@ -23,10 +23,12 @@ "dependencies": { "@hydrofoil/shaperone-core": "^0.12.1", "@open-wc/lit-helpers": "^0.7.0", + "@open-wc/scoped-elements": "^3.0.5", "@rdfine/shacl": "^0.10.5", "@rdfjs-elements/lit-helpers": "^0.3.7", "@tpluscode/rdf-ns-builders": "^4.3.0", "@tpluscode/rdfine": "^0.7.9", + "@webcomponents/scoped-custom-element-registry": "^0.0.9", "@zazuko/env": "^2.1.0", "concat-merge": "^1.0.3", "lit": "^3", diff --git a/packages/wc/renderer/decorator.ts b/packages/wc/renderer/decorator.ts deleted file mode 100644 index ec8e387a..00000000 --- a/packages/wc/renderer/decorator.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Decorator module simplifies extending render templates - * - * @packageDocumentation - * @module @hydrofoil/shaperone-wc/components/decorator - */ - -import { css } from 'lit' -import type { RenderTemplate } from '../templates.js' - -export interface Decorate