Skip to content

Commit 0a6d16b

Browse files
committed
rdf forms component
1 parent 9301716 commit 0a6d16b

3 files changed

Lines changed: 245 additions & 0 deletions

File tree

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { customElement, property, state } from 'lit/decorators.js'
2+
import { html } from 'lit/html.js'
3+
import WebComponent from '../../lib/WebComponent'
4+
import ns from '../../../lib/ns'
5+
import { loadDocument, sortBySequence } from '../../lib/rdfFormsHelper'
6+
import { sym, Namespace } from 'rdflib'
7+
import { store } from 'solid-logic'
8+
9+
@customElement('solid-ui-rdf-form')
10+
export default class RDFForm extends WebComponent {
11+
@state()
12+
private accessor _parsedUrl: URL | null = null
13+
14+
@property({ type: String })
15+
accessor whichForm = 'this'
16+
17+
@property({ type: String })
18+
accessor rdfTurtleFormatSource = ''
19+
20+
@property({ type: String })
21+
accessor rdfName = ''
22+
23+
@property({ type: String })
24+
set rdfURI (value: string) {
25+
try {
26+
this._parsedUrl = new URL(value)
27+
} catch {
28+
this._parsedUrl = null // Handle invalid URL
29+
}
30+
}
31+
32+
get rdfURI (): string {
33+
return this._parsedUrl ? this._parsedUrl.href : ''
34+
}
35+
36+
render () {
37+
// TODO: detect format
38+
loadDocument(store, this.rdfTurtleFormatSource, this.rdfName, this.rdfURI)
39+
const document = sym(this.rdfURI) // rdflib NamedNode for the document
40+
const exactForm = this.whichForm // If there are more 'a ui:Form' elements in a form file
41+
const formThis = Namespace(this.rdfURI + '#')(exactForm) // NamedNode for #this in the form
42+
console.log('formThis:', formThis.value)
43+
44+
const parts = store.each(formThis, ns.ui('parts'), null, document)
45+
const partsBySequence = sortBySequence(store, parts)
46+
const uiFields = partsBySequence.map(item => ((item as any).value || String(item)).split('#').pop())
47+
console.log('document:', document)
48+
console.log('exactForm:', exactForm)
49+
console.log('uiFields:', uiFields)
50+
51+
return html`
52+
${uiFields.map(part => {
53+
switch (part) {
54+
case 'PhoneField':
55+
case 'EmailField':
56+
case 'ColorField':
57+
case 'DateField':
58+
case 'DateTimeField':
59+
case 'TimeField':
60+
case 'NumericField':
61+
case 'IntegerField':
62+
case 'DecimalField':
63+
case 'FloatField':
64+
case 'TextField':
65+
case 'SingleLineTextField':
66+
case 'NamedNodeURIField':
67+
return html`<input rdf=${part}></input>`
68+
case 'MultiLineTextField':
69+
return html`<input rdf=${part}></input>`
70+
case 'BooleanField':
71+
return html`<input rdf=${part}></input>`
72+
case 'TristateField':
73+
return html`<input rdf=${part}></input>`
74+
case 'Classifier':
75+
return html`<input rdf=${part}></input>`
76+
case 'Choice':
77+
return html`<input rdf=${part}></input>`
78+
case 'Multiple':
79+
return html`<input rdf=${part}></input>`
80+
case 'Options':
81+
return html`<input rdf=${part}></input>`
82+
case 'AutocompleteField':
83+
return html`<input rdf=${part}></input>`
84+
case 'Comment':
85+
case 'Heading':
86+
return html`<input rdf=${part}></input>`
87+
default:
88+
return html`<div>Unknown part type: ${part}</div>`
89+
}
90+
})}
91+
`
92+
}
93+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { html } from 'lit'
2+
import { defineStoryRender } from '../../../storybook'
3+
4+
const meta = {
5+
title: 'Design System/RDF Form',
6+
args: {
7+
rdfTurtleFormatSource: `
8+
# A Form with 2 fields and a nested subgroup
9+
10+
:form a ui:Form;
11+
ui:parts (:nameField :emailField :addresses) .
12+
13+
:nameField a ui:SingleLineTextField ;
14+
ui:property vcard:fn;
15+
ui:label "name" .
16+
17+
:emailField a ui:EmailField ;
18+
ui:property vcard:hasEmail; # @@ check
19+
ui:label "email" .
20+
21+
:addresses
22+
a ui:Multiple ; # -- Allows zero or one or more
23+
ui:part :oneAddress ;
24+
ui:property vcard:hasAddress .
25+
26+
:oneAddress
27+
a ui:Group ; # A subgroup of the main form
28+
ui:parts ( :street :locality :postcode :region :country ).
29+
30+
:street
31+
a ui:SingleLineTextField ;
32+
ui:maxLength "128" ;
33+
ui:property vcard:street-address ;
34+
ui:size "40" .
35+
36+
:locality
37+
a ui:SingleLineTextField ;
38+
ui:maxLength "128" ;
39+
ui:property vcard:locality ;
40+
ui:size "40" .
41+
42+
:postcode
43+
a ui:SingleLineTextField ;
44+
ui:maxLength "25" ;
45+
ui:property vcard:postal-code ;
46+
ui:size "25" .
47+
48+
:region
49+
a ui:SingleLineTextField ;
50+
ui:maxLength "128" ;
51+
ui:property vcard:region ;
52+
ui:size "40" .
53+
54+
:country
55+
a ui:SingleLineTextField ;
56+
ui:maxLength "128" ;
57+
ui:property vcard:country-name ;
58+
ui:size "40" .`,
59+
rdfURI: 'https://solidos.solidcommunity.net/public/2021/solidUiFormTestData/dummyFormTestFile.ttl',
60+
whichForm: 'this',
61+
rdfName: 'dummyFormTestFile.ttl'
62+
},
63+
64+
argTypes: {
65+
rdfTurtleFormatSource: { control: 'text' },
66+
rdfURI: { control: 'text' },
67+
whichForm: { control: 'text' },
68+
rdfName: { control: 'text' }
69+
},
70+
} as const
71+
72+
const render = defineStoryRender<typeof meta.argTypes>(({ rdfTurtleFormatSource, rdfURI, whichForm, rdfName }) => {
73+
return html`
74+
<solid-ui-rdf-form
75+
rdfTurtleFormatSource=${rdfTurtleFormatSource}
76+
rdfURI=${rdfURI}
77+
whichForm=${whichForm}
78+
rdfName=${rdfName}>
79+
</solid-ui-rdf-form>
80+
`
81+
})
82+
83+
export default meta
84+
85+
export const Primary = { render }
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { sym, Namespace, parse } from 'rdflib'
2+
import { widgets } from 'solid-ui'
3+
import ns from '../../lib/ns'
4+
5+
const baseUri = 'https://solidos.github.io/solid-ui/src/ontology/'
6+
7+
export function renderForm (
8+
div,
9+
subject, // Represents the RDF that fills the form
10+
formSource, // The imported form Turtle source
11+
formName, // The name of the form file (e.g., 'socialMedia.ttl')
12+
store,
13+
dom,
14+
editableProfile,
15+
whichForm) {
16+
// --- Form resource setup ---
17+
const formUri = baseUri + formName // Full URI to the form file
18+
const exactForm = whichForm || 'this' // If there are more 'a ui:Form' elements in a form file
19+
const formThis = Namespace(formUri + '#')(exactForm) // NamedNode for #this in the form
20+
21+
loadDocument(store, formSource, formName, formUri)
22+
23+
widgets.appendForm(
24+
dom,
25+
div,
26+
{},
27+
subject,
28+
formThis,
29+
editableProfile,
30+
(ok, mes) => {
31+
if (!ok) widgets.errorMessageBlock(dom, mes)
32+
}
33+
)
34+
} // renderForm
35+
36+
// we need to load into the store some additional information about Social Media accounts
37+
export function loadDocument (
38+
store,
39+
documentSource,
40+
documentName,
41+
documentURI
42+
) {
43+
const finalDocumentUri = documentURI || baseUri + documentName // Full URI to the file
44+
const document = sym(finalDocumentUri) // rdflib NamedNode for the document
45+
46+
if (!store.holds(undefined, undefined, undefined, document)) {
47+
// we are using the social media form because it contains the information we need
48+
// the form can be used for both use cases: create UI for edit and render UI for display
49+
parse(documentSource, store, finalDocumentUri, 'text/turtle', () => null) // Load doc directly
50+
}
51+
}
52+
53+
export function sortBySequence (
54+
store,
55+
list
56+
) {
57+
const subfields = list.map(function (p) {
58+
const k = store.any(p, ns.ui('sequence'))
59+
return [k || 9999, p]
60+
})
61+
subfields.sort(function (a, b) {
62+
return a[0] - b[0]
63+
})
64+
return subfields.map(function (pair) {
65+
return pair[1]
66+
})
67+
}

0 commit comments

Comments
 (0)