-
Notifications
You must be signed in to change notification settings - Fork 46
Initial RDF forms component #798
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
Changes from all commits
0a6d16b
7c5a7c3
35c066f
a20fa4f
5cd82c0
344b904
9d5565a
0656653
84345df
372135f
638786e
235b0d0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
|
|
||
| @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) { | ||
| 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 | ||
| } | ||
| } | ||
|
|
||
| get subjectURI (): string { | ||
| return this._parsedUrl2 ? this._parsedUrl2.href : '' | ||
| } | ||
|
|
||
| render () { | ||
| // TODO: detect format | ||
| loadDocument(store, this.rdfTurtleFormatSource, this.rdfName, this.rdfURI) // load form | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of using solid-logic's store directly, maybe we can encapsulate this into the Not sure if we want to couple both concepts though (store and Auth), but maybe we could given that they are already coupled in the solid-logic implementation (I think). This would also allow us to improve the storybook stories so that they don't require solid-logic. Right now, it seems like we're passing the full RDF graph to the component, but that shouldn't be necessary (I suspect that's a workaround because solid-logic doesn't work in storybook). Check out how the authentication is configured in storybook and elsewhere, and make sure to learn about Lit context. |
||
| 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` | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Other than the inputs, shouldn't this output a native |
||
| ${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} | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think all inputs should also have a |
||
| ></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>` | ||
| } | ||
| })} | ||
| ` | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| import { html } from 'lit' | ||
| import { defineStoryRender } from '../../storybook' | ||
| import './RDFForm' | ||
|
|
||
| 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 on lines
+102
to
+108
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need all these attributes? This is a bit confusing, I'm not sure what each property does and why we need so many. Could it work with something like this? <solid-ui-rdf-form
form="https://solidos.solidcommunity.net/public/2021/solidUiFormTestData/dummyFormTestFile.ttl#form"
subject="https://solidos.solidcommunity.net/public/2021/alice.ttl#me"
>
</solid-ui-rdf-form>Internally, it could resolve the form and subject from the store associated to the authenticated user. |
||
| </solid-ui-rdf-form> | ||
| ` | ||
| }) | ||
|
|
||
| export default meta | ||
|
|
||
| export const Primary = { render } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| import RDFForm from './RDFForm' | ||
|
|
||
| export { RDFForm } | ||
| export default RDFForm |
| 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 on lines
+21
to
+22
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of passing the store as a property, I think we should use Lit context instead. See my other comments talking about this. |
||
|
|
||
| @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>` : ''} | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This label won't be associated with the input, given that it doesn't have a <label>
Name:
<input name="name">
</label>I know it looks counterintuitive, but I'm pretty sure this is correct HTML and I've used it many times :). In any case, the |
||
| <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, '') | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| import RDFInput from './RDFInput' | ||
|
|
||
| export { RDFInput } | ||
| export default RDFInput |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think these two propertie (rdfURI and subjectURI) can probably be simplified a lot, I'm not sure we need the
_parsedUrlvariables or duplicating the URL parsing code. Check out Lit converters.