From 0a6d16b4826ce65edbf30e2203ace3318daee077 Mon Sep 17 00:00:00 2001 From: timea-solid <4144203+timea-solid@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:10:18 +0200 Subject: [PATCH 1/6] rdf forms component --- src/primitives/components/rdf-form/RDFForm.ts | 93 +++++++++++++++++++ .../components/rdf-form/RDForm.stories.ts | 85 +++++++++++++++++ src/primitives/lib/rdfFormsHelper.js | 67 +++++++++++++ 3 files changed, 245 insertions(+) create mode 100644 src/primitives/components/rdf-form/RDFForm.ts create mode 100644 src/primitives/components/rdf-form/RDForm.stories.ts create mode 100644 src/primitives/lib/rdfFormsHelper.js diff --git a/src/primitives/components/rdf-form/RDFForm.ts b/src/primitives/components/rdf-form/RDFForm.ts new file mode 100644 index 000000000..d93b731c3 --- /dev/null +++ b/src/primitives/components/rdf-form/RDFForm.ts @@ -0,0 +1,93 @@ +import { customElement, property, state } from 'lit/decorators.js' +import { html } from 'lit/html.js' +import WebComponent from '../../lib/WebComponent' +import ns from '../../../lib/ns' +import { loadDocument, sortBySequence } from '../../lib/rdfFormsHelper' +import { sym, Namespace } from 'rdflib' +import { store } from 'solid-logic' + +@customElement('solid-ui-rdf-form') +export default class RDFForm extends WebComponent { + @state() + private accessor _parsedUrl: URL | null = null + + @property({ type: String }) + accessor whichForm = 'this' + + @property({ type: String }) + accessor rdfTurtleFormatSource = '' + + @property({ type: String }) + accessor rdfName = '' + + @property({ type: String }) + set rdfURI (value: string) { + try { + this._parsedUrl = new URL(value) + } catch { + this._parsedUrl = null // Handle invalid URL + } + } + + get rdfURI (): string { + return this._parsedUrl ? this._parsedUrl.href : '' + } + + render () { + // TODO: detect format + loadDocument(store, this.rdfTurtleFormatSource, this.rdfName, this.rdfURI) + const document = sym(this.rdfURI) // rdflib NamedNode for the document + const exactForm = this.whichForm // If there are more 'a ui:Form' elements in a form file + const formThis = Namespace(this.rdfURI + '#')(exactForm) // NamedNode for #this in the form + console.log('formThis:', formThis.value) + + const parts = store.each(formThis, ns.ui('parts'), null, document) + const partsBySequence = sortBySequence(store, parts) + const uiFields = partsBySequence.map(item => ((item as any).value || String(item)).split('#').pop()) + console.log('document:', document) + console.log('exactForm:', exactForm) + console.log('uiFields:', uiFields) + + return html` + ${uiFields.map(part => { + switch (part) { + case 'PhoneField': + case 'EmailField': + case 'ColorField': + case 'DateField': + case 'DateTimeField': + case 'TimeField': + case 'NumericField': + case 'IntegerField': + case 'DecimalField': + case 'FloatField': + case 'TextField': + case 'SingleLineTextField': + case 'NamedNodeURIField': + return html`` + case 'MultiLineTextField': + return html`` + case 'BooleanField': + return html`` + case 'TristateField': + return html`` + case 'Classifier': + return html`` + case 'Choice': + return html`` + case 'Multiple': + return html`` + case 'Options': + return html`` + case 'AutocompleteField': + return html`` + case 'Comment': + case 'Heading': + return html`` + default: + return html`
Unknown part type: ${part}
` + } + })} + ` + } +} diff --git a/src/primitives/components/rdf-form/RDForm.stories.ts b/src/primitives/components/rdf-form/RDForm.stories.ts new file mode 100644 index 000000000..b44dc8ae1 --- /dev/null +++ b/src/primitives/components/rdf-form/RDForm.stories.ts @@ -0,0 +1,85 @@ +import { html } from 'lit' +import { defineStoryRender } from '../../../storybook' + +const meta = { + title: 'Design System/RDF Form', + args: { + rdfTurtleFormatSource: ` + # A Form with 2 fields and a nested subgroup + + :form a ui:Form; + ui:parts (:nameField :emailField :addresses) . + + :nameField a ui:SingleLineTextField ; + ui:property vcard:fn; + ui:label "name" . + + :emailField a ui:EmailField ; + ui:property vcard:hasEmail; # @@ check + ui:label "email" . + + :addresses + a ui:Multiple ; # -- Allows zero or one or more + ui:part :oneAddress ; + ui:property vcard:hasAddress . + + :oneAddress + a ui:Group ; # A subgroup of the main form + ui:parts ( :street :locality :postcode :region :country ). + + :street + a ui:SingleLineTextField ; + ui:maxLength "128" ; + ui:property vcard:street-address ; + ui:size "40" . + + :locality + a ui:SingleLineTextField ; + ui:maxLength "128" ; + ui:property vcard:locality ; + ui:size "40" . + + :postcode + a ui:SingleLineTextField ; + ui:maxLength "25" ; + ui:property vcard:postal-code ; + ui:size "25" . + + :region + a ui:SingleLineTextField ; + ui:maxLength "128" ; + ui:property vcard:region ; + ui:size "40" . + + :country + a ui:SingleLineTextField ; + ui:maxLength "128" ; + ui:property vcard:country-name ; + ui:size "40" .`, + rdfURI: 'https://solidos.solidcommunity.net/public/2021/solidUiFormTestData/dummyFormTestFile.ttl', + whichForm: 'this', + rdfName: 'dummyFormTestFile.ttl' + }, + + argTypes: { + rdfTurtleFormatSource: { control: 'text' }, + rdfURI: { control: 'text' }, + whichForm: { control: 'text' }, + rdfName: { control: 'text' } + }, +} as const + +const render = defineStoryRender(({ rdfTurtleFormatSource, rdfURI, whichForm, rdfName }) => { + return html` + + + ` +}) + +export default meta + +export const Primary = { render } diff --git a/src/primitives/lib/rdfFormsHelper.js b/src/primitives/lib/rdfFormsHelper.js new file mode 100644 index 000000000..f81dbaa75 --- /dev/null +++ b/src/primitives/lib/rdfFormsHelper.js @@ -0,0 +1,67 @@ +import { sym, Namespace, parse } from 'rdflib' +import { widgets } from 'solid-ui' +import ns from '../../lib/ns' + +const baseUri = 'https://solidos.github.io/solid-ui/src/ontology/' + +export function renderForm ( + div, + subject, // Represents the RDF that fills the form + formSource, // The imported form Turtle source + formName, // The name of the form file (e.g., 'socialMedia.ttl') + store, + dom, + editableProfile, + whichForm) { + // --- Form resource setup --- + const formUri = baseUri + formName // Full URI to the form file + const exactForm = whichForm || 'this' // If there are more 'a ui:Form' elements in a form file + const formThis = Namespace(formUri + '#')(exactForm) // NamedNode for #this in the form + + loadDocument(store, formSource, formName, formUri) + + widgets.appendForm( + dom, + div, + {}, + subject, + formThis, + editableProfile, + (ok, mes) => { + if (!ok) widgets.errorMessageBlock(dom, mes) + } + ) +} // renderForm + +// we need to load into the store some additional information about Social Media accounts +export function loadDocument ( + store, + documentSource, + documentName, + documentURI +) { + const finalDocumentUri = documentURI || baseUri + documentName // Full URI to the file + const document = sym(finalDocumentUri) // rdflib NamedNode for the document + + if (!store.holds(undefined, undefined, undefined, document)) { + // we are using the social media form because it contains the information we need + // the form can be used for both use cases: create UI for edit and render UI for display + parse(documentSource, store, finalDocumentUri, 'text/turtle', () => null) // Load doc directly + } +} + +export function sortBySequence ( + store, + list +) { + const subfields = list.map(function (p) { + const k = store.any(p, ns.ui('sequence')) + return [k || 9999, p] + }) + subfields.sort(function (a, b) { + return a[0] - b[0] + }) + return subfields.map(function (pair) { + return pair[1] + }) +} From 7c5a7c3277802e9382a69d2bdcddbee50edc0dd1 Mon Sep 17 00:00:00 2001 From: timea-solid <4144203+timea-solid@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:40:14 +0200 Subject: [PATCH 2/6] rendered first rdf forms elements --- src/primitives/components/rdf-form/RDFForm.ts | 17 ++++++++++++++++- .../components/rdf-form/RDForm.stories.ts | 13 ++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/primitives/components/rdf-form/RDFForm.ts b/src/primitives/components/rdf-form/RDFForm.ts index d93b731c3..0614c4358 100644 --- a/src/primitives/components/rdf-form/RDFForm.ts +++ b/src/primitives/components/rdf-form/RDFForm.ts @@ -43,7 +43,22 @@ export default class RDFForm extends WebComponent { const parts = store.each(formThis, ns.ui('parts'), null, document) const partsBySequence = sortBySequence(store, parts) - const uiFields = partsBySequence.map(item => ((item as any).value || String(item)).split('#').pop()) + const partItems = (partsBySequence || []).flatMap(item => { + if (item && typeof item === 'object' && 'elements' in item && Array.isArray((item as any).elements)) { + return (item as any).elements + } + return [item] + }) + const uiFields = partItems.map(item => { + const types = store.each(item as any, ns.rdf('type'), null, document) + const typeNode = types[0] + const value = typeNode ? ((typeNode as any).value || String(typeNode)) : ((item as any).value || String(item)) + const hashIndex = value.lastIndexOf('#') + return hashIndex >= 0 ? value.slice(hashIndex + 1) : value + }) + console.log('parts:', parts) + console.log('partsBySequence:', partsBySequence) + console.log('partItems:', partItems) console.log('document:', document) console.log('exactForm:', exactForm) console.log('uiFields:', uiFields) diff --git a/src/primitives/components/rdf-form/RDForm.stories.ts b/src/primitives/components/rdf-form/RDForm.stories.ts index b44dc8ae1..34ad1bbe7 100644 --- a/src/primitives/components/rdf-form/RDForm.stories.ts +++ b/src/primitives/components/rdf-form/RDForm.stories.ts @@ -1,11 +1,17 @@ import { html } from 'lit' import { defineStoryRender } from '../../../storybook' +import './RDFForm' const meta = { title: 'Design System/RDF Form', args: { rdfTurtleFormatSource: ` - # A Form with 2 fields and a nested subgroup + @prefix : . +@prefix dc: . +@prefix ui: . +@prefix vcard: . + +# A Form with 2 fields and a nested subgroup :form a ui:Form; ui:parts (:nameField :emailField :addresses) . @@ -55,9 +61,10 @@ const meta = { a ui:SingleLineTextField ; ui:maxLength "128" ; ui:property vcard:country-name ; - ui:size "40" .`, + ui:size "40" . +`, rdfURI: 'https://solidos.solidcommunity.net/public/2021/solidUiFormTestData/dummyFormTestFile.ttl', - whichForm: 'this', + whichForm: 'form', rdfName: 'dummyFormTestFile.ttl' }, From 35c066fe2e06edf168f54613baf6ad895013b3c6 Mon Sep 17 00:00:00 2001 From: timea-solid <4144203+timea-solid@users.noreply.github.com> Date: Wed, 17 Jun 2026 12:31:39 +0200 Subject: [PATCH 3/6] preliminary work for rdf-input --- .../components/rdf-form/RDForm.stories.ts | 108 +++++++++--------- .../components/rdf-input/RDFInput.ts | 41 +++++++ .../{rdfFormsHelper.js => rdfFormsHelper.ts} | 60 ++++------ 3 files changed, 119 insertions(+), 90 deletions(-) create mode 100644 src/primitives/components/rdf-input/RDFInput.ts rename src/primitives/lib/{rdfFormsHelper.js => rdfFormsHelper.ts} (50%) diff --git a/src/primitives/components/rdf-form/RDForm.stories.ts b/src/primitives/components/rdf-form/RDForm.stories.ts index 34ad1bbe7..bcbfeea1d 100644 --- a/src/primitives/components/rdf-form/RDForm.stories.ts +++ b/src/primitives/components/rdf-form/RDForm.stories.ts @@ -7,63 +7,63 @@ const meta = { args: { rdfTurtleFormatSource: ` @prefix : . -@prefix dc: . -@prefix ui: . -@prefix vcard: . + @prefix dc: . + @prefix ui: . + @prefix vcard: . -# A Form with 2 fields and a nested subgroup + # A Form with 2 fields and a nested subgroup :form a ui:Form; - ui:parts (:nameField :emailField :addresses) . - - :nameField a ui:SingleLineTextField ; - ui:property vcard:fn; - ui:label "name" . - - :emailField a ui:EmailField ; - ui:property vcard:hasEmail; # @@ check - ui:label "email" . - - :addresses - a ui:Multiple ; # -- Allows zero or one or more - ui:part :oneAddress ; - ui:property vcard:hasAddress . - - :oneAddress - a ui:Group ; # A subgroup of the main form - ui:parts ( :street :locality :postcode :region :country ). - - :street - a ui:SingleLineTextField ; - ui:maxLength "128" ; - ui:property vcard:street-address ; - ui:size "40" . - - :locality - a ui:SingleLineTextField ; - ui:maxLength "128" ; - ui:property vcard:locality ; - ui:size "40" . - - :postcode - a ui:SingleLineTextField ; - ui:maxLength "25" ; - ui:property vcard:postal-code ; - ui:size "25" . - - :region - a ui:SingleLineTextField ; - ui:maxLength "128" ; - ui:property vcard:region ; - ui:size "40" . - - :country - a ui:SingleLineTextField ; - ui:maxLength "128" ; - ui:property vcard:country-name ; - ui:size "40" . -`, - rdfURI: 'https://solidos.solidcommunity.net/public/2021/solidUiFormTestData/dummyFormTestFile.ttl', + ui:parts (:nameField :emailField :addresses) . + + :nameField a ui:SingleLineTextField ; + ui:property vcard:fn; + ui:label "name" . + + :emailField a ui:EmailField ; + ui:property vcard:hasEmail; # @@ check + ui:label "email" . + + :addresses + a ui:Multiple ; # -- Allows zero or one or more + ui:part :oneAddress ; + ui:property vcard:hasAddress . + + :oneAddress + a ui:Group ; # A subgroup of the main form + ui:parts ( :street :locality :postcode :region :country ). + + :street + a ui:SingleLineTextField ; + ui:maxLength "128" ; + ui:property vcard:street-address ; + ui:size "40" . + + :locality + a ui:SingleLineTextField ; + ui:maxLength "128" ; + ui:property vcard:locality ; + ui:size "40" . + + :postcode + a ui:SingleLineTextField ; + ui:maxLength "25" ; + ui:property vcard:postal-code ; + ui:size "25" . + + :region + a ui:SingleLineTextField ; + ui:maxLength "128" ; + ui:property vcard:region ; + ui:size "40" . + + :country + a ui:SingleLineTextField ; + ui:maxLength "128" ; + ui:property vcard:country-name ; + ui:size "40" . + `, + rdfURI: 'https://solidos.solidcommunity.net/public/2021/solidUiFormTestData/dummyFormTestFile.ttl', // we need a working URL whichForm: 'form', rdfName: 'dummyFormTestFile.ttl' }, diff --git a/src/primitives/components/rdf-input/RDFInput.ts b/src/primitives/components/rdf-input/RDFInput.ts new file mode 100644 index 000000000..426d4c5b4 --- /dev/null +++ b/src/primitives/components/rdf-input/RDFInput.ts @@ -0,0 +1,41 @@ +import { customElement, property } from 'lit/decorators.js' +import { html } from 'lit/html.js' +import ns from '../../../lib/ns' +import WebComponent from '../../../primitives/lib/WebComponent' +import { store } from 'solid-logic' +import { NamedNode, Namespace, sym } from 'rdflib' +import { label } from '../../../utils' +import { loadDocument } from '../../lib/rdfFormsHelper' + +// import '../input' + +@customElement('solid-ui-rdf-input') +export default class RDFInput extends WebComponent { + // example RDF Turtle format source: + // :nameField a ui:SingleLineTextField ; + // ui:property vcard:fn; + // ui:label "name" . + + // form here is the subject :nameField + @property({ type: String }) + accessor rdf = '' + + render () { + const exactForm = this.whichForm // nameField + const formThis = Namespace(this.rdfURI + '#')(exactForm) // NamedNode for #this in the form + const document = sym(this.rdfURI) + + const uiProperty = label(store.any(formThis, ns.ui('property')), true) as NamedNode | undefined + const uiLabel = store.any(formThis, ns.ui('label')) + const inputLabel = uiLabel ? uiLabel.value : uiProperty ? uiProperty.value.split('#').pop() : 'Input' + + // TODO: I am not finding suppressEmptyUneditable in ui ontology + const suppressEmptyUneditable = store.anyJS(formThis, ns.ui('suppressEmptyUneditable'), null, document) + + const uri = mostSpecificClassURI(form) + let params = fieldParams[uri] + + return html` + + ` +} diff --git a/src/primitives/lib/rdfFormsHelper.js b/src/primitives/lib/rdfFormsHelper.ts similarity index 50% rename from src/primitives/lib/rdfFormsHelper.js rename to src/primitives/lib/rdfFormsHelper.ts index f81dbaa75..c4ff20879 100644 --- a/src/primitives/lib/rdfFormsHelper.js +++ b/src/primitives/lib/rdfFormsHelper.ts @@ -1,44 +1,15 @@ -import { sym, Namespace, parse } from 'rdflib' -import { widgets } from 'solid-ui' +import { sym, LiveStore, parse, NamedNode } from 'rdflib' import ns from '../../lib/ns' +import { label } from '../../utils' const baseUri = 'https://solidos.github.io/solid-ui/src/ontology/' -export function renderForm ( - div, - subject, // Represents the RDF that fills the form - formSource, // The imported form Turtle source - formName, // The name of the form file (e.g., 'socialMedia.ttl') - store, - dom, - editableProfile, - whichForm) { - // --- Form resource setup --- - const formUri = baseUri + formName // Full URI to the form file - const exactForm = whichForm || 'this' // If there are more 'a ui:Form' elements in a form file - const formThis = Namespace(formUri + '#')(exactForm) // NamedNode for #this in the form - - loadDocument(store, formSource, formName, formUri) - - widgets.appendForm( - dom, - div, - {}, - subject, - formThis, - editableProfile, - (ok, mes) => { - if (!ok) widgets.errorMessageBlock(dom, mes) - } - ) -} // renderForm - // we need to load into the store some additional information about Social Media accounts export function loadDocument ( - store, - documentSource, - documentName, - documentURI + store: LiveStore, + documentSource: string, + documentName: string, + documentURI?: string ) { const finalDocumentUri = documentURI || baseUri + documentName // Full URI to the file const document = sym(finalDocumentUri) // rdflib NamedNode for the document @@ -51,7 +22,7 @@ export function loadDocument ( } export function sortBySequence ( - store, + store: LiveStore, list ) { const subfields = list.map(function (p) { @@ -65,3 +36,20 @@ export function sortBySequence ( return pair[1] }) } + +/** + * Which class of field is this? Relies on http://www.w3.org/2000/01/rdf-schema#subClassOf and + * https://linkeddata.github.io/rdflib.js/doc/classes/formula.html#bottomtypeuris + * to find the most specific RDF type if there are multiple. + * + * @param x a form field, e.g. `namedNode('https://timbl.com/timbl/Public/Test/Forms/individualForm.ttl#fullNameField')` + * @returns the URI of the most specific known class, e.g. `http://www.w3.org/ns/ui#SingleLineTextField` + */ +export function mostSpecificClassURI (store: LiveStore,x: Node): string { + const ft = store.findTypeURIs(x as any) + const bot = store.bottomTypeURIs(ft) // most specific + const bots: any[] = [] + for (const b in bot) bots.push(b) + // if (bots.length > 1) throw "Didn't expect "+x+" to have multiple bottom types: "+bots + return bots[0] +} From 344b904ac7de062539347fab985d1c352c3f134c Mon Sep 17 00:00:00 2001 From: timea-solid <4144203+timea-solid@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:32:43 +0200 Subject: [PATCH 4/6] added also data to forms and wired the rdf input --- src/components/rdf-form/RDFForm.ts | 41 ++++++- src/components/rdf-form/RDForm.stories.ts | 34 +++++- src/components/rdf-form/index.ts | 4 + src/components/rdf-input/RDFInput.ts | 77 ++++++++---- src/components/rdf-input/index.ts | 4 + src/lib/forms/fieldParams.ts | 140 ++++++++++++++++++++++ src/lib/forms/rdfFormsHelper.ts | 16 +-- 7 files changed, 277 insertions(+), 39 deletions(-) create mode 100644 src/components/rdf-form/index.ts create mode 100644 src/components/rdf-input/index.ts create mode 100644 src/lib/forms/fieldParams.ts diff --git a/src/components/rdf-form/RDFForm.ts b/src/components/rdf-form/RDFForm.ts index b4982deef..6a6def9bc 100644 --- a/src/components/rdf-form/RDFForm.ts +++ b/src/components/rdf-form/RDFForm.ts @@ -5,11 +5,14 @@ import ns from '../../lib/ns' import { loadDocument, sortBySequence } from '../../lib/forms/rdfFormsHelper' import { sym, Namespace } from 'rdflib' import { store } from 'solid-logic' +import '@/components/rdf-input' @customElement('solid-ui-rdf-form') export default class RDFForm extends WebComponent { @state() private accessor _parsedUrl: URL | null = null + @state() + private accessor _parsedUrl2: URL | null = null @property({ type: String }) accessor whichForm = 'this' @@ -33,9 +36,43 @@ export default class RDFForm extends WebComponent { return this._parsedUrl ? this._parsedUrl.href : '' } + private defaultContexts = ` + @prefix foaf: . + @prefix sched: . + @prefix cal: . + @prefix dc: . + @prefix rdfs: . + @prefix ui: . + @prefix trip: . + @prefix vcard: . + @prefix xsd: . + ` + @property({ type: String }) + accessor whichSubject = 'me' + + @property({ type: String }) + accessor subjectTurtleFormatSource = '' + + @property({ type: String }) + accessor subjectName = '' + + @property({ type: String }) + set subjectURI (value: string) { + try { + this._parsedUrl2 = new URL(value) + } catch { + this._parsedUrl2 = null // Handle invalid URL + } + } + + get subjectURI (): string { + return this._parsedUrl2 ? this._parsedUrl2.href : '' + } + render () { // TODO: detect format - loadDocument(store, this.rdfTurtleFormatSource, this.rdfName, this.rdfURI) + loadDocument(store, this.rdfTurtleFormatSource + this.defaultContexts, this.rdfName, this.rdfURI) // load form + loadDocument(store, this.subjectTurtleFormatSource + this.defaultContexts, this.subjectName, this.subjectURI) // load data const document = sym(this.rdfURI) // rdflib NamedNode for the document const exactForm = this.whichForm // If there are more 'a ui:Form' elements in a form file const formThis = Namespace(this.rdfURI + '#')(exactForm) // NamedNode for #this in the form @@ -79,7 +116,7 @@ export default class RDFForm extends WebComponent { case 'TextField': case 'SingleLineTextField': case 'NamedNodeURIField': - return html`` + return html` ` case 'MultiLineTextField': return html`` case 'BooleanField': diff --git a/src/components/rdf-form/RDForm.stories.ts b/src/components/rdf-form/RDForm.stories.ts index 5d6e7a730..0305b0e2e 100644 --- a/src/components/rdf-form/RDForm.stories.ts +++ b/src/components/rdf-form/RDForm.stories.ts @@ -7,9 +7,15 @@ const meta = { args: { rdfTurtleFormatSource: ` @prefix : . - @prefix dc: . - @prefix ui: . + @prefix foaf: . + @prefix sched: . + @prefix cal: . + @prefix dc: . + @prefix rdfs: . + @prefix ui: . + @prefix trip: . @prefix vcard: . + @prefix xsd: . # A Form with 2 fields and a nested subgroup @@ -65,24 +71,40 @@ const meta = { `, rdfURI: 'https://solidos.solidcommunity.net/public/2021/solidUiFormTestData/dummyFormTestFile.ttl', // we need a working URL whichForm: 'form', - rdfName: 'dummyFormTestFile.ttl' + rdfName: 'dummyFormTestFile.ttl', + whichSubject: 'me', + subjectTurtleFormatSource: ` + @prefix : . + + :me a vcard:Individual ; + vcard:fn "Alice" ; + vcard:hasEmail . + `, + subjectName: 'alice.ttl', + subjectURI: 'https://solidos.solidcommunity.net/public/2021/alice.ttl' }, argTypes: { rdfTurtleFormatSource: { control: 'text' }, rdfURI: { control: 'text' }, whichForm: { control: 'text' }, - rdfName: { control: 'text' } + rdfName: { control: 'text' }, + subjectTurtleFormatSource: { control: 'text' }, + subjectName: { control: 'text' }, + subjectURI: { control: 'text' } }, } as const -const render = defineStoryRender(({ rdfTurtleFormatSource, rdfURI, whichForm, rdfName }) => { +const render = defineStoryRender(({ rdfTurtleFormatSource, rdfURI, whichForm, rdfName, subjectTurtleFormatSource, subjectName, subjectURI }) => { return html` + rdfName=${rdfName} + subjectTurtleFormatSource=${subjectTurtleFormatSource} + subjectName=${subjectName} + subjectURI=${subjectURI}> ` }) diff --git a/src/components/rdf-form/index.ts b/src/components/rdf-form/index.ts new file mode 100644 index 000000000..487b3a932 --- /dev/null +++ b/src/components/rdf-form/index.ts @@ -0,0 +1,4 @@ +import RDFForm from './RDFForm' + +export { RDFForm } +export default RDFForm diff --git a/src/components/rdf-input/RDFInput.ts b/src/components/rdf-input/RDFInput.ts index 843664021..402c9ee84 100644 --- a/src/components/rdf-input/RDFInput.ts +++ b/src/components/rdf-input/RDFInput.ts @@ -2,40 +2,71 @@ import { property } from 'lit/decorators.js' import { html } from 'lit/html.js' import ns from '../../lib/ns' import { customElement, WebComponent } from '@/lib/components' -import { store } from 'solid-logic' -import { NamedNode, Namespace, sym } from 'rdflib' +import { LiveStore, NamedNode } from 'rdflib' import { label } from '../../utils' -import { loadDocument } from '../../lib/forms/rdfFormsHelper' - -// import '../input' +import { mostSpecificClassURI } from '../../lib/forms/rdfFormsHelper' +import { fieldParams, InputType } from '../../lib/forms/fieldParams' +import { ifDefined } from 'lit/directives/if-defined.js' @customElement('solid-ui-rdf-input') export default class RDFInput extends WebComponent { - // example RDF Turtle format source: + // example RDF Turtle format source: // :nameField a ui:SingleLineTextField ; // ui:property vcard:fn; // ui:label "name" . - // form here is the subject :nameField - @property({ type: String }) - accessor rdf = '' + // store needs to contain the form and also the data it applies to + @property({ type: LiveStore }) + accessor store + + // form here is the subject :nameField + @property({ type: String }) + accessor formSubject + + @property({ type: String }) + accessor inputSubject - render () { - const exactForm = this.whichForm // nameField - const formThis = Namespace(this.rdfURI + '#')(exactForm) // NamedNode for #this in the form - const document = sym(this.rdfURI) + render () { + // HTML input part + const uiPropertyTerm = this.store.any(this.formSubject, ns.ui('property')) as NamedNode | undefined + const uiProperty = uiPropertyTerm ? label(uiPropertyTerm, true) : '' + const uiLabel = this.store.any(this.formSubject, ns.ui('label')) + const inputLabel = uiLabel ? uiLabel.value : uiProperty + // readonly + let readonly = false + // TODO: I am not finding suppressEmptyUneditable in ui ontology + const suppressEmptyUneditable = this.store.anyJS(this.formSubject, ns.ui('suppressEmptyUneditable')) + if (suppressEmptyUneditable) { + readonly = true + } - const uiProperty = label(store.any(formThis, ns.ui('property')), true) as NamedNode | undefined - const uiLabel = store.any(formThis, ns.ui('label')) - const inputLabel = uiLabel ? uiLabel.value : uiProperty ? uiProperty.value.split('#').pop() : 'Input' + const uri = mostSpecificClassURI(this.store, this.formSubject) + const params = fieldParams[uri] ?? {} + const inputType: InputType = params.type ?? 'text' - // TODO: I am not finding suppressEmptyUneditable in ui ontology - const suppressEmptyUneditable = store.anyJS(formThis, ns.ui('suppressEmptyUneditable'), null, document) + // input values + const defaultInputValueFromStore = this.store.any(this.formSubject, ns.ui('default')) + const inputValueFromStore = this.store.any(this.inputSubject, ns.ui('property')) - const uri = mostSpecificClassURI(form) - let params = fieldParams[uri] - + let inputTerm: string | undefined + + const term = inputValueFromStore || defaultInputValueFromStore + if (term && 'value' in term && term.value) { + const decoded = decodeURIComponent(term.value) + inputTerm = params.defaultInputValue + ? decoded.replace(params.defaultInputValue, '').replace(/ /g, '') + : decoded + } + + if (inputLabel) { + return html` + + + ` + } else { return html` - - ` + + ` + } + } } diff --git a/src/components/rdf-input/index.ts b/src/components/rdf-input/index.ts new file mode 100644 index 000000000..bffe87ff2 --- /dev/null +++ b/src/components/rdf-input/index.ts @@ -0,0 +1,4 @@ +import RDFInput from './RDFInput' + +export { RDFInput } +export default RDFInput diff --git a/src/lib/forms/fieldParams.ts b/src/lib/forms/fieldParams.ts new file mode 100644 index 000000000..6f8342f2b --- /dev/null +++ b/src/lib/forms/fieldParams.ts @@ -0,0 +1,140 @@ +import ns from '../../lib/ns' +import { style } from '../../lib/style' + +export type InputType = + | 'hidden' + | 'text' + | 'search' + | 'tel' + | 'url' + | 'email' + | 'password' + | 'datetime' + | 'date' + | 'month' + | 'week' + | 'time' + | 'datetime-local' + | 'number' + | 'range' + | 'color' + | 'checkbox' + | 'radio' + | 'file' + | 'submit' + | 'image' + | 'reset' + | 'button' + +export type FieldParamsObject = { + size?: number, // input element size attribute + type?: InputType, // input element type attribute. Default: 'text' (not for Comment and Heading) + element?: string, // element type to use (Comment and Heading only) + style?: string, // style to use + dt?: string, // xsd data type for the RDF Literal corresponding to the field value. Default: xsd:string + defaultInputValue?: string, // e.g. 'mailto:'. Default value in input field, will be removed when displaying actual value to user. + namedNode?: boolean, // if true, field value corresponds to the URI of an RDF NamedNode. Overrides dt and defaultInputValue. + pattern?: RegExp // for client-side input validation; field will go red if violated, green if ok +} + +/** + * The fieldParams object defines various constants + * for use in various form fields. Depending on the + * field in questions, different values may be read + * from here. + */ +export const fieldParams: { [ fieldUri: string ]: FieldParamsObject } = { + /** + * Text field + * + * For possible date popups see e.g. http://www.dynamicdrive.com/dynamicindex7/jasoncalendar.htm + * or use HTML5: http://www.w3.org/TR/2011/WD-html-markup-20110113/input.date.html + */ + [ns.ui('ColorField').uri]: { + size: 9, + type: 'color', + style: 'height: 3em;', // around 1.5em is padding + dt: 'color', + pattern: /^\s*#[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]([0-9a-f][0-9a-f])?\s*$/ + }, // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/color + + [ns.ui('DateField').uri]: { + size: 20, + type: 'date', + dt: 'date', + pattern: /^\s*[0-9][0-9][0-9][0-9](-[0-1]?[0-9]-[0-3]?[0-9])?Z?\s*$/ + }, + + [ns.ui('DateTimeField').uri]: { + size: 20, + type: 'datetime-local', // See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime + dt: 'dateTime', + pattern: /^\s*[0-9][0-9][0-9][0-9](-[0-1]?[0-9]-[0-3]?[0-9])?(T[0-2][0-9]:[0-5][0-9](:[0-5][0-9])?)?Z?\s*$/ + }, + + [ns.ui('TimeField').uri]: { + size: 10, + type: 'time', + dt: 'time', + pattern: /^\s*([0-2]?[0-9]:[0-5][0-9](:[0-5][0-9])?)\s*$/ + }, + + [ns.ui('IntegerField').uri]: { + size: 12, + style: 'text-align: right;', + dt: 'integer', + pattern: /^\s*-?[0-9]+\s*$/ + }, + + [ns.ui('DecimalField').uri]: { + size: 12, + style: 'text-align: right;', + dt: 'decimal', + pattern: /^\s*-?[0-9]*(\.[0-9]*)?\s*$/ + }, + + [ns.ui('FloatField').uri]: { + size: 12, + style: 'text-align: right;', + dt: 'float', + pattern: /^\s*-?[0-9]*(\.[0-9]*)?((e|E)-?[0-9]*)?\s*$/ + }, + + [ns.ui('SingleLineTextField').uri]: { + + }, + [ns.ui('NamedNodeURIField').uri]: { + namedNode: true + }, + [ns.ui('TextField').uri]: { + + }, + + [ns.ui('PhoneField').uri]: { + size: 20, + defaultInputValue: 'tel:', + pattern: /^\+?[\d-]+[\d]*$/ + }, + + [ns.ui('EmailField').uri]: { + size: 30, + defaultInputValue: 'mailto:', + pattern: /^\s*.*@.*\..*\s*$/ // @@ Get the right regexp here + }, + + [ns.ui('Group').uri]: { + style: style.formGroupStyle + }, + + /** + * Non-interactive fields + */ + [ns.ui('Comment').uri]: { + element: 'p', + style: style.commentStyle + }, + [ns.ui('Heading').uri]: { + element: 'h3', + style: style.formHeadingStyle + } +} diff --git a/src/lib/forms/rdfFormsHelper.ts b/src/lib/forms/rdfFormsHelper.ts index d80ddc4af..2f572a28e 100644 --- a/src/lib/forms/rdfFormsHelper.ts +++ b/src/lib/forms/rdfFormsHelper.ts @@ -41,14 +41,14 @@ export function sortBySequence ( * https://linkeddata.github.io/rdflib.js/doc/classes/formula.html#bottomtypeuris * to find the most specific RDF type if there are multiple. * - * @param x a form field, e.g. `namedNode('https://timbl.com/timbl/Public/Test/Forms/individualForm.ttl#fullNameField')` + * @param subject a form field, e.g. `namedNode('https://timbl.com/timbl/Public/Test/Forms/individualForm.ttl#fullNameField')` * @returns the URI of the most specific known class, e.g. `http://www.w3.org/ns/ui#SingleLineTextField` */ -export function mostSpecificClassURI (store: LiveStore,x: Node): string { - const ft = store.findTypeURIs(x as any) - const bot = store.bottomTypeURIs(ft) // most specific - const bots: any[] = [] - for (const b in bot) bots.push(b) - // if (bots.length > 1) throw "Didn't expect "+x+" to have multiple bottom types: "+bots - return bots[0] +export function mostSpecificClassURI (store: LiveStore, subject: Node): string { + const typeUri = store.findTypeURIs(subject as any) + const specificTypes = store.bottomTypeURIs(typeUri) // most specific + const finalTypes: any[] = [] + for (const t in specificTypes) finalTypes.push(t) + // if (finalTypes.length > 1) throw "Didn't expect "+subject+" to have multiple bottom types: "+finalTypes + return finalTypes[0] } From 9d5565a9fd1c5bf6bf42a1d8178c5369a83b2d61 Mon Sep 17 00:00:00 2001 From: timea-solid <4144203+timea-solid@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:51:20 +0200 Subject: [PATCH 5/6] fixed rdf input --- src/components/rdf-form/RDFForm.ts | 39 +++++++++-------------- src/components/rdf-form/RDForm.stories.ts | 1 + src/components/rdf-input/RDFInput.ts | 29 +++++++++++------ src/lib/forms/rdfFormsHelper.ts | 13 +++++--- 4 files changed, 45 insertions(+), 37 deletions(-) diff --git a/src/components/rdf-form/RDFForm.ts b/src/components/rdf-form/RDFForm.ts index 6a6def9bc..d3500c006 100644 --- a/src/components/rdf-form/RDFForm.ts +++ b/src/components/rdf-form/RDFForm.ts @@ -11,6 +11,7 @@ import '@/components/rdf-input' export default class RDFForm extends WebComponent { @state() private accessor _parsedUrl: URL | null = null + @state() private accessor _parsedUrl2: URL | null = null @@ -36,17 +37,6 @@ export default class RDFForm extends WebComponent { return this._parsedUrl ? this._parsedUrl.href : '' } - private defaultContexts = ` - @prefix foaf: . - @prefix sched: . - @prefix cal: . - @prefix dc: . - @prefix rdfs: . - @prefix ui: . - @prefix trip: . - @prefix vcard: . - @prefix xsd: . - ` @property({ type: String }) accessor whichSubject = 'me' @@ -71,12 +61,11 @@ export default class RDFForm extends WebComponent { render () { // TODO: detect format - loadDocument(store, this.rdfTurtleFormatSource + this.defaultContexts, this.rdfName, this.rdfURI) // load form - loadDocument(store, this.subjectTurtleFormatSource + this.defaultContexts, this.subjectName, this.subjectURI) // load data + loadDocument(store, this.rdfTurtleFormatSource, this.rdfName, this.rdfURI) // load form + loadDocument(store, this.subjectTurtleFormatSource, this.subjectName, this.subjectURI) // load data const document = sym(this.rdfURI) // rdflib NamedNode for the document const exactForm = this.whichForm // If there are more 'a ui:Form' elements in a form file const formThis = Namespace(this.rdfURI + '#')(exactForm) // NamedNode for #this in the form - console.log('formThis:', formThis.value) const parts = store.each(formThis, ns.ui('parts'), null, document) const partsBySequence = sortBySequence(store, parts) @@ -91,18 +80,16 @@ export default class RDFForm extends WebComponent { const typeNode = types[0] const value = typeNode ? ((typeNode as any).value || String(typeNode)) : ((item as any).value || String(item)) const hashIndex = value.lastIndexOf('#') - return hashIndex >= 0 ? value.slice(hashIndex + 1) : value + return { + value: item, + fieldValue: hashIndex >= 0 ? value.slice(hashIndex + 1) : value + } }) - console.log('parts:', parts) - console.log('partsBySequence:', partsBySequence) - console.log('partItems:', partItems) - console.log('document:', document) - console.log('exactForm:', exactForm) - console.log('uiFields:', uiFields) + const me = Namespace(this.subjectURI + '#')(this.whichSubject) return html` ${uiFields.map(part => { - switch (part) { + switch (part.fieldValue) { case 'PhoneField': case 'EmailField': case 'ColorField': @@ -115,8 +102,12 @@ export default class RDFForm extends WebComponent { case 'FloatField': case 'TextField': case 'SingleLineTextField': - case 'NamedNodeURIField': - return html` ` + case 'NamedNodeURIField': { + const formSubject = typeof part.value === 'string' + ? store.sym(part.value) + : part.value + return html` ` + } case 'MultiLineTextField': return html`` case 'BooleanField': diff --git a/src/components/rdf-form/RDForm.stories.ts b/src/components/rdf-form/RDForm.stories.ts index 0305b0e2e..fd54bb8d1 100644 --- a/src/components/rdf-form/RDForm.stories.ts +++ b/src/components/rdf-form/RDForm.stories.ts @@ -75,6 +75,7 @@ const meta = { whichSubject: 'me', subjectTurtleFormatSource: ` @prefix : . + @prefix vcard: . :me a vcard:Individual ; vcard:fn "Alice" ; diff --git a/src/components/rdf-input/RDFInput.ts b/src/components/rdf-input/RDFInput.ts index 402c9ee84..f189447c7 100644 --- a/src/components/rdf-input/RDFInput.ts +++ b/src/components/rdf-input/RDFInput.ts @@ -20,33 +20,44 @@ export default class RDFInput extends WebComponent { accessor store // form here is the subject :nameField - @property({ type: String }) + @property({ type: NamedNode }) accessor formSubject - @property({ type: String }) + @property({ type: NamedNode }) accessor inputSubject render () { + const formSubject = typeof this.formSubject === 'string' + ? this.store.sym(this.formSubject) + : this.formSubject + const inputSubject = typeof this.inputSubject === 'string' + ? this.store.sym(this.inputSubject) + : this.inputSubject + + const formGraph = formSubject.doc ? formSubject.doc() : undefined + // HTML input part - const uiPropertyTerm = this.store.any(this.formSubject, ns.ui('property')) as NamedNode | undefined + const uiPropertyTerm = this.store.any(formSubject, ns.ui('property'), null, formGraph) as NamedNode | undefined const uiProperty = uiPropertyTerm ? label(uiPropertyTerm, true) : '' - const uiLabel = this.store.any(this.formSubject, ns.ui('label')) + const uiLabel = this.store.any(formSubject, ns.ui('label'), null, formGraph) const inputLabel = uiLabel ? uiLabel.value : uiProperty - // readonly + let readonly = false // TODO: I am not finding suppressEmptyUneditable in ui ontology - const suppressEmptyUneditable = this.store.anyJS(this.formSubject, ns.ui('suppressEmptyUneditable')) + const suppressEmptyUneditable = this.store.anyJS(formSubject, ns.ui('suppressEmptyUneditable'), null, formGraph) if (suppressEmptyUneditable) { readonly = true } - const uri = mostSpecificClassURI(this.store, this.formSubject) + const uri = mostSpecificClassURI(this.store, formSubject) const params = fieldParams[uri] ?? {} const inputType: InputType = params.type ?? 'text' // input values - const defaultInputValueFromStore = this.store.any(this.formSubject, ns.ui('default')) - const inputValueFromStore = this.store.any(this.inputSubject, ns.ui('property')) + const defaultInputValueFromStore = this.store.any(formSubject, ns.ui('default')) + const inputValueFromStore = uiPropertyTerm + ? this.store.any(inputSubject, uiPropertyTerm) + : undefined let inputTerm: string | undefined diff --git a/src/lib/forms/rdfFormsHelper.ts b/src/lib/forms/rdfFormsHelper.ts index 2f572a28e..645c10beb 100644 --- a/src/lib/forms/rdfFormsHelper.ts +++ b/src/lib/forms/rdfFormsHelper.ts @@ -13,11 +13,16 @@ export function loadDocument ( const finalDocumentUri = documentURI || baseUri + documentName // Full URI to the file const document = sym(finalDocumentUri) // rdflib NamedNode for the document - if (!store.holds(undefined, undefined, undefined, document)) { - // we are using the social media form because it contains the information we need - // the form can be used for both use cases: create UI for edit and render UI for display - parse(documentSource, store, finalDocumentUri, 'text/turtle', () => null) // Load doc directly + if (store.holds(undefined, undefined, undefined, document)) { + store.removeStatements(store.statementsMatching(undefined, undefined, undefined, document)) } + // we are using the social media form because it contains the information we need + // the form can be used for both use cases: create UI for edit and render UI for display + parse(documentSource, store, finalDocumentUri, 'text/turtle', (err) => { + if (err) { + console.error('loadDocument parse error for', finalDocumentUri, err) + } + }) } export function sortBySequence ( From 0656653229e851b17fdddca9fb11366637f9b63c Mon Sep 17 00:00:00 2001 From: timea-solid <4144203+timea-solid@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:49:41 +0200 Subject: [PATCH 6/6] Refactor for better readability Prompt: reading the RDFinput file, pls make suggestions of how to impprve the code to make it easier to follow and read. I beliebe it is difficult to follow the fact that one has a rdf forms subject and a data subject as well and how it is all itertwinded. Co-Authored-By: GitHub Copilot (raptor-mini) --- src/components/rdf-input/RDFInput.ts | 125 +++++++++++++++------------ 1 file changed, 71 insertions(+), 54 deletions(-) diff --git a/src/components/rdf-input/RDFInput.ts b/src/components/rdf-input/RDFInput.ts index f189447c7..1f0cace6a 100644 --- a/src/components/rdf-input/RDFInput.ts +++ b/src/components/rdf-input/RDFInput.ts @@ -5,7 +5,7 @@ import { customElement, WebComponent } from '@/lib/components' import { LiveStore, NamedNode } from 'rdflib' import { label } from '../../utils' import { mostSpecificClassURI } from '../../lib/forms/rdfFormsHelper' -import { fieldParams, InputType } from '../../lib/forms/fieldParams' +import { fieldParams as fieldTypeParams, InputType } from '../../lib/forms/fieldParams' import { ifDefined } from 'lit/directives/if-defined.js' @customElement('solid-ui-rdf-input') @@ -15,69 +15,86 @@ export default class RDFInput extends WebComponent { // ui:property vcard:fn; // ui:label "name" . - // store needs to contain the form and also the data it applies to - @property({ type: LiveStore }) - accessor store + // formSubject describes the field metadata + // dataSubject points to the data resource containing the value - // form here is the subject :nameField - @property({ type: NamedNode }) - accessor formSubject + @property({ attribute: false }) + accessor store!: LiveStore - @property({ type: NamedNode }) - accessor inputSubject + @property({ attribute: false, type: Object }) + accessor formSubject!: NamedNode + + @property({ attribute: false, type: Object }) + accessor dataSubject!: NamedNode render () { - const formSubject = typeof this.formSubject === 'string' - ? this.store.sym(this.formSubject) - : this.formSubject - const inputSubject = typeof this.inputSubject === 'string' - ? this.store.sym(this.inputSubject) - : this.inputSubject - - const formGraph = formSubject.doc ? formSubject.doc() : undefined - - // HTML input part - const uiPropertyTerm = this.store.any(formSubject, ns.ui('property'), null, formGraph) as NamedNode | undefined - const uiProperty = uiPropertyTerm ? label(uiPropertyTerm, true) : '' - const uiLabel = this.store.any(formSubject, ns.ui('label'), null, formGraph) - const inputLabel = uiLabel ? uiLabel.value : uiProperty - - let readonly = false - // TODO: I am not finding suppressEmptyUneditable in ui ontology - const suppressEmptyUneditable = this.store.anyJS(formSubject, ns.ui('suppressEmptyUneditable'), null, formGraph) - if (suppressEmptyUneditable) { - readonly = true - } + const formGraph = this.getFormGraph(this.formSubject) + + // for building the HTML input element + const uiPropertyTerm = this.getFormProperty(this.formSubject, ns.ui('property'), formGraph) + const inputLabel = this.getInputLabel(this.formSubject, uiPropertyTerm, formGraph) + const readonly = this.getReadOnly(this.formSubject, formGraph) - const uri = mostSpecificClassURI(this.store, formSubject) - const params = fieldParams[uri] ?? {} + const fieldType = this.formSubject ? mostSpecificClassURI(this.store, this.formSubject) : undefined + const params = fieldType ? fieldTypeParams[fieldType] ?? {} : {} const inputType: InputType = params.type ?? 'text' - // input values - const defaultInputValueFromStore = this.store.any(formSubject, ns.ui('default')) - const inputValueFromStore = uiPropertyTerm - ? this.store.any(inputSubject, uiPropertyTerm) - : undefined + // for populating the HTML input element + const selectedTerm = this.getSelectedTerm(this.dataSubject, uiPropertyTerm, this.formSubject, params) + const inputValue = this.termToInputValue(selectedTerm, params) + + return html` + ${inputLabel ? html`` : ''} + + ` + } - let inputTerm: string | undefined + private getFormGraph (subject?: NamedNode) { + return subject?.doc ? subject.doc() : undefined + } - const term = inputValueFromStore || defaultInputValueFromStore - if (term && 'value' in term && term.value) { - const decoded = decodeURIComponent(term.value) - inputTerm = params.defaultInputValue - ? decoded.replace(params.defaultInputValue, '').replace(/ /g, '') - : decoded - } + private getFormProperty (subject: NamedNode | undefined, property: NamedNode, graph?: any): NamedNode | undefined { + if (!subject) return undefined + return this.store.any(subject, property, null, graph) as NamedNode | undefined + } + + private getInputLabel (formFieldSubject: NamedNode | undefined, uiPropertyTerm?: NamedNode, graph?: any): string { + if (!formFieldSubject) return '' + const uiLabel = this.store.any(formFieldSubject, ns.ui('label'), null, graph) + const propertyLabel = uiPropertyTerm ? label(uiPropertyTerm, true) : '' + return uiLabel ? uiLabel.value : propertyLabel + } + + private getReadOnly (formFieldSubject?: NamedNode, graph?: any): boolean { + if (!formFieldSubject) return false + return !!this.store.anyJS(formFieldSubject, ns.ui('suppressEmptyUneditable'), null, graph) + } + + private getSelectedTerm ( + dataSubject?: NamedNode, + uiPropertyTerm?: NamedNode, + formFieldSubject?: NamedNode, + params?: { defaultInputValue?: string } + ) { + const defaultTerm = formFieldSubject + ? this.store.any(formFieldSubject, ns.ui('default')) + : undefined - if (inputLabel) { - return html` - - - ` - } else { - return html` - - ` + if (!uiPropertyTerm || !dataSubject) { + return defaultTerm } + + const inputTerm = this.store.any(dataSubject, uiPropertyTerm) + return inputTerm || defaultTerm + } + + private termToInputValue (term: any, params: { defaultInputValue?: string } = {}) { + if (!term || !('value' in term) || !term.value) return undefined + + const decoded = decodeURIComponent(term.value) + if (!params.defaultInputValue) return decoded + + const stripped = decoded.replace(params.defaultInputValue, '') + return stripped.replace(/ /g, '') } }