Skip to content

Commit

Permalink
feat: add object and oneOf renderers to Vue Vanilla
Browse files Browse the repository at this point in the history
Implements ObjectRenderer and OneOfRenderer for Vue Vanilla. Also adds
CombinatorTranslations to JSON Forms core.
  • Loading branch information
butzist authored Jul 7, 2023
1 parent a0fa107 commit f74e9ed
Show file tree
Hide file tree
Showing 14 changed files with 546 additions and 5 deletions.
28 changes: 28 additions & 0 deletions packages/core/src/i18n/combinatorTranslations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export interface CombinatorDefaultTranslation {
key: CombinatorTranslationEnum;
default: (variable?: string) => string;
}

export enum CombinatorTranslationEnum {
clearDialogTitle = 'clearDialogTitle',
clearDialogMessage = 'clearDialogMessage',
clearDialogAccept = 'clearDialogAccept',
clearDialogDecline = 'clearDialogDecline',
}

export type CombinatorTranslations = {
[key in CombinatorTranslationEnum]?: string;
};

export const combinatorDefaultTranslations: CombinatorDefaultTranslation[] = [
{
key: CombinatorTranslationEnum.clearDialogTitle,
default: () => 'Clear form?',
},
{
key: CombinatorTranslationEnum.clearDialogMessage,
default: () => 'Your data will be cleared. Do you want to proceed?',
},
{ key: CombinatorTranslationEnum.clearDialogAccept, default: () => 'Yes' },
{ key: CombinatorTranslationEnum.clearDialogDecline, default: () => 'No' },
];
18 changes: 18 additions & 0 deletions packages/core/src/i18n/i18nUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import {
ArrayDefaultTranslation,
ArrayTranslations,
} from './arrayTranslations';
import {
CombinatorDefaultTranslation,
CombinatorTranslations,
} from './combinatorTranslations';

export const getI18nKeyPrefixBySchema = (
schema: i18nJsonSchema | undefined,
Expand Down Expand Up @@ -173,3 +177,17 @@ export const getArrayTranslations = (
});
return translations;
};

export const getCombinatorTranslations = (
t: Translator,
defaultTranslations: CombinatorDefaultTranslation[],
i18nKeyPrefix: string,
label: string
): CombinatorTranslations => {
const translations: CombinatorTranslations = {};
defaultTranslations.forEach((controlElement) => {
const key = addI18nKeyToPrefix(i18nKeyPrefix, controlElement.key);
translations[controlElement.key] = t(key, controlElement.default(label));
});
return translations;
};
1 change: 1 addition & 0 deletions packages/core/src/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './i18nTypes';
export * from './i18nUtil';
export * from './arrayTranslations';
export * from './combinatorTranslations';
20 changes: 16 additions & 4 deletions packages/core/src/util/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ import {
getI18nKeyPrefixBySchema,
getArrayTranslations,
Translator,
CombinatorTranslations,
getCombinatorTranslations,
combinatorDefaultTranslations,
} from '../i18n';
import {
arrayDefaultTranslations,
Expand Down Expand Up @@ -970,19 +973,25 @@ export interface StatePropsOfCombinator extends StatePropsOfControl {
indexOfFittingSchema: number;
uischemas: JsonFormsUISchemaRegistryEntry[];
data: any;
translations: CombinatorTranslations;
}

export const mapStateToCombinatorRendererProps = (
state: JsonFormsState,
ownProps: OwnPropsOfControl,
keyword: CombinatorKeyword
): StatePropsOfCombinator => {
const { data, schema, rootSchema, ...props } = mapStateToControlProps(
state,
ownProps
);
const { data, schema, rootSchema, i18nKeyPrefix, label, ...props } =
mapStateToControlProps(state, ownProps);

const ajv = state.jsonforms.core.ajv;
const t = getTranslator()(state);
const translations = getCombinatorTranslations(
t,
combinatorDefaultTranslations,
i18nKeyPrefix,
label
);
const structuralKeywords = [
'required',
'additionalProperties',
Expand Down Expand Up @@ -1025,8 +1034,11 @@ export const mapStateToCombinatorRendererProps = (
schema,
rootSchema,
...props,
i18nKeyPrefix,
label,
indexOfFittingSchema,
uischemas: getUISchemas(state),
translations,
};
};

Expand Down
84 changes: 84 additions & 0 deletions packages/vue/vue-vanilla/src/complex/ObjectRenderer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<template>
<div v-if="control.visible">
<dispatch-renderer
:visible="control.visible"
:enabled="control.enabled"
:schema="control.schema"
:uischema="detailUiSchema"
:path="control.path"
:renderers="control.renderers"
:cells="control.cells"
/>
</div>
</template>

<script lang="ts">
import {
JsonFormsRendererRegistryEntry,
rankWith,
ControlElement,
Generate,
GroupLayout,
UISchemaElement,
findUISchema,
isObjectControl,
} from '@jsonforms/core';
import { defineComponent } from 'vue';
import {
DispatchRenderer,
rendererProps,
RendererProps,
useJsonFormsControlWithDetail,
} from '../../config/jsonforms';
import { useVanillaControl } from '../util';
import { isEmpty } from 'lodash';
const controlRenderer = defineComponent({
name: 'ObjectRenderer',
components: {
DispatchRenderer,
},
props: {
...rendererProps<ControlElement>(),
},
setup(props: RendererProps<ControlElement>) {
const control = useVanillaControl(useJsonFormsControlWithDetail(props));
return {
...control,
input: control,
};
},
computed: {
detailUiSchema(): UISchemaElement {
const uiSchemaGenerator = () => {
const uiSchema = Generate.uiSchema(this.control.schema, 'Group');
if (isEmpty(this.control.path)) {
uiSchema.type = 'VerticalLayout';
} else {
(uiSchema as GroupLayout).label = this.control.label;
}
return uiSchema;
};
const result = findUISchema(
this.control.uischemas,
this.control.schema,
this.control.uischema.scope,
this.control.path,
uiSchemaGenerator,
this.control.uischema,
this.control.rootSchema
);
return result;
},
},
});
export default controlRenderer;
export const entry: JsonFormsRendererRegistryEntry = {
renderer: controlRenderer,
tester: rankWith(2, isObjectControl),
};
</script>
196 changes: 196 additions & 0 deletions packages/vue/vue-vanilla/src/complex/OneOfRenderer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
<template>
<div v-if="control.visible">
<combinator-properties
:schema="control.schema"
combinator-keyword="oneOf"
:path="path"
/>

<control-wrapper
v-bind="controlWrapper"
:styles="styles"
:is-focused="isFocused"
:applied-options="appliedOptions"
>
<select
:id="control.id + '-input'"
:class="styles.control.select"
:value="selectIndex"
:disabled="!control.enabled"
:autofocus="appliedOptions.focus"
@change="handleSelectChange"
@focus="isFocused = true"
@blur="isFocused = false"
>
<option
v-for="optionElement in indexedOneOfRenderInfos"
:key="optionElement.index"
:value="optionElement.index"
:label="optionElement.label"
:class="styles.control.option"
></option>
</select>
</control-wrapper>

<dispatch-renderer
v-if="selectedIndex !== undefined && selectedIndex !== null"
:schema="indexedOneOfRenderInfos[selectedIndex].schema"
:uischema="indexedOneOfRenderInfos[selectedIndex].uischema"
:path="control.path"
:renderers="control.renderers"
:cells="control.cells"
:enabled="control.enabled"
/>

<dialog ref="dialog" :class="styles.dialog.root">
<h1 :class="styles.dialog.title">
{{ control.translations.clearDialogTitle }}
</h1>

<p :class="styles.dialog.body">
{{ control.translations.clearDialogMessage }}
</p>

<div :class="styles.dialog.actions">
<button :onclick="onCancel" :class="styles.dialog.buttonSecondary">
{{ control.translations.clearDialogDecline }}
</button>
<button
ref="confirm"
:onclick="onConfirm"
:class="styles.dialog.buttonPrimary"
>
{{ control.translations.clearDialogAccept }}
</button>
</div>
</dialog>
</div>
</template>

<script lang="ts">
import {
CombinatorSubSchemaRenderInfo,
ControlElement,
createCombinatorRenderInfos,
createDefaultValue,
isOneOfControl,
JsonFormsRendererRegistryEntry,
rankWith,
} from '@jsonforms/core';
import {
DispatchRenderer,
rendererProps,
RendererProps,
useJsonFormsOneOfControl,
} from '@jsonforms/vue';
import isEmpty from 'lodash/isEmpty';
import { defineComponent, nextTick, ref } from 'vue';
import { useVanillaControl } from '../util';
import { ControlWrapper } from '../controls';
import CombinatorProperties from './components/CombinatorProperties.vue';
const controlRenderer = defineComponent({
name: 'OneOfRenderer',
components: {
ControlWrapper,
DispatchRenderer,
CombinatorProperties,
},
props: {
...rendererProps<ControlElement>(),
},
setup(props: RendererProps<ControlElement>) {
const input = useJsonFormsOneOfControl(props);
const control = input.control.value;
const selectedIndex = ref(control.indexOfFittingSchema);
const selectIndex = ref(selectedIndex.value);
const newSelectedIndex = ref(0);
const dialog = ref<HTMLDialogElement>();
const confirm = ref<HTMLElement>();
return {
...useVanillaControl(input),
selectedIndex,
selectIndex,
newSelectedIndex,
dialog,
confirm,
};
},
computed: {
indexedOneOfRenderInfos(): (CombinatorSubSchemaRenderInfo & {
index: number;
})[] {
const result = createCombinatorRenderInfos(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.control.schema.oneOf!,
this.control.rootSchema,
'oneOf',
this.control.uischema,
this.control.path,
this.control.uischemas
);
return result
.filter((info) => info.uischema)
.map((info, index) => ({ ...info, index: index }));
},
},
methods: {
handleSelectChange(event: Event): void {
const target = event.target as any;
this.selectIndex = target.value;
if (this.control.enabled && !isEmpty(this.control.data)) {
this.showDialog();
nextTick(() => {
this.newSelectedIndex = this.selectIndex;
// revert the selection while the dialog is open
this.selectIndex = this.selectedIndex;
this.confirm?.focus();
});
} else {
nextTick(() => {
this.selectedIndex = this.selectIndex;
});
}
},
showDialog(): void {
this.dialog?.showModal();
},
closeDialog(): void {
this.dialog?.close();
},
onConfirm(): void {
this.newSelection();
this.closeDialog();
},
onCancel(): void {
this.newSelectedIndex = this.selectedIndex;
this.closeDialog();
},
newSelection(): void {
this.handleChange(
this.control.path,
this.newSelectedIndex !== undefined && this.newSelectedIndex !== null
? createDefaultValue(
this.indexedOneOfRenderInfos[this.newSelectedIndex].schema
)
: {}
);
this.selectIndex = this.newSelectedIndex;
this.selectedIndex = this.newSelectedIndex;
},
},
});
export default controlRenderer;
export const entry: JsonFormsRendererRegistryEntry = {
renderer: controlRenderer,
tester: rankWith(3, isOneOfControl),
};
</script>

Loading

0 comments on commit f74e9ed

Please sign in to comment.