Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0a6d16b
rdf forms component
timea-solid Jun 16, 2026
7c5a7c3
rendered first rdf forms elements
timea-solid Jun 16, 2026
35c066f
preliminary work for rdf-input
timea-solid Jun 17, 2026
a20fa4f
Merge branch 'staging' into rdfComponets
timea-solid Jun 22, 2026
5cd82c0
merge staging and moved components
timea-solid Jun 22, 2026
344b904
added also data to forms and wired the rdf input
timea-solid Jun 22, 2026
9d5565a
fixed rdf input
timea-solid Jun 22, 2026
0656653
Refactor for better readability
timea-solid Jun 22, 2026
84345df
fix types
timea-solid Jun 23, 2026
372135f
reverted the sortBySequence code back to original
timea-solid Jun 23, 2026
638786e
Merge branch 'staging' into rdfComponets
timea-solid Jun 24, 2026
235b0d0
vite config for generating custom elements tag name map
timea-solid Jun 24, 2026
da5c6fa
Merge branch 'staging' into rdfComponets
timea-solid Jun 25, 2026
35f4445
a first FormsContext with a default store from SolidLogic
timea-solid Jun 29, 2026
0d99a1b
added placeholder property to input
timea-solid Jun 29, 2026
7b7f9a9
added readonly to Input and used it in RDFinput
timea-solid Jun 29, 2026
65735ae
improved store usage
timea-solid Jun 29, 2026
cfb0475
added readonly style to input
timea-solid Jun 29, 2026
c81b17e
added save features, copied over code
timea-solid Jun 29, 2026
082b9ee
added an updater to the storybook store
timea-solid Jun 30, 2026
2b53d6c
added data change capabilities, 1st version
timea-solid Jun 30, 2026
403a143
Update src/components/input/Input.styles.css
timea-solid Jun 30, 2026
0af9ce9
rename to NoopStore
timea-solid Jun 30, 2026
9d9f48a
renamed NoopStore
timea-solid Jun 30, 2026
3da4992
cleanup from feedback
timea-solid Jun 30, 2026
643dc5a
imporved component type declarations
timea-solid Jun 30, 2026
d40ac8b
merge staging
timea-solid Jun 30, 2026
b076703
Addeda URL lit converter
timea-solid Jul 2, 2026
242180a
simplify components properties
timea-solid Jul 2, 2026
c410374
merge staging
timea-solid Jul 2, 2026
5fdb7bc
random name attribute
timea-solid Jul 2, 2026
bfeed32
rework logic of readonly
timea-solid Jul 2, 2026
eba9a70
use document instead of uri with fragment
timea-solid Jul 2, 2026
c9c7cf3
fixed statement insert
timea-solid Jul 2, 2026
a0eaae5
decode defensively
timea-solid Jul 2, 2026
d8604c6
small copilot suggested improvments
timea-solid Jul 2, 2026
6280cb4
chnaged from readonly to disabled on select
timea-solid Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions src/components/rdf-form/RDFForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { property, state } from 'lit/decorators.js'
import { html } from 'lit/html.js'
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'

@customElement('solid-ui-rdf-form')
export default class RDFForm extends WebComponent {
@state()
private accessor _parsedUrl: URL | null = null
Comment thread
timea-solid marked this conversation as resolved.
Outdated

@state()
private accessor _parsedUrl2: 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) {
Comment thread
timea-solid marked this conversation as resolved.
Outdated
try {
this._parsedUrl = new URL(value)
} catch {
this._parsedUrl = null // Handle invalid URL
}
}

get rdfURI (): string {
return this._parsedUrl ? this._parsedUrl.href : ''
}

@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
}
Comment on lines +66 to +71
}

get subjectURI (): string {
return this._parsedUrl2 ? this._parsedUrl2.href : ''
}

render () {
// TODO: detect format
loadDocument(store, this.rdfTurtleFormatSource, this.rdfName, this.rdfURI) // load form
Comment thread
timea-solid marked this conversation as resolved.
Outdated
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 = 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
}
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 {
value: item,
fieldValue: hashIndex >= 0 ? value.slice(hashIndex + 1) : value
}
})
const me = Namespace(this.subjectURI + '#')(this.whichSubject)

return html`
Comment thread
timea-solid marked this conversation as resolved.
${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` <solid-ui-rdf-input
.store=${store}
.formSubject=${sym(part.value)}
.dataSubject=${me}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all inputs should also have a name attribute. Otherwise, they'll be ignored by the form (again, important for a11y, etc.).

></solid-ui-rdf-input>`
}
case 'MultiLineTextField':
return html`<input .rdf=${part}></input>`
case 'BooleanField':
return html`<input .rdf=${part}></input>`
case 'TristateField':
return html`<input .rdf=${part}></input>`
case 'Classifier':
return html`<input .rdf=${part}></input>`
case 'Choice':
return html`<input .rdf=${part}></input>`
case 'Multiple':
return html`<input .rdf=${part}></input>`
case 'Options':
return html`<input .rdf=${part}></input>`
case 'AutocompleteField':
return html`<input .rdf=${part}></input>`
case 'Comment':
case 'Heading':
return html`<input .rdf=${part}></input>`
default:
return html`<div>Unknown part type: ${part}</div>`
}
})}
`
}
}
115 changes: 115 additions & 0 deletions src/components/rdf-form/RDForm.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { html } from 'lit'
import { defineStoryRender } from '../../storybook'
import './RDFForm'
Comment thread
timea-solid marked this conversation as resolved.

const meta = {
title: 'Design System/RDF Form',
args: {
rdfTurtleFormatSource: `
@prefix : <https://solidos.solidcommunity.net/public/2021/solidUiFormTestData/dummyFormTestFile.ttl#>.
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
@prefix sched: <http://www.w3.org/ns/pim/schedule#>.
@prefix cal: <http://www.w3.org/2002/12/cal/ical#>.
@prefix dc: <http://purl.org/dc/elements/1.1/>.
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
@prefix ui: <http://www.w3.org/ns/ui#>.
@prefix trip: <http://www.w3.org/ns/pim/trip#>.
@prefix vcard: <http://www.w3.org/2006/vcard/ns#>.
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.

# 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', // we need a working URL
whichForm: 'form',
rdfName: 'dummyFormTestFile.ttl',
whichSubject: 'me',
subjectTurtleFormatSource: `
@prefix : <https://solidos.solidcommunity.net/public/2021/alice.ttl#>.
@prefix vcard: <http://www.w3.org/2006/vcard/ns#>.

:me a vcard:Individual ;
vcard:fn "Alice" ;
vcard:hasEmail <mailto:alice@example.com> .
`,
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' },
subjectTurtleFormatSource: { control: 'text' },
subjectName: { control: 'text' },
subjectURI: { control: 'text' }
},
} as const

const render = defineStoryRender<typeof meta.argTypes>(({ rdfTurtleFormatSource, rdfURI, whichForm, rdfName, subjectTurtleFormatSource, subjectName, subjectURI }) => {
return html`
<solid-ui-rdf-form
rdfTurtleFormatSource=${rdfTurtleFormatSource}
rdfURI=${rdfURI}
whichForm=${whichForm}
rdfName=${rdfName}
subjectTurtleFormatSource=${subjectTurtleFormatSource}
subjectName=${subjectName}
subjectURI=${subjectURI}>
Comment thread
timea-solid marked this conversation as resolved.
Outdated
</solid-ui-rdf-form>
`
})

export default meta

export const Primary = { render }
4 changes: 4 additions & 0 deletions src/components/rdf-form/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import RDFForm from './RDFForm'

export { RDFForm }
export default RDFForm
100 changes: 100 additions & 0 deletions src/components/rdf-input/RDFInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
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 { 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'

@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" .

// formSubject describes the field metadata
// dataSubject points to the data resource containing the value

@property({ attribute: false })
accessor store!: LiveStore
Comment thread
timea-solid marked this conversation as resolved.
Outdated

@property({ attribute: false, type: Object })
accessor formSubject!: NamedNode

@property({ attribute: false, type: Object })
accessor dataSubject!: NamedNode

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 fieldType = this.formSubject ? mostSpecificClassURI(this.store, this.formSubject) : undefined
const params = fieldType ? fieldTypeParams[fieldType] ?? {} : {}
const inputType: InputType = params.type ?? 'text'

// 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`<label>${inputLabel}</label>` : ''}
Comment thread
timea-solid marked this conversation as resolved.
Outdated
<input type=${inputType} value=${ifDefined(inputValue)} ?readonly=${readonly}>
`
}

private getFormGraph (subject?: NamedNode) {
return subject?.doc ? subject.doc() : undefined
}

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 (!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, '')
}
}
4 changes: 4 additions & 0 deletions src/components/rdf-input/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import RDFInput from './RDFInput'

export { RDFInput }
export default RDFInput
Loading
Loading