From 0a6d16b4826ce65edbf30e2203ace3318daee077 Mon Sep 17 00:00:00 2001
From: timea-solid <4144203+timea-solid@users.noreply.github.com>
Date: Tue, 16 Jun 2026 14:10:18 +0200
Subject: [PATCH 1/3] rdf forms component
---
src/primitives/components/rdf-form/RDFForm.ts | 93 +++++++++++++++++++
.../components/rdf-form/RDForm.stories.ts | 85 +++++++++++++++++
src/primitives/lib/rdfFormsHelper.js | 67 +++++++++++++
3 files changed, 245 insertions(+)
create mode 100644 src/primitives/components/rdf-form/RDFForm.ts
create mode 100644 src/primitives/components/rdf-form/RDForm.stories.ts
create mode 100644 src/primitives/lib/rdfFormsHelper.js
diff --git a/src/primitives/components/rdf-form/RDFForm.ts b/src/primitives/components/rdf-form/RDFForm.ts
new file mode 100644
index 000000000..d93b731c3
--- /dev/null
+++ b/src/primitives/components/rdf-form/RDFForm.ts
@@ -0,0 +1,93 @@
+import { customElement, property, state } from 'lit/decorators.js'
+import { html } from 'lit/html.js'
+import WebComponent from '../../lib/WebComponent'
+import ns from '../../../lib/ns'
+import { loadDocument, sortBySequence } from '../../lib/rdfFormsHelper'
+import { sym, Namespace } from 'rdflib'
+import { store } from 'solid-logic'
+
+@customElement('solid-ui-rdf-form')
+export default class RDFForm extends WebComponent {
+ @state()
+ private accessor _parsedUrl: URL | null = null
+
+ @property({ type: String })
+ accessor whichForm = 'this'
+
+ @property({ type: String })
+ accessor rdfTurtleFormatSource = ''
+
+ @property({ type: String })
+ accessor rdfName = ''
+
+ @property({ type: String })
+ set rdfURI (value: string) {
+ try {
+ this._parsedUrl = new URL(value)
+ } catch {
+ this._parsedUrl = null // Handle invalid URL
+ }
+ }
+
+ get rdfURI (): string {
+ return this._parsedUrl ? this._parsedUrl.href : ''
+ }
+
+ render () {
+ // TODO: detect format
+ loadDocument(store, this.rdfTurtleFormatSource, this.rdfName, this.rdfURI)
+ const document = sym(this.rdfURI) // rdflib NamedNode for the document
+ const exactForm = this.whichForm // If there are more 'a ui:Form' elements in a form file
+ const formThis = Namespace(this.rdfURI + '#')(exactForm) // NamedNode for #this in the form
+ console.log('formThis:', formThis.value)
+
+ const parts = store.each(formThis, ns.ui('parts'), null, document)
+ const partsBySequence = sortBySequence(store, parts)
+ const uiFields = partsBySequence.map(item => ((item as any).value || String(item)).split('#').pop())
+ console.log('document:', document)
+ console.log('exactForm:', exactForm)
+ console.log('uiFields:', uiFields)
+
+ return html`
+ ${uiFields.map(part => {
+ switch (part) {
+ case 'PhoneField':
+ case 'EmailField':
+ case 'ColorField':
+ case 'DateField':
+ case 'DateTimeField':
+ case 'TimeField':
+ case 'NumericField':
+ case 'IntegerField':
+ case 'DecimalField':
+ case 'FloatField':
+ case 'TextField':
+ case 'SingleLineTextField':
+ case 'NamedNodeURIField':
+ return html``
+ case 'MultiLineTextField':
+ return html``
+ case 'BooleanField':
+ return html``
+ case 'TristateField':
+ return html``
+ case 'Classifier':
+ return html``
+ case 'Choice':
+ return html``
+ case 'Multiple':
+ return html``
+ case 'Options':
+ return html``
+ case 'AutocompleteField':
+ return html``
+ case 'Comment':
+ case 'Heading':
+ return html``
+ default:
+ return html`
Unknown part type: ${part}
`
+ }
+ })}
+ `
+ }
+}
diff --git a/src/primitives/components/rdf-form/RDForm.stories.ts b/src/primitives/components/rdf-form/RDForm.stories.ts
new file mode 100644
index 000000000..b44dc8ae1
--- /dev/null
+++ b/src/primitives/components/rdf-form/RDForm.stories.ts
@@ -0,0 +1,85 @@
+import { html } from 'lit'
+import { defineStoryRender } from '../../../storybook'
+
+const meta = {
+ title: 'Design System/RDF Form',
+ args: {
+ rdfTurtleFormatSource: `
+ # A Form with 2 fields and a nested subgroup
+
+ :form a ui:Form;
+ ui:parts (:nameField :emailField :addresses) .
+
+ :nameField a ui:SingleLineTextField ;
+ ui:property vcard:fn;
+ ui:label "name" .
+
+ :emailField a ui:EmailField ;
+ ui:property vcard:hasEmail; # @@ check
+ ui:label "email" .
+
+ :addresses
+ a ui:Multiple ; # -- Allows zero or one or more
+ ui:part :oneAddress ;
+ ui:property vcard:hasAddress .
+
+ :oneAddress
+ a ui:Group ; # A subgroup of the main form
+ ui:parts ( :street :locality :postcode :region :country ).
+
+ :street
+ a ui:SingleLineTextField ;
+ ui:maxLength "128" ;
+ ui:property vcard:street-address ;
+ ui:size "40" .
+
+ :locality
+ a ui:SingleLineTextField ;
+ ui:maxLength "128" ;
+ ui:property vcard:locality ;
+ ui:size "40" .
+
+ :postcode
+ a ui:SingleLineTextField ;
+ ui:maxLength "25" ;
+ ui:property vcard:postal-code ;
+ ui:size "25" .
+
+ :region
+ a ui:SingleLineTextField ;
+ ui:maxLength "128" ;
+ ui:property vcard:region ;
+ ui:size "40" .
+
+ :country
+ a ui:SingleLineTextField ;
+ ui:maxLength "128" ;
+ ui:property vcard:country-name ;
+ ui:size "40" .`,
+ rdfURI: 'https://solidos.solidcommunity.net/public/2021/solidUiFormTestData/dummyFormTestFile.ttl',
+ whichForm: 'this',
+ rdfName: 'dummyFormTestFile.ttl'
+ },
+
+ argTypes: {
+ rdfTurtleFormatSource: { control: 'text' },
+ rdfURI: { control: 'text' },
+ whichForm: { control: 'text' },
+ rdfName: { control: 'text' }
+ },
+} as const
+
+const render = defineStoryRender(({ rdfTurtleFormatSource, rdfURI, whichForm, rdfName }) => {
+ return html`
+
+
+ `
+})
+
+export default meta
+
+export const Primary = { render }
diff --git a/src/primitives/lib/rdfFormsHelper.js b/src/primitives/lib/rdfFormsHelper.js
new file mode 100644
index 000000000..f81dbaa75
--- /dev/null
+++ b/src/primitives/lib/rdfFormsHelper.js
@@ -0,0 +1,67 @@
+import { sym, Namespace, parse } from 'rdflib'
+import { widgets } from 'solid-ui'
+import ns from '../../lib/ns'
+
+const baseUri = 'https://solidos.github.io/solid-ui/src/ontology/'
+
+export function renderForm (
+ div,
+ subject, // Represents the RDF that fills the form
+ formSource, // The imported form Turtle source
+ formName, // The name of the form file (e.g., 'socialMedia.ttl')
+ store,
+ dom,
+ editableProfile,
+ whichForm) {
+ // --- Form resource setup ---
+ const formUri = baseUri + formName // Full URI to the form file
+ const exactForm = whichForm || 'this' // If there are more 'a ui:Form' elements in a form file
+ const formThis = Namespace(formUri + '#')(exactForm) // NamedNode for #this in the form
+
+ loadDocument(store, formSource, formName, formUri)
+
+ widgets.appendForm(
+ dom,
+ div,
+ {},
+ subject,
+ formThis,
+ editableProfile,
+ (ok, mes) => {
+ if (!ok) widgets.errorMessageBlock(dom, mes)
+ }
+ )
+} // renderForm
+
+// we need to load into the store some additional information about Social Media accounts
+export function loadDocument (
+ store,
+ documentSource,
+ documentName,
+ documentURI
+) {
+ const finalDocumentUri = documentURI || baseUri + documentName // Full URI to the file
+ const document = sym(finalDocumentUri) // rdflib NamedNode for the document
+
+ if (!store.holds(undefined, undefined, undefined, document)) {
+ // we are using the social media form because it contains the information we need
+ // the form can be used for both use cases: create UI for edit and render UI for display
+ parse(documentSource, store, finalDocumentUri, 'text/turtle', () => null) // Load doc directly
+ }
+}
+
+export function sortBySequence (
+ store,
+ list
+) {
+ const subfields = list.map(function (p) {
+ const k = store.any(p, ns.ui('sequence'))
+ return [k || 9999, p]
+ })
+ subfields.sort(function (a, b) {
+ return a[0] - b[0]
+ })
+ return subfields.map(function (pair) {
+ return pair[1]
+ })
+}
From 7c5a7c3277802e9382a69d2bdcddbee50edc0dd1 Mon Sep 17 00:00:00 2001
From: timea-solid <4144203+timea-solid@users.noreply.github.com>
Date: Tue, 16 Jun 2026 14:40:14 +0200
Subject: [PATCH 2/3] rendered first rdf forms elements
---
src/primitives/components/rdf-form/RDFForm.ts | 17 ++++++++++++++++-
.../components/rdf-form/RDForm.stories.ts | 13 ++++++++++---
2 files changed, 26 insertions(+), 4 deletions(-)
diff --git a/src/primitives/components/rdf-form/RDFForm.ts b/src/primitives/components/rdf-form/RDFForm.ts
index d93b731c3..0614c4358 100644
--- a/src/primitives/components/rdf-form/RDFForm.ts
+++ b/src/primitives/components/rdf-form/RDFForm.ts
@@ -43,7 +43,22 @@ export default class RDFForm extends WebComponent {
const parts = store.each(formThis, ns.ui('parts'), null, document)
const partsBySequence = sortBySequence(store, parts)
- const uiFields = partsBySequence.map(item => ((item as any).value || String(item)).split('#').pop())
+ const partItems = (partsBySequence || []).flatMap(item => {
+ if (item && typeof item === 'object' && 'elements' in item && Array.isArray((item as any).elements)) {
+ return (item as any).elements
+ }
+ return [item]
+ })
+ const uiFields = partItems.map(item => {
+ const types = store.each(item as any, ns.rdf('type'), null, document)
+ const typeNode = types[0]
+ const value = typeNode ? ((typeNode as any).value || String(typeNode)) : ((item as any).value || String(item))
+ const hashIndex = value.lastIndexOf('#')
+ return hashIndex >= 0 ? value.slice(hashIndex + 1) : value
+ })
+ console.log('parts:', parts)
+ console.log('partsBySequence:', partsBySequence)
+ console.log('partItems:', partItems)
console.log('document:', document)
console.log('exactForm:', exactForm)
console.log('uiFields:', uiFields)
diff --git a/src/primitives/components/rdf-form/RDForm.stories.ts b/src/primitives/components/rdf-form/RDForm.stories.ts
index b44dc8ae1..34ad1bbe7 100644
--- a/src/primitives/components/rdf-form/RDForm.stories.ts
+++ b/src/primitives/components/rdf-form/RDForm.stories.ts
@@ -1,11 +1,17 @@
import { html } from 'lit'
import { defineStoryRender } from '../../../storybook'
+import './RDFForm'
const meta = {
title: 'Design System/RDF Form',
args: {
rdfTurtleFormatSource: `
- # A Form with 2 fields and a nested subgroup
+ @prefix : .
+@prefix dc: .
+@prefix ui: .
+@prefix vcard: .
+
+# A Form with 2 fields and a nested subgroup
:form a ui:Form;
ui:parts (:nameField :emailField :addresses) .
@@ -55,9 +61,10 @@ const meta = {
a ui:SingleLineTextField ;
ui:maxLength "128" ;
ui:property vcard:country-name ;
- ui:size "40" .`,
+ ui:size "40" .
+`,
rdfURI: 'https://solidos.solidcommunity.net/public/2021/solidUiFormTestData/dummyFormTestFile.ttl',
- whichForm: 'this',
+ whichForm: 'form',
rdfName: 'dummyFormTestFile.ttl'
},
From 35c066fe2e06edf168f54613baf6ad895013b3c6 Mon Sep 17 00:00:00 2001
From: timea-solid <4144203+timea-solid@users.noreply.github.com>
Date: Wed, 17 Jun 2026 12:31:39 +0200
Subject: [PATCH 3/3] 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]
+}