Skip to content

Commit f74e9ed

Browse files
authored
feat: add object and oneOf renderers to Vue Vanilla
Implements ObjectRenderer and OneOfRenderer for Vue Vanilla. Also adds CombinatorTranslations to JSON Forms core.
1 parent a0fa107 commit f74e9ed

File tree

14 files changed

+546
-5
lines changed

14 files changed

+546
-5
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
export interface CombinatorDefaultTranslation {
2+
key: CombinatorTranslationEnum;
3+
default: (variable?: string) => string;
4+
}
5+
6+
export enum CombinatorTranslationEnum {
7+
clearDialogTitle = 'clearDialogTitle',
8+
clearDialogMessage = 'clearDialogMessage',
9+
clearDialogAccept = 'clearDialogAccept',
10+
clearDialogDecline = 'clearDialogDecline',
11+
}
12+
13+
export type CombinatorTranslations = {
14+
[key in CombinatorTranslationEnum]?: string;
15+
};
16+
17+
export const combinatorDefaultTranslations: CombinatorDefaultTranslation[] = [
18+
{
19+
key: CombinatorTranslationEnum.clearDialogTitle,
20+
default: () => 'Clear form?',
21+
},
22+
{
23+
key: CombinatorTranslationEnum.clearDialogMessage,
24+
default: () => 'Your data will be cleared. Do you want to proceed?',
25+
},
26+
{ key: CombinatorTranslationEnum.clearDialogAccept, default: () => 'Yes' },
27+
{ key: CombinatorTranslationEnum.clearDialogDecline, default: () => 'No' },
28+
];

packages/core/src/i18n/i18nUtil.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import {
77
ArrayDefaultTranslation,
88
ArrayTranslations,
99
} from './arrayTranslations';
10+
import {
11+
CombinatorDefaultTranslation,
12+
CombinatorTranslations,
13+
} from './combinatorTranslations';
1014

1115
export const getI18nKeyPrefixBySchema = (
1216
schema: i18nJsonSchema | undefined,
@@ -173,3 +177,17 @@ export const getArrayTranslations = (
173177
});
174178
return translations;
175179
};
180+
181+
export const getCombinatorTranslations = (
182+
t: Translator,
183+
defaultTranslations: CombinatorDefaultTranslation[],
184+
i18nKeyPrefix: string,
185+
label: string
186+
): CombinatorTranslations => {
187+
const translations: CombinatorTranslations = {};
188+
defaultTranslations.forEach((controlElement) => {
189+
const key = addI18nKeyToPrefix(i18nKeyPrefix, controlElement.key);
190+
translations[controlElement.key] = t(key, controlElement.default(label));
191+
});
192+
return translations;
193+
};

packages/core/src/i18n/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './i18nTypes';
22
export * from './i18nUtil';
33
export * from './arrayTranslations';
4+
export * from './combinatorTranslations';

packages/core/src/util/renderer.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ import {
7070
getI18nKeyPrefixBySchema,
7171
getArrayTranslations,
7272
Translator,
73+
CombinatorTranslations,
74+
getCombinatorTranslations,
75+
combinatorDefaultTranslations,
7376
} from '../i18n';
7477
import {
7578
arrayDefaultTranslations,
@@ -970,19 +973,25 @@ export interface StatePropsOfCombinator extends StatePropsOfControl {
970973
indexOfFittingSchema: number;
971974
uischemas: JsonFormsUISchemaRegistryEntry[];
972975
data: any;
976+
translations: CombinatorTranslations;
973977
}
974978

975979
export const mapStateToCombinatorRendererProps = (
976980
state: JsonFormsState,
977981
ownProps: OwnPropsOfControl,
978982
keyword: CombinatorKeyword
979983
): StatePropsOfCombinator => {
980-
const { data, schema, rootSchema, ...props } = mapStateToControlProps(
981-
state,
982-
ownProps
983-
);
984+
const { data, schema, rootSchema, i18nKeyPrefix, label, ...props } =
985+
mapStateToControlProps(state, ownProps);
984986

985987
const ajv = state.jsonforms.core.ajv;
988+
const t = getTranslator()(state);
989+
const translations = getCombinatorTranslations(
990+
t,
991+
combinatorDefaultTranslations,
992+
i18nKeyPrefix,
993+
label
994+
);
986995
const structuralKeywords = [
987996
'required',
988997
'additionalProperties',
@@ -1025,8 +1034,11 @@ export const mapStateToCombinatorRendererProps = (
10251034
schema,
10261035
rootSchema,
10271036
...props,
1037+
i18nKeyPrefix,
1038+
label,
10281039
indexOfFittingSchema,
10291040
uischemas: getUISchemas(state),
1041+
translations,
10301042
};
10311043
};
10321044

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<template>
2+
<div v-if="control.visible">
3+
<dispatch-renderer
4+
:visible="control.visible"
5+
:enabled="control.enabled"
6+
:schema="control.schema"
7+
:uischema="detailUiSchema"
8+
:path="control.path"
9+
:renderers="control.renderers"
10+
:cells="control.cells"
11+
/>
12+
</div>
13+
</template>
14+
15+
<script lang="ts">
16+
import {
17+
JsonFormsRendererRegistryEntry,
18+
rankWith,
19+
ControlElement,
20+
Generate,
21+
GroupLayout,
22+
UISchemaElement,
23+
findUISchema,
24+
isObjectControl,
25+
} from '@jsonforms/core';
26+
import { defineComponent } from 'vue';
27+
import {
28+
DispatchRenderer,
29+
rendererProps,
30+
RendererProps,
31+
useJsonFormsControlWithDetail,
32+
} from '../../config/jsonforms';
33+
import { useVanillaControl } from '../util';
34+
import { isEmpty } from 'lodash';
35+
36+
const controlRenderer = defineComponent({
37+
name: 'ObjectRenderer',
38+
components: {
39+
DispatchRenderer,
40+
},
41+
props: {
42+
...rendererProps<ControlElement>(),
43+
},
44+
setup(props: RendererProps<ControlElement>) {
45+
const control = useVanillaControl(useJsonFormsControlWithDetail(props));
46+
return {
47+
...control,
48+
input: control,
49+
};
50+
},
51+
computed: {
52+
detailUiSchema(): UISchemaElement {
53+
const uiSchemaGenerator = () => {
54+
const uiSchema = Generate.uiSchema(this.control.schema, 'Group');
55+
if (isEmpty(this.control.path)) {
56+
uiSchema.type = 'VerticalLayout';
57+
} else {
58+
(uiSchema as GroupLayout).label = this.control.label;
59+
}
60+
return uiSchema;
61+
};
62+
63+
const result = findUISchema(
64+
this.control.uischemas,
65+
this.control.schema,
66+
this.control.uischema.scope,
67+
this.control.path,
68+
uiSchemaGenerator,
69+
this.control.uischema,
70+
this.control.rootSchema
71+
);
72+
73+
return result;
74+
},
75+
},
76+
});
77+
78+
export default controlRenderer;
79+
80+
export const entry: JsonFormsRendererRegistryEntry = {
81+
renderer: controlRenderer,
82+
tester: rankWith(2, isObjectControl),
83+
};
84+
</script>
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
<template>
2+
<div v-if="control.visible">
3+
<combinator-properties
4+
:schema="control.schema"
5+
combinator-keyword="oneOf"
6+
:path="path"
7+
/>
8+
9+
<control-wrapper
10+
v-bind="controlWrapper"
11+
:styles="styles"
12+
:is-focused="isFocused"
13+
:applied-options="appliedOptions"
14+
>
15+
<select
16+
:id="control.id + '-input'"
17+
:class="styles.control.select"
18+
:value="selectIndex"
19+
:disabled="!control.enabled"
20+
:autofocus="appliedOptions.focus"
21+
@change="handleSelectChange"
22+
@focus="isFocused = true"
23+
@blur="isFocused = false"
24+
>
25+
<option
26+
v-for="optionElement in indexedOneOfRenderInfos"
27+
:key="optionElement.index"
28+
:value="optionElement.index"
29+
:label="optionElement.label"
30+
:class="styles.control.option"
31+
></option>
32+
</select>
33+
</control-wrapper>
34+
35+
<dispatch-renderer
36+
v-if="selectedIndex !== undefined && selectedIndex !== null"
37+
:schema="indexedOneOfRenderInfos[selectedIndex].schema"
38+
:uischema="indexedOneOfRenderInfos[selectedIndex].uischema"
39+
:path="control.path"
40+
:renderers="control.renderers"
41+
:cells="control.cells"
42+
:enabled="control.enabled"
43+
/>
44+
45+
<dialog ref="dialog" :class="styles.dialog.root">
46+
<h1 :class="styles.dialog.title">
47+
{{ control.translations.clearDialogTitle }}
48+
</h1>
49+
50+
<p :class="styles.dialog.body">
51+
{{ control.translations.clearDialogMessage }}
52+
</p>
53+
54+
<div :class="styles.dialog.actions">
55+
<button :onclick="onCancel" :class="styles.dialog.buttonSecondary">
56+
{{ control.translations.clearDialogDecline }}
57+
</button>
58+
<button
59+
ref="confirm"
60+
:onclick="onConfirm"
61+
:class="styles.dialog.buttonPrimary"
62+
>
63+
{{ control.translations.clearDialogAccept }}
64+
</button>
65+
</div>
66+
</dialog>
67+
</div>
68+
</template>
69+
70+
<script lang="ts">
71+
import {
72+
CombinatorSubSchemaRenderInfo,
73+
ControlElement,
74+
createCombinatorRenderInfos,
75+
createDefaultValue,
76+
isOneOfControl,
77+
JsonFormsRendererRegistryEntry,
78+
rankWith,
79+
} from '@jsonforms/core';
80+
import {
81+
DispatchRenderer,
82+
rendererProps,
83+
RendererProps,
84+
useJsonFormsOneOfControl,
85+
} from '@jsonforms/vue';
86+
import isEmpty from 'lodash/isEmpty';
87+
import { defineComponent, nextTick, ref } from 'vue';
88+
import { useVanillaControl } from '../util';
89+
import { ControlWrapper } from '../controls';
90+
import CombinatorProperties from './components/CombinatorProperties.vue';
91+
92+
const controlRenderer = defineComponent({
93+
name: 'OneOfRenderer',
94+
components: {
95+
ControlWrapper,
96+
DispatchRenderer,
97+
CombinatorProperties,
98+
},
99+
props: {
100+
...rendererProps<ControlElement>(),
101+
},
102+
setup(props: RendererProps<ControlElement>) {
103+
const input = useJsonFormsOneOfControl(props);
104+
const control = input.control.value;
105+
106+
const selectedIndex = ref(control.indexOfFittingSchema);
107+
const selectIndex = ref(selectedIndex.value);
108+
const newSelectedIndex = ref(0);
109+
110+
const dialog = ref<HTMLDialogElement>();
111+
const confirm = ref<HTMLElement>();
112+
113+
return {
114+
...useVanillaControl(input),
115+
selectedIndex,
116+
selectIndex,
117+
newSelectedIndex,
118+
dialog,
119+
confirm,
120+
};
121+
},
122+
computed: {
123+
indexedOneOfRenderInfos(): (CombinatorSubSchemaRenderInfo & {
124+
index: number;
125+
})[] {
126+
const result = createCombinatorRenderInfos(
127+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
128+
this.control.schema.oneOf!,
129+
this.control.rootSchema,
130+
'oneOf',
131+
this.control.uischema,
132+
this.control.path,
133+
this.control.uischemas
134+
);
135+
136+
return result
137+
.filter((info) => info.uischema)
138+
.map((info, index) => ({ ...info, index: index }));
139+
},
140+
},
141+
methods: {
142+
handleSelectChange(event: Event): void {
143+
const target = event.target as any;
144+
this.selectIndex = target.value;
145+
146+
if (this.control.enabled && !isEmpty(this.control.data)) {
147+
this.showDialog();
148+
nextTick(() => {
149+
this.newSelectedIndex = this.selectIndex;
150+
// revert the selection while the dialog is open
151+
this.selectIndex = this.selectedIndex;
152+
this.confirm?.focus();
153+
});
154+
} else {
155+
nextTick(() => {
156+
this.selectedIndex = this.selectIndex;
157+
});
158+
}
159+
},
160+
showDialog(): void {
161+
this.dialog?.showModal();
162+
},
163+
closeDialog(): void {
164+
this.dialog?.close();
165+
},
166+
onConfirm(): void {
167+
this.newSelection();
168+
this.closeDialog();
169+
},
170+
onCancel(): void {
171+
this.newSelectedIndex = this.selectedIndex;
172+
this.closeDialog();
173+
},
174+
newSelection(): void {
175+
this.handleChange(
176+
this.control.path,
177+
this.newSelectedIndex !== undefined && this.newSelectedIndex !== null
178+
? createDefaultValue(
179+
this.indexedOneOfRenderInfos[this.newSelectedIndex].schema
180+
)
181+
: {}
182+
);
183+
this.selectIndex = this.newSelectedIndex;
184+
this.selectedIndex = this.newSelectedIndex;
185+
},
186+
},
187+
});
188+
189+
export default controlRenderer;
190+
191+
export const entry: JsonFormsRendererRegistryEntry = {
192+
renderer: controlRenderer,
193+
tester: rankWith(3, isOneOfControl),
194+
};
195+
</script>
196+

0 commit comments

Comments
 (0)