Skip to content

Commit

Permalink
WIP: Update ui-solid to use new client interface
Browse files Browse the repository at this point in the history
1. Reactivity doesn’t work at all (in the engine’s implementation of the interface)
2. Even if it did, something causes a stack explosion passing `createMutable` as the actual state factory

Even so, I’m reasonably certain that this is otherwise a faithful migration of the whole Solid UI to the new APIs.
  • Loading branch information
eyelidlessness committed Mar 21, 2024
1 parent a53ea43 commit 3de8af8
Show file tree
Hide file tree
Showing 26 changed files with 273 additions and 229 deletions.
14 changes: 6 additions & 8 deletions packages/ui-solid/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,27 @@
import { EntryState } from '@odk-web-forms/xforms-engine';
import type { RootNode } from '@odk-web-forms/xforms-engine';
import { Divider, Stack } from '@suid/material';
import { Show, type JSX } from 'solid-js';
import { Page } from './components/Page/Page.tsx';
import { ThemeProvider } from './components/ThemeProvider.tsx';
import { XFormDetails } from './components/XForm/XFormDetails.tsx';
import { XFormView } from './components/XForm/XFormView.tsx';

interface AppProps {
readonly extras?: JSX.Element;
readonly entry: EntryState | null;
readonly root: RootNode | null;
}

export const App = (props: AppProps) => {
return (
<ThemeProvider>
<Page entry={props.entry}>
<Page root={props.root}>
{props.extras}
<Show when={props.entry} keyed={true}>
{(entry) => {
<Show when={props.root} keyed={true}>
{(root) => {
return (
<Stack spacing={4}>
<Stack spacing={7}>
<XFormView entry={entry} />
<XFormView root={root} />
<Divider />
<XFormDetails entry={entry} />
</Stack>
</Stack>
);
Expand Down
30 changes: 17 additions & 13 deletions packages/ui-solid/src/Demo.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import { EntryState, XFormDefinition } from '@odk-web-forms/xforms-engine';
import { createEffect, createMemo, createResource, createSignal, on, untrack } from 'solid-js';
import { initializeForm } from '@odk-web-forms/xforms-engine';
import { createEffect, createMemo, createResource, createSignal, on } from 'solid-js';
import { App } from './App.tsx';
import { DemoFixturesList, type SelectedDemoFixture } from './components/Demo/DemoFixturesList.tsx';

export const Demo = () => {
const [fixture, setFixture] = createSignal<SelectedDemoFixture | null>(null);
const [fixtureSourceXML, { refetch }] = createResource(async () => {
return await Promise.resolve(fixture()?.xml);
});
const entry = createMemo(() => {
const sourceXML = fixtureSourceXML();
const [fixtureForm, { refetch }] = createResource(async () => {
const sourceXML = fixture()?.xml;

if (sourceXML == null) {
return null;
if (sourceXML != null) {
return initializeForm(sourceXML, {
config: {
stateFactory: (input) => {
return input;
},
},
});
}

const definition = new XFormDefinition(sourceXML);

return untrack(() => new EntryState(definition));
return null;
});
const entry = createMemo(() => {
return fixtureForm() ?? null;
});

createEffect(
Expand All @@ -26,5 +30,5 @@ export const Demo = () => {
})
);

return <App extras={<DemoFixturesList setDemoFixture={setFixture} />} entry={entry()} />;
return <App extras={<DemoFixturesList setDemoFixture={setFixture} />} root={entry()} />;
};
22 changes: 15 additions & 7 deletions packages/ui-solid/src/components/FormLanguageMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// TODO: lots of this should get broken out

import type { TranslationState } from '@odk-web-forms/xforms-engine';
import type { FormLanguage, RootNode } from '@odk-web-forms/xforms-engine';
import Check from '@suid/icons-material/Check';
import ExpandMore from '@suid/icons-material/ExpandMore';
import Language from '@suid/icons-material/Language';
Expand Down Expand Up @@ -30,7 +30,7 @@ const MenuItemSmallTypography = styled(Typography)({
});

interface FormLanguageMenuProps {
readonly translations: TranslationState;
readonly root: RootNode;
}

export const FormLanguageMenu = (props: FormLanguageMenuProps) => {
Expand All @@ -41,6 +41,12 @@ export const FormLanguageMenu = (props: FormLanguageMenuProps) => {
setIsOpen(false);
};

const activeLanguage = () => props.root.currentState.activeLanguage;
const formLanguages = () =>
props.root.languages.filter((language): language is FormLanguage => {
return !language.isSyntheticDefault;
});

return (
<div>
<PageMenuButton
Expand All @@ -56,7 +62,9 @@ export const FormLanguageMenu = (props: FormLanguageMenuProps) => {
>
<Stack alignItems="center" direction="row">
<FormLanguageMenuButtonIcon fontSize="small" />
<span style={{ 'line-height': 1 }}>{props.translations.getActiveLanguage()}</span>
<span style={{ 'line-height': 1 }}>
{props.root.currentState.activeLanguage.language}
</span>
<FormLanguageMenuExpandMoreIcon fontSize="small" />
</Stack>
</PageMenuButton>
Expand All @@ -81,10 +89,10 @@ export const FormLanguageMenu = (props: FormLanguageMenuProps) => {
horizontal: 'right',
}}
>
<For each={props.translations.getLanguages()}>
<For each={formLanguages()}>
{(language) => {
const isSelected = () => {
return language === props.translations.getActiveLanguage();
return language === activeLanguage();
};

return (
Expand All @@ -93,7 +101,7 @@ export const FormLanguageMenu = (props: FormLanguageMenuProps) => {
dense={true}
selected={isSelected()}
onClick={() => {
props.translations.setActiveLanguage(language);
props.root.setLanguage(language);
closeMenu();
}}
>
Expand All @@ -104,7 +112,7 @@ export const FormLanguageMenu = (props: FormLanguageMenuProps) => {
</Show>

<ListItemText inset={!isSelected()} disableTypography={true}>
<MenuItemSmallTypography>{language}</MenuItemSmallTypography>
<MenuItemSmallTypography>{language.language}</MenuItemSmallTypography>
</ListItemText>
</MenuItem>
);
Expand Down
6 changes: 3 additions & 3 deletions packages/ui-solid/src/components/Page/Page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { EntryState } from '@odk-web-forms/xforms-engine';
import type { RootNode } from '@odk-web-forms/xforms-engine';
import { GlobalStyles, Stack, useTheme } from '@suid/material';
import type { JSX } from 'solid-js';
import { PageContainer } from '../styled/PageContainer.tsx';
Expand All @@ -8,7 +8,7 @@ import { PageMain } from './PageMain.tsx';

interface PageProps {
readonly children?: JSX.Element;
readonly entry: EntryState | null;
readonly root: RootNode | null;
}

export const Page = (props: PageProps) => {
Expand Down Expand Up @@ -38,7 +38,7 @@ export const Page = (props: PageProps) => {
/>
<PageContainer>
<Stack spacing={2}>
<PageHeader entry={props.entry} />
<PageHeader root={props.root} />
<PageMain elevation={2}>{props.children}</PageMain>
<PageFooter />
</Stack>
Expand Down
10 changes: 5 additions & 5 deletions packages/ui-solid/src/components/Page/PageHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import type { EntryState } from '@odk-web-forms/xforms-engine';
import type { RootNode } from '@odk-web-forms/xforms-engine';
import { Stack } from '@suid/material';
import { Show } from 'solid-js';
import { FormLanguageMenu } from '../FormLanguageMenu.tsx';

interface PageHeaderProps {
readonly entry: EntryState | null;
readonly root: RootNode | null;
}

export const PageHeader = (props: PageHeaderProps) => {
return (
<Show when={props.entry?.translations} keyed={true}>
{(translations) => {
<Show when={props.root} keyed={true}>
{(root) => {
return (
<Stack direction="row" justifyContent="flex-end">
<FormLanguageMenu translations={translations} />
<FormLanguageMenu root={root} />
</Stack>
);
}}
Expand Down
29 changes: 17 additions & 12 deletions packages/ui-solid/src/components/Widget/MultiSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,46 @@
import type { ValueNodeState } from '@odk-web-forms/xforms-engine';
import type { SelectItem, SelectNode } from '@odk-web-forms/xforms-engine';
import { Checkbox, FormControlLabel, FormGroup } from '@suid/material';
import { createMemo, For, Show } from 'solid-js';
import { For, Show, createMemo } from 'solid-js';
import type { SelectNDefinition } from '../XForm/controls/SelectControl.tsx';
import { XFormControlLabel } from '../XForm/controls/XFormControlLabel.tsx';

export interface MultiSelectProps {
readonly control: SelectNDefinition;
readonly state: ValueNodeState;
readonly node: SelectNode;
}

export const MultiSelect = (props: MultiSelectProps) => {
const selectState = props.state.createSelect(props.control);
const isDisabled = createMemo(() => {
return props.state.isReadonly() === true || props.state.isRelevant() === false;
return props.node.currentState.readonly || !props.node.currentState.relevant;
});

const isSelected = (item: SelectItem) => {
return props.node.currentState.value.includes(item);
};

return (
<FormGroup role="group">
<Show when={props.control.label} keyed={true}>
<Show when={props.node.currentState.label} keyed={true}>
{(label) => {
return <XFormControlLabel state={props.state} label={label} />;
return <XFormControlLabel node={props.node} label={label} />;
}}
</Show>
<For each={selectState.items()}>
<For each={props.node.currentState.valueOptions}>
{(item) => {
const label = () => item.label?.asString ?? item.value;

return (
<FormControlLabel
label={item.label()}
label={label()}
disabled={isDisabled()}
control={
<Checkbox
checked={selectState.isSelected(item)}
checked={isSelected(item)}
onChange={(_, checked) => {
if (checked) {
selectState.select(item);
props.node.select(item);
} else {
selectState.deselect(item);
props.node.deselect(item);
}
}}
/>
Expand Down
48 changes: 37 additions & 11 deletions packages/ui-solid/src/components/Widget/SingleSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,64 @@
import type { ValueNodeState } from '@odk-web-forms/xforms-engine';
import type { SelectNode } from '@odk-web-forms/xforms-engine';
import { FormControlLabel, Radio, RadioGroup } from '@suid/material';
import type { ChangeEvent } from '@suid/types';
import { createMemo, For, Show } from 'solid-js';
import { For, Show, createMemo } from 'solid-js';
import type { Select1Definition } from '../XForm/controls/SelectControl.tsx';
import { XFormControlLabel } from '../XForm/controls/XFormControlLabel.tsx';

export interface SingleSelectProps {
readonly control: Select1Definition;
readonly state: ValueNodeState;
readonly node: SelectNode;
}

export const SingleSelect = (props: SingleSelectProps) => {
const selectState = props.state.createSelect(props.control);
const isDisabled = createMemo(() => {
return props.state.isReadonly() === true || props.state.isRelevant() === false;
return props.node.currentState.readonly || !props.node.currentState.relevant;
});
const selectedItem = () => {
const [item] = props.node.currentState.value;

return item;
};
const getItem = (value: string) => {
return props.node.currentState.valueOptions.find((item) => {
return item.value === value;
});
};
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
selectState.setValue(event.target.value);
const item = getItem(event.target.value);

if (item == null) {
const currentItem = selectedItem();

if (currentItem != null) {
props.node.deselect(currentItem);
}
} else {
props.node.select(item);
}
};
const value = () => {
const [item] = props.node.currentState.value;

return item?.value ?? null;
};

return (
<RadioGroup name={props.state.reference} value={props.state.getValue()} onChange={handleChange}>
<Show when={props.control.label} keyed={true}>
<RadioGroup name={props.node.currentState.reference} value={value()} onChange={handleChange}>
<Show when={props.node.currentState.label} keyed={true}>
{(label) => {
return <XFormControlLabel state={props.state} label={label} />;
return <XFormControlLabel node={props.node} label={label} />;
}}
</Show>
<For each={selectState.items()}>
<For each={props.node.currentState.valueOptions}>
{(item) => {
const label = () => item.label?.asString ?? item.value;

return (
<FormControlLabel
value={item.value}
control={<Radio />}
label={item.label()}
label={label()}
disabled={isDisabled()}
/>
);
Expand Down
19 changes: 9 additions & 10 deletions packages/ui-solid/src/components/Widget/TextWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
import type { InputDefinition, ValueNodeState } from '@odk-web-forms/xforms-engine';
import type { StringNode } from '@odk-web-forms/xforms-engine';
import { Show, createMemo } from 'solid-js';
import { XFormControlLabel } from '../XForm/controls/XFormControlLabel.tsx';
import { DefaultTextField } from '../styled/DefaultTextField.tsx';
import { DefaultTextFormControl } from '../styled/DefaultTextFormControl.tsx';

export interface TextWidgetProps {
readonly control: InputDefinition;
readonly state: ValueNodeState;
readonly node: StringNode;
}

export const TextWidget = (props: TextWidgetProps) => {
const isDisabled = createMemo(() => {
return props.state.isReadonly() === true || props.state.isRelevant() === false;
return props.node.currentState.readonly || !props.node.currentState.relevant;
});

return (
<DefaultTextFormControl fullWidth={true}>
<Show when={props.control.label} keyed={true}>
<Show when={props.node.currentState.label} keyed={true}>
{(label) => {
return <XFormControlLabel state={props.state} label={label} />;
return <XFormControlLabel node={props.node} label={label} />;
}}
</Show>
<DefaultTextField
id={props.state.reference}
value={props.state.getValue()}
id={props.node.currentState.reference}
value={props.node.currentState.value}
onChange={(event) => {
props.state.setValue(event.target.value);
props.node.setValue(event.target.value);
}}
disabled={isDisabled()}
inputProps={{
disabled: isDisabled(),
readonly: isDisabled(),
required: props.state.isRequired() ?? false,
required: props.node.currentState.required ?? false,
}}
/>
</DefaultTextFormControl>
Expand Down
Loading

0 comments on commit 3de8af8

Please sign in to comment.