Hello, i created a custom table renderer for angular material.
I used this as a base ArrayLayoutRenderer
What i wanted is to mimic is the react array renderer that has an accordion built into it. But what i ended up with is not very efficient. So i wanted to ask for feedback/improvements.
In the search of improving the code i found this issue that has been solved with pipes, so i implemented it, without significant improvement.
I have 2 main issues.
- My Array Renderer is being re-initialized once per item in the array, so if i have 6 items in the array, the component is being initialized 6 times. The more items the array has the slower is the form to fully render.
- The getStringKeyValue$ is called multiple times, even on the action button hover. (Also im looking for a better way to implement this, pipes, etc.)
Here is the code:
Angular Component Code
import { ce } from '@shared/scripts/compute-engine';
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, OnInit, ViewChildren } from '@angular/core';
import { MatBadgeModule } from '@angular/material/badge';
import { MatBadgeHarness } from '@angular/material/badge/testing';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
import {
JsonFormsAngularService,
JsonFormsAbstractControl,
JsonFormsModule,
} from '@jsonforms/angular';
import {
arrayDefaultTranslations,
ArrayLayoutProps,
ArrayTranslations,
createDefaultValue,
defaultJsonFormsI18nState,
findUISchema,
getArrayTranslations,
isObjectArrayWithNesting,
JsonFormsState,
mapDispatchToArrayControlProps,
mapStateToArrayLayoutProps,
OwnPropsOfRenderer,
Paths,
RankedTester,
rankWith,
setReadonly,
StatePropsOfArrayLayout,
UISchemaElement,
UISchemaTester,
unsetReadonly,
} from '@jsonforms/core';
import { distinctUntilChanged, map, Observable } from 'rxjs';
import { GetProps } from './get-props.pipe';
declare const MathLive: any;
@Component({
selector: 'app-array-layout-renderer',
templateUrl: 'custom-array-renderer.component.html',
styleUrls: ['./custom-array-renderer.component.scss'],
imports: [GetProps, MatIconModule, MatCardModule, MatButtonModule, CommonModule, JsonFormsModule, MatTooltipModule, MatBadgeModule, MatExpansionModule],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomArrayRendererComponent
extends JsonFormsAbstractControl<StatePropsOfArrayLayout>
implements OnInit {
noData!: boolean;
translations: ArrayTranslations = {};
addItem: ((path: string, value: any) => () => void) | undefined;
moveItemUp: ((path: string, index: number) => () => void) | undefined;
moveItemDown: ((path: string, index: number) => () => void) | undefined;
removeItems: ((path: string, toDelete: number[]) => () => void) | undefined;
uischemas: {
tester: UISchemaTester;
uischema: UISchemaElement;
}[] | undefined;
constructor(jsonFormsService: JsonFormsAngularService) {
super(jsonFormsService);
}
mapToProps(
state: JsonFormsState
): StatePropsOfArrayLayout & { translations: ArrayTranslations } {
const props = mapStateToArrayLayoutProps(state, this.getOwnProps()) as StatePropsOfArrayLayout;
// console.log('Data:', props);
const t =
state.jsonforms.i18n?.translate ?? defaultJsonFormsI18nState.translate;
const translations = getArrayTranslations(
t,
arrayDefaultTranslations,
props.i18nKeyPrefix || '',
props.label
);
return { ...props, translations };
}
remove(index: number): void {
if (this.removeItems) {
this.removeItems(this.propsPath, [index])();
}
}
add(): void {
if (this.addItem) {
this.addItem(
this.propsPath,
createDefaultValue(this.scopedSchema, this.rootSchema)
)();
}
}
up(index: number): void {
if (this.moveItemUp) {
this.moveItemUp(this.propsPath, index)();
}
}
down(index: number): void {
if (this.moveItemDown) {
this.moveItemDown(this.propsPath, index)();
}
}
override ngOnInit() {
console.log('init')
super.ngOnInit();
const { addItem, removeItems, moveUp, moveDown } =
mapDispatchToArrayControlProps(
this.jsonFormsService.updateCore.bind(this.jsonFormsService)
);
this.addItem = addItem;
this.moveItemUp = moveUp;
this.moveItemDown = moveDown;
this.removeItems = removeItems;
}
override mapAdditionalProps(
props: ArrayLayoutProps & { translations: ArrayTranslations }
) {
this.noData = !props.data || props.data === 0;
this.uischemas = props.uischemas;
this.translations = props.translations;
}
getProps(index: number): OwnPropsOfRenderer {
const uischema = findUISchema(
this.uischemas ?? [],
this.scopedSchema,
this.uischema.scope,
this.propsPath,
undefined,
this.uischema,
this.rootSchema
);
if (this.isEnabled()) {
unsetReadonly(uischema);
} else {
setReadonly(uischema);
}
return {
schema: this.scopedSchema,
path: Paths.compose(this.propsPath, `${index}`),
uischema,
};
}
trackByFn(index: number) {
return index;
}
hasSingleKey(item: any): boolean {
return item && Object.keys(item).length === 1;
}
getStringKeyValue$(idx: number): Observable<string> {
return this.jsonFormsService.$state.pipe(
distinctUntilChanged(),
map(state => {
if (this.uischema?.options?.['elementLabelProp']) {
const label = this.uischema?.options?.['elementLabelProp']
if ((this.scopedSchema as any)?.['properties']?.[label].options?.['formula']) {
const instance = state.jsonforms.core?.data[this.propsPath]?.[idx]?.label;
const v = instance
? ce.serialize(JSON.parse(instance), { canonical: false })
: '';
return MathLive.convertLatexToMarkup(v);
}
} else{
const key = Object.keys((this.scopedSchema as any)?.['properties']).filter((key: any)=>{
return ((this.scopedSchema as any)?.['properties']?.[key].type == 'string')
})[0]
return key? state.jsonforms.core?.data[this.propsPath]?.[idx]?.[key] : `Item ${idx + 1}`
}
})
);
}
}
export const CustomArrayLayoutRendererTester: RankedTester = rankWith(
5,
isObjectArrayWithNesting
);
HTML template Code
<div class="array-layout">
<div class="array-layout-toolbar">
<h4 class="mat-h2 array-layout-title">{{ label }}</h4>
<span></span>
<mat-icon
*ngIf="error?.length"
color="warn"
matBadge="{{ error && error.split('').length }}"
matBadgeColor="warn"
matTooltip="{{ error }}"
matTooltipClass="error-message-tooltip"
>
error_outline
</mat-icon>
<span></span>
<button
mat-button
matTooltip="{{ translations.addTooltip }}"
[disabled]="!isEnabled()"
(click)="add()"
>
<mat-icon>add</mat-icon>
</button>
</div>
<p *ngIf="noData">{{ translations.noDataMessage }}</p>
<mat-accordion>
@for(item of [].constructor(data); track $index; let idx = $index; let last
= $last; let first = $first){
<mat-expansion-panel>
<mat-expansion-panel-header class="panel-header">
<mat-panel-title
[innerHTML]="getStringKeyValue$(idx) | async"
></mat-panel-title>
<mat-panel-description style="justify-content: flex-end">
<div class="action-buttons">
<button
*ngIf="uischema?.options?.['showSortButtons']"
class="item-up"
mat-button
[disabled]="first"
(click)="$event.stopPropagation(); up(idx)"
matTooltip="{{ translations.up }}"
matTooltipPosition="right"
>
<mat-icon>arrow_upward</mat-icon>
</button>
<button
*ngIf="uischema?.options?.['showSortButtons']"
class="item-down"
mat-button
[disabled]="last"
(click)="$event.stopPropagation(); down(idx)"
matTooltip="{{ translations.down }}"
matTooltipPosition="right"
>
<mat-icon>arrow_downward</mat-icon>
</button>
<button
mat-button
color="warn"
(click)="$event.stopPropagation(); remove(idx)"
matTooltip="{{ translations.removeTooltip }}"
matTooltipPosition="right"
>
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-panel-description>
</mat-expansion-panel-header>
<mat-card class="array-item" appearance="outlined">
<mat-card-content>
<jsonforms-outlet
[renderProps]="
idx
| getProps
: uischemas
: scopedSchema
: uischema
: propsPath
: rootSchema
"
></jsonforms-outlet>
</mat-card-content>
</mat-card>
</mat-expansion-panel>
}
</mat-accordion>
</div>
Pipe
import { Pipe, PipeTransform } from '@angular/core';
import { findUISchema, Paths } from '@jsonforms/core';
@Pipe({ name: 'getProps', standalone: true })
export class GetProps implements PipeTransform {
transform(
index: number,
uischemas: any,
scopedSchema: any,
uischema: any,
propsPath: any,
rootSchema: any
) {
const uischemaInstance = findUISchema(
uischemas ?? [],
scopedSchema,
uischema.scope,
propsPath,
undefined,
uischema,
rootSchema
);
return {
schema: scopedSchema,
path: Paths.compose(propsPath, `${index}`),
uischema: uischemaInstance,
};
}
}
Result

