Angular 通用:"Cannot read property '_updateTreeValidity' of undefined"
Angular Universal: "Cannot read property '_updateTreeValidity' of undefined"
我正在尝试在 Angular Universal 中构建动态表单。表单的设置使得某些问题是强制性的,而其他问题则不是,这取决于表单的创建者是如何设计的。当显示表单供用户填写时,它在没有 Angular Universal(JiT 编译器,vs AoT 编译器)的情况下工作得很好。
该表单包含一个自定义组件,该组件被注入到每个表单问题中,允许用户将他们的回答输入到组件中以供提交。
应用程序编译正常,但是当加载这个组件时,出现很多错误:
Cannot read property '_updateTreeValidity' of undefined
Cannot read property 'setParent' of undefined
at e._registerControl
ERROR Error: formGroup expects a FormGroup instance. Please pass one in.
这是表单的打字稿:
import { Component, OnInit, ViewChild, OnDestroy, NgZone, EventEmitter } from '@angular/core';
import { Router, ActivatedRoute, Params } from '@angular/router';
import { AuthenticationService } from '../../services/authentication.service';
import { ProgramsService } from '../../services/programs.service';
import { InfoService } from '../../services/info.service';
import { FormGroup, FormControl, FormBuilder, Validators, FormArray } from '@angular/forms';
import { SessionsService } from '../../services/sessions.service';
import { Answers } from '../formComponents/answers';
import { AnswerComponent } from '../formComponents/answer.component';
@Component({
templateUrl: './intro.component.html',
styleUrls: ['./intro.component.css'],
providers: [SessionsService, InfoService, ProgramsService]
})
export class IntroComponent implements OnInit, OnDestroy{
constructor(private router: Router, private route: ActivatedRoute, private _programsService: ProgramsService, private zone: NgZone, private _fb: FormBuilder, private _sessionsService: SessionsService){
this.myForm = this._fb.group({
questions: this._fb.array([])
});
this.subcribeToFormChanges();
}
programId: string;
sessionId: string;
private sub: any;
res;
coachUsername;
welcomeHeader;
welcomeDescription;
welcomeFiles;
requiredQuestions;
formData: FormData;
public myForm: FormGroup;
public events: any[] = [];
loading;
introCompleted;
isForm = false;
ngOnInit() {
this.sub = this.route.params.subscribe(params => {
this.sessionId = params['id'];
this.getSessionInfo(this.sessionId);
});
}
getSessionInfo(id){
this.loading = true;
this._sessionsService.getSession(id).subscribe(res=>{
this.loading = false;
console.log(res.programId);
this.programId = res.programId;
if(res.introSessionCompleted == false){
this.introCompleted = false;
this.getForm();
}
if(res.introSessionCompleted == true){
this.introCompleted = true;
this.getAnswers();
}
})
}
getAnswers(){
this._sessionsService.getAnswerIntro(this.sessionId).subscribe(res=>{
console.log('session answers');
console.log(res);
this.fillAnswers(res);
})
}
fillAnswers(res){
this.getFormLight();
for(var i=0; i<res.questions.length; i++){
this.answerQuestion(res.questions[i].type, res.questions[i].placeholder, res.questions[i].title, res.questions[i].required, res.questions[i].answer);
}
}
getFormLight(){
this._programsService.clientGetIntro(this.programId).subscribe(res=>{
console.log('Intro res:')
console.log(res)
this.welcomeDescription = res.welcomeDescription;
this.welcomeHeader = res.welcomeHeader;
this.welcomeFiles = res.welcomeFiles;
})
}
getForm(){
this._programsService.clientGetIntro(this.programId).subscribe(res=>{
this.isForm = true;
console.log('Intro res:')
console.log(res)
this.res = res;
this.welcomeDescription = res.welcomeDescription;
this.welcomeHeader = res.welcomeHeader;
this.welcomeFiles = res.welcomeFiles;
this.requiredQuestions = res.requiredQuestions;
if(this.requiredQuestions){
for(var i=0; i<this.requiredQuestions.length; i++){
this.addQuestion(this.requiredQuestions[i].type, this.requiredQuestions[i].placeholder, this.requiredQuestions[i].title, this.requiredQuestions[i].required);
}
}
})
}
initQuestion(type, placeholder, title, required) {
if(required == 'yes'){
return this._fb.group({
type: [type],
placeholder: [placeholder],
title: [title],
required: [required],
answer: ['', Validators.required]
});
}
if(required == 'no'){
return this._fb.group({
type: [type],
placeholder: [placeholder],
title: [title],
required: [required],
answer: ['']
});
}
}
initAnswerQuestion(type, placeholder, title, required, answer){
return this._fb.group({
type: [type],
placeholder: [placeholder],
title: [title],
required: [required],
answer: new FormControl({value: answer, disabled: true})
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
save(model: Answers, isValid: boolean) {
this._sessionsService.answerIntro(this.programId, this.sessionId, JSON.stringify(model)).subscribe(res=>{
console.log(res);
if(res.message == 'Finished updating answers'){
this.router.navigateByUrl('/account/sessions/view/'+this.sessionId)
}
})
}
addQuestion(type, placeholder, title, required) {
const control = <FormArray>this.myForm.controls['questions'];
const addrCtrl = this.initQuestion(type, placeholder, title, required);
control.push(addrCtrl);
}
answerQuestion(type, placeholder, title, required, answer) {
const control = <FormArray>this.myForm.controls['questions'];
const addrCtrl = this.initAnswerQuestion(type, placeholder, title, required, answer);
control.push(addrCtrl);
}
removeQuestion(i: number) {
const control = <FormArray>this.myForm.controls['questions'];
control.removeAt(i);
}
subcribeToFormChanges() {
const myFormStatusChanges$ = this.myForm.statusChanges;
const myFormValueChanges$ = this.myForm.valueChanges;
myFormStatusChanges$.subscribe(x => this.events.push({ event: 'STATUS_CHANGED', object: x }));
myFormValueChanges$.subscribe(x => this.events.push({ event: 'VALUE_CHANGED', object: x }));
}
}
这是表单的 html:
<div class="container animated fadeIn" style="padding-top:140px" >
<div class="session-container">
<div class="row nomargin">
<div class="session-container-header">
<div *ngIf="introCompleted" class="back" [routerLink]="['/account/sessions/view', sessionId]">
<i class="fa fa-arrow-circle-left"></i>Session Dashboard
</div>
<div *ngIf="!introCompleted" class="back" [routerLink]="['/account/sessions']">
<i class="fa fa-arrow-circle-left"></i>All Sessions
</div>
<div class="title">
Session Introduction
</div>
<div *ngIf="introCompleted" class="settings-icon">
<i class="fa fa-gear" [class.fa-spin]="mouseOvered" (mouseover)="mouseOvered=true" (mouseleave)="mouseOvered=false" [routerLink]="['/account/sessions/view/'+id+'/settings']"></i>
</div>
</div>
</div>
<div class="row nomargin">
<div *ngIf="!loading" class="session-container-content">
<div class="row nomargin">
<div class="intro-container">
<div class="intro-welcome">
<a>{{welcomeHeader}}</a>
</div>
<div class="intro-photo">
</div>
<div class="intro-description">
<a>{{welcomeDescription}}</a>
</div>
<div class="intro-files">
<div class="intro-border">
<a>Introduction Files</a>
</div>
<div class="files-row">
<div *ngFor="let file of welcomeFiles" class="file-container" [style.background]="'url('+file.url+')'">
<div class="overlay">
<a [href]="file.url" class="file-url"><div class="download-button">
<a><i class="fa fa-cloud-download" style="margin-right: 5px;"></i>Download</a>
</div></a>
</div>
</div>
</div>
</div>
<div class="intro-questions">
<div class="intro-border">
<a>Introduction Questions</a>
</div>
<form *ngIf="isForm == true;" [formGroup]="myForm">
<div class="intro-answers-container">
<div *ngFor="let question of myForm.controls.questions.controls; let i=index" class="panel panel-default">
<answer [group]="myForm.controls.questions[i]"></answer>
</div>
</div>
</form>
</div>
<div *ngIf="!introCompleted && !loading" class="intro-submit">
<button *ngIf="myForm.valid" class="submit-button" (click)="save(myForm.value, true)">Complete Program Introduction</button>
<button *ngIf="!myForm.valid" class="submit-button invalid" >Complete Program Introduction</button>
</div>
</div>
</div>
</div>
<div *ngIf="loading" class="session-container-content">
<img class="loading-ring" src="../assets/img/ring.svg">
</div>
</div>
</div>
</div>
这是自定义答案组件打字稿
import { Component, Input, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
@Component({
moduleId: module.id,
selector: 'answer',
templateUrl: 'answer.component.html',
styleUrls: ['./answer.component.css']
})
export class AnswerComponent implements OnInit {
@Input('group')
public answerForm: FormGroup;
type;
title;
required;
disabled;
ngOnInit(){
this.type = this.answerForm.controls['type'].value;
this.required = this.answerForm.controls['required'].value;
this.title = this.answerForm.controls['title'].value;
if(this.answerForm.controls['answer'].value != null && this.answerForm.controls['answer'].value != ''){
this.disabled = true;
console.log('disabled');
}
}
}
最后,这里是自定义组件html
<div [formGroup]="answerForm">
<div class="form-group">
<div class="left-hand">
<div class="question-text"><a>{{title}}</a></div>
</div>
<div class="right-hand">
<textarea *ngIf="type=='text'" class="form-answer" placeholder="Type answer here..." formControlName="answer"></textarea>
<input class="number" type="number" placeholder="Enter answer here..." formControlName="answer" *ngIf="type=='number'" >
<select *ngIf="type=='yesno'" formControlName="answer" >
<option value="" disabled selected>Choose answer</option>
<option>Yes</option>
<option>No</option>
</select>
</div>
</div>
</div>
我发现了问题,数组中 ngFor 对象的响应之一是空值,因此弄乱了组件。
我正在尝试在 Angular Universal 中构建动态表单。表单的设置使得某些问题是强制性的,而其他问题则不是,这取决于表单的创建者是如何设计的。当显示表单供用户填写时,它在没有 Angular Universal(JiT 编译器,vs AoT 编译器)的情况下工作得很好。
该表单包含一个自定义组件,该组件被注入到每个表单问题中,允许用户将他们的回答输入到组件中以供提交。
应用程序编译正常,但是当加载这个组件时,出现很多错误:
Cannot read property '_updateTreeValidity' of undefined
Cannot read property 'setParent' of undefined at e._registerControl
ERROR Error: formGroup expects a FormGroup instance. Please pass one in.
这是表单的打字稿:
import { Component, OnInit, ViewChild, OnDestroy, NgZone, EventEmitter } from '@angular/core';
import { Router, ActivatedRoute, Params } from '@angular/router';
import { AuthenticationService } from '../../services/authentication.service';
import { ProgramsService } from '../../services/programs.service';
import { InfoService } from '../../services/info.service';
import { FormGroup, FormControl, FormBuilder, Validators, FormArray } from '@angular/forms';
import { SessionsService } from '../../services/sessions.service';
import { Answers } from '../formComponents/answers';
import { AnswerComponent } from '../formComponents/answer.component';
@Component({
templateUrl: './intro.component.html',
styleUrls: ['./intro.component.css'],
providers: [SessionsService, InfoService, ProgramsService]
})
export class IntroComponent implements OnInit, OnDestroy{
constructor(private router: Router, private route: ActivatedRoute, private _programsService: ProgramsService, private zone: NgZone, private _fb: FormBuilder, private _sessionsService: SessionsService){
this.myForm = this._fb.group({
questions: this._fb.array([])
});
this.subcribeToFormChanges();
}
programId: string;
sessionId: string;
private sub: any;
res;
coachUsername;
welcomeHeader;
welcomeDescription;
welcomeFiles;
requiredQuestions;
formData: FormData;
public myForm: FormGroup;
public events: any[] = [];
loading;
introCompleted;
isForm = false;
ngOnInit() {
this.sub = this.route.params.subscribe(params => {
this.sessionId = params['id'];
this.getSessionInfo(this.sessionId);
});
}
getSessionInfo(id){
this.loading = true;
this._sessionsService.getSession(id).subscribe(res=>{
this.loading = false;
console.log(res.programId);
this.programId = res.programId;
if(res.introSessionCompleted == false){
this.introCompleted = false;
this.getForm();
}
if(res.introSessionCompleted == true){
this.introCompleted = true;
this.getAnswers();
}
})
}
getAnswers(){
this._sessionsService.getAnswerIntro(this.sessionId).subscribe(res=>{
console.log('session answers');
console.log(res);
this.fillAnswers(res);
})
}
fillAnswers(res){
this.getFormLight();
for(var i=0; i<res.questions.length; i++){
this.answerQuestion(res.questions[i].type, res.questions[i].placeholder, res.questions[i].title, res.questions[i].required, res.questions[i].answer);
}
}
getFormLight(){
this._programsService.clientGetIntro(this.programId).subscribe(res=>{
console.log('Intro res:')
console.log(res)
this.welcomeDescription = res.welcomeDescription;
this.welcomeHeader = res.welcomeHeader;
this.welcomeFiles = res.welcomeFiles;
})
}
getForm(){
this._programsService.clientGetIntro(this.programId).subscribe(res=>{
this.isForm = true;
console.log('Intro res:')
console.log(res)
this.res = res;
this.welcomeDescription = res.welcomeDescription;
this.welcomeHeader = res.welcomeHeader;
this.welcomeFiles = res.welcomeFiles;
this.requiredQuestions = res.requiredQuestions;
if(this.requiredQuestions){
for(var i=0; i<this.requiredQuestions.length; i++){
this.addQuestion(this.requiredQuestions[i].type, this.requiredQuestions[i].placeholder, this.requiredQuestions[i].title, this.requiredQuestions[i].required);
}
}
})
}
initQuestion(type, placeholder, title, required) {
if(required == 'yes'){
return this._fb.group({
type: [type],
placeholder: [placeholder],
title: [title],
required: [required],
answer: ['', Validators.required]
});
}
if(required == 'no'){
return this._fb.group({
type: [type],
placeholder: [placeholder],
title: [title],
required: [required],
answer: ['']
});
}
}
initAnswerQuestion(type, placeholder, title, required, answer){
return this._fb.group({
type: [type],
placeholder: [placeholder],
title: [title],
required: [required],
answer: new FormControl({value: answer, disabled: true})
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
save(model: Answers, isValid: boolean) {
this._sessionsService.answerIntro(this.programId, this.sessionId, JSON.stringify(model)).subscribe(res=>{
console.log(res);
if(res.message == 'Finished updating answers'){
this.router.navigateByUrl('/account/sessions/view/'+this.sessionId)
}
})
}
addQuestion(type, placeholder, title, required) {
const control = <FormArray>this.myForm.controls['questions'];
const addrCtrl = this.initQuestion(type, placeholder, title, required);
control.push(addrCtrl);
}
answerQuestion(type, placeholder, title, required, answer) {
const control = <FormArray>this.myForm.controls['questions'];
const addrCtrl = this.initAnswerQuestion(type, placeholder, title, required, answer);
control.push(addrCtrl);
}
removeQuestion(i: number) {
const control = <FormArray>this.myForm.controls['questions'];
control.removeAt(i);
}
subcribeToFormChanges() {
const myFormStatusChanges$ = this.myForm.statusChanges;
const myFormValueChanges$ = this.myForm.valueChanges;
myFormStatusChanges$.subscribe(x => this.events.push({ event: 'STATUS_CHANGED', object: x }));
myFormValueChanges$.subscribe(x => this.events.push({ event: 'VALUE_CHANGED', object: x }));
}
}
这是表单的 html:
<div class="container animated fadeIn" style="padding-top:140px" >
<div class="session-container">
<div class="row nomargin">
<div class="session-container-header">
<div *ngIf="introCompleted" class="back" [routerLink]="['/account/sessions/view', sessionId]">
<i class="fa fa-arrow-circle-left"></i>Session Dashboard
</div>
<div *ngIf="!introCompleted" class="back" [routerLink]="['/account/sessions']">
<i class="fa fa-arrow-circle-left"></i>All Sessions
</div>
<div class="title">
Session Introduction
</div>
<div *ngIf="introCompleted" class="settings-icon">
<i class="fa fa-gear" [class.fa-spin]="mouseOvered" (mouseover)="mouseOvered=true" (mouseleave)="mouseOvered=false" [routerLink]="['/account/sessions/view/'+id+'/settings']"></i>
</div>
</div>
</div>
<div class="row nomargin">
<div *ngIf="!loading" class="session-container-content">
<div class="row nomargin">
<div class="intro-container">
<div class="intro-welcome">
<a>{{welcomeHeader}}</a>
</div>
<div class="intro-photo">
</div>
<div class="intro-description">
<a>{{welcomeDescription}}</a>
</div>
<div class="intro-files">
<div class="intro-border">
<a>Introduction Files</a>
</div>
<div class="files-row">
<div *ngFor="let file of welcomeFiles" class="file-container" [style.background]="'url('+file.url+')'">
<div class="overlay">
<a [href]="file.url" class="file-url"><div class="download-button">
<a><i class="fa fa-cloud-download" style="margin-right: 5px;"></i>Download</a>
</div></a>
</div>
</div>
</div>
</div>
<div class="intro-questions">
<div class="intro-border">
<a>Introduction Questions</a>
</div>
<form *ngIf="isForm == true;" [formGroup]="myForm">
<div class="intro-answers-container">
<div *ngFor="let question of myForm.controls.questions.controls; let i=index" class="panel panel-default">
<answer [group]="myForm.controls.questions[i]"></answer>
</div>
</div>
</form>
</div>
<div *ngIf="!introCompleted && !loading" class="intro-submit">
<button *ngIf="myForm.valid" class="submit-button" (click)="save(myForm.value, true)">Complete Program Introduction</button>
<button *ngIf="!myForm.valid" class="submit-button invalid" >Complete Program Introduction</button>
</div>
</div>
</div>
</div>
<div *ngIf="loading" class="session-container-content">
<img class="loading-ring" src="../assets/img/ring.svg">
</div>
</div>
</div>
</div>
这是自定义答案组件打字稿
import { Component, Input, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
@Component({
moduleId: module.id,
selector: 'answer',
templateUrl: 'answer.component.html',
styleUrls: ['./answer.component.css']
})
export class AnswerComponent implements OnInit {
@Input('group')
public answerForm: FormGroup;
type;
title;
required;
disabled;
ngOnInit(){
this.type = this.answerForm.controls['type'].value;
this.required = this.answerForm.controls['required'].value;
this.title = this.answerForm.controls['title'].value;
if(this.answerForm.controls['answer'].value != null && this.answerForm.controls['answer'].value != ''){
this.disabled = true;
console.log('disabled');
}
}
}
最后,这里是自定义组件html
<div [formGroup]="answerForm">
<div class="form-group">
<div class="left-hand">
<div class="question-text"><a>{{title}}</a></div>
</div>
<div class="right-hand">
<textarea *ngIf="type=='text'" class="form-answer" placeholder="Type answer here..." formControlName="answer"></textarea>
<input class="number" type="number" placeholder="Enter answer here..." formControlName="answer" *ngIf="type=='number'" >
<select *ngIf="type=='yesno'" formControlName="answer" >
<option value="" disabled selected>Choose answer</option>
<option>Yes</option>
<option>No</option>
</select>
</div>
</div>
</div>
我发现了问题,数组中 ngFor 对象的响应之一是空值,因此弄乱了组件。