I'm implemeting dynamic form system in my current project using ANgular 2, and so far is going good but I found the following problem:
I have two components that represent a form control like for example:
#Component({
selector: 'app-text-area',
templateUrl: './text-area.component.html',
styleUrls: ['./text-area.component.css']
})
export class TextAreaComponent implements OnInit {
label: string;
formGroup: FormGroup;
formControlName: string;
constructor(private injector: Injector) { }
ngOnInit() {
this.label = this.injector.get('label');
this.formGroup = this.injector.get('formGroup');
this.formControlName = this.injector.get('formControlName');
}
}
And:
#Component({
selector: 'app-input-text',
templateUrl: './input-text.component.html',
styleUrls: ['./input-text.component.css']
})
export class InputTextComponent implements OnInit{
label: string;
formGroup: FormGroup;
formControlName: string;
constructor(private injector: Injector) { }
ngOnInit() {
this.label = this.injector.get('label');
this.formGroup = this.injector.get('formGroup');
this.formControlName = this.injector.get('formControlName');
}
}
As you see both are identical except for the templateUrl, which is displaying different html elements.
So I would like to refactor the code and to create an abstract component to provide the common attributes and logic, and make then, the child classes to inherit the base class (as I would do when using Java). So I have created this class:
export class AbstractFormControl implements OnInit {
label: string;
formGroup: FormGroup;
formControlName: string;
constructor(private injector: Injector) { }
ngOnInit() {
this.label = this.injector.get('label');
this.formGroup = this.injector.get('formGroup');
this.formControlName = this.injector.get('formControlName');
}
}
And I have make the child classes extend the base class like this:
#Component({
selector: 'app-input-text',
templateUrl: './input-text.component.html',
styleUrls: ['./input-text.component.css']
})
export class InputTextComponent extends AbstractFormControl{
}
However now I'm getting the following error:
Uncaught Error: Can't resolve all parameters for InputTextComponent: (?).
Can someone explain me what's the right way to do this, or what I'm doing wrong?
Angular dependency injection system should know which type has been passed to constructor. When you inherit component such way typescript won't keep information about parameter private injector. You have two options:
1) Duplicate initializing
#Component({
...
})
export class InputTextComponent extends AbstractFormControl{
constructor(injector: Injector) { super(injector);}
}
But in your case you have the same number of parameters in your base and inherited classes and this solution seems redundant because we can omit constructor in our derived class.
We can omit constructor in derived class if we want to only use dependencies from parent class.
So let's say we have parent class like:
abstract class Parent {
constructor(private a: string, private b: number) {}
}
we can extend this class either
class Foo extends Parent {
constructor(a: string, b: number) {
super(a, b);
}
}
or
class Foo extends Parent {
}
because the second option will generate code like
function Foo() {
return _super !== null && _super.apply(this, arguments) || this;
}
Plunker Example
2) Use #Injectable for base class.
#Injectable()
export class AbstractFormControl {
this way typescript will translate the code above into
AbstractFormControl = __decorate([
core_1.Injectable(),
__metadata("design:paramtypes", [core_1.Injector])
], AbstractFormControl);
Plunker Example
and angular reflector can easily read this information
3) Use #Inject() for each of parameters
export class AbstractFormControl implements OnInit {
constructor(#Inject(Injector) private injector: Injector) { }
Related
I have the following declaration of a annotation in Angular:
class ModalContainer {
public destroy: Function;
public closeModal() {
this.destroy();
}
}
export function Modal() {
return function (target: any) {
Object.assign(target.prototype, ModalContainer.prototype);
};
}
I want to use the annotation inside a component:
#Component({
selector: 'my-component',
templateUrl: 'my-component.template.html',
styleUrls: ['my-component.style.scss']
})
#Modal()
export class MyComponent implements OnInit {
private EVENT_SAVE = 'EVENT_SAVE';
private EVENT_CANCEL = 'EVENT_CANCEL';
private buttonClick(event: any, eventType: string){
if (eventType === this.EVENT_CANCEL){
this.closeModal();
} else if(eventType === this.EVENT_SAVE){
this.closeModal();
}
}
}
The problem is, TypeScript is not able to compile, since this method is not known during compile time. However, when I'm using the same method call inside the template, then it works. That means, that the the prototype was assigned.
The compiler shows this error message:
ERROR in [at-loader] ./src/main/webapp/ui/src/my-component.component.ts:128:18
TS2339: Property 'closeModal' does not exist on type 'MyComponent'.
Does anyone know, how I cann solve this?
By adding this line the compiler will not complain anymore and it works.
private closeModal: Function;
The class then looks like this:
#Component({
selector: 'my-component',
templateUrl: 'my-component.template.html',
styleUrls: ['my-component.style.scss']
})
#Modal()
export class MyComponent implements OnInit {
private EVENT_SAVE = 'EVENT_SAVE';
private EVENT_CANCEL = 'EVENT_CANCEL';
private closeModal: Function;
private buttonClick(event: any, eventType: string){
if (eventType === this.EVENT_CANCEL){
this.closeModal();
} else if(eventType === this.EVENT_SAVE){
this.closeModal();
}
}
}
My reactive form is three component levels deep. The parent component creates a new form without any fields and passes it down to child components.
At first the outer form is valid. Later on a child component adds new form elements with validators (that fail) making the outer form invalid.
I am getting an ExpressionChangedAfterItHasBeenCheckedError error in the console. I want to fix that error.
Somehow this only happens when I add the third level of nesting. The same approach seemed to work for two levels of nesting.
Plunker: https://plnkr.co/edit/GymI5CqSACFEvhhz55l1?p=preview
Parent component
#Component({
selector: 'app-root',
template: `
myForm.valid: <b>{{myForm.valid}}</b>
<form>
<app-subform [myForm]="myForm"></app-subform>
</form>
`
})
export class AppComponent implements OnInit {
...
ngOnInit() {
this.myForm = this.formBuilder.group({});
}
}
Sub component
#Component({
selector: 'app-subform',
template: `
<app-address-form *ngFor="let addressData of addressesData;"
[addressesForm]="addressesForm">
</app-address-form>
`
})
export class SubformComponent implements OnInit {
...
addressesData = [...];
constructor() { }
ngOnInit() {
this.addressesForm = new FormArray([]);
this.myForm.addControl('addresses', this.addressesForm);
}
Child component
#Component({
selector: 'app-address-form',
template: `
<input [formControl]="addressForm.controls.addressLine1">
<input [formControl]="addressForm.controls.city">
`
})
export class AddressFormComponent implements OnInit {
...
ngOnInit() {
this.addressForm = this.formBuilder.group({
addressLine1: [
this.addressData.addressLine1,
[ Validators.required ]
],
city: [
this.addressData.city
]
});
this.addressesForm.push(this.addressForm);
}
}
To understand the problem you need to read Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error article.
For your particular case the problem is that you're creating a form in the AppComponent and use a {{myForm.valid}} interpolation in the DOM. It means that Angular will run create and run updateRenderer function for the AppComponent that updates DOM. Then you use the ngOnInit lifecycle hook of subcomponent to add subgroup with control to this form:
export class AddressFormComponent implements OnInit {
#Input() addressesForm;
#Input() addressData;
ngOnInit() {
this.addressForm = this.formBuilder.group({
addressLine1: [
this.addressData.addressLine1,
[ Validators.required ] <-----------
]
this.addressesForm.push(this.addressForm); <--------
The control becomes invalid because you don't supply initial value and you specify a required validator. Hence the entire form becomes invalid and the expression {{myForm.valid}} evaluates to false. But when Angular ran change detection for the AppComponent it evaluated to true. And that's what the error says.
One possible fix could be to mark the form as invalid in the start since you're planning to add required validator, but it seems Angular doesn't provide such method. Your best choice is probably to add controls asynchronously. In fact, this is what Angular does itself in the sources:
const resolvedPromise = Promise.resolve(null);
export class NgForm extends ControlContainer implements Form {
...
addControl(dir: NgModel): void {
// adds controls asynchronously using Promise
resolvedPromise.then(() => {
const container = this._findContainer(dir.path);
dir._control = <FormControl>container.registerControl(dir.name, dir.control);
setUpControl(dir.control, dir);
dir.control.updateValueAndValidity({emitEvent: false});
});
}
So for you case it will be:
const resolvedPromise = Promise.resolve(null);
#Component({
...
export class AddressFormComponent implements OnInit {
#Input() addressesForm;
#Input() addressData;
addressForm;
ngOnInit() {
this.addressForm = this.formBuilder.group({
addressLine1: [
this.addressData.addressLine1,
[ Validators.required ]
],
city: [
this.addressData.city
]
});
resolvedPromise.then(() => {
this.addressesForm.push(this.addressForm); <-------
})
}
}
Or use some variable in the AppComponent to hold form state and use it in the template:
{{formIsValid}}
export class AppComponent implements OnInit {
myForm: FormGroup;
formIsValid = false;
constructor(private formBuilder: FormBuilder) {}
ngOnInit() {
this.myForm = this.formBuilder.group({});
this.myForm.statusChanges((status)=>{
formIsValid = status;
})
}
}
import {ChangeDetectorRef} from '#angular/core';
....
export class SomeComponent {
form: FormGroup;
constructor(private fb: FormBuilder,
private ref: ChangeDetectorRef) {
this.form = this.fb.group({
myArray: this.fb.array([])
});
}
get myArray(): FormArray {
return this.form.controls.myArray as FormArray;
}
addGroup(): void {
const newGroup = this.fb.group({
prop1: [''],
prop2: ['']
});
this.myArray.push(newGroup);
this.ref.detectChanges();
}
}
I had the same scenario and same issue in Angular 9 and above solution works fine. I tweaked it a little bit: synchroinously adding the control without validators and adding the required validators asynchronously... Because I needed the controls immediately otherwise I got an error cannot find formControl ....
This is my solution based on the accepted answer above:
const resolvedPromise = Promise.resolve(null);
#Component({
selector: 'password-input',
templateUrl: './password-input.component.html',
styleUrls: ['./password-input.component.css']
})
export class PasswordInputComponent implements OnInit {
#Input() parentFormGroup : FormGroup;
#Input() controlName : string = 'password';
#Input() placeholder : string = 'Password';
#Input() label : string = null;
ngOnInit(): void {
this.parentFormGroup.addControl(this.controlName, new FormControl(null));
resolvedPromise.then( () => {
this.parentFormGroup.get(this.controlName).setValidators(Validators.required)
this.parentFormGroup.get(this.controlName).updateValueAndValidity();
});
}
Following the solution of Max Koretskyi, we can use async/await pattern on ngOnInit.
Here is an example:
#Component({
selector: 'my-sub-component',
templateUrl: 'my-sub-component.component.html',
})
export class MySubComponent implements OnInit {
#Input()
form: FormGroup;
async ngOnInit() {
// Avoid ExpressionChangedAfterItHasBeenCheckedError
await Promise.resolve();
this.form.removeControl('config');
this.form.addControl('config', new FormControl("", [Validator.required]));
}
}
I have a component in angular 4 that is called three times. In template metadata I have a div with a directive with some bindings like this.
#import {gServ} from '../gServ.service';
#Component: ({
selector: 'sr-comp',
template: `<div gDirective [cOptions]="dataChart">`
})
export class SGComponent implements OnInit {
#Input('report') public report: IReportInstance;
cOptions:any;
constructor(private gServ: gServ) {
}
ngOnInit(){
this.cOptions = {};
this.cOptions = this.gServ.objectMerge(this.gServ.defaultOpt, this.report.opt);
//this.report.opt is binded to a component when is instantiated.
//this.gServ.objectMerge is a function that merge the two objects
}
}
this.cOptions change for every instance of the component, then in the directive I have this:
import { Directive, ElementRef, HostListener, Input, OnInit } from '#angular/core';
#Directive({
selector: '[gDirective]'
})
export class SGDirective implements OnInit {
public _element: any;
#Input() public cOptions: string;
constructor(public element: ElementRef) {
this._element = this.element.nativeElement;
}
ngOnInit() {
console.log(this.cOptions);
}
}
The problem is that console.log(this.cOptions); always print the same object, even when component set cOptions with diferent values in ngOnInit method of the compnent.
Do you have some idea what is wrong?
Your component property binding [cOptions]="dataChart" doesn't look good, reason being your dataChart is not even defined. it should be like [DIRECTIVE_PROPERTY]="COMPONENT_PROPERTY" and your COMPONENT_PROPERTY is not even defined in SGComponent component class.
Your component class should be something like this:
#import {gServ} from '../gServ.service';
#Component: ({
selector: 'sr-comp',
template: `<div gDirective [cOptions]="Options">`
})
export class SGComponent implements OnInit {
#Input('report') public report: IReportInstance;
Options:any;
constructor(private gServ: gServ) {
}
ngOnInit(){
this.Options = {};
this.Options = this.gServ.objectMerge(this.gServ.defaultOpt, this.report.opt);
}
}
#Ashwani points out a valid problem with your code. The way your template is wiring things up, nothing will ever be passed to the SGDirective input.
Another potential problem you could be running into has to do with the gServ code. If gServ is a singleton (which is probably the case) and it is returning the same object to each of the SGComponents, then all the SGDirectives will have the same value. A simple way to test this is to put {{Options | json}} in the SGComponent template.
To create a new instance of the gServ service for each SGComponent you can add a providers array to the #Component metadata. It would look like this:
import {gServ} from '../gServ.service';
#Component({
selector: 'sr-comp',
template: `{{Options | json}}<div gDirective [cOptions]="Options"></div>`
providers: [gServ],
})
export class SGComponent implements OnInit {
#Input('report') public report: IReportInstance;
Options:any;
constructor(private gServ: gServ) {
}
ngOnInit(){
this.Options = this.gServ.objectMerge(this.gServ.defaultOpt, this.report.opt);
}
}
You have probably the same return/value at this.gServ.objectMerge) (you can test it wihtout calling the service, and passing each one one different objet make by you)
#import {gServ} from '../gServ.service';
#Component: ({
selector: 'sr-comp',
template: `<div gDirective [cOptions]="dataChart">`
})
export class SGComponent implements OnInit {
//#Input('report') public report: IReportInstance;
cOptions:any;
constructor(private gServ: gServ) {
}
ngOnInit(){
this.cOptions = {nicolas: 'nicolas1'}; //change this in the next component that use the directive
}
}
If that is the case, your problem is that gServ is provide at the same rootComponent. with angular, service provider at the same rootComponent are singleton.
And use the same type in your directive and your component!!
I have a KnowledgeElement model like this:
export interface KnowledgeElement {
public knowledgE_USER_ID: number;
public knowledgE_NAME: string;
public knowledgE_DESCRIPTION: string;
public knowledgE_USER_SCORE: number;
}
And a component like this:
#Component({
selector: 'survey',
moduleId: module.id,
styleUrls: ['survey.component.css'],
templateUrl: 'survey.component.html',
providers: [KnowledgeElementDataService]
})
export class SurveyComponent implements OnInit {
public knowledgeElement: KnowledgeElement = {};
constructor(private _knowledgeList: KnowledgeElementDataService){
}
submitSurvey(): void {
this.knowledgeElement.knowledgE_USER_ID = 1;
this.knowledgeElement.knowledgE_NAME = 'Empty';
this.knowledgeElement.knowledgE_DESCRIPTION = 'Empty';
this.knowledgeElement.knowledgE_USER_SCORE = 1;
this._knowledgeList.updateKnowledge(knowledgeElement);
}
}
When I try to assign properties to the variable its stating:
"caused by: Cannot set property 'knowledgE_USER_ID' of undefined"
How can I assign properties to that variable so I can pass it over with the method to the service?
this._knowledgeList.updateKnowledge(knowledgeElement);
should be
this._knowledgeList.updateKnowledge(this.knowledgeElement);
you have a "typo", you should be passing this.knowledgeElement not knowledgeElement like a local variable :-)
You try it please
export class SurveyComponent implements OnInit {
knowledgeElement: KnowledgeElement;
submitSurvey(): void {
this.knowledgeElement = {knowledgE_USER_ID: 1,
knowledgE_NAME: "string",
knowledgE_DESCRIPTION: "string",
knowledgE_USER_SCORE: 2};
}
...
And remove public modifier in interface.
Check whether null or not. like this. After print
<div *ngIf="knowledgeElement">{{knowledgeElement.knowledgE_USER_ID}}</div>
currently your instance member is
public knowledgeElement: KnowledgeElement = {};
change it to
public knowledgeElement = new KnowledgeElement();
and also change interface to class
yours
export interface KnowledgeElement {
change it to
export class KnowledgeElement {
I don't even know if this is possible in TypeScript, but I'm trying to inherit a function from a Class, like this:
import {Component, AfterViewInit, ElementRef} from 'angular2/core';
#Component({})
class Class1 {
name: string;
constructor(private el: ElementRef) {}
private setName() {
this.name = "test";
}
ngAfterViewInit() {
this.setName();
}
}
#Component({
selector: 'test'
})
export class Class2 extends Class1 {
ngAfterViewInit() {
super.ngAfterViewInit();
console.log(this.name);
}
}
but I'm getting the following error in console when calling the setName() function:
EXCEPTION: TypeError: this.el is undefined
Why isn't this working?
Constructors are not inherited.
They are. Following sample shows this:
class Parent {
constructor(foo:number){}
}
class Child extends Parent {
}
const child1 = new Child(); // Error!
const child2 = new Child(123); // OKAY!
But this is angular
However they are not analyzed for dependency injection. This means that your child class constructor isn't called with the same parameters as expected by the parent (in your case `el). You need to specify all the DI elements on each child class. So by chance the correct code is the one from the accepted answer:
#Component({
selector: 'test'
})
export class Class2 extends Class1 {
constructor(el: ElementRef) {
super(el);
}
}
Constructors are not inherited. You need to define them in each subclass
#Component({
selector: 'test'
})
export class Class2 extends Class1 {
constructor(el: ElementRef) {
super(el);
}
ngAfterViewInit() {
super.ngAfterViewInit();
console.log(this.name);
}
}
Consider updating el's scope to protected, meaning it can be accessed by both the class where it's declared and any derived classes.
// before
constructor(private el: ElementRef) {}
// after
constructor(protected el: ElementRef) {}