diff --git a/packages/angular-material/src/library/controls/autocomplete.renderer.ts b/packages/angular-material/src/library/controls/autocomplete.renderer.ts
index 69e308c32..e7ec957f9 100644
--- a/packages/angular-material/src/library/controls/autocomplete.renderer.ts
+++ b/packages/angular-material/src/library/controls/autocomplete.renderer.ts
@@ -34,10 +34,15 @@ import {
Actions,
composeWithUi,
ControlElement,
+ EnumOption,
isEnumControl,
+ JsonFormsState,
+ mapStateToEnumControlProps,
OwnPropsOfControl,
+ OwnPropsOfEnum,
RankedTester,
rankWith,
+ StatePropsOfControl,
} from '@jsonforms/core';
import type { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
@@ -67,12 +72,13 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
autoActiveFirstOption
#auto="matAutocomplete"
(optionSelected)="onSelect($event)"
+ [displayWith]="displayFn"
>
- {{ option }}
+ {{ option.label }}
{{
@@ -105,14 +111,22 @@ export class AutocompleteControlRenderer
extends JsonFormsControl
implements OnInit
{
- @Input() options: string[];
- filteredOptions: Observable;
+ @Input() options?: EnumOption[] | string[];
+ translatedOptions?: EnumOption[];
+ filteredOptions: Observable;
shouldFilter: boolean;
focused = false;
constructor(jsonformsService: JsonFormsAngularService) {
super(jsonformsService);
}
+
+ protected mapToProps(
+ state: JsonFormsState
+ ): StatePropsOfControl & OwnPropsOfEnum {
+ return mapStateToEnumControlProps(state, this.getOwnProps());
+ }
+
getEventValue = (event: any) => event.target.value;
ngOnInit() {
@@ -124,6 +138,10 @@ export class AutocompleteControlRenderer
);
}
+ mapAdditionalProps(_props: StatePropsOfControl & OwnPropsOfEnum) {
+ this.translatedOptions = _props.options;
+ }
+
updateFilter(event: any) {
// ENTER
if (event.keyCode === 13) {
@@ -136,30 +154,49 @@ export class AutocompleteControlRenderer
onSelect(ev: MatAutocompleteSelectedEvent) {
const path = composeWithUi(this.uischema as ControlElement, this.path);
this.shouldFilter = false;
- this.jsonFormsService.updateCore(
- Actions.update(path, () => ev.option.value)
- );
+ const option: EnumOption = ev.option.value;
+ this.jsonFormsService.updateCore(Actions.update(path, () => option.value));
this.triggerValidation();
}
- filter(val: string): string[] {
- return (this.options || this.scopedSchema.enum || []).filter(
+ displayFn(option?: EnumOption): string {
+ return option?.label ?? '';
+ }
+
+ filter(val: string): EnumOption[] {
+ return (this.translatedOptions || []).filter(
(option) =>
!this.shouldFilter ||
!val ||
- option.toLowerCase().indexOf(val.toLowerCase()) === 0
+ option.label.toLowerCase().indexOf(val.toLowerCase()) === 0
);
}
- protected getOwnProps(): OwnPropsOfAutoComplete {
+ protected getOwnProps(): OwnPropsOfControl & OwnPropsOfEnum {
return {
...super.getOwnProps(),
- options: this.options,
+ options: this.stringOptionsToEnumOptions(this.options),
};
}
-}
-export const enumControlTester: RankedTester = rankWith(2, isEnumControl);
+ /**
+ * For {@link options} input backwards compatibility
+ */
+ protected stringOptionsToEnumOptions(
+ options: typeof this.options
+ ): EnumOption[] | undefined {
+ if (!options) {
+ return undefined;
+ }
-interface OwnPropsOfAutoComplete extends OwnPropsOfControl {
- options: string[];
+ return options.every((item) => typeof item === 'string')
+ ? options.map((str) => {
+ return {
+ label: str,
+ value: str,
+ } satisfies EnumOption;
+ })
+ : options;
+ }
}
+
+export const enumControlTester: RankedTester = rankWith(2, isEnumControl);
diff --git a/packages/angular-material/test/autocomplete-control.spec.ts b/packages/angular-material/test/autocomplete-control.spec.ts
index 53a0bd645..8c25e362e 100644
--- a/packages/angular-material/test/autocomplete-control.spec.ts
+++ b/packages/angular-material/test/autocomplete-control.spec.ts
@@ -44,7 +44,13 @@ import {
setupMockStore,
getJsonFormsService,
} from './common';
-import { ControlElement, JsonSchema, Actions } from '@jsonforms/core';
+import {
+ ControlElement,
+ JsonSchema,
+ Actions,
+ JsonFormsCore,
+ EnumOption,
+} from '@jsonforms/core';
import { AutocompleteControlRenderer } from '../src';
import { JsonFormsAngularService } from '@jsonforms/angular';
import { ErrorObject } from 'ajv';
@@ -52,7 +58,12 @@ import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatAutocompleteHarness } from '@angular/material/autocomplete/testing';
-const data = { foo: 'A' };
+const data = {
+ foo: {
+ label: 'A',
+ value: 'a',
+ },
+};
const schema: JsonSchema = {
type: 'object',
properties: {
@@ -107,7 +118,10 @@ describe('Autocomplete control Base Tests', () => {
component.ngOnInit();
fixture.detectChanges();
tick();
- expect(component.data).toBe('A');
+ expect(component.data).toEqual({
+ label: 'A',
+ value: 'a',
+ });
expect(inputElement.value).toBe('A');
expect(inputElement.disabled).toBe(false);
}));
@@ -120,10 +134,20 @@ describe('Autocomplete control Base Tests', () => {
component.ngOnInit();
fixture.detectChanges();
tick();
- getJsonFormsService(component).updateCore(Actions.update('foo', () => 'B'));
+ getJsonFormsService(component).updateCore(
+ Actions.update('foo', () => {
+ return {
+ label: 'B',
+ value: 'b',
+ } satisfies EnumOption;
+ })
+ );
tick();
fixture.detectChanges();
- expect(component.data).toBe('B');
+ expect(component.data).toEqual({
+ label: 'B',
+ value: 'b',
+ } satisfies EnumOption);
expect(inputElement.value).toBe('B');
}));
@@ -165,11 +189,28 @@ describe('Autocomplete control Base Tests', () => {
component.ngOnInit();
fixture.detectChanges();
tick();
- getJsonFormsService(component).updateCore(Actions.update('foo', () => 'A'));
- getJsonFormsService(component).updateCore(Actions.update('bar', () => 'B'));
+ getJsonFormsService(component).updateCore(
+ Actions.update('foo', () => {
+ return {
+ label: 'A',
+ value: 'a',
+ } satisfies EnumOption;
+ })
+ );
+ getJsonFormsService(component).updateCore(
+ Actions.update('bar', () => {
+ return {
+ label: 'B',
+ value: 'b',
+ } satisfies EnumOption;
+ })
+ );
fixture.detectChanges();
tick();
- expect(component.data).toBe('A');
+ expect(component.data).toEqual({
+ label: 'A',
+ value: 'a',
+ } satisfies EnumOption);
expect(inputElement.value).toBe('A');
}));
// store needed as we evaluate the calculated enabled value to disable/enable the control
@@ -213,6 +254,7 @@ describe('AutoComplete control Input Event Tests', () => {
let fixture: ComponentFixture;
let component: AutocompleteControlRenderer;
let loader: HarnessLoader;
+ let inputElement: HTMLInputElement;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [componentUT, ...imports],
@@ -223,6 +265,8 @@ describe('AutoComplete control Input Event Tests', () => {
fixture = TestBed.createComponent(componentUT);
component = fixture.componentInstance;
loader = TestbedHarnessEnvironment.loader(fixture);
+
+ inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
}));
it('should update via input event', fakeAsync(async () => {
@@ -249,7 +293,11 @@ describe('AutoComplete control Input Event Tests', () => {
const event = spy.calls.mostRecent()
.args[0] as MatAutocompleteSelectedEvent;
- expect(event.option.value).toBe('B');
+ expect(event.option.value).toEqual({
+ label: 'B',
+ value: 'B',
+ } satisfies EnumOption);
+ expect(inputElement.value).toBe('B');
}));
it('options should prefer own props', fakeAsync(async () => {
setupMockStore(fixture, { uischema, schema, data });
@@ -273,7 +321,57 @@ describe('AutoComplete control Input Event Tests', () => {
const event = spy.calls.mostRecent()
.args[0] as MatAutocompleteSelectedEvent;
- expect(event.option.value).toBe('Y');
+ expect(event.option.value).toEqual({
+ label: 'Y',
+ value: 'Y',
+ } satisfies EnumOption);
+ expect(inputElement.value).toBe('Y');
+ }));
+ it('should render translated enum correctly', fakeAsync(async () => {
+ setupMockStore(fixture, { uischema, schema, data });
+ const state: JsonFormsCore = {
+ data,
+ schema,
+ uischema,
+ };
+ getJsonFormsService(component).init({
+ core: state,
+ i18n: {
+ translate: (key, defaultMessage) => {
+ const translations: { [key: string]: string } = {
+ 'foo.A': 'Translated A',
+ 'foo.B': 'Translated B',
+ 'foo.C': 'Translated C',
+ };
+ return translations[key] ?? defaultMessage;
+ },
+ },
+ });
+ getJsonFormsService(component).updateCore(
+ Actions.init(data, schema, uischema)
+ );
+ component.ngOnInit();
+ fixture.detectChanges();
+ const spy = spyOn(component, 'onSelect');
+
+ await (await loader.getHarness(MatAutocompleteHarness)).focus();
+ fixture.detectChanges();
+
+ await (
+ await loader.getHarness(MatAutocompleteHarness)
+ ).selectOption({
+ text: 'Translated B',
+ });
+ fixture.detectChanges();
+ tick();
+
+ const event = spy.calls.mostRecent()
+ .args[0] as MatAutocompleteSelectedEvent;
+ expect(event.option.value).toEqual({
+ label: 'Translated B',
+ value: 'B',
+ } satisfies EnumOption);
+ expect(inputElement.value).toBe('Translated B');
}));
});
describe('AutoComplete control Error Tests', () => {
diff --git a/packages/examples/src/examples/enumI18n.ts b/packages/examples/src/examples/enumI18n.ts
new file mode 100644
index 000000000..fdfdf733a
--- /dev/null
+++ b/packages/examples/src/examples/enumI18n.ts
@@ -0,0 +1,138 @@
+/*
+ The MIT License
+
+ Copyright (c) 2017-2019 EclipseSource Munich
+ https://github.com/eclipsesource/jsonforms
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+*/
+import { registerExamples } from '../register';
+import { Translator } from '@jsonforms/core';
+import get from 'lodash/get';
+
+export const schema = {
+ type: 'object',
+ properties: {
+ country: {
+ type: 'string',
+ enum: ['DE', 'IT', 'JP', 'US', 'RU', 'Other'],
+ },
+ countryNoAutocomplete: {
+ type: 'string',
+ enum: ['DE', 'IT', 'JP', 'US', 'RU', 'Other'],
+ },
+ status: {
+ type: 'string',
+ oneOf: [
+ { const: 'pending', title: 'Pending' },
+ { const: 'approved', title: 'Approved' },
+ { const: 'rejected', title: 'Rejected' },
+ ],
+ },
+ },
+};
+
+export const uischema = {
+ type: 'VerticalLayout',
+ elements: [
+ {
+ type: 'Group',
+ label: 'Enum with i18n (Autocomplete)',
+ elements: [
+ {
+ type: 'Control',
+ scope: '#/properties/country',
+ label: 'Country (with autocomplete)',
+ },
+ ],
+ },
+ {
+ type: 'Group',
+ label: 'Enum with i18n (Dropdown)',
+ elements: [
+ {
+ type: 'Control',
+ scope: '#/properties/countryNoAutocomplete',
+ label: 'Country (dropdown)',
+ options: {
+ autocomplete: false,
+ },
+ },
+ ],
+ },
+ {
+ type: 'Group',
+ label: 'OneOf Enum with i18n',
+ elements: [
+ {
+ type: 'Control',
+ scope: '#/properties/status',
+ label: 'Status',
+ },
+ ],
+ },
+ ],
+};
+
+export const data = {
+ country: 'DE',
+};
+
+export const translations: Record = {
+ // Translations for country enum values
+ // Key format: .
+ 'country.DE': 'Germany',
+ 'country.IT': 'Italy',
+ 'country.JP': 'Japan',
+ 'country.US': 'United States',
+ 'country.RU': 'Russia',
+ 'country.Other': 'Other Country',
+ // Same translations for the non-autocomplete version
+ 'countryNoAutocomplete.DE': 'Germany',
+ 'countryNoAutocomplete.IT': 'Italy',
+ 'countryNoAutocomplete.JP': 'Japan',
+ 'countryNoAutocomplete.US': 'United States',
+ 'countryNoAutocomplete.RU': 'Russia',
+ 'countryNoAutocomplete.Other': 'Other Country',
+ // Translations for status oneOf enum
+ 'status.pending': 'Awaiting Review',
+ 'status.approved': 'Approved',
+ 'status.rejected': 'Declined',
+};
+
+export const translate: Translator = (
+ key: string,
+ defaultMessage: string | undefined
+) => {
+ return get(translations, key) ?? defaultMessage;
+};
+
+registerExamples([
+ {
+ name: 'enum-i18n',
+ label: 'Enums (i18n)',
+ data,
+ schema,
+ uischema,
+ i18n: {
+ translate: translate,
+ locale: 'en',
+ },
+ },
+]);
diff --git a/packages/examples/src/index.ts b/packages/examples/src/index.ts
index e200dc8e3..34adc0330 100644
--- a/packages/examples/src/index.ts
+++ b/packages/examples/src/index.ts
@@ -67,6 +67,7 @@ import * as onChange from './examples/onChange';
import * as enumExample from './examples/enum';
import * as radioGroupExample from './examples/radioGroup';
import * as multiEnum from './examples/enum-multi';
+import * as enumI18n from './examples/enumI18n';
import * as enumInArray from './examples/enumInArray';
import * as readonly from './examples/readonly';
import * as listWithDetailPrimitives from './examples/list-with-detail-primitives';
@@ -134,6 +135,7 @@ export {
radioGroupExample,
multiEnum,
multiEnumWithLabelAndDesc,
+ enumI18n,
enumInArray,
readonly,
listWithDetailPrimitives,