Skip to content

Commit f82fa67

Browse files
committed
angular-material: Translate enum for AutocompleteControlRenderer
1 parent e9bc501 commit f82fa67

2 files changed

Lines changed: 193 additions & 28 deletions

File tree

packages/angular-material/src/library/controls/autocomplete.renderer.ts

Lines changed: 90 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,15 @@ import {
3434
Actions,
3535
composeWithUi,
3636
ControlElement,
37+
EnumOption,
3738
isEnumControl,
39+
JsonFormsState,
40+
mapStateToEnumControlProps,
3841
OwnPropsOfControl,
42+
OwnPropsOfEnum,
3943
RankedTester,
4044
rankWith,
45+
StatePropsOfControl,
4146
} from '@jsonforms/core';
4247
import type { Observable } from 'rxjs';
4348
import { map, startWith } from 'rxjs/operators';
@@ -67,12 +72,13 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
6772
autoActiveFirstOption
6873
#auto="matAutocomplete"
6974
(optionSelected)="onSelect($event)"
75+
[displayWith]="displayFn"
7076
>
7177
<mat-option
7278
*ngFor="let option of filteredOptions | async"
7379
[value]="option"
7480
>
75-
{{ option }}
81+
{{ option.label }}
7682
</mat-option>
7783
</mat-autocomplete>
7884
<mat-hint *ngIf="shouldShowUnfocusedDescription() || focused">{{
@@ -105,16 +111,39 @@ export class AutocompleteControlRenderer
105111
extends JsonFormsControl
106112
implements OnInit
107113
{
108-
@Input() options: string[];
109-
filteredOptions: Observable<string[]>;
114+
@Input() options?: EnumOption[] | string[];
115+
valuesToTranslatedOptions?: Map<string, EnumOption>;
116+
filteredOptions: Observable<EnumOption[]>;
110117
shouldFilter: boolean;
111118
focused = false;
112119

113120
constructor(jsonformsService: JsonFormsAngularService) {
114121
super(jsonformsService);
115122
}
123+
124+
protected mapToProps(
125+
state: JsonFormsState
126+
): StatePropsOfControl & OwnPropsOfEnum {
127+
return mapStateToEnumControlProps(state, this.getOwnProps());
128+
}
129+
116130
getEventValue = (event: any) => event.target.value;
117131

132+
onChange(ev: any) {
133+
const eventValue = this.getEventValue(ev);
134+
const option = Array.from(this.valuesToTranslatedOptions?.values()).find(
135+
(option) => option.label === eventValue
136+
);
137+
if (!option) {
138+
super.onChange(ev);
139+
}
140+
141+
this.jsonFormsService.updateCore(
142+
Actions.update(this.propsPath, () => option.value)
143+
);
144+
this.triggerValidation();
145+
}
146+
118147
ngOnInit() {
119148
super.ngOnInit();
120149
this.shouldFilter = false;
@@ -124,6 +153,12 @@ export class AutocompleteControlRenderer
124153
);
125154
}
126155

156+
mapAdditionalProps(_props: StatePropsOfControl & OwnPropsOfEnum) {
157+
this.valuesToTranslatedOptions = new Map(
158+
_props.options.map((option) => [option.value, option])
159+
);
160+
}
161+
127162
updateFilter(event: any) {
128163
// ENTER
129164
if (event.keyCode === 13) {
@@ -136,30 +171,67 @@ export class AutocompleteControlRenderer
136171
onSelect(ev: MatAutocompleteSelectedEvent) {
137172
const path = composeWithUi(this.uischema as ControlElement, this.path);
138173
this.shouldFilter = false;
139-
this.jsonFormsService.updateCore(
140-
Actions.update(path, () => ev.option.value)
141-
);
174+
const option: EnumOption = ev.option.value;
175+
this.jsonFormsService.updateCore(Actions.update(path, () => option.value));
142176
this.triggerValidation();
143177
}
144178

145-
filter(val: string): string[] {
146-
return (this.options || this.scopedSchema.enum || []).filter(
147-
(option) =>
148-
!this.shouldFilter ||
149-
!val ||
150-
option.toLowerCase().indexOf(val.toLowerCase()) === 0
179+
// use arrow function to bind "this" reference
180+
displayFn = (option?: string | EnumOption): string => {
181+
if (!option) {
182+
return '';
183+
}
184+
185+
if (typeof option === 'string') {
186+
if (!this.valuesToTranslatedOptions) {
187+
return '';
188+
}
189+
190+
// if no option matches, it is a manual input
191+
return this.valuesToTranslatedOptions.get(option)?.label ?? option;
192+
}
193+
194+
return option?.label ?? '';
195+
};
196+
197+
filter(val: string | EnumOption | undefined): EnumOption[] {
198+
const options = Array.from(this.valuesToTranslatedOptions?.values() || []);
199+
200+
if (!val || !this.shouldFilter) {
201+
return options;
202+
}
203+
204+
const label = typeof val === 'string' ? val : val.label;
205+
return options.filter((option) =>
206+
option.label.toLowerCase().startsWith(label.toLowerCase())
151207
);
152208
}
153-
protected getOwnProps(): OwnPropsOfAutoComplete {
209+
protected getOwnProps(): OwnPropsOfControl & OwnPropsOfEnum {
154210
return {
155211
...super.getOwnProps(),
156-
options: this.options,
212+
options: this.stringOptionsToEnumOptions(this.options),
157213
};
158214
}
159-
}
160215

161-
export const enumControlTester: RankedTester = rankWith(2, isEnumControl);
216+
/**
217+
* For {@link options} input backwards compatibility
218+
*/
219+
protected stringOptionsToEnumOptions(
220+
options: typeof this.options
221+
): EnumOption[] | undefined {
222+
if (!options) {
223+
return undefined;
224+
}
162225

163-
interface OwnPropsOfAutoComplete extends OwnPropsOfControl {
164-
options: string[];
226+
return options.every((item) => typeof item === 'string')
227+
? options.map((str) => {
228+
return {
229+
label: str,
230+
value: str,
231+
} satisfies EnumOption;
232+
})
233+
: options;
234+
}
165235
}
236+
237+
export const enumControlTester: RankedTester = rankWith(2, isEnumControl);

packages/angular-material/test/autocomplete-control.spec.ts

Lines changed: 103 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,26 @@ import {
4444
setupMockStore,
4545
getJsonFormsService,
4646
} from './common';
47-
import { ControlElement, JsonSchema, Actions } from '@jsonforms/core';
47+
import {
48+
ControlElement,
49+
JsonSchema,
50+
Actions,
51+
JsonFormsCore,
52+
EnumOption,
53+
} from '@jsonforms/core';
4854
import { AutocompleteControlRenderer } from '../src';
4955
import { JsonFormsAngularService } from '@jsonforms/angular';
5056
import { ErrorObject } from 'ajv';
5157
import { HarnessLoader } from '@angular/cdk/testing';
5258
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
5359
import { MatAutocompleteHarness } from '@angular/material/autocomplete/testing';
5460

55-
const data = { foo: 'A' };
61+
const data = {
62+
foo: {
63+
label: 'A',
64+
value: 'a',
65+
},
66+
};
5667
const schema: JsonSchema = {
5768
type: 'object',
5869
properties: {
@@ -107,7 +118,10 @@ describe('Autocomplete control Base Tests', () => {
107118
component.ngOnInit();
108119
fixture.detectChanges();
109120
tick();
110-
expect(component.data).toBe('A');
121+
expect(component.data).toEqual({
122+
label: 'A',
123+
value: 'a',
124+
});
111125
expect(inputElement.value).toBe('A');
112126
expect(inputElement.disabled).toBe(false);
113127
}));
@@ -120,10 +134,20 @@ describe('Autocomplete control Base Tests', () => {
120134
component.ngOnInit();
121135
fixture.detectChanges();
122136
tick();
123-
getJsonFormsService(component).updateCore(Actions.update('foo', () => 'B'));
137+
getJsonFormsService(component).updateCore(
138+
Actions.update('foo', () => {
139+
return {
140+
label: 'B',
141+
value: 'b',
142+
} satisfies EnumOption;
143+
})
144+
);
124145
tick();
125146
fixture.detectChanges();
126-
expect(component.data).toBe('B');
147+
expect(component.data).toEqual({
148+
label: 'B',
149+
value: 'b',
150+
} satisfies EnumOption);
127151
expect(inputElement.value).toBe('B');
128152
}));
129153

@@ -165,11 +189,28 @@ describe('Autocomplete control Base Tests', () => {
165189
component.ngOnInit();
166190
fixture.detectChanges();
167191
tick();
168-
getJsonFormsService(component).updateCore(Actions.update('foo', () => 'A'));
169-
getJsonFormsService(component).updateCore(Actions.update('bar', () => 'B'));
192+
getJsonFormsService(component).updateCore(
193+
Actions.update('foo', () => {
194+
return {
195+
label: 'A',
196+
value: 'a',
197+
} satisfies EnumOption;
198+
})
199+
);
200+
getJsonFormsService(component).updateCore(
201+
Actions.update('bar', () => {
202+
return {
203+
label: 'B',
204+
value: 'b',
205+
} satisfies EnumOption;
206+
})
207+
);
170208
fixture.detectChanges();
171209
tick();
172-
expect(component.data).toBe('A');
210+
expect(component.data).toEqual({
211+
label: 'A',
212+
value: 'a',
213+
} satisfies EnumOption);
173214
expect(inputElement.value).toBe('A');
174215
}));
175216
// store needed as we evaluate the calculated enabled value to disable/enable the control
@@ -252,7 +293,10 @@ describe('AutoComplete control Input Event Tests', () => {
252293
const event = spy.calls.mostRecent()
253294
.args[0] as MatAutocompleteSelectedEvent;
254295

255-
expect(event.option.value).toBe('B');
296+
expect(event.option.value).toEqual({
297+
label: 'B',
298+
value: 'B',
299+
} satisfies EnumOption);
256300
expect(inputElement.value).toBe('B');
257301
}));
258302
it('options should prefer own props', fakeAsync(async () => {
@@ -277,9 +321,58 @@ describe('AutoComplete control Input Event Tests', () => {
277321

278322
const event = spy.calls.mostRecent()
279323
.args[0] as MatAutocompleteSelectedEvent;
280-
expect(event.option.value).toBe('Y');
324+
expect(event.option.value).toEqual({
325+
label: 'Y',
326+
value: 'Y',
327+
} satisfies EnumOption);
281328
expect(inputElement.value).toBe('Y');
282329
}));
330+
it('should render translated enum correctly', fakeAsync(async () => {
331+
setupMockStore(fixture, { uischema, schema, data });
332+
const state: JsonFormsCore = {
333+
data,
334+
schema,
335+
uischema,
336+
};
337+
getJsonFormsService(component).init({
338+
core: state,
339+
i18n: {
340+
translate: (key, defaultMessage) => {
341+
const translations: { [key: string]: string } = {
342+
'foo.A': 'Translated A',
343+
'foo.B': 'Translated B',
344+
'foo.C': 'Translated C',
345+
};
346+
return translations[key] ?? defaultMessage;
347+
},
348+
},
349+
});
350+
getJsonFormsService(component).updateCore(
351+
Actions.init(data, schema, uischema)
352+
);
353+
component.ngOnInit();
354+
fixture.detectChanges();
355+
const spy = spyOn(component, 'onSelect');
356+
357+
await (await loader.getHarness(MatAutocompleteHarness)).focus();
358+
fixture.detectChanges();
359+
360+
await (
361+
await loader.getHarness(MatAutocompleteHarness)
362+
).selectOption({
363+
text: 'Translated B',
364+
});
365+
fixture.detectChanges();
366+
tick();
367+
368+
const event = spy.calls.mostRecent()
369+
.args[0] as MatAutocompleteSelectedEvent;
370+
expect(event.option.value).toEqual({
371+
label: 'Translated B',
372+
value: 'B',
373+
} satisfies EnumOption);
374+
expect(inputElement.value).toBe('Translated B');
375+
}));
283376
});
284377
describe('AutoComplete control Error Tests', () => {
285378
let fixture: ComponentFixture<AutocompleteControlRenderer>;

0 commit comments

Comments
 (0)