Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -67,13 +72,13 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
autoActiveFirstOption
#auto="matAutocomplete"
(optionSelected)="onSelect($event)"
[displayWith]="displayFn"
>
<mat-option
*ngFor="let option of filteredOptions | async"
[value]="option"
>
{{ option }}
@for (option of filteredOptions | async; track option.value) {
<mat-option [value]="option">
{{ option.label }}
</mat-option>
}
</mat-autocomplete>
<mat-hint *ngIf="shouldShowUnfocusedDescription() || focused">{{
description
Expand Down Expand Up @@ -105,16 +110,40 @@ export class AutocompleteControlRenderer
extends JsonFormsControl
implements OnInit
{
@Input() options: string[];
filteredOptions: Observable<string[]>;
@Input() options?: EnumOption[] | string[];
valuesToTranslatedOptions?: Map<string, EnumOption>;
filteredOptions: Observable<EnumOption[]>;
shouldFilter: boolean;
focused = false;

constructor(jsonformsService: JsonFormsAngularService) {
super(jsonformsService);
}

protected override mapToProps(
state: JsonFormsState
): StatePropsOfControl & OwnPropsOfEnum {
return mapStateToEnumControlProps(state, this.getOwnProps());
}

getEventValue = (event: any) => event.target.value;

override onChange(ev: any) {
const eventValue = this.getEventValue(ev);
const option = Array.from(
this.valuesToTranslatedOptions?.values() ?? []
).find((option) => option.label === eventValue);
if (!option) {
super.onChange(ev);
return;
}

this.jsonFormsService.updateCore(
Actions.update(this.propsPath, () => option.value)
);
this.triggerValidation();
}

ngOnInit() {
super.ngOnInit();
this.shouldFilter = false;
Expand All @@ -124,6 +153,12 @@ export class AutocompleteControlRenderer
);
}

override mapAdditionalProps(_props: StatePropsOfControl & OwnPropsOfEnum) {
this.valuesToTranslatedOptions = new Map(
(_props.options ?? []).map((option) => [option.value, option])
);
}

updateFilter(event: any) {
// ENTER
if (event.keyCode === 13) {
Expand All @@ -136,30 +171,67 @@ 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(
(option) =>
!this.shouldFilter ||
!val ||
option.toLowerCase().indexOf(val.toLowerCase()) === 0
// use arrow function to bind "this" reference
displayFn = (option?: string | EnumOption): string => {
if (!option) {
return '';
}

if (typeof option === 'string') {
if (!this.valuesToTranslatedOptions) {
return option; // show raw value until translations are ready
}

// if no option matches, it is a manual input
return this.valuesToTranslatedOptions.get(option)?.label ?? option;
}

return option?.label ?? '';
};

filter(val: string | EnumOption | undefined): EnumOption[] {
const options = Array.from(this.valuesToTranslatedOptions?.values() || []);

if (!val || !this.shouldFilter) {
return options;
}

const label = typeof val === 'string' ? val : val.label;
return options.filter((option) =>
option.label.toLowerCase().startsWith(label.toLowerCase())
);
}
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);
69 changes: 66 additions & 3 deletions packages/angular-material/test/autocomplete-control.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -213,6 +219,7 @@ describe('AutoComplete control Input Event Tests', () => {
let fixture: ComponentFixture<AutocompleteControlRenderer>;
let component: AutocompleteControlRenderer;
let loader: HarnessLoader;
let inputElement: HTMLInputElement;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [componentUT, ...imports],
Expand All @@ -223,6 +230,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 () => {
Expand All @@ -249,7 +258,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 });
Expand All @@ -273,7 +286,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', () => {
Expand Down
Loading
Loading