I'm building a micro app for editing JSON objects before saving them to my database. The JSON object comes from another micro app I made for parsing SVG code and creating a data object shaped like the interfaces bellow.
export interface OvaadSvgStyleProperty{
property: string;
setting: string;
}
export interface OvaadGraphicAttribute{
attribute: string;
setting: string;
bind? : string;
}
export interface OvaadGraphicObject{
element: string;
selfClosing: boolean;
attributes: OvaadGraphicAttribute[];
styles?: OvaadSvgStyleProperty[];
subElements?: OvaadGraphicObject[];
}
export interface ViewBoxParameters{
x: string;
y: string;
width: string;
height: string;
}
export interface OvaadSvgDataObject extends TitleCore{
graphicId: string;
viewBox: ViewBoxParameters;
coreAttributes: OvaadGraphicAttribute[];
coreStyles: OvaadSvgStyleProperty[];
elements: OvaadGraphicObject[];
}
If you pay attention to the OvaadGraphicObject
interface you'll see the subElements
property is of type OvaadGraphicObject[]
which is to handle instances of <g>
tags and other svg elements that can potentially nest things thousands of layers deep if that were needed for some reason.
I've learned through my struggles so far with Angular Forms that it likes for the form to be a singular monolithic thing so I decided to create a family of functions for generating the form and passing in the values from the JSON object and passing the completed FormGroup
into my form with [formGroup]="myFormVar"
.
This provides the FormGroup
shaped accordingly with all the values passed in as expected. If I treat the data like any other data and pass it into @Input()
s everything breaks down and goes to where it needs to go, but unfortunately it breaks apart the communication within the form. You can view a stackblitz demo here to see the entire mechanism so far but for the sake of this post I'll be focusing on the elements
aspect of the form which will pretty much crack the riddle of how to handle everything else.
To start, the function I use for creating the element FormGroup
looks like this
export function CreateGraphicElementForm(data?: OvaadGraphicObject): FormGroup{
let elementForm: FormGroup = new FormGroup({
element : new FormControl((data ? data.element : ''), Validators.required),
selfClosing : new FormControl((data? data.selfClosing : false), Validators.required),
attributes : new FormArray([]),
styles : new FormArray([]),
subElements : new FormArray([])
});
if(data && data.attributes.length > 0){
data.attributes.forEach((a: OvaadGraphicAttribute)=>{
let attrArray: FormArray = elementForm.get('attributes') as FormArray;
attrArray.push(CreateAttributeForm(a));
});
}
if(data && data.styles.length > 0){
data.styles.forEach((a: OvaadSvgStyleProperty)=>{
let styleArray: FormArray = elementForm.get('styles') as FormArray;
styleArray.push(CreateStylePropertyForm(a));
});
}
if(data && data.subElements){
data.subElements.forEach((a:OvaadGraphicObject)=>{
let subElementArray: FormArray = elementForm.get('subElements') as FormArray;
subElementArray.push(CreateGraphicElementForm(a));
});
}
return elementForm as FormGroup;
}
So within this FormGroup
there's 2 FormControl
s and 3 FormArray
s that need to be passed down into their respective components. The way I'm handling this part is with what I'm calling and Element List Component
that takes an array of these FormGroup
s and is responsible for adding or deleting them. Then I have an ElementFormComponent
which receives a FormGroup
from the ElementListComponent
. Then of course the ElementListComponent
gets it's data from the parent component where the form is created which I call the SvgObjectFormComponent
. Here's what the ElementListComponent
and ElementFormComponent
looks like.
ElementList.component.ts
export class ElementListComponent implements OnInit {
@Input() ElementListData: FormArray;
constructor() { }
ngOnInit() {
}
addElement(): void{
const newElement: FormGroup = CreateGraphicElementForm();
let elementList: FormArray = <FormArray>this.ElementListData as FormArray;
console.log(newElement);
elementList.push(newElement);
}
deleteElement(item: number): void{
let elementList:FormArray = this.ElementListData as FormArray;
console.log(item);
elementList.removeAt(item);
}
}
ElementList.component.html
<section [formGroup]="ElementListData">
<article *ngIf="!ElementListData">
<p>loading</p>
</article>
<article *ngIf="ElementListData">
<h5 *ngIf="ElementListData.controls.length === 0" class="articleText">no elements</h5>
<section *ngIf="ElementListData.controls.length > 0">
<article *ngFor="let item of ElementListData.controls; let i = index" class="list-grid">
<p class="index-area articleText">{{i}}</p>
<!-- I removed attempts at passing in data to avoid discussing things we already know are wrong-->
<element-form-component class="component-area"></element-form-component>
<article class="delete-area">
<button (click)="deleteElement(i)">delete</button>
</article>
</article>
</section>
</article>
<article>
<button (click)="addElement()">add</button>
</article>
</section>
ElementForm.component.ts
export class ElementFormComponent implements OnInit, ControlValueAccessor {
@Input() ElementFormData: FormGroup;
constructor() { }
ngOnInit() {}
}
ElementForm.component.html
<section [formGroup]="ElementFormData">
<article class="text-control-section">
<label class="control-label">
element:
<input type="text" class="text-control" formControlName="element" />
</label>
<label class="control-label">
self closing:
<input type="text" class="text-control" formControlName="selfClosing" />
</label>
</article>
<!-- again I eliminated failed attempts at passing objects into components to reduce confusion -->
<article>
<h3>Attributes</h3>
<attribute-list-component></attribute-list-component>
</article>
<article>
<h3>Styles</h3>
<style-list-component></style-list-component>
</article>
<section>
<h3>Sub Elements</h3>
<p *ngIf="ElementFormData.controls.subElements.length === 0">no sub elements</p>
<article *ngFor="let item of ElementFormData.controls.subElements; let i = index" class="list-grid">
<p class="index-area">{{i}}</p>
<!-- this is where the recursive behavior begins if the element has nested elements -->
<element-form-component class="component-area"></element-form-component>
<article class="delete-area">
<button>delete element</button>
</article>
</article>
</section>
</section>
I've been trying everything from using [(ngModel)]
and [FormControl]
vs. formControlName
vs. [formControlName]
and tinkering with the ControlValueAccessor
just to discover it's only meant for single FormControl
s. On another question I asked which was based around the ControlValueAccessor
someone made the suggestion that I should add this to my component providers
viewProviders: [
{ provide: ControlContainer, useExisting: FormGroupDirective }
which eliminates the need for the ControlValueAccesssor
allowing all the controls to be accessed, but that assumes I'm only using my component in one place in the form syncing to only one FormGroup
when as you see from this example I need to figure out how to provide a recursive connection to the top of the component tree. How can this type of behavior be achieved with Angular Forms?
My stackblitz contains all the components, functions and a data object for demo as well as my most recent attempts at getting this behavior to work which was through the ControlValueAcessor
. Someone please help me figure out how to accomplish this because Angular Forms is the one thing I haven't been able to get a grasp on the entire time I've been using the platform due to not knowing whatever it is I can't seem to find my way in front of.
I came across this article which covers how to accomplish this behavior by using ChangeDetectionStrategy.push
on our components. We add it to our component like this
@Component({
selector : 'element-list-component',
templateUrl : 'element-list.component.html',
StyleUrls : ['element-list.component.css'],
// add change detection here
changeDetection: ChangeDetectionStrategy.push
})
export class ElementListComponent {
@Input() ElementListData : FormGroup;
//.......
}
Then in our Component class we can use a regular @Input()
to pass in the form object as we do with any other piece of data. In the parent component we need to pass in the object like this
<section [formGroup]="FormDataVar">
<element-list-component [ElementListData]="FormDataVar.controls.elements"></element-list-component
</section>
From there we connect the FormControls
to our inputs with formControlName="yourControl"
and everything stays synced to the top of the tree :).