I know this question might sound familiar and repetitive and already answered plenty of times. Please bear with me. I'm posting only because I could not resolve my issue despite trying the solutions expressed in other questions.
I have a component <od-resource-table [..] [..]></od-resource-table> rendered 2 times in the page. It's basically a table and comes with controls to show/hide columns. The problem I'm facing is that when I tried to hide a column in second table it's affecting the first table and not the table I intended. If I remove the first table, then no problems. Please advise. What am I missing Thanks in advance.
Here's the code that emits
onChange(event: MouseEvent, field: string) {
this.onToggleColumnVisibility.next(field)
event.stopPropagation();
}
Here's the gif showcasing the behavior
http://g.recordit.co/AoKbazXdr2.gif
search control component contains column picker controls
#Component({
selector: 'od-search-control',
templateUrl: './search-control.component.html',
styleUrls: ['./search-control.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
})
export class SearchControlComponent {
#Input() searchText: string = '';
#Input() searchPlaceholder: string = 'Search';
#Input() downloadUrl: any = '';
#Input() columnPickerDefs: Array<ISearchControl.ColumnPickerDef> = [];
#Output() onRefresh = new EventEmitter<any>();
#Output() onToggleColumnVisibility = new EventEmitter<any>();
#Output() onTextChange = new EventEmitter<any>();
constructor() { }
onChange(event: MouseEvent, field: string) {
event.stopPropagation();
this.onToggleColumnVisibility.next(field)
}
}
resource table component
<div class="od-search-controls-bar">
<div class="header">
<h3>
<span class="page-title">{{title}}</span>
<span class="estimate">{{(estimate$ | async) | lpad : 2 : '0'}}</span>
</h3>
<div class="search-control-bar-wrapper">
<od-search-control
...
...
...
(onToggleColumnVisibility)="onToggleColumnVisibility($event)"
></od-search-control>
</div>
</div>
</div>
<od-data-table
#resTable
[columnDefs]="columnDefs"
[rowData]="rowData$ | async"
[estimates]="estimate$ | async"
[currentPage]="currentPage$ | async"
[itemsPerPage]="itemsPerPage$ | async"
(onPageChange)="onPageChange($event)"
(sortModel)="onSort($event)"
></od-data-table>
#Component({
selector: "od-resource-table",
templateUrl: "./resource-table.component.html",
styleUrls: ["./resource-table.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ResourceTableComponent implements OnInit, OnDestroy, OnChanges {
#Input() category: string;
...
...
onToggleColumnVisibility(columnField: string) {
let columnIndex = this.columnDefs.findIndex(col => col.field === columnField);
if (columnIndex > -1) {
this.columnDefs[columnIndex].hide = !this.columnDefs[columnIndex].hide;
this.columnDefs = [...this.columnDefs];
}
}
}
Page containing 2 tables
#Component({
selector: 'od-resource-assignment-view',
template: `
<div class="resource-navigation-panel">
<label class="active"
routerLink="/console-settings/resource-groups/"
>Resource Groups</label>
<label class="text-ellipsis"><span class="active">/</span>{{resourceGroupName}}</label>
</div>
<div class="assignment-resource-wrapper">
<od-resource-table #assignedResourcesTable
[resources]="(resourceGroupType$ | async)"
[resourceGroupId]="resourceGroupId"
[enableNodeSelection]="true"
title="Assigned Resources"
category="resource group">
</od-resource-table>
<div class="assign-button-wrapper">
<button type="button" class="btn btn-secondary" (click)="unassignResources()">
DEASSIGN
</button>
</div>
</div>
<div>
<od-resource-table #enterpriseResourcesTable
[resources]="(resourceGroupType$ | async)"
[enableNodeSelection]="true"
[exclude]="(excludeResources$ | async)"
title="Total Resources"
category="enterprise">
</od-resource-table>
<div class="assign-button-wrapper">
<button type="button" class="btn btn-secondary" (click)="assignResources()">
ASSIGN
</button>
</div>
</div>
`,
styleUrls: ['./resource-assignment-view.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
})
export class ResourcesAssignmentViewComponent implements OnInit, OnDestroy {
#Input() resourceGroupId: string;
#Input() resourceGroupName: string;
#ViewChild('assignedResourcesTable') assignedResourcesTable: ResourceTableComponent
#ViewChild('enterpriseResourcesTable') enterpriseResourcesTable: ResourceTableComponent
resourceGroupType$: Observable<string>
excludeResources$: Observable<string[]>
resourceGroup: IResourceGroups.ResourceGroupItem
_subs: Subscription[] = new Array<Subscription>()
constructor(private _store: Store<IResourceGroups.ResourceGroupsState>) { }
ngOnInit() {}
...
...
...
}
That is because both of the tables are using the same instance of onChange and when you perform action, the first table receives the event first and then you execute event.stopPropagation();
within the listener.
Solution:
Try to differentiate the tables in the listener (onChange) or use the separate instance of the listener.
Related
I wonder how to set default value in RxJs with BehaviorSubject, so I would like to have default value 15 images. From my interface I would like take only 15 urls, and how to subscibe them. I would be very grateful if someone could explain to me how to make it work.
app component
import { Component, OnInit } from '#angular/core';
import { BehaviorSubject } from 'rxjs';
import { DogInfo } from './interface/dogInfo';
import { HttpService } from './service/http.service';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit{
constructor(private httpService: HttpService) { }
items$: any = new BehaviorSubject<DogInfo[]>();
ngOnInit() {
this.items$.subscribe((item: any) => this.httpService.fetchDogsFromApi());
}
}
interface
export interface DogInfo{
id: number;
name?: string;
breadGroup?: string;
lifeSpan?: string;
breadFor?: string;
temperament?: string[];
url: string;
}
You can use like below code:
items$: any = new BehaviorSubject<DogInfo[]>([{id: 0, url: 'your-url'}]);
Probably there is a better way, you could use take and grab the first 15 items from dogs, and set that value to your custom Subject as:
items$ = new BehaviorSubject<DogInfo[]>([]); // initialize as an empty array
private dogsArray: DogInfo[] = [];
constructor(private http: HttpService) {}
ngOnInit(): void {
this.http.fetchDogsFromApi().pipe(
take(15),
tap((dog) => this.dogsArray.push(dog))
).subscribe({
// after the 15 dogs are taken, it will emit that data to the template
complete: () => this.items$.next(this.dogsArray)
});
}
HTML
<ng-container *ngIf="(items$ | async) as dogs">
<div *ngFor="let dog of dogs; let i = index">
<p>Dog #: {{ i }}</p>
<p>Name: {{ dog.name }}</p>
<p>...</p>
</div>
</ng-container>
currently I trying to project a third component in a child component which is projected inside ngFor loop (inside child), but in parent whenever I change or set some property in the projected content using index of query list (ViewChildren('#thirdComponent')) in parent all the child's projected content shows same change. Is there any proper way of doing this.
Is it due to duplicating of select property binding at the place of content projection in child component.Child's projection is done inside a accordion with one or many panels opened at a time.
#Component({
selector: "my-app",
template: `
<child-comp #child>
<ng-container selected>
<some-other-comp #someOtherComp></some-other-comp>
</ng-container>
</child-comp>
`,
styleUrls: ["./app.component.css"]
})
export class AppComponent implements AfterViewInit {
h = 0;
i = 1;
j = 2;
k = 3;
#ViewChildren("someOtherComp") otherCompList: QueryList<SomeOtherComponent>;
ngAfterViewInit(): void {
this.otherCompList.toArray()[this.h].prop = this.h;
// below will result in undefined due to QueryList of size 1
// this.otherCompList.toArray()[this.i].prop = this.i;
// this.otherCompList.toArray()[this.j].prop = this.j;
// this.otherCompList.toArray()[this.k].prop = this.k;
}
}
#Component({
selector: "child-comp",
template: `
<div *ngFor="let value of [1, 2, 3]; let i = index">
<!-- if ngIf is removed than only the last projection is diplayed -->
<div *ngIf="i === 0">
<ng-content select="[selected]"> </ng-content>
</div>
</div>
`,
styleUrls: ["./app.component.css"]
})
export class ChildComponent {}
#Component({
selector: "some-other-comp",
template: `
<p>{{ prop }}</p>
`,
styleUrls: ["./app.component.css"]
})
export class SomeOtherComponent {
prop: any;
}
Stackblitz
Utilizing *ngTemplateOutlet and let-variables
We can pass along a template into our child-component, and utilize the #Input() decorator in conjunction with *ngTemplateOutlet to directly access the property from the HTML template in the parent.
Example
First, I've defined an array in my parent component which I want to use as the basis for my loop in my outer-child component.
Parent Component
#Component({
selector: 'parent',
templateUrl: 'parent.component.html',
styleUrls: ['parent.component.scss']
})
export class ParentComponent implements OnInit {
dataItems: { title: string, description: string }[] = [{
title: 'First Element',
description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eveniet, nihil!'
}...] // remaining items truncated for brevity.
constructor() {
}
ngOnInit(): void {
}
}
This parent component then has a child component, which takes an input of the entire list of items
<child [items]="dataItems"></child>
Child-Component (fist level)
#Component({
selector: 'child',
templateUrl: 'child.component.html',
styleUrls: ['child.component.scss']
})
export class ChildComponent implements OnInit {
#Input() items!: any[];
constructor() {
}
ngOnInit(): void {
}
}
<ng-container *ngFor="let childItem of items">
<projected [item]="childItem">
<ng-template let-item>
<h4>{{item.title}}</h4>
<p>{{item.description}}</p>
</ng-template>
</projected>
</ng-container>
Projected component (sub-child)
#Component({
selector: 'projected',
templateUrl: 'projected.component.html',
styleUrls: ['projected.component.scss']
})
export class ProjectedComponent implements OnInit {
#Input() item: any;
#ContentChild(TemplateRef) templateOutlet!: TemplateRef<any>
constructor() {
}
ngOnInit(): void {
}
}
<ng-container *ngTemplateOutlet="templateOutlet; context: {$implicit: item}"></ng-container>
<ng-content></ng-content>
How does it work
The Parent Component isn't strictly necessary in this relationship, as we aren't projecting content directly from the parent into the ProjectedComponent, I simply chose to define a list of items here to keep a hierarchy similar to your question.
The Child Component
The child component does two things:
Defines a *ngFor loop to loop thru some collection of elements.
Defines a template for how these elements should be utilized in the ProjectedComponent's template.
In the ProjectedComponent we utilize the #ContentChild decorator to select the TemplateRef which we expect to be given via <ng-content>
This template is then put into a container using the *ngTemplateOutlet which also allows us to create a data-binding context to a local variable.
the context: {$implicit: item} tells Angular that any let-* variable defined on the template without any explicit binding should bind to the item property in our component.
Thus, we are able to reference this property in the template at the parent-component level.
Edit
Technically, the context binding is not necessary if you want to define the template directly inside of the child component, as you have a direct reference to the *ngFor template, however it becomes necessary if you want to lift the template out to the ParentComponent level to make the solution more reusable.
You are correct the reason for the bug (changing just the last element) is because when rendered you have multiple elements with the same select value.
A possible solution is to use template reference to pass the desired child component from the top level to the place where you want it to be projected.
Here is a working StackBlitz
import {
AfterViewInit,
Component,
Input,
QueryList,
ViewChildren
} from "#angular/core";
#Component({
selector: "my-app",
template: `
<child-comp #child [templateRef]="templateRef"> </child-comp>
<ng-template #templateRef>
<some-other-comp #someOtherComp></some-other-comp>
</ng-template>
`,
styleUrls: ["./app.component.css"]
})
export class AppComponent implements AfterViewInit {
h = 0;
i = 1;
j = 2;
k = 3;
#ViewChildren("someOtherComp") otherCompList: QueryList<SomeOtherComponent>;
ngAfterViewInit(): void {
this.otherCompList.toArray()[this.h].prop = this.h;
this.otherCompList.toArray()[this.i].prop = this.i;
this.otherCompList.toArray()[this.j].prop = this.j;
this.otherCompList.toArray()[this.k].prop = this.k;
}
}
#Component({
selector: "child-comp",
template: `
<div *ngFor="let value of [1, 2, 3, 4]; let i = index">
<!-- if ngIf is removed than only the last projection is diplayed -->
<ng-container *ngTemplateOutlet="templateRef"></ng-container>
</div>
`,
styleUrls: ["./app.component.css"]
})
export class ChildComponent {
#Input() templateRef;
}
#Component({
selector: "some-other-comp",
template: `
<p>{{ prop }}</p>
`,
styleUrls: ["./app.component.css"]
})
export class SomeOtherComponent {
prop: any;
}
I am trying to display popover inside container but it does not work,
if I remove the *ngIf it works
where ever there if *ngIf it doesn't render
<div class="container" *ngIf="data" >
<button
class="popover"
data-trigger="hover"
data-toggle="tooltip"
data-content="hello"
data-container="body">
<mat-icon>
info
</mat-icon>
</button>
</div>
//ts file
export class SomeComponent implements OnInit {
//... variables
//... constructor
ngOnInit() {
$('.popover').popover({
boundary: 'viewport',
placement: 'top',
container:'body',
sanitize: true,
appendToBody: true
})
}
}```
This does not work because the div.container is not rendered by angular by the time when ngOnInit() is called by angular. Instead you can use the AfterViewInit Lifecycle hook as shown below.
Pay attention that the paragraph with the ngIf can't be loaded in the ngOnInit but it can be loaded in the ngAfterViewInit.
Stackblitz code: https://stackblitz.com/edit/angular-ngif-lifecycle-hook
component.html
<p id="p1">
Without ngIf
</p>
<p id="p2" *ngIf="data">
With ngIf
</p>
component.ts
import { Component, VERSION, OnInit, AfterViewInit } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit, AfterViewInit{
data = {random: 'text'};
ngOnInit() {
const withoutNgIf = document.getElementById('p1');
const withNgIf = document.getElementById('p2');
console.log('OnInit without ngIf: ', withoutNgIf);
# Output: HTMLParagraphElement
console.log('OnInit with ngIf: ', withNgIf);
# Output: null
}
ngAfterViewInit() {
const withNgIf = document.getElementById('p2');
console.log('AfterViewInit with ngIf: ', withNgIf);
# Output: HTMLParagraphElement
}
}
I hope that helps you to understand the problem.
Tip: I would suggest to use the ViewChild decorator to access the DOM instead of jquery if you are using angular. (Example: https://dev.to/danielpdev/how-to-use-viewchild-decorator-in-angular-9-i0)
I am actually trying to inject the array and the data inside the array to another component but is constantly getting errors.
My list.component.ts
Here i injected the itemList array from app.component and this component is working just fine. No errors here.
import { Component, OnInit, Input, Output, EventEmitter } from '#angular/core';
import {List} from './list.model'
#Component({
selector: 'app-list',
templateUrl: './list.component.html',
styleUrls: ['./list.component.css']
})
export class ListComponent implements OnInit {
#Input() itemList: List[] = [];
#Output() onItemSelected: EventEmitter<List>;
private currentItem: List;
constructor(){
this.onItemSelected = new EventEmitter();
}
onClick(list: List): void {
this.currentItem = list;
this.onItemSelected.emit(list);
console.log(`clicking list title: ${list.title}`);
}
isSelected(list: List): boolean {
if (!list || !this.currentItem) {
return false;
}
return list.title === this.currentItem.title;
}
ngOnInit() {
}
}
list.component.html
Here i try to inject both the array and then using ngFor i try to inject the single list also.
<div class="ui grid posts">
<app-list-row
[lists]="itemList"
*ngFor="let list of itemList"
[list]="list"
(click)='onClick(list)'
[class.selected]="isSelected(list)">
</app-list-row>
</div>
list-row.component.ts
I am mainly trying to input the array in this component so that i can use the splice method to delete my list. I tried the delete list;method but this says i cannot use delete in strict mode. Therefore i am trying to input the array and use the splice method.
import { Component, OnInit, Input} from '#angular/core';
import {List} from '.././list/list.model';
#Component({
selector: 'app-list-row',
inputs: ['list: List'],
templateUrl: './list-row.component.html',
styleUrls: ['./list-row.component.css'],
host: {'class': 'row'},
})
export class ListRowComponent implements OnInit {
list: List;
#Input() lists: List[];
deletelist(list: List): void {
let index: number = this.lists.indexOf(list);
if (index !== -1) {
this.lists.splice(index, 1);
}
}
ngOnInit() {
}
}
list-row.component.html
In this write a div and use a lable of delete icon and give an event of click with the "deleteList(list)".
<div class="Eight wide column left aligned title">
<div class="value">
<div class = "hello">
<b>
{{ list.title | uppercase }}
</b>
<div style="float: right;" class="ui label">
<i class="delete icon"
(click)="deleteList(list)"
></i>
</div>
</div>
</div>
</div>
These are my codes and i dont know whether i can do the dependency injection of both the array and its single data in the array. If i can, what ways are there to do it. while running in server the console error is
Unhandled Promise rejection: Template parse errors:
Can't bind to 'list' since it isn't a known property of 'app-list-row'.
1. If 'app-list-row' is an Angular component and it has 'list' input, then verify that it is part of this module.
2. If 'app-list-row' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '#NgModule.schemas' of this component to suppress this message.
("
[lists]="itemList"
*ngFor="let list of itemList"
[ERROR ->][list]="list"
(click)='onClick(list)'
[class.selected]="isSelected(list)">
"): ListComponent#4:2 ; Zone: <root> ; Task: Promise.then ; Value: SyntaxError {__zone_symbol__error: Error: Template parse errors:
Can't bind to 'list' since it isn't a known property of 'app-list-row'…, _nativeError: ZoneAwareError, __zone_symbol__currentTask: ZoneTask, __zone_symbol__stack: "Error: Template parse errors:↵Can't bind to 'list'…ttp://localhost:4200/polyfills.bundle.js:6060:47)", __zone_symbol__message: "Template parse errors:↵Can't bind to 'list' since …lected]="isSelected(list)">↵"): ListComponent#4:2"} Error: Template parse errors:
Can't bind to 'list' since it isn't a known property of 'app-list-row'.
1. If 'app-list-row' is an Angular component and it has 'list' input, then verify that it is part of this module.
2. If 'app-list-row' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '#NgModule.schemas' of this component to suppress this message.
("
[lists]="itemList"
*ngFor="let list of itemList"
[ERROR ->][list]="list"
(click)='onClick(list)'
[class.selected]="isSelected(list)">
Thank you.
Add #Input() to list variable in ListRowComponent class and check if it is working or not and remove inputs from metadata.
import { Component, OnInit, Input} from '#angular/core';
import {List} from '.././list/list.model';
#Component({
selector: 'app-list-row',
templateUrl: './list-row.component.html',
styleUrls: ['./list-row.component.css'],
host: {'class': 'row'},
})
export class ListRowComponent implements OnInit {
#Input() list: List;
#Input() lists: List[];
deletelist(list: List): void {
let index: number = this.lists.indexOf(list);
if (index !== -1) {
this.lists.splice(index, 1);
}
}
ngOnInit() {
}
}
or
Remove :List from inputs as
import { Component, OnInit, Input} from '#angular/core';
import {List} from '.././list/list.model';
#Component({
selector: 'app-list-row',
templateUrl: './list-row.component.html',
inputs :['list']
styleUrls: ['./list-row.component.css'],
host: {'class': 'row'},
})
export class ListRowComponent implements OnInit {
list: List;
#Input() lists: List[];
deletelist(list: List): void {
let index: number = this.lists.indexOf(list);
if (index !== -1) {
this.lists.splice(index, 1);
}
}
ngOnInit() {
}
}
I got the answer. I did not do two input bindings but i created a custom event in the list-row.component and emmited the list to list.Component.
import { Component, Input, Output, EventEmitter} from '#angular/core';
import {List} from '.././list/list.model';
#Component({
selector: 'app-list-row',
templateUrl: './list-row.component.html',
styleUrls: ['./list-row.component.css'],
host: {
'class': 'row'
}
})
export class ListRowComponent {
#Input() list: List;
#Output() deleted = new EventEmitter<List>();
deletedl() {
const listing: List = this.list;
this.deleted.emit(listing);
}
In the template I used the click event to call the deletel() method.
<div class="Eight wide column left aligned title">
<div class="value">
<div class = "hello">
<b>
{{ list.title | uppercase }}
</b>
<div style="float: right;" class="ui label">
<i class="delete icon"
(click)="deletedl()">
</i>
</div>
</div>
</div>
</div>
Then I just called the event in the list.component
list.component.html
<div class="ui grid posts">
<app-list-row
*ngFor="let list of itemList"
[list]="list"
(click)='onClick(list)'
[class.selected]="isSelected(list)"
(deleted)="deletedl($event)">
</app-list-row>
</div>
Then i called a method in list.component to delete the list from the array using Splice method.
list.component
import { Component, Input} from '#angular/core';
import {List} from './list.model';
#Component({
selector: 'app-list',
templateUrl: './list.component.html',
styleUrls: ['./list.component.css']
})
export class ListComponent {
#Input() itemList: List[] = [];
private currentItem: List;
onClick(list: List): void {
this.currentItem = list;
console.log(`clicking list title: ${list.title}`);
}
isSelected(list: List): boolean {
if (!list || !this.currentItem) {
return false;
}
return list.title === this.currentItem.title;
}
deletedl(list: List) {
console.log(`deleting list title: ${list.title}`);
let index: number = this.itemList.indexOf(list);
if (index !== -1) {
this.itemList.splice(index, 1);
}
}
}
I have learned that if we want to get some input from the parent component than using property binding helps and when we want to run some output from our parent component then event binding is helpful.
I would like to pass the parent component's FormGroup to its child for the purpose of displaying an error-message using the child.
Given the following parent:
parent.component.ts
import { Component, OnInit } from '#angular/core'
import {
REACTIVE_FORM_DIRECTIVES, AbstractControl, FormBuilder, FormControl, FormGroup, Validators
} from '#angular/forms'
#Component({
moduleId: module.id,
selector: 'parent-cmp',
templateUrl: 'language.component.html',
styleUrls: ['language.component.css'],
directives: [ErrorMessagesComponent]
})
export class ParentCmp implements OnInit {
form: FormGroup;
first: AbstractControl;
second: AbstractControl;
constructor(private _fb: FormBuilder) {
this.first = new FormControl('');
this.second = new FormControl('')
}
ngOnInit() {
this.form = this._fb.group({
'first': this.first,
'second': this.second
});
}
}
I would now like to pass the form:FormGroup variable above to the child component below:
error-message.component.ts
import { Component, OnInit, Input } from '#angular/core'
import { NgIf } from '#angular/common'
import {REACTIVE_FORM_DIRECTIVES, FormGroup } from '#angular/forms'
#Component({
moduleId: module.id,
selector: 'epimss-error-messages',
template: `<span class="error" *ngIf="errorMessage !== null">{{errorMessage}}</span>`,
styles: [],
directives: [REACTIVE_FORM_DIRECTIVES, NgIf]
})
export class ErrorMessagesComponent implements OnInit {
#Input() ctrlName: string
constructor(private _form: FormGroup) { }
ngOnInit() { }
get errorMessage() {
// Find the control in the Host (Parent) form
let ctrl = this._form.find(this.ctrlName);
console.log('ctrl| ', ctrl);
// for (let propertyName of ctrl.errors) {
// // If control has a error
// if (ctrl.errors.hasOwnProperty(propertyName) && ctrl.touched) {
// // Return the appropriate error message from the Validation Service
// return CustomValidators.getValidatorErrorMessage(propertyName);
// }
// }
return null;
}
The constructor formGroup represents the FormGroup of the parent - in its present form it does not work.
I am trying to follow this obsolete example at http://iterity.io/2016/05/01/angular/angular-2-forms-and-advanced-custom-validation/
In the parent component do this:
<div [formGroup]="form">
<div>Your parent controls here</div>
<your-child-component [formGroup]="form"></your-child-component>
</div>
And then in your child component you can get hold of that reference like so:
export class YourChildComponent implements OnInit {
public form: FormGroup;
// Let Angular inject the control container
constructor(private controlContainer: ControlContainer) { }
ngOnInit() {
// Set our form property to the parent control
// (i.e. FormGroup) that was passed to us, so that our
// view can data bind to it
this.form = <FormGroup>this.controlContainer.control;
}
}
You can even ensure either formGroupName or [formGroup] is specified on your component by changing its selector like so:
selector: '[formGroup] epimss-error-messages,[formGroupName] epimss-error-messages'
This answer should be sufficient for your needs, but if you want to know more I've written a blog entry here:
https://mrpmorris.blogspot.co.uk/2017/08/angular-composite-controls-formgroup-formgroupname-reactiveforms.html
For Angular 11 I tried all the above answers, and in different combinations, but nothing quite worked for me. So I ended up with the following solution which worked for me just as I wanted.
TypeScript
#Component({
selector: 'fancy-input',
templateUrl: './fancy-input.component.html',
styleUrls: ['./fancy-input.component.scss']
})
export class FancyInputComponent implements OnInit {
valueFormGroup?: FormGroup;
valueFormControl?: FormControl;
constructor(
private formGroupDirective: FormGroupDirective,
private formControlNameDirective: FormControlName
) {}
ngOnInit() {
this.valueFormGroup = this.formGroupDirective.form;
this.valueFormControl = this.formGroupDirective.getControl(this.formControlNameDirective);
}
get controlName() {
return this.formControlNameDirective.name;
}
get enabled() {
return this.valueFormControl?.enabled
}
}
HTML
<div *ngIf="valueFormGroup && valueFormControl">
<!-- Edit -->
<div *ngIf="enabled; else notEnabled" [formGroup]="valueFormGroup">
<input class="input" type="text" [formControlName]="controlName">
</div>
<!-- View only -->
<ng-template #notEnabled>
<div>
{{valueFormControl?.value}}
</div>
</ng-template>
</div>
Usage
Note that I had to add ngDefaultControl otherwise it would give no default value accessor error in console (if somebody knows how to get rid of it without error - will be much appreciated).
<form [formGroup]="yourFormGroup" (ngSubmit)="save()">
<fancy-input formControlName="yourFormControlName" ngDefaultControl></fancy-input>
</form>
this is an example of child component used inside parent formGroup :
child component ts:
import { Component, OnInit, Input } from '#angular/core';
import { FormGroup, ControlContainer, FormControl } from '#angular/forms';
#Component({
selector: 'app-date-picker',
template: `
<mat-form-field [formGroup]="form" style="width:100%;">
<input matInput [matDatepicker]="picker" [placeholder]="placeHolder" [formControl]="control" readonly>
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<mat-icon (click)="clearDate()">replay</mat-icon>`,
styleUrls: ['./date-picker.component.scss']
})
export class DatePickerComponent implements OnInit {
public form: FormGroup;
public control : FormControl;
#Input() controlName : string;
#Input() placeHolder : string;
constructor(private controlContainer: ControlContainer) {
}
clearDate(){
this.control.reset();
}
ngOnInit() {
this.form = <FormGroup>this.controlContainer.control;
this.control = <FormControl>this.form.get(this.controlName);
}
}
css date picker :
mat-icon{
position: absolute;
left: 83%;
top: 31%;
transform: scale(0.9);
cursor: pointer;
}
and used like this :
<app-date-picker class="col-md-4" [formGroup]="feuilleForm" controlName="dateCreation" placeHolder="Date de création"></app-date-picker>
Parent Component :
#Component({
selector: 'app-arent',
templateUrl: `<form [formGroup]="parentFormGroup" #formDir="ngForm">
<app-child [formGroup]="parentFormGroup"></app-child>
</form> `
})
export class ParentComponent implements {
parentFormGroup :formGroup
ngOnChanges() {
console.log(this.parentFormGroup.value['name'])
}
}
Child Component :
#Component({
selector: 'app-Child',
templateUrl: `<form [formGroup]="childFormGroup" #formDir="ngForm">
<input id="nameTxt" formControlName="name">
</form> `
})
export class ChildComponent implements OnInit {
#Input() formGroup: FormGroup
childFormGroup :FormGroup
ngOnInit() {
// Build your child from
this.childFormGroup.addControl('name', new FormControl(''))
/* Bind your child form control to parent form group
changes in 'nameTxt' directly reflect to your parent
component formGroup
*/
this.formGroup.addControl("name", this.childFormGroup.controls.name);
}
}
The ngOnInit was important - this did not work in the constructor.
And I prefer looking for the FormControlDirective - its the first one found in the child component's ancestor hierarchy
constructor(private formGroupDirective: FormGroupDirective) {}
ngOnInit() {
this.formGroupDirective.control.addControl('password', this.newPasswordControl);
this.formGroupDirective.control.addControl('confirmPassword', this.confirmPasswordControl);
this.formGroup = this.formGroupDirective.control;
}
I would do this in this way, i have passed child form data as group to parent so you can have separated form data in submit call.
Parent:
<form [formGroup]="registerStudentForm" (ngSubmit)="onSubmit()">
<app-basic-info [breakpoint]="breakpoint" [formGroup]="registerStudentForm"></app-basic-info>
<button mat-button>Submit</button>
</form>
Child:
<mat-card [formGroup]="basicInfo">
<mat-card-title>Basic Information</mat-card-title>
<mat-card-content>
<mat-grid-list
[gutterSize]="'20px'"
[cols]="breakpoint"
rowHeight="60px"
>
<mat-grid-tile>
<mat-form-field appearance="legacy" class="full-width-field">
<mat-label>Full name</mat-label>
<input matInput formControlName="full_name" />
</mat-form-field>
</mat-grid-tile>
</mat-grid-list>
</mat-card-content>
</mat-card>
Parent.ts:
export class RegisterComponent implements OnInit {
constructor() { }
registerForm = new FormGroup({});
onSubmit() {
console.warn(this.registerForm.value);
}
}
Child.ts
export class BasicInfoComponent implements OnInit {
#Input() breakpoint;
#Input() formGroup: FormGroup;
basicInfo: FormGroup;
constructor() { }
ngOnInit(): void {
this.basicInfo = new FormGroup({
full_name: new FormControl('Riki maru'),
dob: new FormControl(''),
});
this.formGroup.addControl('basicInfo', this.basicInfo);
}
}
Here in your child form components #Input() formGroup: FormGroup; part would be reference of parent component
I would pass the form as an input to the child component;
#Component(
{
moduleId: module.id,
selector: 'epimss-error-messages',
template: `
<span class="error" *ngIf="errorMessage !== null">{{errorMessage}}</span>`,
styles: [],
directives: [REACTIVE_FORM_DIRECTIVES, NgIf]
})
export class ErrorMessagesComponent implements OnInit {
#Input()
ctrlName: string
#Input('form') _form;
ngOnInit() {
this.errorMessage();
}
errorMessage() {
// Find the control in the Host (Parent) form
let ctrl = this._form.find(this.ctrlName);
console.log('ctrl| ', ctrl)
// for (let propertyName of ctrl.errors) {
// // If control has a error
// if (ctrl.errors.hasOwnProperty(propertyName) && ctrl.touched) {
// // Return the appropriate error message from the Validation Service
// return CustomValidators.getValidatorErrorMessage(propertyName);
// }
// }
return null;
}
And of course you'll need o pass the form from the parent component to the child, which you can do it in different ways , but the simplest is :
Somewhere in your parent ;
<epimss-error-messages [form]='form'></epimss-error-messages>
If you want to access the parent from the child component, you can access parent property of the FormControl instance, https://angular.io/api/forms/AbstractControl#parent
To get the parent error:
const parent = control.parent;
const errors = parent.errors;