Skip to content

Commit

Permalink
Body definition: more explicit structure, expand support to repeats…
Browse files Browse the repository at this point in the history
… and evidently partial support for outputs as well(!)

As anticipated by an earlier comment, the original view child class has been supplanted by a set of different types corresponding to their various body child usage scenarios.

While this is large, hopefully it’s relatively self-explanatory.
  • Loading branch information
eyelidlessness committed Nov 15, 2023
1 parent b976533 commit 6a82c5d
Show file tree
Hide file tree
Showing 51 changed files with 1,667 additions and 523 deletions.
41 changes: 41 additions & 0 deletions packages/common/src/lib/dom/compatibility.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { getScopeChildBySelector } from './compatibility';

describe('DOM compatibility library functions', () => {
describe('querying direct children of an element', () => {
let root: Element;
let child: Element;
let descendant: Element;

beforeEach(() => {
const domParser = new DOMParser();
const { documentElement } = domParser.parseFromString(
/* xml */ `
<root>
<child>
<descendant />
</child>
</root>
`.trim(),
'text/xml'
);

root = documentElement;
child = root.firstElementChild!;
descendant = child.firstElementChild!;
});

it('gets a direct child matching a selector', () => {
const result = getScopeChildBySelector(root, ':scope > child', 'child');

expect(result).toBe(child);
});

it('does not get a descendant matching the selector', () => {
const result = getScopeChildBySelector(root, ':scope > descendant', 'descendant');

expect(result).not.toBe(descendant);
expect(result).toBe(null);
});
});
});
54 changes: 54 additions & 0 deletions packages/common/src/lib/dom/compatibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const SUPPORTS_SCOPE_CHILD_SELECTOR = (() => {
const parent = document.createElement('parent');
const child1 = document.createElement('child1');
const child2 = document.createElement('child2');

child2.append(child1);
parent.append(child2);

return (
parent.querySelector(':scope > child1') === child1 &&
parent.querySelector(':scope > child2') !== child2
);
})();

type ScopedSelector = `:scope > ${string}`;

type UnscopedSelector<Selector extends ScopedSelector> =
Selector extends `:scope > ${infer Unscoped}` ? Unscoped : never;

type GetScopeChildBySelector = <Selector extends ScopedSelector>(
element: Element,
scopedSelector: Selector,
unscopedSelector: UnscopedSelector<Selector>
) => Element | null;

/**
* Provides compatibility for `ParentNode.querySelector(':scope > $SELECTOR')`
* in environments where the `:scope > ` prefix is ignored (e.g.
* {@link https://github.com/jsdom/jsdom/issues/3067 | jsdom}).
*
* Both the scoped and unscoped selector should be provided, to avoid a common
* deoptimization caused by producing selectors dynamically.
*/
export const getScopeChildBySelector = (() => {
if (SUPPORTS_SCOPE_CHILD_SELECTOR) {
return ((element, scopedSelector) => {
return element.querySelector(scopedSelector);
}) satisfies GetScopeChildBySelector;
// ^ Note `satisfies` here (and below) allows TypeScript to infer the
// literal type rather than referencing it by name, which is generally
// easier to work with from a call site.
}

// `scopedSelector` isn't used for this compatibility fallback, but it's
// included here as part of the inferred function signature.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return ((element, scopedSelector, unscopedSelector) => {
const children = Array.from(element.children);
const result = children.find((child) => child.matches(unscopedSelector));

return result ?? null;
}) satisfies GetScopeChildBySelector;
})();
7 changes: 7 additions & 0 deletions packages/common/src/lib/dom/predicates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const { COMMENT_NODE, ELEMENT_NODE, TEXT_NODE } = Node;

export const isCommentNode = (node: Node): node is Comment => node.nodeType === COMMENT_NODE;

export const isElementNode = (node: Node): node is Element => node.nodeType === ELEMENT_NODE;

export const isTextNode = (node: Node): node is Text => node.nodeType === TEXT_NODE;
2 changes: 1 addition & 1 deletion packages/common/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"extends": "../../tsconfig.json",
"include": ["src/**/*.ts", "test/**/*.ts", "types/**/*.ts", "vite-env.d.ts"],
"compilerOptions": {
"lib": ["ES2022"],
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?xml version="1.0"?>
<h:html xmlns="http://www.w3.org/2002/xforms"
xmlns:ev="http://www.w3.org/2001/xml-events"
xmlns:h="http://www.w3.org/1999/xhtml"
xmlns:jr="http://openrosa.org/javarosa"
xmlns:orx="http://openrosa.org/xforms/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<h:head>
<h:title>Outputs</h:title>
<model>
<instance>
<root id="outputs">
<a />
<b>default value of /root/b!</b>
<c>defualt value of c</c>
<d>10</d>
<e />
<meta>
<instanceID/>
</meta>
</root>
</instance>
<bind nodeset="/root/a" />
<bind nodeset="/root/b" />
<bind nodeset="/root/c" />
<bind nodeset="/root/d" />
<bind nodeset="/root/e" calculate="/root/d * 2" />
</model>
</h:head>
<h:body>
<input ref="/root/a">
<label>
1. Whoops, accidentally built part of output functionality!
<output value="/root/b" />
</label>
</input>
<input ref="/root/c">
<label>
2. Wonder if calculate also works...
d: <output value="/root/d" />, e (d * 2): <output value="/root/e" />
</label>
</input>
</h:body>
</h:html>
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<instance>
<root id="repeat-basic">
<rep>
<a />
<a>default value</a>
</rep>
<meta>
<instanceID/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?xml version="1.0"?>
<h:html xmlns="http://www.w3.org/2002/xforms"
xmlns:ev="http://www.w3.org/2001/xml-events"
xmlns:h="http://www.w3.org/1999/xhtml"
xmlns:jr="http://openrosa.org/javarosa"
xmlns:orx="http://openrosa.org/xforms/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<h:head>
<h:title>Repeat (grouped)</h:title>
<model>
<instance>
<root id="repeat-grouped">
<rep>
<a>default value</a>
</rep>
<meta>
<instanceID/>
</meta>
</root>
</instance>
<bind nodeset="/root/rep/a"/>
<bind nodeset="/root/meta/instanceID" type="string"/>
</model>
</h:head>
<h:body>
<group ref="/root/rep">
<label>Repeat group</label>
<repeat nodeset="/root/rep">
<input ref="/root/rep/a">
<label>Repeat input a</label>
</input>
</repeat>
</group>
</h:body>
</h:html>
7 changes: 3 additions & 4 deletions packages/odk-web-forms/src/components/Widget/TextWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { Show, createMemo, createUniqueId } from 'solid-js';
import type { XFormEntryBinding } from '../../lib/xform/XFormEntryBinding.ts';
import type { XFormViewLabel } from '../../lib/xform/XFormViewLabel.ts';
import type { InputDefinition } from '../../lib/xform/body/control/InputDefinition.ts';
import { XFormControlLabel } from '../XForm/controls/XFormControlLabel.tsx';
import { DefaultTextField } from '../styled/DefaultTextField.tsx';
import { DefaultTextFormControl } from '../styled/DefaultTextFormControl.tsx';

export interface TextWidgetProps {
readonly label: XFormViewLabel | null;
readonly ref: string | null;
readonly binding: XFormEntryBinding | null;
readonly input: InputDefinition;
}

export const TextWidget = (props: TextWidgetProps) => {
Expand All @@ -23,7 +22,7 @@ export const TextWidget = (props: TextWidgetProps) => {

return (
<DefaultTextFormControl fullWidth={true}>
<Show when={props.label} keyed={true}>
<Show when={props.input.label} keyed={true}>
{(label) => {
return <XFormControlLabel id={id} binding={binding} label={label} />;
}}
Expand Down
41 changes: 41 additions & 0 deletions packages/odk-web-forms/src/components/XForm/XFormBodyElement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Match, Switch } from 'solid-js';
import type { XFormEntry } from '../../lib/xform/XFormEntry.ts';
import {
controlElementDefinition,
groupElementDefinition,
type AnyBodyElementDefinition,
} from '../../lib/xform/body/BodyDefinition.ts';
import { XFormGroup } from './containers/XFormGroup.tsx';
import { XFormControl } from './controls/XFormControl.tsx';

interface XFormUnknownElementProps {
readonly entry: XFormEntry;
readonly element: AnyBodyElementDefinition;
}

const XFormUnknownElement = (props: XFormUnknownElementProps) => {
props;
return <></>;
};

export interface XFormBodyElementProps {
readonly entry: XFormEntry;
readonly element: AnyBodyElementDefinition;
}

export const XFormBodyElement = (props: XFormBodyElementProps) => {
return (
<Switch fallback={<XFormUnknownElement {...props} />}>
<Match when={groupElementDefinition(props.element)} keyed={true}>
{(groupElement) => {
return <XFormGroup entry={props.entry} group={groupElement} />;
}}
</Match>
<Match when={controlElementDefinition(props.element)} keyed={true}>
{(controlElement) => {
return <XFormControl entry={props.entry} control={controlElement} />;
}}
</Match>
</Switch>
);
};
43 changes: 0 additions & 43 deletions packages/odk-web-forms/src/components/XForm/XFormControl.tsx

This file was deleted.

17 changes: 13 additions & 4 deletions packages/odk-web-forms/src/components/XForm/XFormLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
import { Show } from 'solid-js';
import { For, Show, createMemo } from 'solid-js';
import type { XFormEntryBinding } from '../../lib/xform/XFormEntryBinding';
import type { XFormViewLabel } from '../../lib/xform/XFormViewLabel';
import type { LabelDefinition } from '../../lib/xform/body/text/LabelDefinition';
import { DefaultLabel } from '../styled/DefaultLabel';
import { DefaultLabelRequiredIndicator } from '../styled/DefaultLabelRequiredIndicator';

export interface XFormLabelProps {
readonly as?: 'span';
readonly binding: XFormEntryBinding;
readonly id: string;
readonly label: XFormViewLabel;
readonly label: LabelDefinition;
}

export const XFormLabel = (props: XFormLabelProps) => {
const modelElement = createMemo(() => {
return props.binding.getModelElement();
});

return (
<>
<Show when={props.binding.isRequired()}>
<DefaultLabelRequiredIndicator>* </DefaultLabelRequiredIndicator>
</Show>
<DefaultLabel as={props.as ?? 'label'} for={props.id}>
{props.label.labelText}
<For each={props.label.parts}>
{(part) => {
console.log('eval output? model el', modelElement());

Check warning on line 27 in packages/odk-web-forms/src/components/XForm/XFormLabel.tsx

View workflow job for this annotation

GitHub Actions / Lint (global) (20.8.1)

Unexpected console statement
return part.evaluate(props.binding.evaluator, modelElement());
}}
</For>
</DefaultLabel>
</>
);
Expand Down
22 changes: 22 additions & 0 deletions packages/odk-web-forms/src/components/XForm/XFormQuestionList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { For } from 'solid-js';
import type { XFormEntry } from '../../lib/xform/XFormEntry.ts';
import type { BodyElementDefinitionArray } from '../../lib/xform/body/BodyDefinition.ts';
import { XFormBodyElement } from './XFormBodyElement.tsx';
import { XFormControlStack } from './XFormControlStack.tsx';

interface XFormQuestionListProps {
readonly entry: XFormEntry;
readonly elements: BodyElementDefinitionArray;
}

export const XFormQuesetionList = (props: XFormQuestionListProps) => {
return (
<XFormControlStack>
<For each={props.elements}>
{(element) => {
return <XFormBodyElement entry={props.entry} element={element} />;
}}
</For>
</XFormControlStack>
);
};
Loading

0 comments on commit 6a82c5d

Please sign in to comment.