ng-form-foundry 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/ng-package.json +7 -0
- package/package.json +12 -0
- package/src/lib/core/dynamic-recursive-forms-builder.ts +129 -0
- package/src/lib/core/utils.ts +33 -0
- package/src/lib/dynamic-recursive-form/anon-leaf-renderer/anon-leaf-renderer.component.html +31 -0
- package/src/lib/dynamic-recursive-form/anon-leaf-renderer/anon-leaf-renderer.component.scss +11 -0
- package/src/lib/dynamic-recursive-form/anon-leaf-renderer/anon-leaf-renderer.component.spec.ts +22 -0
- package/src/lib/dynamic-recursive-form/anon-leaf-renderer/anon-leaf-renderer.component.ts +39 -0
- package/src/lib/dynamic-recursive-form/dynamic-recursive-form.component.html +168 -0
- package/src/lib/dynamic-recursive-form/dynamic-recursive-form.component.scss +128 -0
- package/src/lib/dynamic-recursive-form/dynamic-recursive-form.component.spec.ts +23 -0
- package/src/lib/dynamic-recursive-form/dynamic-recursive-form.component.ts +71 -0
- package/src/lib/dynamic-recursive-form/dynamic-recursive-form.stories.ts +36 -0
- package/src/lib/dynamic-recursive-form/leaf-enum-renderer/leaf-enum-renderer.component.html +8 -0
- package/src/lib/dynamic-recursive-form/leaf-enum-renderer/leaf-enum-renderer.component.scss +9 -0
- package/src/lib/dynamic-recursive-form/leaf-enum-renderer/leaf-enum-renderer.component.spec.ts +22 -0
- package/src/lib/dynamic-recursive-form/leaf-enum-renderer/leaf-enum-renderer.component.ts +21 -0
- package/src/lib/dynamic-recursive-form/leaf-list-renderer/leaf-list-renderer.component.html +31 -0
- package/src/lib/dynamic-recursive-form/leaf-list-renderer/leaf-list-renderer.component.scss +57 -0
- package/src/lib/dynamic-recursive-form/leaf-list-renderer/leaf-list-renderer.component.spec.ts +23 -0
- package/src/lib/dynamic-recursive-form/leaf-list-renderer/leaf-list-renderer.component.ts +52 -0
- package/src/lib/dynamic-recursive-form/leaf-renderer/leaf-renderer.component.html +17 -0
- package/src/lib/dynamic-recursive-form/leaf-renderer/leaf-renderer.component.scss +11 -0
- package/src/lib/dynamic-recursive-form/leaf-renderer/leaf-renderer.component.spec.ts +22 -0
- package/src/lib/dynamic-recursive-form/leaf-renderer/leaf-renderer.component.ts +41 -0
- package/src/lib/dynamic-recursive-form/node-group-list-renderer/node-group-list-renderer.component.html +24 -0
- package/src/lib/dynamic-recursive-form/node-group-list-renderer/node-group-list-renderer.component.scss +48 -0
- package/src/lib/dynamic-recursive-form/node-group-list-renderer/node-group-list-renderer.component.spec.ts +22 -0
- package/src/lib/dynamic-recursive-form/node-group-list-renderer/node-group-list-renderer.component.ts +101 -0
- package/src/lib/types/dynamic-recursive.types.ts +97 -0
- package/src/lib/types/examples/complex-oai.ts +938 -0
- package/src/public-api.ts +12 -0
- package/tsconfig.lib.json +18 -0
- package/tsconfig.lib.prod.json +11 -0
- package/tsconfig.spec.json +14 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
|
2
|
+
import { LeafRendererComponent } from './leaf-renderer/leaf-renderer.component';
|
|
3
|
+
import { NodeGroup, NodeType } from '../types/dynamic-recursive.types';
|
|
4
|
+
import {
|
|
5
|
+
FormArray,
|
|
6
|
+
FormControl,
|
|
7
|
+
FormGroup,
|
|
8
|
+
ReactiveFormsModule,
|
|
9
|
+
} from '@angular/forms';
|
|
10
|
+
import { NodeGroupListRendererComponent } from './node-group-list-renderer/node-group-list-renderer.component';
|
|
11
|
+
import { LeafListRendererComponent } from './leaf-list-renderer/leaf-list-renderer.component';
|
|
12
|
+
import { MatExpansionModule } from '@angular/material/expansion';
|
|
13
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
14
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
15
|
+
import { MatCardModule } from '@angular/material/card';
|
|
16
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
17
|
+
import { asFormArray, asFormControl, asFormGroup } from '../core/utils';
|
|
18
|
+
|
|
19
|
+
@Component({
|
|
20
|
+
imports: [
|
|
21
|
+
LeafRendererComponent,
|
|
22
|
+
NodeGroupListRendererComponent,
|
|
23
|
+
LeafListRendererComponent,
|
|
24
|
+
ReactiveFormsModule,
|
|
25
|
+
MatExpansionModule,
|
|
26
|
+
MatIconModule,
|
|
27
|
+
MatButtonModule,
|
|
28
|
+
NgTemplateOutlet,
|
|
29
|
+
MatCardModule,
|
|
30
|
+
MatExpansionModule,
|
|
31
|
+
],
|
|
32
|
+
selector: 'nff-dynamic-recursive-form',
|
|
33
|
+
standalone: true,
|
|
34
|
+
styleUrl: './dynamic-recursive-form.component.scss',
|
|
35
|
+
templateUrl: './dynamic-recursive-form.component.html',
|
|
36
|
+
})
|
|
37
|
+
export class DynamicRecursiveFormComponent implements OnInit {
|
|
38
|
+
@Input({ required: true }) schema!: NodeGroup;
|
|
39
|
+
@Input() initialValue!: any;
|
|
40
|
+
@Input() formGroup = new FormGroup<any>({});
|
|
41
|
+
@Input() index: number | null = null;
|
|
42
|
+
@Input() removable: boolean = false;
|
|
43
|
+
@Output() remove = new EventEmitter();
|
|
44
|
+
@Input() title!: string;
|
|
45
|
+
@Input() editable = false;
|
|
46
|
+
root: boolean = false;
|
|
47
|
+
|
|
48
|
+
ngOnInit() {
|
|
49
|
+
this.root = this.schema.root ?? false;
|
|
50
|
+
if (this.initialValue) {
|
|
51
|
+
this.formGroup.patchValue(this.initialValue);
|
|
52
|
+
console.log(this.formGroup.value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get nodeGroupChildrenList(): Array<{ key: string; value: NodeType }> {
|
|
57
|
+
const children = this.schema.children ?? {};
|
|
58
|
+
return Object.entries(children).map(([key, value]) => ({
|
|
59
|
+
key,
|
|
60
|
+
value: value as NodeType,
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
emitRemoveEvent() {
|
|
65
|
+
this.remove.emit();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
protected readonly asFormGroup = asFormGroup;
|
|
69
|
+
protected readonly asFormArray = asFormArray;
|
|
70
|
+
protected readonly asFormControl = asFormControl;
|
|
71
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/angular';
|
|
2
|
+
import { DynamicRecursiveFormComponent } from './dynamic-recursive-form.component';
|
|
3
|
+
import { buildFormFromSchema } from '../core/dynamic-recursive-forms-builder';
|
|
4
|
+
import { DUConfigSchema, sampleValue } from '../types/examples/complex-oai'
|
|
5
|
+
|
|
6
|
+
const meta: Meta<DynamicRecursiveFormComponent> = {
|
|
7
|
+
title: 'ng-form-foundry/DynamicRecursiveFormComponent',
|
|
8
|
+
component:DynamicRecursiveFormComponent,
|
|
9
|
+
};
|
|
10
|
+
export default meta;
|
|
11
|
+
|
|
12
|
+
type Story = StoryObj<DynamicRecursiveFormComponent>;
|
|
13
|
+
|
|
14
|
+
export const complex: Story = ({
|
|
15
|
+
name: "Complex Form",
|
|
16
|
+
args: {
|
|
17
|
+
schema: DUConfigSchema,
|
|
18
|
+
initialValue: sampleValue,
|
|
19
|
+
},
|
|
20
|
+
render: (args) => {
|
|
21
|
+
const formGroup = buildFormFromSchema(args.schema);
|
|
22
|
+
return {
|
|
23
|
+
props: {
|
|
24
|
+
...args,
|
|
25
|
+
formGroup,
|
|
26
|
+
},
|
|
27
|
+
template: `
|
|
28
|
+
<nff-dynamic-recursive-form
|
|
29
|
+
[schema]="DUConfigSchema"
|
|
30
|
+
[formGroup]="formGroup"
|
|
31
|
+
[initialValue]="sampleValue"
|
|
32
|
+
></nff-dynamic-recursive-form>
|
|
33
|
+
`,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<mat-form-field [appearance]="editable ? 'fill' : 'outline'">
|
|
2
|
+
<mat-label>{{ leafEnum.label ?? leafEnum.name }}</mat-label>
|
|
3
|
+
<mat-select [formControl]="control" value="{{ control.value }}">
|
|
4
|
+
@for (option of leafEnum.enum; track $index) {
|
|
5
|
+
<mat-option [value]="option">{{ leafEnum.enumLabel?.[$index] ?? option }}</mat-option>
|
|
6
|
+
}
|
|
7
|
+
</mat-select>
|
|
8
|
+
</mat-form-field>
|
package/src/lib/dynamic-recursive-form/leaf-enum-renderer/leaf-enum-renderer.component.spec.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
|
|
3
|
+
import { LeafEnumRendererComponent } from './leaf-enum-renderer.component';
|
|
4
|
+
|
|
5
|
+
describe('DrfLeafEnumRendererComponent', () => {
|
|
6
|
+
let component: LeafEnumRendererComponent;
|
|
7
|
+
let fixture: ComponentFixture<LeafEnumRendererComponent>;
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
await TestBed.configureTestingModule({
|
|
11
|
+
imports: [LeafEnumRendererComponent],
|
|
12
|
+
}).compileComponents();
|
|
13
|
+
|
|
14
|
+
fixture = TestBed.createComponent(LeafEnumRendererComponent);
|
|
15
|
+
component = fixture.componentInstance;
|
|
16
|
+
fixture.detectChanges();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should create', () => {
|
|
20
|
+
expect(component).toBeTruthy();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Component, Input } from '@angular/core';
|
|
2
|
+
import { LeafEnum } from '../../types/dynamic-recursive.types';
|
|
3
|
+
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
|
4
|
+
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
5
|
+
import { MatSelectModule } from '@angular/material/select';
|
|
6
|
+
|
|
7
|
+
@Component({
|
|
8
|
+
selector: 'nff-leaf-enum-renderer',
|
|
9
|
+
standalone: true,
|
|
10
|
+
imports: [ReactiveFormsModule, MatFormFieldModule, MatSelectModule],
|
|
11
|
+
templateUrl: './leaf-enum-renderer.component.html',
|
|
12
|
+
styleUrl: './leaf-enum-renderer.component.scss',
|
|
13
|
+
})
|
|
14
|
+
export class LeafEnumRendererComponent {
|
|
15
|
+
@Input() leafEnum!: LeafEnum;
|
|
16
|
+
@Input() initialValue!: string;
|
|
17
|
+
@Input() control = new FormControl();
|
|
18
|
+
@Input() removable: boolean = false;
|
|
19
|
+
@Input() remove: any;
|
|
20
|
+
@Input() editable: boolean = true;
|
|
21
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
@if (formArray && leaf_) {
|
|
2
|
+
<div class="fields">
|
|
3
|
+
@for (control of formArray.controls; track $index) {
|
|
4
|
+
<div class="field-item">
|
|
5
|
+
<nff-anon-leaf-renderer
|
|
6
|
+
[AnonLeaf]="leaf_" [control]="control"
|
|
7
|
+
[removable]="minItems == null || formArray.length > minItems"
|
|
8
|
+
[index]="$index"
|
|
9
|
+
[editable]="editable"
|
|
10
|
+
[label]="leaf_.label ?? leaf_.name"
|
|
11
|
+
(remove)="removeItem($index)"
|
|
12
|
+
/>
|
|
13
|
+
<div class="actions">
|
|
14
|
+
@if (formArray.length > minItems && editable) {
|
|
15
|
+
<button class="remove-button small-icon-button" matIconButton (click)="removeItem($index)">
|
|
16
|
+
<mat-icon matPrefix>delete</mat-icon>
|
|
17
|
+
</button>
|
|
18
|
+
}
|
|
19
|
+
@if ($last) {
|
|
20
|
+
@if (editable) {
|
|
21
|
+
<button class="add-button small-icon-button" matIconButton (click)="addItem()">
|
|
22
|
+
<mat-icon matPrefix>add</mat-icon>
|
|
23
|
+
</button>
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
<div class="actions-spacer"></div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
}
|
|
30
|
+
</div>
|
|
31
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
@use '@angular/material' as mat;
|
|
2
|
+
|
|
3
|
+
:host {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
flex-wrap: wrap;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.fields-container {
|
|
10
|
+
position: relative;
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-wrap: wrap;
|
|
13
|
+
gap: 8px;
|
|
14
|
+
align-items: baseline;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.field-item {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: row;
|
|
20
|
+
gap: 8px;
|
|
21
|
+
align-items: center;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.actions {
|
|
25
|
+
z-index: 100;
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column; /* stack buttons vertically */
|
|
28
|
+
align-items: flex-end;
|
|
29
|
+
gap: 4px;
|
|
30
|
+
height: 100%;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.actions-spacer {
|
|
34
|
+
height: 20px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
nff-anon-leaf-renderer {
|
|
38
|
+
width: 100%;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.add-button {
|
|
42
|
+
background-color: var(--mat-sys-primary-container);
|
|
43
|
+
@include mat.elevation(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.remove-button {
|
|
47
|
+
background-color: var(--mat-sys-error-container);
|
|
48
|
+
@include mat.elevation(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.title {
|
|
52
|
+
display: flex;
|
|
53
|
+
flex-direction: row;
|
|
54
|
+
gap: 8px;
|
|
55
|
+
align-items: center;
|
|
56
|
+
}
|
|
57
|
+
|
package/src/lib/dynamic-recursive-form/leaf-list-renderer/leaf-list-renderer.component.spec.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
|
|
3
|
+
import { LeafListRendererComponent } from './leaf-list-renderer.component';
|
|
4
|
+
|
|
5
|
+
describe('DrfLeafListRendererComponent', () => {
|
|
6
|
+
let component: LeafListRendererComponent;
|
|
7
|
+
let fixture: ComponentFixture<LeafListRendererComponent>;
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
await TestBed.configureTestingModule({
|
|
11
|
+
imports: [LeafListRendererComponent]
|
|
12
|
+
})
|
|
13
|
+
.compileComponents();
|
|
14
|
+
|
|
15
|
+
fixture = TestBed.createComponent(LeafListRendererComponent);
|
|
16
|
+
component = fixture.componentInstance;
|
|
17
|
+
fixture.detectChanges();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should create', () => {
|
|
21
|
+
expect(component).toBeTruthy();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Component, EventEmitter, inject, Input, OnInit, Output } from '@angular/core';
|
|
2
|
+
import { LeafList } from '../../types/dynamic-recursive.types';
|
|
3
|
+
import { FormControl } from '@angular/forms';
|
|
4
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
5
|
+
import { AnonLeafRendererComponent } from '../anon-leaf-renderer/anon-leaf-renderer.component';
|
|
6
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
7
|
+
import { MatDialog } from '@angular/material/dialog';
|
|
8
|
+
import { MatPrefix } from '@angular/material/input';
|
|
9
|
+
|
|
10
|
+
@Component({
|
|
11
|
+
selector: 'nff-leaf-list-renderer',
|
|
12
|
+
standalone: true,
|
|
13
|
+
imports: [
|
|
14
|
+
MatIconModule,
|
|
15
|
+
AnonLeafRendererComponent,
|
|
16
|
+
MatButtonModule,
|
|
17
|
+
MatPrefix,
|
|
18
|
+
],
|
|
19
|
+
templateUrl: './leaf-list-renderer.component.html',
|
|
20
|
+
styleUrl: './leaf-list-renderer.component.scss',
|
|
21
|
+
})
|
|
22
|
+
export class LeafListRendererComponent implements OnInit {
|
|
23
|
+
@Input() leaf_!: LeafList;
|
|
24
|
+
@Input() initialValue!: number[] | string[] | boolean[];
|
|
25
|
+
@Input() formArray!: any;
|
|
26
|
+
@Input() editable: boolean = true;
|
|
27
|
+
@Input() minItems: number = 1;
|
|
28
|
+
@Output() message = new EventEmitter();
|
|
29
|
+
|
|
30
|
+
matDialog = inject(MatDialog);
|
|
31
|
+
|
|
32
|
+
ngOnInit() {
|
|
33
|
+
if (this.initialValue) {
|
|
34
|
+
this.formArray.patchValue(this.initialValue);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
removeItem($index: number) {
|
|
39
|
+
if (this.formArray.length <= this.minItems) {
|
|
40
|
+
this.message.emit({
|
|
41
|
+
message: 'You cannot remove the last item!',
|
|
42
|
+
type: 'error',
|
|
43
|
+
})
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
this.formArray.removeAt($index);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
addItem() {
|
|
50
|
+
this.formArray.push(new FormControl());
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
@if ('name' in leaf_) {
|
|
2
|
+
@if (leaf_.type === 'string') {
|
|
3
|
+
<mat-form-field [appearance]="editable ? 'fill' : 'outline'">
|
|
4
|
+
<mat-label>{{ leaf_.label ?? leaf_.name }}</mat-label>
|
|
5
|
+
<input [readonly]="!editable" matInput type="text" [formControl]="control">
|
|
6
|
+
</mat-form-field>
|
|
7
|
+
} @else if (leaf_.type === 'number') {
|
|
8
|
+
<mat-form-field [appearance]="editable ? 'fill' : 'outline'">
|
|
9
|
+
<mat-label>{{ leaf_.label ?? leaf_.name }}</mat-label>
|
|
10
|
+
<input [readonly]="!editable" matInput type="number" [formControl]="control">
|
|
11
|
+
</mat-form-field>
|
|
12
|
+
} @else if (leaf_.type === 'boolean') {
|
|
13
|
+
<mat-checkbox [formControl]="control">{{ leaf_.label ?? leaf_.name }}</mat-checkbox>
|
|
14
|
+
} @else if (leaf_.type === 'enum') {
|
|
15
|
+
<nff-leaf-enum-renderer [editable]="editable" [leafEnum]="leaf_" [control]="control"></nff-leaf-enum-renderer>
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
|
|
3
|
+
import { LeafRendererComponent } from './leaf-renderer.component';
|
|
4
|
+
|
|
5
|
+
describe('DrfLeafRendererComponent', () => {
|
|
6
|
+
let component: LeafRendererComponent;
|
|
7
|
+
let fixture: ComponentFixture<LeafRendererComponent>;
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
await TestBed.configureTestingModule({
|
|
11
|
+
imports: [LeafRendererComponent],
|
|
12
|
+
}).compileComponents();
|
|
13
|
+
|
|
14
|
+
fixture = TestBed.createComponent(LeafRendererComponent);
|
|
15
|
+
component = fixture.componentInstance;
|
|
16
|
+
fixture.detectChanges();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should create', () => {
|
|
20
|
+
expect(component).toBeTruthy();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
|
2
|
+
import { AnonLeaf, Leaf } from '../../types/dynamic-recursive.types';
|
|
3
|
+
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
|
4
|
+
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
5
|
+
import { MatInputModule } from '@angular/material/input';
|
|
6
|
+
import { MatCheckboxModule } from '@angular/material/checkbox';
|
|
7
|
+
import { MatSelectModule } from '@angular/material/select';
|
|
8
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
9
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
10
|
+
import { LeafEnumRendererComponent } from '../leaf-enum-renderer/leaf-enum-renderer.component';
|
|
11
|
+
|
|
12
|
+
@Component({
|
|
13
|
+
selector: 'nff-leaf-renderer',
|
|
14
|
+
standalone: true,
|
|
15
|
+
imports: [
|
|
16
|
+
MatFormFieldModule,
|
|
17
|
+
ReactiveFormsModule,
|
|
18
|
+
MatInputModule,
|
|
19
|
+
MatCheckboxModule,
|
|
20
|
+
MatSelectModule,
|
|
21
|
+
MatIconModule,
|
|
22
|
+
MatButtonModule,
|
|
23
|
+
LeafEnumRendererComponent,
|
|
24
|
+
],
|
|
25
|
+
templateUrl: './leaf-renderer.component.html',
|
|
26
|
+
styleUrl: './leaf-renderer.component.scss',
|
|
27
|
+
})
|
|
28
|
+
export class LeafRendererComponent implements OnInit {
|
|
29
|
+
@Input() leaf_!: Leaf | AnonLeaf;
|
|
30
|
+
@Input() control: FormControl = new FormControl();
|
|
31
|
+
@Input() initialValue?: any;
|
|
32
|
+
@Input() removable: boolean = false;
|
|
33
|
+
@Input() editable = true;
|
|
34
|
+
@Output() remove: EventEmitter<number> = new EventEmitter();
|
|
35
|
+
|
|
36
|
+
ngOnInit(): void {
|
|
37
|
+
if (this.initialValue) {
|
|
38
|
+
this.control.patchValue(this.initialValue);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
@if (formArray && nodeGroupList) {
|
|
2
|
+
<div class="fields-container">
|
|
3
|
+
@for (group of formArray.controls; track $index) {
|
|
4
|
+
<div class="field-item">
|
|
5
|
+
<nff-dynamic-recursive-form
|
|
6
|
+
[schema]="nodeGroupList.type"
|
|
7
|
+
[formGroup]="asFormGroup(group)"
|
|
8
|
+
[title]="getTitle($index)"
|
|
9
|
+
[index]="$index"
|
|
10
|
+
[removable]="formArray.length > minItems"
|
|
11
|
+
(remove)="removeItem($index)"
|
|
12
|
+
[editable]="editable"
|
|
13
|
+
/>
|
|
14
|
+
<div class="actions">
|
|
15
|
+
<button matIconButton class="small-icon-button"
|
|
16
|
+
matTooltip=" + Add new {{ nodeGroupList.type.label ?? nodeGroupList.type.name }}"
|
|
17
|
+
(click)="addItem()">
|
|
18
|
+
<mat-icon>add</mat-icon>
|
|
19
|
+
</button>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
}
|
|
23
|
+
</div>
|
|
24
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
position: relative;
|
|
3
|
+
width: 100%;
|
|
4
|
+
flex: 1 1 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.fields {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
flex-wrap: wrap;
|
|
11
|
+
width: 100%;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.fields-container {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.field-item {
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: row;
|
|
22
|
+
gap: 8px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.actions {
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: row;
|
|
28
|
+
gap: 8px;
|
|
29
|
+
align-items: center;
|
|
30
|
+
margin-top: 0;
|
|
31
|
+
padding: 0;
|
|
32
|
+
min-width: 24px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.title {
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
gap: 8px;
|
|
39
|
+
align-items: center;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
nff-dynamic-recursive-form {
|
|
43
|
+
flex: 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.add-button {
|
|
47
|
+
background-color: var(--mat-sys-primary-container);
|
|
48
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
|
|
3
|
+
import { NodeGroupListRendererComponent } from './node-group-list-renderer.component';
|
|
4
|
+
|
|
5
|
+
describe('DrfNodeListRendererComponent', () => {
|
|
6
|
+
let component: NodeGroupListRendererComponent;
|
|
7
|
+
let fixture: ComponentFixture<NodeGroupListRendererComponent>;
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
await TestBed.configureTestingModule({
|
|
11
|
+
imports: [NodeGroupListRendererComponent],
|
|
12
|
+
}).compileComponents();
|
|
13
|
+
|
|
14
|
+
fixture = TestBed.createComponent(NodeGroupListRendererComponent);
|
|
15
|
+
component = fixture.componentInstance;
|
|
16
|
+
fixture.detectChanges();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should create', () => {
|
|
20
|
+
expect(component).toBeTruthy();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AfterViewInit,
|
|
3
|
+
ChangeDetectorRef,
|
|
4
|
+
Component,
|
|
5
|
+
EventEmitter,
|
|
6
|
+
forwardRef,
|
|
7
|
+
inject,
|
|
8
|
+
Input,
|
|
9
|
+
OnInit,
|
|
10
|
+
Output,
|
|
11
|
+
QueryList,
|
|
12
|
+
ViewChildren
|
|
13
|
+
} from '@angular/core';
|
|
14
|
+
import { NodeGroupList } from '../../types/dynamic-recursive.types';
|
|
15
|
+
import { FormArray, FormGroup } from '@angular/forms';
|
|
16
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
17
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
18
|
+
import { DynamicRecursiveFormComponent } from '../dynamic-recursive-form.component';
|
|
19
|
+
import { buildFormFromSchema } from '../../core/dynamic-recursive-forms-builder';
|
|
20
|
+
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
21
|
+
|
|
22
|
+
@Component({
|
|
23
|
+
selector: 'nff-node-group-list-renderer',
|
|
24
|
+
standalone: true,
|
|
25
|
+
imports: [
|
|
26
|
+
forwardRef(() => DynamicRecursiveFormComponent),
|
|
27
|
+
MatButtonModule,
|
|
28
|
+
MatIconModule,
|
|
29
|
+
MatTooltipModule,
|
|
30
|
+
],
|
|
31
|
+
templateUrl: './node-group-list-renderer.component.html',
|
|
32
|
+
styleUrl: './node-group-list-renderer.component.scss',
|
|
33
|
+
})
|
|
34
|
+
export class NodeGroupListRendererComponent implements OnInit, AfterViewInit {
|
|
35
|
+
@Input() nodeGroupList!: NodeGroupList;
|
|
36
|
+
@Input() initialValue!: number[] | string[] | boolean[];
|
|
37
|
+
@Input() formArray = new FormArray<any>([]);
|
|
38
|
+
@Input() editable: boolean = true;
|
|
39
|
+
@Input() minItems: number = 1;
|
|
40
|
+
@Input() maxItems: number = 1;
|
|
41
|
+
@Output() message = new EventEmitter();
|
|
42
|
+
@ViewChildren(DynamicRecursiveFormComponent) items!: QueryList<DynamicRecursiveFormComponent>;
|
|
43
|
+
|
|
44
|
+
cdr = inject(ChangeDetectorRef);
|
|
45
|
+
|
|
46
|
+
ngOnInit() {
|
|
47
|
+
if (this.initialValue) {
|
|
48
|
+
this.formArray.patchValue(this.initialValue);
|
|
49
|
+
}
|
|
50
|
+
if (this.nodeGroupList.maxItems) {
|
|
51
|
+
this.maxItems = this.nodeGroupList.maxItems;
|
|
52
|
+
}
|
|
53
|
+
if (this.nodeGroupList.minItems) {
|
|
54
|
+
this.minItems = this.nodeGroupList.minItems;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
ngAfterViewInit() {
|
|
59
|
+
this.items.changes.subscribe(() => {
|
|
60
|
+
this.setLastEditable();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
removeItem($index: number) {
|
|
65
|
+
if (this.formArray.length <= this.minItems) {
|
|
66
|
+
this.message.emit({
|
|
67
|
+
message: `You cannot remove the last ${this.nodeGroupList.type.name} configuration!`,
|
|
68
|
+
type: 'error',
|
|
69
|
+
})
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
this.formArray.removeAt($index);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
addItem() {
|
|
76
|
+
this.formArray.push(buildFormFromSchema(this.nodeGroupList.type, null));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
setLastEditable() {
|
|
80
|
+
const lastItem = this.items.last;
|
|
81
|
+
if (lastItem) {
|
|
82
|
+
lastItem.editable = this.editable;
|
|
83
|
+
}
|
|
84
|
+
this.cdr.detectChanges();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
asFormGroup(group: any) {
|
|
88
|
+
return group as FormGroup;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getTitle($index: number) {
|
|
92
|
+
let title = `${this.nodeGroupList.type.label ?? this.nodeGroupList.type.name}`;
|
|
93
|
+
if (this.formArray.length > 1) {
|
|
94
|
+
return `${title} #${$index + 1}`;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
return title;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
}
|
|
101
|
+
}
|