Skip to content

Commit 3a9bc74

Browse files
authored
angular-material: Add i18n support to autocomplete control
- Add i18n support to autocomplete control - Extend tests to check option events - Add enum i18n example
1 parent e3d804d commit 3a9bc74

4 files changed

Lines changed: 300 additions & 25 deletions

File tree

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

Lines changed: 94 additions & 22 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,13 +72,13 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
6772
autoActiveFirstOption
6873
#auto="matAutocomplete"
6974
(optionSelected)="onSelect($event)"
75+
[displayWith]="displayFn"
7076
>
71-
<mat-option
72-
*ngFor="let option of filteredOptions | async"
73-
[value]="option"
74-
>
75-
{{ option }}
77+
@for (option of filteredOptions | async; track option.value) {
78+
<mat-option [value]="option">
79+
{{ option.label }}
7680
</mat-option>
81+
}
7782
</mat-autocomplete>
7883
<mat-hint *ngIf="shouldShowUnfocusedDescription() || focused">{{
7984
description
@@ -105,16 +110,40 @@ export class AutocompleteControlRenderer
105110
extends JsonFormsControl
106111
implements OnInit
107112
{
108-
@Input() options: string[];
109-
filteredOptions: Observable<string[]>;
113+
@Input() options?: EnumOption[] | string[];
114+
valuesToTranslatedOptions?: Map<string, EnumOption>;
115+
filteredOptions: Observable<EnumOption[]>;
110116
shouldFilter: boolean;
111117
focused = false;
112118

113119
constructor(jsonformsService: JsonFormsAngularService) {
114120
super(jsonformsService);
115121
}
122+
123+
protected override mapToProps(
124+
state: JsonFormsState
125+
): StatePropsOfControl & OwnPropsOfEnum {
126+
return mapStateToEnumControlProps(state, this.getOwnProps());
127+
}
128+
116129
getEventValue = (event: any) => event.target.value;
117130

131+
override onChange(ev: any) {
132+
const eventValue = this.getEventValue(ev);
133+
const option = Array.from(
134+
this.valuesToTranslatedOptions?.values() ?? []
135+
).find((option) => option.label === eventValue);
136+
if (!option) {
137+
super.onChange(ev);
138+
return;
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+
override 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 option; // show raw value until translations are ready
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: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,13 @@ 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';
@@ -213,6 +219,7 @@ describe('AutoComplete control Input Event Tests', () => {
213219
let fixture: ComponentFixture<AutocompleteControlRenderer>;
214220
let component: AutocompleteControlRenderer;
215221
let loader: HarnessLoader;
222+
let inputElement: HTMLInputElement;
216223
beforeEach(waitForAsync(() => {
217224
TestBed.configureTestingModule({
218225
imports: [componentUT, ...imports],
@@ -223,6 +230,8 @@ describe('AutoComplete control Input Event Tests', () => {
223230
fixture = TestBed.createComponent(componentUT);
224231
component = fixture.componentInstance;
225232
loader = TestbedHarnessEnvironment.loader(fixture);
233+
234+
inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
226235
}));
227236

228237
it('should update via input event', fakeAsync(async () => {
@@ -249,7 +258,11 @@ describe('AutoComplete control Input Event Tests', () => {
249258
const event = spy.calls.mostRecent()
250259
.args[0] as MatAutocompleteSelectedEvent;
251260

252-
expect(event.option.value).toBe('B');
261+
expect(event.option.value).toEqual({
262+
label: 'B',
263+
value: 'B',
264+
} satisfies EnumOption);
265+
expect(inputElement.value).toBe('B');
253266
}));
254267
it('options should prefer own props', fakeAsync(async () => {
255268
setupMockStore(fixture, { uischema, schema, data });
@@ -273,7 +286,57 @@ describe('AutoComplete control Input Event Tests', () => {
273286

274287
const event = spy.calls.mostRecent()
275288
.args[0] as MatAutocompleteSelectedEvent;
276-
expect(event.option.value).toBe('Y');
289+
expect(event.option.value).toEqual({
290+
label: 'Y',
291+
value: 'Y',
292+
} satisfies EnumOption);
293+
expect(inputElement.value).toBe('Y');
294+
}));
295+
it('should render translated enum correctly', fakeAsync(async () => {
296+
setupMockStore(fixture, { uischema, schema, data });
297+
const state: JsonFormsCore = {
298+
data,
299+
schema,
300+
uischema,
301+
};
302+
getJsonFormsService(component).init({
303+
core: state,
304+
i18n: {
305+
translate: (key, defaultMessage) => {
306+
const translations: { [key: string]: string } = {
307+
'foo.A': 'Translated A',
308+
'foo.B': 'Translated B',
309+
'foo.C': 'Translated C',
310+
};
311+
return translations[key] ?? defaultMessage;
312+
},
313+
},
314+
});
315+
getJsonFormsService(component).updateCore(
316+
Actions.init(data, schema, uischema)
317+
);
318+
component.ngOnInit();
319+
fixture.detectChanges();
320+
const spy = spyOn(component, 'onSelect');
321+
322+
await (await loader.getHarness(MatAutocompleteHarness)).focus();
323+
fixture.detectChanges();
324+
325+
await (
326+
await loader.getHarness(MatAutocompleteHarness)
327+
).selectOption({
328+
text: 'Translated B',
329+
});
330+
fixture.detectChanges();
331+
tick();
332+
333+
const event = spy.calls.mostRecent()
334+
.args[0] as MatAutocompleteSelectedEvent;
335+
expect(event.option.value).toEqual({
336+
label: 'Translated B',
337+
value: 'B',
338+
} satisfies EnumOption);
339+
expect(inputElement.value).toBe('Translated B');
277340
}));
278341
});
279342
describe('AutoComplete control Error Tests', () => {

0 commit comments

Comments
 (0)