Skip to content

Commit

Permalink
web-forms (Vue UI): integrate value type suport for selects
Browse files Browse the repository at this point in the history
Note that this doesn’t _currently_ do anything special for individual value types! I think that’s expected for the subset of value types we presently implement (which are all represented as primitive values, and all presentable in the current UI controls the same way string values have been). That will necessarily change as we add support for more complex value type representations (e.g. geo).
  • Loading branch information
eyelidlessness committed Jan 14, 2025
1 parent 0f668a4 commit 4c94f85
Show file tree
Hide file tree
Showing 13 changed files with 268 additions and 63 deletions.
187 changes: 187 additions & 0 deletions packages/common/src/fixtures/select/5-select-types.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<?xml version="1.0"?>
<h:html xmlns="http://www.w3.org/2002/xforms" xmlns:h="http://www.w3.org/1999/xhtml"
xmlns:jr="http://openrosa.org/javarosa" xmlns:odk="http://www.opendatakit.org/xforms"
xmlns:orx="http://openrosa.org/xforms">
<h:head>
<h:title>Select types</h:title>
<model>
<instance>
<root id="select-types">
<select-relevance>yes</select-relevance>

<sel-1>
<string-value>explicit string</string-value>
<implicit-string-value>implicit string</implicit-string-value>
<int-value>123</int-value>
<decimal-value>45.67</decimal-value>
</sel-1>

<sel-n>
<string-value>explicit string</string-value>
<implicit-string-value>implicit string</implicit-string-value>
<int-value>123</int-value>
<decimal-value>45.67</decimal-value>
</sel-n>
</root>
</instance>
<instance id="strings">
<root>
<!-- for select1 -->
<item>
<value>implicit string</value>
<label>Implicit String</label>
</item>
<item>
<value>explicit string</value>
<label>Explicit String</label>
</item>
<item>
<value>updated string</value>
<label>Updated String</label>
</item>

<!-- for select (multiple) -->
<item>
<value>implicit</value>
<label>Implicit</label>
</item>
<item>
<value>explicit</value>
<label>Explicit</label>
</item>
<item>
<value>updated</value>
<label>Updated</label>
</item>
<item>
<value>string</value>
<label>string</label>
</item>
</root>
</instance>
<instance id="ints">
<root>
<item>
<value>123</value>
<label>123</label>
</item>
<item>
<value>10</value>
<label>10</label>
</item>
<item>
<value>23</value>
<label>23</label>
</item>
<item>
<value>89</value>
<label>89</label>
</item>
</root>
</instance>
<instance id="decimals">
<root>
<item>
<value>45.67</value>
<label>45.67</label>
</item>
<item>
<value>89</value>
<label>89</label>
</item>
<item>
<value>10</value>
<label>10</label>
</item>
<item>
<value>23.4</value>
<label>23.4</label>
</item>
</root>
</instance>
<bind nodeset="/root/sel-1/string-value" type="string"
relevant="/root/select-relevance = 'yes'" />
<bind nodeset="/root/sel-1/implicit-string-value" relevant="/root/select-relevance = 'yes'" />
<bind nodeset="/root/sel-1/int-value" type="int" relevant="/root/select-relevance = 'yes'" />
<bind nodeset="/root/sel-1/decimal-value" type="decimal"
relevant="/root/select-relevance = 'yes'" />
<bind nodeset="/root/sel-n/string-value" type="string"
relevant="/root/select-relevance = 'yes'" />
<bind nodeset="/root/sel-n/implicit-string-value" relevant="/root/select-relevance = 'yes'" />
<bind nodeset="/root/sel-n/int-value" type="int" relevant="/root/select-relevance = 'yes'" />
<bind nodeset="/root/sel-n/decimal-value" type="decimal"
relevant="/root/select-relevance = 'yes'" />
</model>
</h:head>
<h:body>
<select1 ref="/root/select-relevance">
<label>All selects relevant?</label>
<item>
<label>Yes</label>
<value>yes</value>
</item>
<item>
<label>No</label>
<value>no</value>
</item>
</select1>

<group ref="/root/sel-1">
<label>Select (1) with types</label>

<select1 ref="/root/sel-1/string-value">
<itemset nodeset="instance('strings')/root/item">
<value ref="value" />
<label ref="label" />
</itemset>
</select1>
<select1 ref="/root/sel-1/implicit-string-value">
<itemset nodeset="instance('strings')/root/item">
<value ref="value" />
<label ref="label" />
</itemset>
</select1>
<select1 ref="/root/sel-1/int-value">
<itemset nodeset="instance('ints')/root/item">
<value ref="value" />
<label ref="label" />
</itemset>
</select1>
<select1 ref="/root/sel-1/decimal-value">
<itemset nodeset="instance('decimals')/root/item">
<value ref="value" />
<label ref="label" />
</itemset>
</select1>
</group>

<group ref="/root/sel-n">
<label>Select (N) with types</label>

<select ref="/root/sel-n/string-value">
<itemset nodeset="instance('strings')/root/item[not(contains(value, ' '))]">
<value ref="value" />
<label ref="label" />
</itemset>
</select>
<select ref="/root/sel-n/implicit-string-value">
<itemset nodeset="instance('strings')/root/item[not(contains(value, ' '))]">
<value ref="value" />
<label ref="label" />
</itemset>
</select>
<select ref="/root/sel-n/int-value">
<itemset nodeset="instance('ints')/root/item">
<value ref="value" />
<label ref="label" />
</itemset>
</select>
<select ref="/root/sel-n/decimal-value">
<itemset nodeset="instance('decimals')/root/item">
<value ref="value" />
<label ref="label" />
</itemset>
</select>
</group>
</h:body>
</h:html>
4 changes: 2 additions & 2 deletions packages/web-forms/src/components/FormQuestion.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type {
AnyControlNode,
AnyInputNode,
AnyNoteNode,
AnySelectNode,
AnyUnsupportedControlNode,
SelectNode,
} from '@getodk/xforms-engine';
import { inject } from 'vue';
import InputControl from './controls/Input/InputControl.vue';
Expand All @@ -19,7 +19,7 @@ type ControlNode = AnyControlNode | AnyUnsupportedControlNode;
defineProps<{ question: ControlNode }>();
const isInputNode = (node: ControlNode): node is AnyInputNode => node.nodeType === 'input';
const isSelectNode = (node: ControlNode): node is SelectNode => node.nodeType === 'select';
const isSelectNode = (node: ControlNode): node is AnySelectNode => node.nodeType === 'select';
const isNoteNode = (node: ControlNode): node is AnyNoteNode => node.nodeType === 'note';
const isRangeNode = (node: ControlNode) => node.nodeType === 'range';
const isTriggerNode = (node: ControlNode) => node.nodeType === 'trigger';
Expand Down
8 changes: 6 additions & 2 deletions packages/web-forms/src/components/controls/Select1Control.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { SelectNode } from '@getodk/xforms-engine';
import type { AnySelectNode } from '@getodk/xforms-engine';
import { inject, ref } from 'vue';
import ControlText from '../ControlText.vue';
import ValidationMessage from '../ValidationMessage.vue';
Expand All @@ -10,7 +10,11 @@ import RadioButton from '../widgets/RadioButton.vue';
import SearchableDropdown from '../widgets/SearchableDropdown.vue';
import UnsupportedAppearance from './UnsupportedAppearance.vue';
const props = defineProps<{ question: SelectNode }>();
interface Select1ControlProps {
readonly question: AnySelectNode;
}
const props = defineProps<Select1ControlProps>();
const hasColumnsAppearance =
[...props.question.appearances].filter((a) => a.startsWith('columns')).length > 0;
Expand Down
8 changes: 6 additions & 2 deletions packages/web-forms/src/components/controls/SelectControl.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
<script setup lang="ts">
import type { SelectNode } from '@getodk/xforms-engine';
import type { AnySelectNode } from '@getodk/xforms-engine';
import Select1Control from './Select1Control.vue';
import SelectNControl from './SelectNControl.vue';
defineProps<{ question: SelectNode }>();
interface SelectControlProps {
readonly question: AnySelectNode;
}
defineProps<SelectControlProps>();
</script>

<template>
Expand Down
8 changes: 6 additions & 2 deletions packages/web-forms/src/components/controls/SelectNControl.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { SelectNode } from '@getodk/xforms-engine';
import type { AnySelectNode } from '@getodk/xforms-engine';
import { inject, ref } from 'vue';
import ControlText from '../ControlText.vue';
import ValidationMessage from '../ValidationMessage.vue';
Expand All @@ -9,7 +9,11 @@ import CheckboxWidget from '../widgets/CheckboxWidget.vue';
import MultiselectDropdown from '../widgets/MultiselectDropdown.vue';
import UnsupportedAppearance from './UnsupportedAppearance.vue';
const props = defineProps<{ question: SelectNode }>();
interface SelectNControlProps {
readonly question: AnySelectNode;
}
const props = defineProps<SelectNControlProps>();
const hasColumnsAppearance =
[...props.question.appearances].filter((a) => a.startsWith('columns')).length > 0;
Expand Down
23 changes: 11 additions & 12 deletions packages/web-forms/src/components/widgets/CheckboxWidget.vue
Original file line number Diff line number Diff line change
@@ -1,34 +1,33 @@
<script lang="ts" setup>
import type { SelectNode } from '@getodk/xforms-engine';
<script lang="ts" setup generic="V extends ValueType">
import { selectOptionId } from '@/lib/format/selectOptionId.ts';
import type { SelectNode, SelectValues, ValueType } from '@getodk/xforms-engine';
import PrimeCheckbox from 'primevue/checkbox';
interface CheckboxWidgetProps {
readonly question: SelectNode;
const props = defineProps<{
readonly question: SelectNode<V>;
readonly style?: string;
}
const props = defineProps<CheckboxWidgetProps>();
}>();
defineEmits(['update:modelValue', 'change']);
const selectValues = (values: readonly string[]) => {
const selectValues = (values: SelectValues<V>) => {
props.question.selectValues(values);
};
</script>

<template>
<label
v-for="option of question.currentState.valueOptions"
:key="option.value"
:key="option.asString"
:class="[{
'value-option': true,
active: question.currentState.value.find((value) => value === option.value),
active: question.isSelected(option.value),
disabled: question.currentState.readonly,
'no-buttons': question.appearances['no-buttons'] }]"
:for="question.nodeId + '_' + option.value"
:for="selectOptionId(question, option)"
>
<PrimeCheckbox
:input-id="question.nodeId + '_' + option.value"
:input-id="selectOptionId(question, option)"
:name="question.nodeId"
:value="option.value"
:disabled="question.currentState.readonly"
Expand Down
8 changes: 6 additions & 2 deletions packages/web-forms/src/components/widgets/LikertWidget.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
<script setup lang="ts">
import type { SelectNode } from '@getodk/xforms-engine';
import type { AnySelectNode } from '@getodk/xforms-engine';
import RadioButton from '../widgets/RadioButton.vue';
defineProps<{ question: SelectNode }>();
interface LikertWidgetProps {
readonly question: AnySelectNode;
}
defineProps<LikertWidgetProps>();
defineEmits(['change']);
</script>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
<script lang="ts" setup>
import type { SelectNode } from '@getodk/xforms-engine';
<script lang="ts" setup generic="V extends ValueType">
import type { SelectItemValue, SelectNode, SelectValues, ValueType } from '@getodk/xforms-engine';
import PrimeMultiSelect from 'primevue/multiselect';
import { computed } from 'vue';
interface MultiselectDropdownProps {
readonly question: SelectNode;
const props = defineProps<{
readonly question: SelectNode<V>;
readonly style?: string;
}
const props = defineProps<MultiselectDropdownProps>();
}>();
defineEmits(['update:modelValue', 'change']);
const options = computed(() => {
return props.question.currentState.valueOptions.map((option) => option.value);
});
const selectValues = (values: readonly string[]) => {
const selectValues = (values: SelectValues<V>) => {
props.question.selectValues(values);
};
const getOptionLabel = (value: string) => {
const getOptionLabel = (value: SelectItemValue<V>) => {
const option = props.question.getValueOption(value);
if (option == null) {
Expand Down
Loading

0 comments on commit 4c94f85

Please sign in to comment.