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 01/31] 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 02/31] 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 03/31] 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 04/31] 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 05/31] 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 06/31] 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, '')
}
}
From 84345dfd8a97d73b974a80a4c94f5cfd8455c8e2 Mon Sep 17 00:00:00 2001
From: timea-solid <4144203+timea-solid@users.noreply.github.com>
Date: Tue, 23 Jun 2026 12:47:41 +0200
Subject: [PATCH 07/31] fix types
---
src/components/rdf-form/RDFForm.ts | 9 ++++---
src/lib/forms/rdfFormsHelper.ts | 39 +++++++++++++++++++-----------
src/types/custom-elements.d.ts | 5 ++++
3 files changed, 35 insertions(+), 18 deletions(-)
create mode 100644 src/types/custom-elements.d.ts
diff --git a/src/components/rdf-form/RDFForm.ts b/src/components/rdf-form/RDFForm.ts
index d3500c006..ec1529621 100644
--- a/src/components/rdf-form/RDFForm.ts
+++ b/src/components/rdf-form/RDFForm.ts
@@ -103,10 +103,11 @@ export default class RDFForm extends WebComponent {
case 'TextField':
case 'SingleLineTextField':
case 'NamedNodeURIField': {
- const formSubject = typeof part.value === 'string'
- ? store.sym(part.value)
- : part.value
- return html` `
+ return html` `
}
case 'MultiLineTextField':
return html``
diff --git a/src/lib/forms/rdfFormsHelper.ts b/src/lib/forms/rdfFormsHelper.ts
index 645c10beb..6d0e2e108 100644
--- a/src/lib/forms/rdfFormsHelper.ts
+++ b/src/lib/forms/rdfFormsHelper.ts
@@ -1,4 +1,7 @@
import { sym, LiveStore, parse } from 'rdflib'
+import type { Term } from 'rdflib/lib/tf-types'
+// eslint-disable-next-line camelcase
+import type { Quad_Subject } from 'rdflib/lib/tf-types'
import ns from '../../lib/ns'
const baseUri = 'https://solidos.github.io/solid-ui/src/ontology/'
@@ -17,7 +20,7 @@ export function loadDocument (
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
+ // 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)
@@ -27,18 +30,25 @@ export function loadDocument (
export function sortBySequence (
store: LiveStore,
- list
+ list: Term[]
) {
- 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]
- })
+ const subfields = list
+ .filter(
+ // eslint-disable-next-line camelcase
+ (p): p is Quad_Subject =>
+ p.termType === 'NamedNode' ||
+ p.termType === 'BlankNode' ||
+ p.termType === 'Variable'
+ )
+ .map((p) => {
+ const k = store.any(p, ns.ui('sequence'))
+ const seq = k ? Number((k as { value: string }).value) : 9999
+ return [Number.isNaN(seq) ? 9999 : seq, p] as const
+ })
+
+ subfields.sort((a, b) => a[0] - b[0])
+
+ return subfields.map(pair => pair[1])
}
/**
@@ -49,8 +59,9 @@ export function sortBySequence (
* @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, subject: Node): string {
- const typeUri = store.findTypeURIs(subject as any)
+// eslint-disable-next-line camelcase
+export function mostSpecificClassURI (store: LiveStore, subject: Quad_Subject): string {
+ const typeUri = store.findTypeURIs(subject)
const specificTypes = store.bottomTypeURIs(typeUri) // most specific
const finalTypes: any[] = []
for (const t in specificTypes) finalTypes.push(t)
diff --git a/src/types/custom-elements.d.ts b/src/types/custom-elements.d.ts
new file mode 100644
index 000000000..c113c356e
--- /dev/null
+++ b/src/types/custom-elements.d.ts
@@ -0,0 +1,5 @@
+declare global {
+ interface HTMLElementTagNameMap {
+ 'solid-ui-rdf-input': RDFInput
+ }
+}
From 372135f25154fe285a8136a2dfca1690ae3752ba Mon Sep 17 00:00:00 2001
From: timea-solid <4144203+timea-solid@users.noreply.github.com>
Date: Tue, 23 Jun 2026 13:16:22 +0200
Subject: [PATCH 08/31] reverted the sortBySequence code back to original
---
src/components/rdf-form/RDFForm.ts | 18 +++++++++---------
src/lib/forms/rdfFormsHelper.ts | 18 +++++-------------
2 files changed, 14 insertions(+), 22 deletions(-)
diff --git a/src/components/rdf-form/RDFForm.ts b/src/components/rdf-form/RDFForm.ts
index ec1529621..de868c576 100644
--- a/src/components/rdf-form/RDFForm.ts
+++ b/src/components/rdf-form/RDFForm.ts
@@ -110,24 +110,24 @@ export default class RDFForm extends WebComponent {
>`
}
case 'MultiLineTextField':
- return html``
+ return html``
case 'BooleanField':
- return html``
+ return html``
case 'TristateField':
- return html``
+ return html``
case 'Classifier':
- return html``
+ return html``
case 'Choice':
- return html``
+ return html``
case 'Multiple':
- return html``
+ return html``
case 'Options':
- return html``
+ return html``
case 'AutocompleteField':
- return html``
+ return html``
case 'Comment':
case 'Heading':
- return html``
+ return html``
default:
return html`Unknown part type: ${part}
`
}
diff --git a/src/lib/forms/rdfFormsHelper.ts b/src/lib/forms/rdfFormsHelper.ts
index 6d0e2e108..02cf1b5c3 100644
--- a/src/lib/forms/rdfFormsHelper.ts
+++ b/src/lib/forms/rdfFormsHelper.ts
@@ -32,19 +32,11 @@ export function sortBySequence (
store: LiveStore,
list: Term[]
) {
- const subfields = list
- .filter(
- // eslint-disable-next-line camelcase
- (p): p is Quad_Subject =>
- p.termType === 'NamedNode' ||
- p.termType === 'BlankNode' ||
- p.termType === 'Variable'
- )
- .map((p) => {
- const k = store.any(p, ns.ui('sequence'))
- const seq = k ? Number((k as { value: string }).value) : 9999
- return [Number.isNaN(seq) ? 9999 : seq, p] as const
- })
+ const subfields = list.map((p) => {
+ const k = store.any(p as any, ns.ui('sequence'))
+ const seq = k ? Number((k as { value: string }).value) : 9999
+ return [Number.isNaN(seq) ? 9999 : seq, p] as const
+ })
subfields.sort((a, b) => a[0] - b[0])
From 235b0d02bf6ad51a5cd2ee1d1705316d6e285372 Mon Sep 17 00:00:00 2001
From: timea-solid <4144203+timea-solid@users.noreply.github.com>
Date: Wed, 24 Jun 2026 10:14:45 +0200
Subject: [PATCH 09/31] vite config for generating custom elements tag name map
Prompt: can this #sym:HTMLElementTagNameMap be generated automatically from vite-config/ components.ts for all RDF suffixed components?
Co-Authored-By: GitHub Copilot (raptor-mini)
---
src/types/custom-elements.d.ts | 9 +++++++
vite-config/components.ts | 47 +++++++++++++++++++++++++++++++++-
vite.config.ts | 4 ++-
3 files changed, 58 insertions(+), 2 deletions(-)
diff --git a/src/types/custom-elements.d.ts b/src/types/custom-elements.d.ts
index c113c356e..6c765dd06 100644
--- a/src/types/custom-elements.d.ts
+++ b/src/types/custom-elements.d.ts
@@ -1,5 +1,14 @@
+/**
+ * This file is auto-generated by vite-config/components.ts.
+ * Do not edit this file directly.
+ */
+
+import type RDFForm from '../components/rdf-form/RDFForm'
+import type RDFInput from '../components/rdf-input/RDFInput'
+
declare global {
interface HTMLElementTagNameMap {
+ 'solid-ui-rdf-form': RDFForm
'solid-ui-rdf-input': RDFInput
}
}
diff --git a/vite-config/components.ts b/vite-config/components.ts
index 5681693ea..72b81a296 100644
--- a/vite-config/components.ts
+++ b/vite-config/components.ts
@@ -1,9 +1,11 @@
-import { existsSync, readdirSync } from 'node:fs'
+import { existsSync, readdirSync, writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
const projectRoot = resolve(import.meta.dirname, '..')
export const componentsSrcDir = join(projectRoot, 'src/components')
+export const customElementsTypesPath = join(projectRoot, 'src/types/custom-elements.d.ts')
+const rdfComponentPrefix = 'rdf-'
export function discoverComponents(): string[] {
return readdirSync(componentsSrcDir, { withFileTypes: true })
@@ -15,3 +17,46 @@ export function discoverComponents(): string[] {
.map((entry) => entry.name)
.sort()
}
+
+export function discoverRdfComponents(): string[] {
+ return discoverComponents().filter((name) => name.startsWith(rdfComponentPrefix))
+}
+
+function getPascalCase(name: string): string {
+ return name
+ .split('-')
+ .map((segment) => {
+ if (segment === 'rdf') return 'RDF'
+ if (segment.length <= 2) return segment.toUpperCase()
+ return segment.charAt(0).toUpperCase() + segment.slice(1)
+ })
+ .join('')
+}
+
+export function generateCustomElementsTypes(): void {
+ const rdfComponents = discoverRdfComponents()
+
+ const lines = [
+ '/**',
+ ' * This file is auto-generated by vite-config/components.ts.',
+ ' * Do not edit this file directly.',
+ ' */',
+ '',
+ ]
+
+ for (const component of rdfComponents) {
+ const className = getPascalCase(component)
+ lines.push(`import type ${className} from '../components/${component}/${className}'`)
+ }
+
+ lines.push('', 'declare global {', ' interface HTMLElementTagNameMap {')
+
+ for (const component of rdfComponents) {
+ const className = getPascalCase(component)
+ lines.push(` 'solid-ui-${component}': ${className}`)
+ }
+
+ lines.push(' }', '}', '')
+
+ writeFileSync(customElementsTypesPath, lines.join('\n'), 'utf-8')
+}
diff --git a/vite.config.ts b/vite.config.ts
index 6b95b2afb..987b406b5 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -6,9 +6,11 @@ import babel from './vite-config/babel'
import css from './vite-config/css'
import icons from './vite-config/icons'
import { cdnLegacyConfig, cdnConfig } from './vite-config/cdn'
-import { discoverComponents } from './vite-config/components'
+import { discoverComponents, generateCustomElementsTypes } from './vite-config/components'
import { stylesConfig } from './vite-config/styles'
+generateCustomElementsTypes()
+
const basePlugins = [
css(),
icons(),
From 35f44458f46a44d9a5cefdda72e758e6fc942731 Mon Sep 17 00:00:00 2001
From: timea-solid <4144203+timea-solid@users.noreply.github.com>
Date: Mon, 29 Jun 2026 10:36:19 +0200
Subject: [PATCH 10/31] a first FormsContext with a default store from
SolidLogic
---
src/components/rdf-form/RDFForm.ts | 19 ++++++++++++-------
src/components/rdf-input/RDFInput.ts | 22 +++++++++++++---------
src/lib/forms/FormsContext.ts | 10 ++++++++++
3 files changed, 35 insertions(+), 16 deletions(-)
create mode 100644 src/lib/forms/FormsContext.ts
diff --git a/src/components/rdf-form/RDFForm.ts b/src/components/rdf-form/RDFForm.ts
index de868c576..bf084e09f 100644
--- a/src/components/rdf-form/RDFForm.ts
+++ b/src/components/rdf-form/RDFForm.ts
@@ -4,11 +4,17 @@ import { customElement, WebComponent } from '@/lib/components'
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'
+import { consume } from '@lit/context'
+import { DEFAULT_STORE, formsContext, FormsContext } from '@/lib/forms/FormsContext'
@customElement('solid-ui-rdf-form')
export default class RDFForm extends WebComponent {
+ @consume({ context: formsContext, subscribe: true })
+ private accessor formsContext: FormsContext = {
+ store: DEFAULT_STORE,
+ }
+
@state()
private accessor _parsedUrl: URL | null = null
@@ -61,14 +67,14 @@ export default class RDFForm extends WebComponent {
render () {
// TODO: detect format
- loadDocument(store, this.rdfTurtleFormatSource, this.rdfName, this.rdfURI) // load form
- loadDocument(store, this.subjectTurtleFormatSource, this.subjectName, this.subjectURI) // load data
+ loadDocument(this.formsContext.store, this.rdfTurtleFormatSource, this.rdfName, this.rdfURI) // load form
+ loadDocument(this.formsContext.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
- const parts = store.each(formThis, ns.ui('parts'), null, document)
- const partsBySequence = sortBySequence(store, parts)
+ const parts = this.formsContext.store.each(formThis, ns.ui('parts'), null, document)
+ const partsBySequence = sortBySequence(this.formsContext.store, parts)
const partItems = (partsBySequence || []).flatMap(item => {
if (item && typeof item === 'object' && 'elements' in item && Array.isArray((item as any).elements)) {
return (item as any).elements
@@ -76,7 +82,7 @@ export default class RDFForm extends WebComponent {
return [item]
})
const uiFields = partItems.map(item => {
- const types = store.each(item as any, ns.rdf('type'), null, document)
+ const types = this.formsContext.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('#')
@@ -104,7 +110,6 @@ export default class RDFForm extends WebComponent {
case 'SingleLineTextField':
case 'NamedNodeURIField': {
return html` `
diff --git a/src/components/rdf-input/RDFInput.ts b/src/components/rdf-input/RDFInput.ts
index 1f0cace6a..cad24ff4d 100644
--- a/src/components/rdf-input/RDFInput.ts
+++ b/src/components/rdf-input/RDFInput.ts
@@ -2,11 +2,13 @@ import { property } from 'lit/decorators.js'
import { html } from 'lit/html.js'
import ns from '../../lib/ns'
import { customElement, WebComponent } from '@/lib/components'
-import { LiveStore, NamedNode } from 'rdflib'
+import { NamedNode } from 'rdflib'
import { label } from '../../utils'
import { mostSpecificClassURI } from '../../lib/forms/rdfFormsHelper'
import { fieldParams as fieldTypeParams, InputType } from '../../lib/forms/fieldParams'
import { ifDefined } from 'lit/directives/if-defined.js'
+import { DEFAULT_STORE, formsContext, FormsContext } from '@/lib/forms/FormsContext'
+import { consume } from '@lit/context'
@customElement('solid-ui-rdf-input')
export default class RDFInput extends WebComponent {
@@ -18,8 +20,10 @@ export default class RDFInput extends WebComponent {
// formSubject describes the field metadata
// dataSubject points to the data resource containing the value
- @property({ attribute: false })
- accessor store!: LiveStore
+ @consume({ context: formsContext, subscribe: true })
+ private accessor formsContext: FormsContext = {
+ store: DEFAULT_STORE,
+ }
@property({ attribute: false, type: Object })
accessor formSubject!: NamedNode
@@ -35,7 +39,7 @@ export default class RDFInput extends WebComponent {
const inputLabel = this.getInputLabel(this.formSubject, uiPropertyTerm, formGraph)
const readonly = this.getReadOnly(this.formSubject, formGraph)
- const fieldType = this.formSubject ? mostSpecificClassURI(this.store, this.formSubject) : undefined
+ const fieldType = this.formSubject ? mostSpecificClassURI(this.formsContext.store, this.formSubject) : undefined
const params = fieldType ? fieldTypeParams[fieldType] ?? {} : {}
const inputType: InputType = params.type ?? 'text'
@@ -55,19 +59,19 @@ export default class RDFInput extends WebComponent {
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
+ return this.formsContext.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 uiLabel = this.formsContext.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)
+ return !!this.formsContext.store.anyJS(formFieldSubject, ns.ui('suppressEmptyUneditable'), null, graph)
}
private getSelectedTerm (
@@ -77,14 +81,14 @@ export default class RDFInput extends WebComponent {
params?: { defaultInputValue?: string }
) {
const defaultTerm = formFieldSubject
- ? this.store.any(formFieldSubject, ns.ui('default'))
+ ? this.formsContext.store.any(formFieldSubject, ns.ui('default'))
: undefined
if (!uiPropertyTerm || !dataSubject) {
return defaultTerm
}
- const inputTerm = this.store.any(dataSubject, uiPropertyTerm)
+ const inputTerm = this.formsContext.store.any(dataSubject, uiPropertyTerm)
return inputTerm || defaultTerm
}
diff --git a/src/lib/forms/FormsContext.ts b/src/lib/forms/FormsContext.ts
new file mode 100644
index 000000000..4a26301c1
--- /dev/null
+++ b/src/lib/forms/FormsContext.ts
@@ -0,0 +1,10 @@
+import { createContext } from '@lit/context'
+import { LiveStore } from 'rdflib'
+import { solidLogicSingleton } from 'solid-logic'
+
+export interface FormsContext {
+ store: LiveStore
+}
+
+export const DEFAULT_STORE: LiveStore = solidLogicSingleton.store
+export const formsContext = createContext(Symbol('rdfForms'))
From 0d99a1b09fa6f4c8ae15ab9dc2a072ed3054b9d6 Mon Sep 17 00:00:00 2001
From: timea-solid <4144203+timea-solid@users.noreply.github.com>
Date: Mon, 29 Jun 2026 10:56:53 +0200
Subject: [PATCH 11/31] added placeholder property to input
---
src/components/rdf-input/RDFInput.ts | 20 ++++++++++++++------
1 file changed, 14 insertions(+), 6 deletions(-)
diff --git a/src/components/rdf-input/RDFInput.ts b/src/components/rdf-input/RDFInput.ts
index cad24ff4d..e8b42c4c7 100644
--- a/src/components/rdf-input/RDFInput.ts
+++ b/src/components/rdf-input/RDFInput.ts
@@ -45,11 +45,17 @@ export default class RDFInput extends WebComponent {
// for populating the HTML input element
const selectedTerm = this.getSelectedTerm(this.dataSubject, uiPropertyTerm, this.formSubject, params)
- const inputValue = this.termToInputValue(selectedTerm, params)
+ const placeholder = this.defaultInputValue(params)
+ const inputValue = this.termToInputValue(selectedTerm)
return html`
- ${inputLabel ? html`` : ''}
-
+ ${inputLabel
+ ? html`
+ `
+ : html``}
`
}
@@ -92,13 +98,15 @@ export default class RDFInput extends WebComponent {
return inputTerm || defaultTerm
}
- private termToInputValue (term: any, params: { defaultInputValue?: string } = {}) {
+ private termToInputValue (term: any) {
if (!term || !('value' in term) || !term.value) return undefined
const decoded = decodeURIComponent(term.value)
- if (!params.defaultInputValue) return decoded
+ return decoded
+ }
- const stripped = decoded.replace(params.defaultInputValue, '')
+ private defaultInputValue (params: { defaultInputValue?: string } = {}) {
+ const stripped = params.defaultInputValue ?? ''
return stripped.replace(/ /g, '')
}
}
From 7b7f9a9e4f70db8e390f730a3a1a3810245c1126 Mon Sep 17 00:00:00 2001
From: timea-solid <4144203+timea-solid@users.noreply.github.com>
Date: Mon, 29 Jun 2026 14:45:54 +0200
Subject: [PATCH 12/31] added readonly to Input and used it in RDFinput
---
src/components/combobox/Combobox.ts | 4 ++++
src/components/input/Input.ts | 4 ++++
src/components/rdf-input/RDFInput.ts | 28 +++++++++++++++----------
src/components/select/Select.ts | 4 ++++
src/lib/components/traits/InputTrait.ts | 1 +
5 files changed, 30 insertions(+), 11 deletions(-)
diff --git a/src/components/combobox/Combobox.ts b/src/components/combobox/Combobox.ts
index ec5534de2..5b0d84fe6 100644
--- a/src/components/combobox/Combobox.ts
+++ b/src/components/combobox/Combobox.ts
@@ -34,6 +34,9 @@ export default class Combobox extends WebComponent {
@query('input')
private accessor inputElement: HTMLInputElement | null = null
+ @property({ type: Boolean, reflect: true })
+ accessor readonly = false
+
@state()
private accessor filter = ''
@@ -67,6 +70,7 @@ export default class Combobox extends WebComponent {
name=${this.name}
?placeholder=${this.placeholder}
?required=${this.required}
+ ?readonly=${this.readonly}
.value=${this.value}
@keydown=${this.onInputKeyDown}
@click=${this.onInputClick}
diff --git a/src/components/input/Input.ts b/src/components/input/Input.ts
index 44d333f37..0a5a264c5 100644
--- a/src/components/input/Input.ts
+++ b/src/components/input/Input.ts
@@ -28,6 +28,9 @@ export default class Input extends WebComponent {
@property({ type: Boolean, reflect: true })
accessor required = false;
+ @property({ type: Boolean, reflect: true })
+ accessor readonly = false;
+
@query('input')
private accessor inputElement: HTMLInputElement | null = null;
@@ -54,6 +57,7 @@ export default class Input extends WebComponent {
placeholder=${this.placeholder}
?required=${this.required}
.value=${this.value}
+ ?readonly=${this.readonly}
@input=${() => this.inputTrait.onInput()}
@keydown=${this.onKeyDown}
/>
diff --git a/src/components/rdf-input/RDFInput.ts b/src/components/rdf-input/RDFInput.ts
index e8b42c4c7..c3a71aab8 100644
--- a/src/components/rdf-input/RDFInput.ts
+++ b/src/components/rdf-input/RDFInput.ts
@@ -6,7 +6,6 @@ import { NamedNode } from 'rdflib'
import { label } from '../../utils'
import { mostSpecificClassURI } from '../../lib/forms/rdfFormsHelper'
import { fieldParams as fieldTypeParams, InputType } from '../../lib/forms/fieldParams'
-import { ifDefined } from 'lit/directives/if-defined.js'
import { DEFAULT_STORE, formsContext, FormsContext } from '@/lib/forms/FormsContext'
import { consume } from '@lit/context'
@@ -31,13 +30,19 @@ export default class RDFInput extends WebComponent {
@property({ attribute: false, type: Object })
accessor dataSubject!: NamedNode
+ @property({ type: String, reflect: true })
+ accessor name = '';
+
+ @property({ type: Boolean, reflect: true })
+ accessor readonly = true;
+
render () {
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 readonly = this.getReadOnly(this.readonly, this.formSubject, formGraph)
const fieldType = this.formSubject ? mostSpecificClassURI(this.formsContext.store, this.formSubject) : undefined
const params = fieldType ? fieldTypeParams[fieldType] ?? {} : {}
@@ -49,14 +54,14 @@ export default class RDFInput extends WebComponent {
const inputValue = this.termToInputValue(selectedTerm)
return html`
- ${inputLabel
- ? html`
- `
- : html``}
- `
+ `
}
private getFormGraph (subject?: NamedNode) {
@@ -75,7 +80,8 @@ export default class RDFInput extends WebComponent {
return uiLabel ? uiLabel.value : propertyLabel
}
- private getReadOnly (formFieldSubject?: NamedNode, graph?: any): boolean {
+ private getReadOnly (readonly?: boolean, formFieldSubject?: NamedNode, graph?: any): boolean {
+ if (readonly !== undefined) return readonly
if (!formFieldSubject) return false
return !!this.formsContext.store.anyJS(formFieldSubject, ns.ui('suppressEmptyUneditable'), null, graph)
}
diff --git a/src/components/select/Select.ts b/src/components/select/Select.ts
index 4aacf4cb7..794ff5383 100644
--- a/src/components/select/Select.ts
+++ b/src/components/select/Select.ts
@@ -28,6 +28,9 @@ export default class Select extends WebComponent {
@query('select')
accessor inputElement: HTMLSelectElement | null = null;
+ @property({ type: Boolean, reflect: true })
+ accessor readonly = false;
+
private inputTrait: InputTrait
constructor () {
@@ -48,6 +51,7 @@ export default class Select extends WebComponent {
id="${this.inputTrait.inputId}"
name=${this.name}
?required=${this.required}
+ ?readonly=${this.readonly}
@change=${() => this.inputTrait.onInput()}
>
${this.getOptions().map(
diff --git a/src/lib/components/traits/InputTrait.ts b/src/lib/components/traits/InputTrait.ts
index 73a5374cd..d841e99eb 100644
--- a/src/lib/components/traits/InputTrait.ts
+++ b/src/lib/components/traits/InputTrait.ts
@@ -10,6 +10,7 @@ export type InputTraitTarget = WebComponent & {
label: string;
required: boolean;
value: string;
+ readonly: boolean;
}
export interface InputTraitConfig {
From 65735ae7890456414fad49071fda2f5a22fa776f Mon Sep 17 00:00:00 2001
From: timea-solid <4144203+timea-solid@users.noreply.github.com>
Date: Mon, 29 Jun 2026 15:50:57 +0200
Subject: [PATCH 13/31] improved store usage
---
src/components/rdf-form/RDFForm.ts | 35 ++++++++++++-------
src/components/rdf-form/RDForm.stories.ts | 2 ++
src/components/rdf-input/RDFInput.ts | 20 +++++------
src/lib/forms/FormsContext.ts | 10 ------
src/lib/forms/store/RDFFormsStore.ts | 8 +++++
src/lib/forms/store/StoreContext.ts | 10 ++++++
src/storybook/components/StorybookProvider.ts | 5 +++
src/storybook/store/StorybookStore.ts | 7 ++++
8 files changed, 64 insertions(+), 33 deletions(-)
delete mode 100644 src/lib/forms/FormsContext.ts
create mode 100644 src/lib/forms/store/RDFFormsStore.ts
create mode 100644 src/lib/forms/store/StoreContext.ts
create mode 100644 src/storybook/store/StorybookStore.ts
diff --git a/src/components/rdf-form/RDFForm.ts b/src/components/rdf-form/RDFForm.ts
index bf084e09f..0af821a3d 100644
--- a/src/components/rdf-form/RDFForm.ts
+++ b/src/components/rdf-form/RDFForm.ts
@@ -1,19 +1,20 @@
import { property, state } from 'lit/decorators.js'
import { html } from 'lit/html.js'
+import { consume } from '@lit/context'
import { customElement, WebComponent } from '@/lib/components'
import ns from '../../lib/ns'
import { loadDocument, sortBySequence } from '../../lib/forms/rdfFormsHelper'
-import { sym, Namespace } from 'rdflib'
+import { sym, Namespace, LiveStore } from 'rdflib'
import '@/components/rdf-input'
-import { consume } from '@lit/context'
-import { DEFAULT_STORE, formsContext, FormsContext } from '@/lib/forms/FormsContext'
+import { DEFAULT_STORE, storeContext, StoreContext } from '@/lib/forms/store/StoreContext'
@customElement('solid-ui-rdf-form')
export default class RDFForm extends WebComponent {
- @consume({ context: formsContext, subscribe: true })
- private accessor formsContext: FormsContext = {
- store: DEFAULT_STORE,
- }
+ @consume({ context: storeContext, subscribe: true })
+ private accessor storeContext: StoreContext = DEFAULT_STORE
+
+ @property({ attribute: false })
+ accessor passedInStore: LiveStore | null = null
@state()
private accessor _parsedUrl: URL | null = null
@@ -66,15 +67,25 @@ export default class RDFForm extends WebComponent {
}
render () {
+ const currentStoreContext = this.passedInStore
+ ? { store: this.passedInStore }
+ : this.storeContext
+
+ if (!currentStoreContext?.store) {
+ console.warn('RDFForm: store context not available yet')
+ return html``
+ }
+
+ const store = currentStoreContext.store
// TODO: detect format
- loadDocument(this.formsContext.store, this.rdfTurtleFormatSource, this.rdfName, this.rdfURI) // load form
- loadDocument(this.formsContext.store, this.subjectTurtleFormatSource, 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
- const parts = this.formsContext.store.each(formThis, ns.ui('parts'), null, document)
- const partsBySequence = sortBySequence(this.formsContext.store, parts)
+ const parts = store.each(formThis, ns.ui('parts'), null, document)
+ const partsBySequence = sortBySequence(store, parts)
const partItems = (partsBySequence || []).flatMap(item => {
if (item && typeof item === 'object' && 'elements' in item && Array.isArray((item as any).elements)) {
return (item as any).elements
@@ -82,7 +93,7 @@ export default class RDFForm extends WebComponent {
return [item]
})
const uiFields = partItems.map(item => {
- const types = this.formsContext.store.each(item as any, ns.rdf('type'), null, document)
+ 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('#')
diff --git a/src/components/rdf-form/RDForm.stories.ts b/src/components/rdf-form/RDForm.stories.ts
index fd54bb8d1..9571a52bf 100644
--- a/src/components/rdf-form/RDForm.stories.ts
+++ b/src/components/rdf-form/RDForm.stories.ts
@@ -98,6 +98,7 @@ const meta = {
const render = defineStoryRender(({ rdfTurtleFormatSource, rdfURI, whichForm, rdfName, subjectTurtleFormatSource, subjectName, subjectURI }) => {
return html`
+
(({ rdfTurtleFormatSource,
subjectName=${subjectName}
subjectURI=${subjectURI}>
+
`
})
diff --git a/src/components/rdf-input/RDFInput.ts b/src/components/rdf-input/RDFInput.ts
index c3a71aab8..a17fe3ed8 100644
--- a/src/components/rdf-input/RDFInput.ts
+++ b/src/components/rdf-input/RDFInput.ts
@@ -6,7 +6,7 @@ import { NamedNode } from 'rdflib'
import { label } from '../../utils'
import { mostSpecificClassURI } from '../../lib/forms/rdfFormsHelper'
import { fieldParams as fieldTypeParams, InputType } from '../../lib/forms/fieldParams'
-import { DEFAULT_STORE, formsContext, FormsContext } from '@/lib/forms/FormsContext'
+import { DEFAULT_STORE, storeContext, StoreContext } from '@/lib/forms/store/StoreContext'
import { consume } from '@lit/context'
@customElement('solid-ui-rdf-input')
@@ -19,10 +19,8 @@ export default class RDFInput extends WebComponent {
// formSubject describes the field metadata
// dataSubject points to the data resource containing the value
- @consume({ context: formsContext, subscribe: true })
- private accessor formsContext: FormsContext = {
- store: DEFAULT_STORE,
- }
+ @consume({ context: storeContext, subscribe: true })
+ private accessor storeContext: StoreContext = DEFAULT_STORE
@property({ attribute: false, type: Object })
accessor formSubject!: NamedNode
@@ -44,7 +42,7 @@ export default class RDFInput extends WebComponent {
const inputLabel = this.getInputLabel(this.formSubject, uiPropertyTerm, formGraph)
const readonly = this.getReadOnly(this.readonly, this.formSubject, formGraph)
- const fieldType = this.formSubject ? mostSpecificClassURI(this.formsContext.store, this.formSubject) : undefined
+ const fieldType = this.formSubject ? mostSpecificClassURI(this.storeContext.store, this.formSubject) : undefined
const params = fieldType ? fieldTypeParams[fieldType] ?? {} : {}
const inputType: InputType = params.type ?? 'text'
@@ -70,12 +68,12 @@ export default class RDFInput extends WebComponent {
private getFormProperty (subject: NamedNode | undefined, property: NamedNode, graph?: any): NamedNode | undefined {
if (!subject) return undefined
- return this.formsContext.store.any(subject, property, null, graph) as NamedNode | undefined
+ return this.storeContext.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.formsContext.store.any(formFieldSubject, ns.ui('label'), null, graph)
+ const uiLabel = this.storeContext.store.any(formFieldSubject, ns.ui('label'), null, graph)
const propertyLabel = uiPropertyTerm ? label(uiPropertyTerm, true) : ''
return uiLabel ? uiLabel.value : propertyLabel
}
@@ -83,7 +81,7 @@ export default class RDFInput extends WebComponent {
private getReadOnly (readonly?: boolean, formFieldSubject?: NamedNode, graph?: any): boolean {
if (readonly !== undefined) return readonly
if (!formFieldSubject) return false
- return !!this.formsContext.store.anyJS(formFieldSubject, ns.ui('suppressEmptyUneditable'), null, graph)
+ return !!this.storeContext.store.anyJS(formFieldSubject, ns.ui('suppressEmptyUneditable'), null, graph)
}
private getSelectedTerm (
@@ -93,14 +91,14 @@ export default class RDFInput extends WebComponent {
params?: { defaultInputValue?: string }
) {
const defaultTerm = formFieldSubject
- ? this.formsContext.store.any(formFieldSubject, ns.ui('default'))
+ ? this.storeContext.store.any(formFieldSubject, ns.ui('default'))
: undefined
if (!uiPropertyTerm || !dataSubject) {
return defaultTerm
}
- const inputTerm = this.formsContext.store.any(dataSubject, uiPropertyTerm)
+ const inputTerm = this.storeContext.store.any(dataSubject, uiPropertyTerm)
return inputTerm || defaultTerm
}
diff --git a/src/lib/forms/FormsContext.ts b/src/lib/forms/FormsContext.ts
deleted file mode 100644
index 4a26301c1..000000000
--- a/src/lib/forms/FormsContext.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { createContext } from '@lit/context'
-import { LiveStore } from 'rdflib'
-import { solidLogicSingleton } from 'solid-logic'
-
-export interface FormsContext {
- store: LiveStore
-}
-
-export const DEFAULT_STORE: LiveStore = solidLogicSingleton.store
-export const formsContext = createContext(Symbol('rdfForms'))
diff --git a/src/lib/forms/store/RDFFormsStore.ts b/src/lib/forms/store/RDFFormsStore.ts
new file mode 100644
index 000000000..1dc73d2c7
--- /dev/null
+++ b/src/lib/forms/store/RDFFormsStore.ts
@@ -0,0 +1,8 @@
+import { LiveStore } from 'rdflib'
+import { StoreContext } from './StoreContext'
+
+export default class RDFFormsStore implements StoreContext {
+ get store (): LiveStore {
+ throw new Error('Can\'t use RDF forms without a store')
+ }
+}
diff --git a/src/lib/forms/store/StoreContext.ts b/src/lib/forms/store/StoreContext.ts
new file mode 100644
index 000000000..e0f374a6e
--- /dev/null
+++ b/src/lib/forms/store/StoreContext.ts
@@ -0,0 +1,10 @@
+import { createContext } from '@lit/context'
+import { LiveStore } from 'rdflib'
+import RDFFormsStore from './RDFFormsStore'
+
+export interface StoreContext {
+ store: LiveStore
+}
+
+export const DEFAULT_STORE = new RDFFormsStore()
+export const storeContext = createContext(Symbol('storeContext'))
diff --git a/src/storybook/components/StorybookProvider.ts b/src/storybook/components/StorybookProvider.ts
index b34d4391c..502eaeec8 100644
--- a/src/storybook/components/StorybookProvider.ts
+++ b/src/storybook/components/StorybookProvider.ts
@@ -6,6 +6,8 @@ import StorybookAuth from '../auth/StorybookAuth'
import { Account, authContext } from '@/lib/auth'
import '@/components/dialogs-root'
+import { storeContext, StoreContext } from '@/lib/forms/store/StoreContext'
+import StorybookStore from '../store/StorybookStore'
@customElement('storybook-provider')
export class StorybookProvider extends WebComponent {
@@ -18,6 +20,9 @@ export class StorybookProvider extends WebComponent {
@provide({ context: authContext })
private accessor auth = new StorybookAuth()
+ @provide({ context: storeContext })
+ private accessor store: StoreContext = new StorybookStore()
+
willUpdate (changedProperties: Map) {
super.willUpdate(changedProperties)
diff --git a/src/storybook/store/StorybookStore.ts b/src/storybook/store/StorybookStore.ts
new file mode 100644
index 000000000..e09421bcb
--- /dev/null
+++ b/src/storybook/store/StorybookStore.ts
@@ -0,0 +1,7 @@
+import { StoreContext } from '@/lib/forms/store/StoreContext'
+import * as rdf from 'rdflib'
+import { LiveStore } from 'rdflib'
+
+export default class StorybookStore implements StoreContext {
+ public store: LiveStore = rdf.graph() as LiveStore
+}
From cfb0475de23db73fec55d796cde218ac93047ebc Mon Sep 17 00:00:00 2001
From: timea-solid <4144203+timea-solid@users.noreply.github.com>
Date: Mon, 29 Jun 2026 16:40:14 +0200
Subject: [PATCH 14/31] added readonly style to input
---
src/components/input/Input.styles.css | 7 ++
src/components/rdf-form/RDFForm.ts | 107 +++++++++++-------
src/components/rdf-form/RDForm.stories.ts | 7 +-
src/components/rdf-input/RDFInput.ts | 14 +--
src/storybook/components/StorybookProvider.ts | 1 +
5 files changed, 85 insertions(+), 51 deletions(-)
diff --git a/src/components/input/Input.styles.css b/src/components/input/Input.styles.css
index cd29288a8..4a6ec23b1 100644
--- a/src/components/input/Input.styles.css
+++ b/src/components/input/Input.styles.css
@@ -23,5 +23,12 @@
color: var(--solid-ui-color-gray-700);
font-size: inherit;
}
+
+ input:read-only {
+ border: none;
+ padding: 0;
+ cursor: pointer;
+ }
}
+
}
diff --git a/src/components/rdf-form/RDFForm.ts b/src/components/rdf-form/RDFForm.ts
index 0af821a3d..d3d9ddb5b 100644
--- a/src/components/rdf-form/RDFForm.ts
+++ b/src/components/rdf-form/RDFForm.ts
@@ -105,49 +105,70 @@ export default class RDFForm extends WebComponent {
const me = Namespace(this.subjectURI + '#')(this.whichSubject)
return html`
- ${uiFields.map(part => {
- switch (part.fieldValue) {
- 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}
`
- }
- })}
+
`
}
+
+ private async onSubmit (e: Event) {
+ e.preventDefault()
+
+ /* this.failed = false
+
+ this.submitting = true
+
+ try {
+ await this.auth.login(this.issuerInputValue)
+ } catch (error) {
+ console.error(error)
+
+ this.failed = true
+ } finally {
+ this.submitting = false
+ } */
+ }
}
diff --git a/src/components/rdf-form/RDForm.stories.ts b/src/components/rdf-form/RDForm.stories.ts
index 9571a52bf..f10f70d10 100644
--- a/src/components/rdf-form/RDForm.stories.ts
+++ b/src/components/rdf-form/RDForm.stories.ts
@@ -20,16 +20,21 @@ const meta = {
# A Form with 2 fields and a nested subgroup
:form a ui:Form;
- ui:parts (:nameField :emailField :addresses) .
+ ui:parts (:nameField :emailField :phoneField :addresses) .
:nameField a ui:SingleLineTextField ;
ui:property vcard:fn;
ui:label "name" .
+
:emailField a ui:EmailField ;
ui:property vcard:hasEmail; # @@ check
ui:label "email" .
+ :phoneField a ui:PhoneField ;
+ ui:property vcard:hasTelephone;
+ ui:label "phone" .
+
:addresses
a ui:Multiple ; # -- Allows zero or one or more
ui:part :oneAddress ;
diff --git a/src/components/rdf-input/RDFInput.ts b/src/components/rdf-input/RDFInput.ts
index a17fe3ed8..3f76997c8 100644
--- a/src/components/rdf-input/RDFInput.ts
+++ b/src/components/rdf-input/RDFInput.ts
@@ -1,13 +1,14 @@
import { property } from 'lit/decorators.js'
import { html } from 'lit/html.js'
import ns from '../../lib/ns'
-import { customElement, WebComponent } from '@/lib/components'
+import { customElement, generateId, WebComponent } from '@/lib/components'
import { NamedNode } from 'rdflib'
import { label } from '../../utils'
import { mostSpecificClassURI } from '../../lib/forms/rdfFormsHelper'
import { fieldParams as fieldTypeParams, InputType } from '../../lib/forms/fieldParams'
import { DEFAULT_STORE, storeContext, StoreContext } from '@/lib/forms/store/StoreContext'
import { consume } from '@lit/context'
+import '@/components/input'
@customElement('solid-ui-rdf-input')
export default class RDFInput extends WebComponent {
@@ -28,14 +29,13 @@ export default class RDFInput extends WebComponent {
@property({ attribute: false, type: Object })
accessor dataSubject!: NamedNode
- @property({ type: String, reflect: true })
- accessor name = '';
-
@property({ type: Boolean, reflect: true })
accessor readonly = true;
render () {
const formGraph = this.getFormGraph(this.formSubject)
+ const statementCount = this.storeContext.store?.statements?.length ?? 0
+ console.log('RDFInput render statement count:', statementCount)
// for building the HTML input element
const uiPropertyTerm = this.getFormProperty(this.formSubject, ns.ui('property'), formGraph)
@@ -48,13 +48,13 @@ export default class RDFInput extends WebComponent {
// for populating the HTML input element
const selectedTerm = this.getSelectedTerm(this.dataSubject, uiPropertyTerm, this.formSubject, params)
- const placeholder = this.defaultInputValue(params)
+ const placeholder = readonly ? '' : this.defaultInputValue(params)
const inputValue = this.termToInputValue(selectedTerm)
return html`
Date: Mon, 29 Jun 2026 18:18:04 +0200
Subject: [PATCH 15/31] added save features, copied over code
---
src/components/rdf-form/RDFForm.ts | 45 +++++++++--
src/components/rdf-form/RDForm.stories.ts | 2 +-
src/components/rdf-input/RDFInput.ts | 98 +++++++++++++++++++++--
3 files changed, 130 insertions(+), 15 deletions(-)
diff --git a/src/components/rdf-form/RDFForm.ts b/src/components/rdf-form/RDFForm.ts
index d3d9ddb5b..c7a76271a 100644
--- a/src/components/rdf-form/RDFForm.ts
+++ b/src/components/rdf-form/RDFForm.ts
@@ -16,6 +16,23 @@ export default class RDFForm extends WebComponent {
@property({ attribute: false })
accessor passedInStore: LiveStore | null = null
+ private get currentStoreContext (): StoreContext | null {
+ if (this.passedInStore) {
+ return { store: this.passedInStore }
+ }
+
+ return this.storeContext !== DEFAULT_STORE ? this.storeContext : null
+ }
+
+ @state()
+ private accessor failed: boolean = false
+
+ @state()
+ private accessor submitting: boolean = false
+
+ @state()
+ private accessor entireDataIsReadonly: boolean = true
+
@state()
private accessor _parsedUrl: URL | null = null
@@ -67,16 +84,19 @@ export default class RDFForm extends WebComponent {
}
render () {
- const currentStoreContext = this.passedInStore
- ? { store: this.passedInStore }
- : this.storeContext
+ const currentStoreContext = this.currentStoreContext
- if (!currentStoreContext?.store) {
+ if (!currentStoreContext) {
console.warn('RDFForm: store context not available yet')
return html``
}
const store = currentStoreContext.store
+
+ if (!store.updater?.editable(this.subjectURI)) {
+ this.entireDataIsReadonly = true
+ }
+
// TODO: detect format
loadDocument(store, this.rdfTurtleFormatSource, this.rdfName, this.rdfURI) // load form
loadDocument(store, this.subjectTurtleFormatSource, this.subjectName, this.subjectURI) // load data
@@ -124,6 +144,7 @@ export default class RDFForm extends WebComponent {
return html`
`
}
@@ -150,6 +171,13 @@ export default class RDFForm extends WebComponent {
return html`Unknown part type: ${part}
`
}
})}
+
+ Save
+
`
}
@@ -157,18 +185,21 @@ export default class RDFForm extends WebComponent {
private async onSubmit (e: Event) {
e.preventDefault()
- /* this.failed = false
+ this.failed = false
this.submitting = true
try {
- await this.auth.login(this.issuerInputValue)
+ const currentStoreContext = this.currentStoreContext
+ if (currentStoreContext?.store.updater?.editable(this.subjectURI)) {
+ // this.saveStatements()
+ }
} catch (error) {
console.error(error)
this.failed = true
} finally {
this.submitting = false
- } */
+ }
}
}
diff --git a/src/components/rdf-form/RDForm.stories.ts b/src/components/rdf-form/RDForm.stories.ts
index f10f70d10..e97448630 100644
--- a/src/components/rdf-form/RDForm.stories.ts
+++ b/src/components/rdf-form/RDForm.stories.ts
@@ -17,7 +17,7 @@ const meta = {
@prefix vcard: .
@prefix xsd: .
- # A Form with 2 fields and a nested subgroup
+ # A Form with 3 fields and a nested subgroup
:form a ui:Form;
ui:parts (:nameField :emailField :phoneField :addresses) .
diff --git a/src/components/rdf-input/RDFInput.ts b/src/components/rdf-input/RDFInput.ts
index 3f76997c8..474fe12e7 100644
--- a/src/components/rdf-input/RDFInput.ts
+++ b/src/components/rdf-input/RDFInput.ts
@@ -1,11 +1,11 @@
import { property } from 'lit/decorators.js'
import { html } from 'lit/html.js'
import ns from '../../lib/ns'
-import { customElement, generateId, WebComponent } from '@/lib/components'
-import { NamedNode } from 'rdflib'
+import { customElement, WebComponent } from '@/lib/components'
+import { Literal, LiveStore, NamedNode, Statement, st } from 'rdflib'
import { label } from '../../utils'
import { mostSpecificClassURI } from '../../lib/forms/rdfFormsHelper'
-import { fieldParams as fieldTypeParams, InputType } from '../../lib/forms/fieldParams'
+import { FieldParamsObject, fieldParams as fieldTypeParams, InputType } from '../../lib/forms/fieldParams'
import { DEFAULT_STORE, storeContext, StoreContext } from '@/lib/forms/store/StoreContext'
import { consume } from '@lit/context'
import '@/components/input'
@@ -33,9 +33,7 @@ export default class RDFInput extends WebComponent {
accessor readonly = true;
render () {
- const formGraph = this.getFormGraph(this.formSubject)
- const statementCount = this.storeContext.store?.statements?.length ?? 0
- console.log('RDFInput render statement count:', statementCount)
+ const formGraph = this.getGraph(this.formSubject)
// for building the HTML input element
const uiPropertyTerm = this.getFormProperty(this.formSubject, ns.ui('property'), formGraph)
@@ -59,10 +57,11 @@ export default class RDFInput extends WebComponent {
placeholder="${placeholder}"
type="${inputType}"
?readonly=${readonly}
+ @input=${this.updateData()}
>`
}
- private getFormGraph (subject?: NamedNode) {
+ private getGraph (subject?: NamedNode) {
return subject?.doc ? subject.doc() : undefined
}
@@ -113,4 +112,89 @@ export default class RDFInput extends WebComponent {
const stripped = params.defaultInputValue ?? ''
return stripped.replace(/ /g, '')
}
+
+ private updateData () {
+ return (e: CustomEvent) => {
+ const newValue = (e.target as HTMLInputElement).value
+
+ const uiPropertyTerm = this.getFormProperty(this.formSubject, ns.ui('property'), this.getGraph(this.formSubject))
+ if (!uiPropertyTerm || !this.dataSubject) return
+
+ const currentStoreContext = this.storeContext.store
+ if (!currentStoreContext.updater.editable(this.dataSubject)) return
+
+ const toDeleteSt = currentStoreContext.statementsMatching(this.dataSubject, uiPropertyTerm)
+
+ if (newValue) {
+ let objectFromNewValue
+ const fieldType = this.formSubject ? mostSpecificClassURI(this.storeContext.store, this.formSubject) : undefined
+ const params: FieldParamsObject = fieldType ? fieldTypeParams[fieldType] ?? {} : {}
+ if (params.namedNode) {
+ objectFromNewValue = currentStoreContext.sym(newValue)
+ } else if (params.defaultInputValue) {
+ objectFromNewValue = encodeURIComponent(newValue.replace(/ /g, ''))
+ objectFromNewValue = currentStoreContext.sym(params.defaultInputValue + objectFromNewValue)
+ } else {
+ if (params.dt) {
+ objectFromNewValue = new Literal(
+ newValue.trim(),
+ undefined,
+ ns.xsd(params.dt)
+ )
+ } else {
+ objectFromNewValue = new Literal(newValue)
+ }
+ }
+ let toInsertSt = toDeleteSt.map(statement => st(statement.subject, statement.predicate, objectFromNewValue, statement.why)) // can include >1 doc
+ if (toInsertSt.length === 0) {
+ toInsertSt = [st(this.formSubject, property as any, objectFromNewValue, this.getGraph(this.dataSubject))]
+ }
+
+ this.updateMany(currentStoreContext, toDeleteSt, toInsertSt)
+ }
+ }
+ }
+
+ private updateMany (
+ store: LiveStore,
+ ds: Statement[],
+ is: Statement[]
+ ) {
+ const getDocUri = (statement: Statement) => {
+ const why = statement.why as any
+ return why?.uri ?? why?.value
+ }
+
+ const docs: string[] = []
+ is.forEach(st => {
+ const uri = getDocUri(st)
+ if (uri && !docs.includes(uri)) docs.push(uri)
+ })
+ ds.forEach(st => {
+ const uri = getDocUri(st)
+ if (uri && !docs.includes(uri)) docs.push(uri)
+ })
+ if (docs.length === 0) {
+ throw new Error('No concrete document to update')
+ }
+ if (!store.updater) {
+ throw new Error('Store has no updater')
+ }
+ if (docs.length === 1) {
+ return store.updater.update(ds, is as any)
+ }
+
+ const doc = docs.pop()
+ const is1 = is.filter(st => getDocUri(st) === doc)
+ const is2 = is.filter(st => getDocUri(st) !== doc)
+ const ds1 = ds.filter(st => getDocUri(st) === doc)
+ const ds2 = ds.filter(st => getDocUri(st) !== doc)
+ store.updater.update(ds1, is1 as any, (uri, ok, body) => {
+ if (ok) {
+ this.updateMany(store, ds2, is2)
+ } else {
+ throw new Error(`Failed to update data for ${uri}: ${body}`)
+ }
+ })
+ }
}
From 082b9ee1eee178ea571c838996d75a9434290f3b Mon Sep 17 00:00:00 2001
From: timea-solid <4144203+timea-solid@users.noreply.github.com>
Date: Tue, 30 Jun 2026 12:35:46 +0200
Subject: [PATCH 16/31] added an updater to the storybook store
---
src/storybook/store/StorybookStore.ts | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/src/storybook/store/StorybookStore.ts b/src/storybook/store/StorybookStore.ts
index e09421bcb..1e7add43e 100644
--- a/src/storybook/store/StorybookStore.ts
+++ b/src/storybook/store/StorybookStore.ts
@@ -3,5 +3,12 @@ import * as rdf from 'rdflib'
import { LiveStore } from 'rdflib'
export default class StorybookStore implements StoreContext {
- public store: LiveStore = rdf.graph() as LiveStore
+ public store: LiveStore = createStore()
+}
+
+function createStore (): rdf.LiveStore {
+ const store = rdf.graph() as LiveStore
+ store.updater = new rdf.UpdateManager(store) // Add real-time live updates store.updater
+ store.features = [] // disable automatic node merging on store load
+ return store
}
From 2b53d6c051c40e41365bc8035457fe13acc9eec7 Mon Sep 17 00:00:00 2001
From: timea-solid <4144203+timea-solid@users.noreply.github.com>
Date: Tue, 30 Jun 2026 12:36:27 +0200
Subject: [PATCH 17/31] added data change capabilities, 1st version
---
src/components/rdf-form/RDFForm.ts | 75 ++++++++------
src/components/rdf-input/RDFInput.ts | 144 +++++++++++++--------------
src/lib/forms/rdfFormsHelper.ts | 49 +++++++--
3 files changed, 152 insertions(+), 116 deletions(-)
diff --git a/src/components/rdf-form/RDFForm.ts b/src/components/rdf-form/RDFForm.ts
index c7a76271a..268b5a4c7 100644
--- a/src/components/rdf-form/RDFForm.ts
+++ b/src/components/rdf-form/RDFForm.ts
@@ -24,14 +24,8 @@ export default class RDFForm extends WebComponent {
return this.storeContext !== DEFAULT_STORE ? this.storeContext : null
}
- @state()
- private accessor failed: boolean = false
-
- @state()
- private accessor submitting: boolean = false
-
@state()
- private accessor entireDataIsReadonly: boolean = true
+ private accessor entireDataIsReadonly: boolean = false
@state()
private accessor _parsedUrl: URL | null = null
@@ -39,6 +33,12 @@ export default class RDFForm extends WebComponent {
@state()
private accessor _parsedUrl2: URL | null = null
+ @state()
+ private accessor _loadVersion = 0
+
+ @state()
+ private accessor _documentsLoaded = false
+
@property({ type: String })
accessor whichForm = 'this'
@@ -91,15 +91,16 @@ export default class RDFForm extends WebComponent {
return html``
}
+ if (!this._documentsLoaded) {
+ return html``
+ }
+
const store = currentStoreContext.store
- if (!store.updater?.editable(this.subjectURI)) {
+ if (store.updater?.editable(this.subjectURI) === false) {
this.entireDataIsReadonly = true
}
- // TODO: detect format
- 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
@@ -125,7 +126,7 @@ export default class RDFForm extends WebComponent {
const me = Namespace(this.subjectURI + '#')(this.whichSubject)
return html`
-
`
}
- private async onSubmit (e: Event) {
- e.preventDefault()
+ protected updated (changedProperties: Map) {
+ super.updated(changedProperties)
+ if (
+ changedProperties.has('rdfTurtleFormatSource') ||
+ changedProperties.has('rdfName') ||
+ changedProperties.has('rdfURI') ||
+ changedProperties.has('subjectTurtleFormatSource') ||
+ changedProperties.has('subjectName') ||
+ changedProperties.has('subjectURI') ||
+ changedProperties.has('passedInStore')
+ ) {
+ this.loadDocumentsIfNeeded()
+ }
+ }
- this.failed = false
+ private async loadDocumentsIfNeeded () {
+ const currentStoreContext = this.currentStoreContext
+ if (!currentStoreContext) return
- this.submitting = true
+ const store = currentStoreContext.store
+ const rdfURI = this.rdfURI
+ const subjectURI = this.subjectURI
+
+ if (!rdfURI || !subjectURI) return
try {
- const currentStoreContext = this.currentStoreContext
- if (currentStoreContext?.store.updater?.editable(this.subjectURI)) {
- // this.saveStatements()
- }
+ await loadDocument(store, this.rdfTurtleFormatSource, this.rdfName, rdfURI, false)
+ await loadDocument(store, this.subjectTurtleFormatSource, this.subjectName, subjectURI, true)
+ this._loadVersion += 1
+ this._documentsLoaded = true
} catch (error) {
- console.error(error)
-
- this.failed = true
- } finally {
- this.submitting = false
+ console.error('Failed to load RDF documents', error)
}
}
}
diff --git a/src/components/rdf-input/RDFInput.ts b/src/components/rdf-input/RDFInput.ts
index 474fe12e7..2a08d7ddc 100644
--- a/src/components/rdf-input/RDFInput.ts
+++ b/src/components/rdf-input/RDFInput.ts
@@ -2,7 +2,7 @@ import { property } from 'lit/decorators.js'
import { html } from 'lit/html.js'
import ns from '../../lib/ns'
import { customElement, WebComponent } from '@/lib/components'
-import { Literal, LiveStore, NamedNode, Statement, st } from 'rdflib'
+import { Literal, NamedNode, st } from 'rdflib'
import { label } from '../../utils'
import { mostSpecificClassURI } from '../../lib/forms/rdfFormsHelper'
import { FieldParamsObject, fieldParams as fieldTypeParams, InputType } from '../../lib/forms/fieldParams'
@@ -29,8 +29,14 @@ export default class RDFInput extends WebComponent {
@property({ attribute: false, type: Object })
accessor dataSubject!: NamedNode
+ @property({ type: Number })
+ accessor storeVersion = 0
+
+ private _updateInFlight = false
+ private _pendingUpdateValue: string | null = null
+
@property({ type: Boolean, reflect: true })
- accessor readonly = true;
+ accessor readonly = false;
render () {
const formGraph = this.getGraph(this.formSubject)
@@ -57,7 +63,7 @@ export default class RDFInput extends WebComponent {
placeholder="${placeholder}"
type="${inputType}"
?readonly=${readonly}
- @input=${this.updateData()}
+ @input=${this.updateData}
>`
}
@@ -113,88 +119,78 @@ export default class RDFInput extends WebComponent {
return stripped.replace(/ /g, '')
}
- private updateData () {
- return (e: CustomEvent) => {
- const newValue = (e.target as HTMLInputElement).value
+ private async updateData (e: CustomEvent) {
+ const newValue = (e.target as HTMLInputElement).value
+ this._pendingUpdateValue = newValue
- const uiPropertyTerm = this.getFormProperty(this.formSubject, ns.ui('property'), this.getGraph(this.formSubject))
- if (!uiPropertyTerm || !this.dataSubject) return
+ if (this._updateInFlight) {
+ return
+ }
- const currentStoreContext = this.storeContext.store
- if (!currentStoreContext.updater.editable(this.dataSubject)) return
+ await this.runPendingUpdate()
+ }
- const toDeleteSt = currentStoreContext.statementsMatching(this.dataSubject, uiPropertyTerm)
+ private async runPendingUpdate () {
+ if (this._pendingUpdateValue === null) {
+ return
+ }
- if (newValue) {
- let objectFromNewValue
- const fieldType = this.formSubject ? mostSpecificClassURI(this.storeContext.store, this.formSubject) : undefined
- const params: FieldParamsObject = fieldType ? fieldTypeParams[fieldType] ?? {} : {}
- if (params.namedNode) {
- objectFromNewValue = currentStoreContext.sym(newValue)
- } else if (params.defaultInputValue) {
- objectFromNewValue = encodeURIComponent(newValue.replace(/ /g, ''))
- objectFromNewValue = currentStoreContext.sym(params.defaultInputValue + objectFromNewValue)
- } else {
- if (params.dt) {
- objectFromNewValue = new Literal(
- newValue.trim(),
- undefined,
- ns.xsd(params.dt)
- )
- } else {
- objectFromNewValue = new Literal(newValue)
- }
- }
- let toInsertSt = toDeleteSt.map(statement => st(statement.subject, statement.predicate, objectFromNewValue, statement.why)) // can include >1 doc
- if (toInsertSt.length === 0) {
- toInsertSt = [st(this.formSubject, property as any, objectFromNewValue, this.getGraph(this.dataSubject))]
- }
+ const newValue = this._pendingUpdateValue
+ this._pendingUpdateValue = null
+ this._updateInFlight = true
- this.updateMany(currentStoreContext, toDeleteSt, toInsertSt)
- }
+ const uiPropertyTerm = this.getFormProperty(this.formSubject, ns.ui('property'), this.getGraph(this.formSubject))
+ if (!uiPropertyTerm || !this.dataSubject) {
+ this._updateInFlight = false
+ return
}
- }
- private updateMany (
- store: LiveStore,
- ds: Statement[],
- is: Statement[]
- ) {
- const getDocUri = (statement: Statement) => {
- const why = statement.why as any
- return why?.uri ?? why?.value
+ const currentStoreContext = this.storeContext.store
+ if (currentStoreContext.updater?.editable(this.dataSubject) === false) {
+ this._updateInFlight = false
+ return
}
- const docs: string[] = []
- is.forEach(st => {
- const uri = getDocUri(st)
- if (uri && !docs.includes(uri)) docs.push(uri)
- })
- ds.forEach(st => {
- const uri = getDocUri(st)
- if (uri && !docs.includes(uri)) docs.push(uri)
- })
- if (docs.length === 0) {
- throw new Error('No concrete document to update')
- }
- if (!store.updater) {
- throw new Error('Store has no updater')
+ const toDeleteSt = currentStoreContext.statementsMatching(this.dataSubject, uiPropertyTerm)
+ let toInsertSt: Array> = []
+
+ if (newValue) {
+ let objectFromNewValue
+ const fieldType = this.formSubject ? mostSpecificClassURI(this.storeContext.store, this.formSubject) : undefined
+ const params: FieldParamsObject = fieldType ? fieldTypeParams[fieldType] ?? {} : {}
+ if (params.namedNode) {
+ objectFromNewValue = currentStoreContext.sym(newValue)
+ } else if (params.defaultInputValue) {
+ objectFromNewValue = encodeURIComponent(newValue.replace(/ /g, ''))
+ objectFromNewValue = currentStoreContext.sym(params.defaultInputValue + objectFromNewValue)
+ } else {
+ if (params.dt) {
+ objectFromNewValue = new Literal(
+ newValue.trim(),
+ undefined,
+ ns.xsd(params.dt)
+ )
+ } else {
+ objectFromNewValue = new Literal(newValue)
+ }
+ }
+ toInsertSt = toDeleteSt.map(statement => st(statement.subject, statement.predicate, objectFromNewValue, statement.why))
+ if (toInsertSt.length === 0) {
+ toInsertSt = [st(this.formSubject, property as any, objectFromNewValue, this.getGraph(this.dataSubject))]
+ }
}
- if (docs.length === 1) {
- return store.updater.update(ds, is as any)
+
+ try {
+ await currentStoreContext.updater.updateMany(toDeleteSt, toInsertSt as any)
+ this.storeVersion += 1
+ } catch (err) {
+ console.error('RDFInput update failed', err)
+ } finally {
+ this._updateInFlight = false
}
- const doc = docs.pop()
- const is1 = is.filter(st => getDocUri(st) === doc)
- const is2 = is.filter(st => getDocUri(st) !== doc)
- const ds1 = ds.filter(st => getDocUri(st) === doc)
- const ds2 = ds.filter(st => getDocUri(st) !== doc)
- store.updater.update(ds1, is1 as any, (uri, ok, body) => {
- if (ok) {
- this.updateMany(store, ds2, is2)
- } else {
- throw new Error(`Failed to update data for ${uri}: ${body}`)
- }
- })
+ if (this._pendingUpdateValue !== null) {
+ await this.runPendingUpdate()
+ }
}
}
diff --git a/src/lib/forms/rdfFormsHelper.ts b/src/lib/forms/rdfFormsHelper.ts
index 02cf1b5c3..1335eda5e 100644
--- a/src/lib/forms/rdfFormsHelper.ts
+++ b/src/lib/forms/rdfFormsHelper.ts
@@ -6,12 +6,12 @@ import ns from '../../lib/ns'
const baseUri = 'https://solidos.github.io/solid-ui/src/ontology/'
-// we need to load into the store some additional information about Social Media accounts
export function loadDocument (
store: LiveStore,
documentSource: string,
documentName: string,
- documentURI?: string
+ documentURI?: string,
+ preferRemote = false
) {
const finalDocumentUri = documentURI || baseUri + documentName // Full URI to the file
const document = sym(finalDocumentUri) // rdflib NamedNode for the document
@@ -19,13 +19,44 @@ export function loadDocument (
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)
- }
- })
+
+ const parseSource = () => {
+ return new Promise((resolve, reject) => {
+ parse(documentSource, store, finalDocumentUri, 'text/turtle', (err) => {
+ if (err) {
+ console.error('Parse document error for ', finalDocumentUri, err)
+ reject(err)
+ } else {
+ resolve()
+ }
+ })
+ })
+ }
+
+ if (preferRemote && documentURI) {
+ return store.fetcher.load(documentURI, {
+ force: true,
+ clearPreviousData: true,
+ }).then(() => {}).catch((err) => {
+ if (documentSource && documentSource.trim().length > 0) {
+ return parseSource()
+ }
+ throw err
+ })
+ }
+
+ if (documentSource && documentSource.trim().length > 0) {
+ return parseSource()
+ }
+
+ if (documentURI) {
+ return store.fetcher.load(documentURI, {
+ force: true,
+ clearPreviousData: true,
+ }).then(() => {})
+ }
+
+ return Promise.reject(new Error(`No document source or URI for ${documentName}`))
}
export function sortBySequence (
From 403a143175624b35be53357425b8424d1586314b Mon Sep 17 00:00:00 2001
From: Timea <4144203+timea-solid@users.noreply.github.com>
Date: Tue, 30 Jun 2026 12:37:39 +0200
Subject: [PATCH 18/31] Update src/components/input/Input.styles.css
Co-authored-by: Noel De Martin
---
src/components/input/Input.styles.css | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/input/Input.styles.css b/src/components/input/Input.styles.css
index 4a6ec23b1..e593ce1b6 100644
--- a/src/components/input/Input.styles.css
+++ b/src/components/input/Input.styles.css
@@ -27,7 +27,7 @@
input:read-only {
border: none;
padding: 0;
- cursor: pointer;
+ cursor: not-allowed;
}
}
From 0af9ce9dbfb05137526b817caf9a7e5a62607077 Mon Sep 17 00:00:00 2001
From: timea-solid <4144203+timea-solid@users.noreply.github.com>
Date: Tue, 30 Jun 2026 12:40:11 +0200
Subject: [PATCH 19/31] rename to NoopStore
---
src/lib/forms/store/{RDFFormsStore.ts => NoopStore.ts} | 2 +-
src/lib/forms/store/StoreContext.ts | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
rename src/lib/forms/store/{RDFFormsStore.ts => NoopStore.ts} (74%)
diff --git a/src/lib/forms/store/RDFFormsStore.ts b/src/lib/forms/store/NoopStore.ts
similarity index 74%
rename from src/lib/forms/store/RDFFormsStore.ts
rename to src/lib/forms/store/NoopStore.ts
index 1dc73d2c7..ace119e7a 100644
--- a/src/lib/forms/store/RDFFormsStore.ts
+++ b/src/lib/forms/store/NoopStore.ts
@@ -1,7 +1,7 @@
import { LiveStore } from 'rdflib'
import { StoreContext } from './StoreContext'
-export default class RDFFormsStore implements StoreContext {
+export default class NoopStore implements StoreContext {
get store (): LiveStore {
throw new Error('Can\'t use RDF forms without a store')
}
diff --git a/src/lib/forms/store/StoreContext.ts b/src/lib/forms/store/StoreContext.ts
index e0f374a6e..8cd36d6be 100644
--- a/src/lib/forms/store/StoreContext.ts
+++ b/src/lib/forms/store/StoreContext.ts
@@ -1,10 +1,10 @@
import { createContext } from '@lit/context'
import { LiveStore } from 'rdflib'
-import RDFFormsStore from './RDFFormsStore'
+import NoopStore from './NoopStore'
export interface StoreContext {
store: LiveStore
}
-export const DEFAULT_STORE = new RDFFormsStore()
+export const DEFAULT_STORE = new NoopStore()
export const storeContext = createContext(Symbol('storeContext'))
From 9d9f48abf505e19811643f4368118d3e39ec2304 Mon Sep 17 00:00:00 2001
From: timea-solid <4144203+timea-solid@users.noreply.github.com>
Date: Tue, 30 Jun 2026 13:02:24 +0200
Subject: [PATCH 20/31] renamed NoopStore
---
src/components/rdf-form/RDFForm.ts | 22 ++++++----------------
src/components/rdf-input/RDFInput.ts | 11 +++++------
src/lib/forms/store/NoopStore.ts | 2 +-
3 files changed, 12 insertions(+), 23 deletions(-)
diff --git a/src/components/rdf-form/RDFForm.ts b/src/components/rdf-form/RDFForm.ts
index 268b5a4c7..2ddb06659 100644
--- a/src/components/rdf-form/RDFForm.ts
+++ b/src/components/rdf-form/RDFForm.ts
@@ -16,15 +16,15 @@ export default class RDFForm extends WebComponent {
@property({ attribute: false })
accessor passedInStore: LiveStore | null = null
- private get currentStoreContext (): StoreContext | null {
+ private get currentStoreContext (): StoreContext {
if (this.passedInStore) {
- return { store: this.passedInStore }
+ this.storeContext.store = this.passedInStore
}
- return this.storeContext !== DEFAULT_STORE ? this.storeContext : null
+ return this.storeContext
}
- @state()
+ @state()
private accessor entireDataIsReadonly: boolean = false
@state()
@@ -84,18 +84,11 @@ export default class RDFForm extends WebComponent {
}
render () {
- const currentStoreContext = this.currentStoreContext
-
- if (!currentStoreContext) {
- console.warn('RDFForm: store context not available yet')
- return html``
- }
-
if (!this._documentsLoaded) {
return html``
}
- const store = currentStoreContext.store
+ const store = this.currentStoreContext.store
if (store.updater?.editable(this.subjectURI) === false) {
this.entireDataIsReadonly = true
@@ -193,10 +186,7 @@ export default class RDFForm extends WebComponent {
}
private async loadDocumentsIfNeeded () {
- const currentStoreContext = this.currentStoreContext
- if (!currentStoreContext) return
-
- const store = currentStoreContext.store
+ const store = this.currentStoreContext.store
const rdfURI = this.rdfURI
const subjectURI = this.subjectURI
diff --git a/src/components/rdf-input/RDFInput.ts b/src/components/rdf-input/RDFInput.ts
index 2a08d7ddc..42ce17788 100644
--- a/src/components/rdf-input/RDFInput.ts
+++ b/src/components/rdf-input/RDFInput.ts
@@ -145,13 +145,12 @@ export default class RDFInput extends WebComponent {
return
}
- const currentStoreContext = this.storeContext.store
- if (currentStoreContext.updater?.editable(this.dataSubject) === false) {
+ if (this.storeContext.store.updater?.editable(this.dataSubject) === false) {
this._updateInFlight = false
return
}
- const toDeleteSt = currentStoreContext.statementsMatching(this.dataSubject, uiPropertyTerm)
+ const toDeleteSt = this.storeContext.store.statementsMatching(this.dataSubject, uiPropertyTerm)
let toInsertSt: Array> = []
if (newValue) {
@@ -159,10 +158,10 @@ export default class RDFInput extends WebComponent {
const fieldType = this.formSubject ? mostSpecificClassURI(this.storeContext.store, this.formSubject) : undefined
const params: FieldParamsObject = fieldType ? fieldTypeParams[fieldType] ?? {} : {}
if (params.namedNode) {
- objectFromNewValue = currentStoreContext.sym(newValue)
+ objectFromNewValue = this.storeContext.store.sym(newValue)
} else if (params.defaultInputValue) {
objectFromNewValue = encodeURIComponent(newValue.replace(/ /g, ''))
- objectFromNewValue = currentStoreContext.sym(params.defaultInputValue + objectFromNewValue)
+ objectFromNewValue = this.storeContext.store.sym(params.defaultInputValue + objectFromNewValue)
} else {
if (params.dt) {
objectFromNewValue = new Literal(
@@ -181,7 +180,7 @@ export default class RDFInput extends WebComponent {
}
try {
- await currentStoreContext.updater.updateMany(toDeleteSt, toInsertSt as any)
+ await this.storeContext.store.updater.updateMany(toDeleteSt, toInsertSt as any)
this.storeVersion += 1
} catch (err) {
console.error('RDFInput update failed', err)
diff --git a/src/lib/forms/store/NoopStore.ts b/src/lib/forms/store/NoopStore.ts
index ace119e7a..bc5b5b93a 100644
--- a/src/lib/forms/store/NoopStore.ts
+++ b/src/lib/forms/store/NoopStore.ts
@@ -3,6 +3,6 @@ import { StoreContext } from './StoreContext'
export default class NoopStore implements StoreContext {
get store (): LiveStore {
- throw new Error('Can\'t use RDF forms without a store')
+ throw new Error('Can not use RDF forms without a store')
}
}
From 3da49925b6c853c9742e7c1d85a68b3226cc968e Mon Sep 17 00:00:00 2001
From: timea-solid <4144203+timea-solid@users.noreply.github.com>
Date: Tue, 30 Jun 2026 13:09:56 +0200
Subject: [PATCH 21/31] cleanup from feedback
---
.gitignore | 1 +
src/components/rdf-form/RDForm.stories.ts | 20 +++++++++-----------
2 files changed, 10 insertions(+), 11 deletions(-)
diff --git a/.gitignore b/.gitignore
index 92168dbd6..f94e75f5a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
node_modules
dist
src/versionInfo.ts
+src/types/custom-elements.d.ts
.idea
.vscode
coverage
diff --git a/src/components/rdf-form/RDForm.stories.ts b/src/components/rdf-form/RDForm.stories.ts
index e97448630..1c3a97dcf 100644
--- a/src/components/rdf-form/RDForm.stories.ts
+++ b/src/components/rdf-form/RDForm.stories.ts
@@ -103,17 +103,15 @@ const meta = {
const render = defineStoryRender(({ rdfTurtleFormatSource, rdfURI, whichForm, rdfName, subjectTurtleFormatSource, subjectName, subjectURI }) => {
return html`
-
-
-
-
+
+
`
})
From 643dc5a2230eae5095bce4a707a419c297c4a4b0 Mon Sep 17 00:00:00 2001
From: timea-solid <4144203+timea-solid@users.noreply.github.com>
Date: Tue, 30 Jun 2026 13:32:08 +0200
Subject: [PATCH 22/31] imporved component type declarations
---
src/types/custom-elements.d.ts | 50 ++++++
.../components/actions/button/Button.test.ts | 164 ------------------
src/v2/components/actions/button/index.ts | 6 -
vite-config/components.ts | 26 ++-
vite.config.ts | 5 +-
5 files changed, 70 insertions(+), 181 deletions(-)
delete mode 100644 src/v2/components/actions/button/Button.test.ts
diff --git a/src/types/custom-elements.d.ts b/src/types/custom-elements.d.ts
index 6c765dd06..7d5ffa0be 100644
--- a/src/types/custom-elements.d.ts
+++ b/src/types/custom-elements.d.ts
@@ -3,12 +3,62 @@
* Do not edit this file directly.
*/
+import type Account from '../components/account/Account'
+import type Avatar from '../components/avatar/Avatar'
+import type Button from '../components/button/Button'
+import type Combobox from '../components/combobox/Combobox'
+import type ComboboxOption from '../components/combobox-option/ComboboxOption'
+import type Dialog from '../components/dialog/Dialog'
+import type DialogContent from '../components/dialog-content/DialogContent'
+import type DialogFooter from '../components/dialog-footer/DialogFooter'
+import type DialogHeader from '../components/dialog-header/DialogHeader'
+import type DialogProvider from '../components/dialog-provider/DialogProvider'
+import type DialogsRoot from '../components/dialogs-root/DialogsRoot'
+import type Footer from '../components/footer/Footer'
+import type Guard from '../components/guard/Guard'
+import type Header from '../components/header/Header'
+import type Input from '../components/input/Input'
+import type LoginButton from '../components/login-button/LoginButton'
+import type LoginModal from '../components/login-modal/LoginModal'
+import type LogoutButton from '../components/logout-button/LogoutButton'
+import type Menu from '../components/menu/Menu'
+import type MenuItem from '../components/menu-item/MenuItem'
+import type PhotoCapture from '../components/photo-capture/PhotoCapture'
+import type Provider from '../components/provider/Provider'
import type RDFForm from '../components/rdf-form/RDFForm'
import type RDFInput from '../components/rdf-input/RDFInput'
+import type Select from '../components/select/Select'
+import type SelectOption from '../components/select-option/SelectOption'
+import type SignupButton from '../components/signup-button/SignupButton'
declare global {
interface HTMLElementTagNameMap {
+ 'solid-ui-account': Account
+ 'solid-ui-avatar': Avatar
+ 'solid-ui-button': Button
+ 'solid-ui-combobox': Combobox
+ 'solid-ui-combobox-option': ComboboxOption
+ 'solid-ui-dialog': Dialog
+ 'solid-ui-dialog-content': DialogContent
+ 'solid-ui-dialog-footer': DialogFooter
+ 'solid-ui-dialog-header': DialogHeader
+ 'solid-ui-dialog-provider': DialogProvider
+ 'solid-ui-dialogs-root': DialogsRoot
+ 'solid-ui-footer': Footer
+ 'solid-ui-guard': Guard
+ 'solid-ui-header': Header
+ 'solid-ui-input': Input
+ 'solid-ui-login-button': LoginButton
+ 'solid-ui-login-modal': LoginModal
+ 'solid-ui-logout-button': LogoutButton
+ 'solid-ui-menu': Menu
+ 'solid-ui-menu-item': MenuItem
+ 'solid-ui-photo-capture': PhotoCapture
+ 'solid-ui-provider': Provider
'solid-ui-rdf-form': RDFForm
'solid-ui-rdf-input': RDFInput
+ 'solid-ui-select': Select
+ 'solid-ui-select-option': SelectOption
+ 'solid-ui-signup-button': SignupButton
}
}
diff --git a/src/v2/components/actions/button/Button.test.ts b/src/v2/components/actions/button/Button.test.ts
deleted file mode 100644
index eb0ac217e..000000000
--- a/src/v2/components/actions/button/Button.test.ts
+++ /dev/null
@@ -1,164 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { Button } from './Button'
-import './index'
-
-describe('SolidUIButton', () => {
- beforeEach(() => {
- document.body.innerHTML = ''
- })
-
- it('is defined as a custom element', () => {
- expect(customElements.get('solid-ui-button')).toBe(Button)
- })
-
- it('renders a secondary button by default', async () => {
- const button = new Button()
- button.label = 'Upload'
-
- document.body.appendChild(button)
- await button.updateComplete
-
- const nativeButton = button.shadowRoot?.querySelector(
- 'button'
- ) as HTMLButtonElement
-
- expect(button.variant).toBe('secondary')
- expect(nativeButton.type).toBe('button')
- expect(nativeButton.textContent?.trim()).toBe('Upload')
- })
-
- it('supports a selected state without forcing toggle semantics', async () => {
- const button = new Button()
- button.selected = true
-
- document.body.appendChild(button)
- await button.updateComplete
-
- const nativeButton = button.shadowRoot?.querySelector(
- 'button'
- ) as HTMLButtonElement
-
- expect(button.hasAttribute('selected')).toBe(true)
- expect(nativeButton.hasAttribute('aria-pressed')).toBe(false)
- expect(nativeButton.hasAttribute('aria-selected')).toBe(false)
- })
-
- it('calls the callback property and still emits the native click event', async () => {
- const button = new Button()
- const handleClick = vi.fn()
- const clickListener = vi.fn()
- button.handleClick = handleClick
- button.addEventListener('click', clickListener)
-
- document.body.appendChild(button)
- await button.updateComplete
-
- const nativeButton = button.shadowRoot?.querySelector(
- 'button'
- ) as HTMLButtonElement
- nativeButton.click()
-
- expect(handleClick).toHaveBeenCalledTimes(1)
- expect(clickListener).toHaveBeenCalledTimes(1)
- })
-
- it('renders an image icon when the icon property is provided', async () => {
- const button = new Button()
- button.icon = 'data:image/svg+xml,%3Csvg%3E%3C/svg%3E'
-
- document.body.appendChild(button)
- await button.updateComplete
-
- const icon = button.shadowRoot?.querySelector(
- '.button__icon-image'
- ) as HTMLImageElement
- expect(icon.getAttribute('src')).toBe(button.icon)
- })
-
- it('supports an icon-only variant without rendering the label text', async () => {
- const button = new Button()
- button.variant = 'icon'
- button.icon = 'data:image/svg+xml,%3Csvg%3E%3C/svg%3E'
- button.label = 'Settings'
-
- document.body.appendChild(button)
- await button.updateComplete
-
- const label = button.shadowRoot?.querySelector(
- '.button__label'
- ) as HTMLSpanElement
- const icon = button.shadowRoot?.querySelector(
- '.button__icon-image'
- ) as HTMLImageElement
-
- expect(button.variant).toBe('icon')
- expect(label).not.toBeNull()
- expect(icon.getAttribute('src')).toBe(button.icon)
- })
-
- it('prefers slotted icon content over the icon property fallback', async () => {
- const button = document.createElement('solid-ui-button') as Button
- button.icon = 'data:image/svg+xml,%3Csvg%3E%3C/svg%3E'
-
- const slottedIcon = document.createElement('span')
- slottedIcon.slot = 'icon'
- slottedIcon.textContent = 'icon'
- button.appendChild(slottedIcon)
-
- document.body.appendChild(button)
- await button.updateComplete
- await Promise.resolve()
- await button.updateComplete
-
- expect(button.shadowRoot?.querySelector('slot[name="icon"]')).not.toBeNull()
- expect(button.shadowRoot?.querySelector('.button__icon-image')).toBeNull()
- })
-
- it('renders slotted icon content without requiring an icon fallback property', async () => {
- const button = document.createElement('solid-ui-button') as Button
-
- const slottedIcon = document.createElement('span')
- slottedIcon.slot = 'icon'
- slottedIcon.textContent = 'icon'
- button.appendChild(slottedIcon)
-
- document.body.appendChild(button)
- await button.updateComplete
- await Promise.resolve()
- await button.updateComplete
-
- expect(button.shadowRoot?.querySelector('slot[name="icon"]')).not.toBeNull()
- expect(button.shadowRoot?.querySelector('.button__icon')).not.toBeNull()
- expect(button.shadowRoot?.querySelector('.button__icon-image')).toBeNull()
- })
-
- it('applies layout styling hooks exposed through CSS custom properties', async () => {
- const stylesheetText = Array.isArray(Button.styles)
- ? Button.styles.map((styleSheet) => styleSheet.toString()).join('\n')
- : Button.styles.toString()
-
- expect(stylesheetText).toContain(
- '--button-padding-sm: 0 var(--button-padding-x-sm);'
- )
- expect(stylesheetText).toContain('--button-border-width: 1px;')
- expect(stylesheetText).toContain('padding: var(--button-padding-md);')
- expect(stylesheetText).toContain(
- 'border: var(--button-border-width) solid var(--button-border);'
- )
- expect(stylesheetText).toContain(
- 'border-radius: var(--button-border-radius);'
- )
- expect(stylesheetText).toContain('font-weight: var(--button-font-weight);')
- expect(stylesheetText).toContain('line-height: var(--button-line-height);')
- expect(stylesheetText).toContain(
- 'justify-content: var(--button-justify-content);'
- )
- expect(stylesheetText).toContain(
- 'box-shadow: var(--button-hover-box-shadow, var(--button-box-shadow));'
- )
- expect(stylesheetText).toContain('outline: var(--button-focus-outline);')
- expect(stylesheetText).toContain(
- 'transform: var(--button-active-transform);'
- )
- })
-})
diff --git a/src/v2/components/actions/button/index.ts b/src/v2/components/actions/button/index.ts
index 3904c59e2..36ddb12a7 100644
--- a/src/v2/components/actions/button/index.ts
+++ b/src/v2/components/actions/button/index.ts
@@ -4,12 +4,6 @@ export { Button }
const BUTTON_TAG_NAME = 'solid-ui-button'
-declare global {
- interface HTMLElementTagNameMap {
- 'solid-ui-button': Button
- }
-}
-
if (!customElements.get(BUTTON_TAG_NAME)) {
customElements.define(BUTTON_TAG_NAME, Button)
}
diff --git a/vite-config/components.ts b/vite-config/components.ts
index 72b81a296..0817ffca3 100644
--- a/vite-config/components.ts
+++ b/vite-config/components.ts
@@ -1,11 +1,11 @@
import { existsSync, readdirSync, writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
+import type { Plugin } from 'vite'
const projectRoot = resolve(import.meta.dirname, '..')
export const componentsSrcDir = join(projectRoot, 'src/components')
export const customElementsTypesPath = join(projectRoot, 'src/types/custom-elements.d.ts')
-const rdfComponentPrefix = 'rdf-'
export function discoverComponents(): string[] {
return readdirSync(componentsSrcDir, { withFileTypes: true })
@@ -18,10 +18,6 @@ export function discoverComponents(): string[] {
.sort()
}
-export function discoverRdfComponents(): string[] {
- return discoverComponents().filter((name) => name.startsWith(rdfComponentPrefix))
-}
-
function getPascalCase(name: string): string {
return name
.split('-')
@@ -34,7 +30,7 @@ function getPascalCase(name: string): string {
}
export function generateCustomElementsTypes(): void {
- const rdfComponents = discoverRdfComponents()
+ const components = discoverComponents()
const lines = [
'/**',
@@ -44,14 +40,14 @@ export function generateCustomElementsTypes(): void {
'',
]
- for (const component of rdfComponents) {
+ for (const component of components) {
const className = getPascalCase(component)
lines.push(`import type ${className} from '../components/${component}/${className}'`)
}
lines.push('', 'declare global {', ' interface HTMLElementTagNameMap {')
- for (const component of rdfComponents) {
+ for (const component of components) {
const className = getPascalCase(component)
lines.push(` 'solid-ui-${component}': ${className}`)
}
@@ -60,3 +56,17 @@ export function generateCustomElementsTypes(): void {
writeFileSync(customElementsTypesPath, lines.join('\n'), 'utf-8')
}
+
+export function customElementsTypesPlugin(): Plugin {
+ return {
+ name: 'solid-ui-custom-elements-types',
+ buildStart() {
+ generateCustomElementsTypes()
+ },
+ handleHotUpdate(context) {
+ if (context.file.startsWith(componentsSrcDir)) {
+ generateCustomElementsTypes()
+ }
+ },
+ }
+}
diff --git a/vite.config.ts b/vite.config.ts
index 987b406b5..8aee7f719 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -6,14 +6,13 @@ import babel from './vite-config/babel'
import css from './vite-config/css'
import icons from './vite-config/icons'
import { cdnLegacyConfig, cdnConfig } from './vite-config/cdn'
-import { discoverComponents, generateCustomElementsTypes } from './vite-config/components'
+import { discoverComponents, customElementsTypesPlugin } from './vite-config/components'
import { stylesConfig } from './vite-config/styles'
-generateCustomElementsTypes()
-
const basePlugins = [
css(),
icons(),
+ customElementsTypesPlugin(),
]
function defaultConfig(): UserConfig {
From b0767039323350ebfa4fa26936e6401924713ecc Mon Sep 17 00:00:00 2001
From: timea-solid <4144203+timea-solid@users.noreply.github.com>
Date: Thu, 2 Jul 2026 10:01:16 +0200
Subject: [PATCH 23/31] Addeda URL lit converter Prompt: I have this accessors
rdfURI and subjectURi which should be URIs. I would like to use a lit
converter for them instead of my code:
https://lit.dev/docs/components/properties/#conversion-converter Help me with
it. Prompt: I want the converter to only return URL or null
Co-Authored-By: GitHub Copilot (raptor-mini)
---
src/components/rdf-form/RDFForm.ts | 68 +++++++++++------------
src/components/rdf-form/RDForm.stories.ts | 2 +-
2 files changed, 33 insertions(+), 37 deletions(-)
diff --git a/src/components/rdf-form/RDFForm.ts b/src/components/rdf-form/RDFForm.ts
index 2ddb06659..1d39f6fc9 100644
--- a/src/components/rdf-form/RDFForm.ts
+++ b/src/components/rdf-form/RDFForm.ts
@@ -8,6 +8,25 @@ import { sym, Namespace, LiveStore } from 'rdflib'
import '@/components/rdf-input'
import { DEFAULT_STORE, storeContext, StoreContext } from '@/lib/forms/store/StoreContext'
+const urlConverter = {
+ fromAttribute (value: string | null): URL | null {
+ if (!value) return null
+
+ try {
+ return new URL(value)
+ } catch {
+ return null
+ }
+ },
+ toAttribute (value: URL | null) {
+ if (!value) return null
+ return value
+ }
+}
+
+const hrefFromUrlValue = (value: URL | null): string =>
+ value?.href ?? ''
+
@customElement('solid-ui-rdf-form')
export default class RDFForm extends WebComponent {
@consume({ context: storeContext, subscribe: true })
@@ -27,12 +46,6 @@ export default class RDFForm extends WebComponent {
@state()
private accessor entireDataIsReadonly: boolean = false
- @state()
- private accessor _parsedUrl: URL | null = null
-
- @state()
- private accessor _parsedUrl2: URL | null = null
-
@state()
private accessor _loadVersion = 0
@@ -48,18 +61,8 @@ export default class RDFForm extends WebComponent {
@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 : ''
- }
+ @property({ converter: urlConverter })
+ accessor rdfURI: URL | null = null
@property({ type: String })
accessor whichSubject = 'me'
@@ -70,33 +73,26 @@ export default class RDFForm extends WebComponent {
@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 : ''
- }
+ @property({ converter: urlConverter })
+ accessor subjectURI: URL | null = null
render () {
+ console.log('subjectURI ', this.subjectURI)
+ console.log('rdfURI ', this.rdfURI)
if (!this._documentsLoaded) {
return html``
}
const store = this.currentStoreContext.store
+ const subjectURI = hrefFromUrlValue(this.subjectURI)
- if (store.updater?.editable(this.subjectURI) === false) {
+ if (subjectURI && store.updater?.editable(subjectURI) === false) {
this.entireDataIsReadonly = true
}
- const document = sym(this.rdfURI) // rdflib NamedNode for the document
+ const document = sym(hrefFromUrlValue(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
+ const formThis = Namespace(`${hrefFromUrlValue(this.rdfURI)}#`)(exactForm) // NamedNode for #this in the form
const parts = store.each(formThis, ns.ui('parts'), null, document)
const partsBySequence = sortBySequence(store, parts)
@@ -116,7 +112,7 @@ export default class RDFForm extends WebComponent {
fieldValue: hashIndex >= 0 ? value.slice(hashIndex + 1) : value
}
})
- const me = Namespace(this.subjectURI + '#')(this.whichSubject)
+ const me = Namespace(`${hrefFromUrlValue(this.subjectURI)}#`)(this.whichSubject)
return html`