如何在从父组件中的 ajax 调用接收数据后更新子组件
How to update child components after receiving data from an ajax call in parent
我的应用程序结构如下,我的问题是如何在收到初始或未来数据时更新子组件视图,假设我只有一个服务有一个事件 OnDataUpdate,所有子组件都接收相同的实例该服务自从它在应用程序模块提供者部分声明以来,另一方面,我已经尝试了所有这些方法但没有奏效:
- ApplicationRef.tick()
- ChangeDetectionRef.markForCheck()
- ChangeDetectionStrategy
- 组件间的共享服务,OnDataRecieved事件是这样的
@Injectable()
export class ApiService {
public OnDataRecieved: EventEmitter<Model> = new EventEmitter<Model>();
constructor(private http: HttpClient, private ngZone: NgZone) {
}
public getDataAsync(): Observable<Model> {
return this.http
.get<Model>('url')
.pipe(catchError(er => throwError(er)));
}
}
and in App root component this is like below code
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
changeDetection: ChangeDetectionStrategy.Default
})
export class AppComponent implements DoCheck {
model: BehaviorSubject<Model> = new BehaviorSubject<Model>(new Model()); //with default values
subModel: BehaviorSubject<SubModel>;
constructor(private apiService: ApiService,
private zone: NgZone) {
this.apiService.getDashboard().subscribe((data) => {
this.zone.run(() => {
this.apiService.OnDataReceived.emit(data);
this.model = new BehaviorSubject<Model>(data);
});
});
this.model.subscribe((mdl) => {
this.subModel = new BehaviorSubject<SubModel>(mdl.subModel));
});
}
ngDoCheck() {
}
}
imagine the model is nested and propagated through the child components as data is loaded or changed, the structure can be like this
__ AppRootComponent
|_____ Component1
|_________SubCompoent1-1
|_________SubCompoent1-2
|_____ Component2
|_________SubCompoent2-1
|____________SubCompoent2-1-1
I receive the data changes in ngDoCheck, no need to trigger the detect changes, but the UI and child components does not get updated!
我意识到如何解决那个问题,那个组件的结构是分层的,我通过@Input() 传递了每个组件的模型,问题是初始请求是异步的,组件在接收到真正的父对象,从服务器接收到父对象后,传递的 Input 对象没有对象引用,因此它们不会得到更改。
那么,我们该如何解决这个问题呢?简单的!删除所有输入并使用事件驱动编程
如何?为每个对象创建一个事件或为所有其他对象所依赖的父(根)对象创建一个事件,在全局服务中共享事件,trigger/emit 收到根对象后的事件,并在子组件中订阅该事件.让我在下面向您展示一个简单的片段:
import { HttpClient, HttpParams, HttpErrorResponse } from '@angular/common/http';
import { Injectable, EventEmitter } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { RootDto } from 'src/app/model/root.dto.model';
@Injectable()
export class CoreApiService {
public onDataReceived: EventEmitter<RootDto> = new EventEmitter<RootDto>();
constructor(private http: HttpClient) {
}
public getRootObject(objectId: number): Observable<RootDto> {
// const _params = new HttpParams().set('objectId', objectId);
return this.http
.get<RootDto>(`${Constants.ApiUrl}/root/${objectId}`)
.pipe(catchError((err: HttpErrorResponse) => {
return throwError(err);
}));
}
}
the root componant is like below
import {
Component,
OnInit
} from '@angular/core';
import { CoreApiService } from './core/services/core-api.service';
import { RootDto } from 'src/app/model/root.dto.model';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
constructor(private apiService: CoreApiService) {
}
ngOnInit() {
this.apiService.getRootObject().subscribe((data: RootDto) => {
// todo: do something here
this.apiService.onDataReceived.emit(data);
},
(err: HttpErrorResponse) => {
if (err.status === 401 || err.status === 403) {
// not authorized
}else {
// todo: error happened!
}
}
);
}
}
the child components are like below
import {
Component,
OnInit,
NgZone
} from '@angular/core';
import { CoreApiService } from '../core/services/core-api.service';
import { RootDto } from 'src/app/model/root.dto.model';
import { ChildDto } from '../model/child.dto.model';
@Component({
selector: 'app-first-child',
templateUrl: './firstChild.component.html',
styleUrls: ['./firstChild.component.css']
})
export class FirstChildComponent implements OnInit {
dto: ChildDto;
isLoaded = false;
constructor(private apiService: CoreApiService, private zone: NgZone) {
this.apiService.onDataReceived.subscribe((rootDto: RootDto) => {
this.zone.run(() => {
this.dto = Utils.ObjectFactory.Create(rootDto.firstChildDto); // to make sure that we will have a new reference (so that change detction will be triggered) i use object instantiation
// NOTICE:
// for arrays don't simply assign or push new item to the array, because the reference is not changed the change detection is not triggered
// if the array size is small before assigning new value, you can simply empty (myArray = [];) the array otherwise don't do that
this.isLoaded = true;
});
});
}
ngOnInit() {
}
// the rest of logic
}
you can do the same for all other components and even you can create more events in share service and trigger it as you wish
让我们从一些一般性建议开始:
您通常不需要使数据从组件流向其(孙)子组件的服务。为此使用 @Input
绑定。
当使用服务来管理数据流时,使用纯 RxJS,即 Observable
、Subject
、BehaviorSubject
等的实例 EventEmitter
是 Angular 特定的,用于处理来自组件和指令的输出事件。如果您想使用 BehaviorSubject
.
查看类似的解决方案,请选中
您不需要在默认区域中告诉 Angular 到 运行 代码。它默认执行此操作。
具体来说,您的服务可以像下面这样简单:
@Injectable({
providedIn: 'root'
})
export class ApiService {
constructor(private http: HttpClient) { }
getData(): Observable<Model> {
return this.http.get<Model>('...');
}
}
然后一个组件可以订阅它并将值存储在普通同步 属性 中。在这条路上,这个 属性 可以作为 @Input()
传递给子组件:
@Component({
selector: 'my-app',
template: `
<strong>AppComponent:</strong> {{model | json}}
<child [model]="model"></child>
`
})
export class AppComponent implements OnInit {
model: Model;
constructor(private apiService: ApiService) { }
ngOnInit(): void {
this.apiService.getData()
.subscribe(model => this.model = model);
}
}
您还可以随时更新 model
属性,更改将传播到子组件和孙组件。
Here's a Stackblitz 带有示例实现。
我的应用程序结构如下,我的问题是如何在收到初始或未来数据时更新子组件视图,假设我只有一个服务有一个事件 OnDataUpdate,所有子组件都接收相同的实例该服务自从它在应用程序模块提供者部分声明以来,另一方面,我已经尝试了所有这些方法但没有奏效:
- ApplicationRef.tick()
- ChangeDetectionRef.markForCheck()
- ChangeDetectionStrategy
- 组件间的共享服务,OnDataRecieved事件是这样的
@Injectable()
export class ApiService {
public OnDataRecieved: EventEmitter<Model> = new EventEmitter<Model>();
constructor(private http: HttpClient, private ngZone: NgZone) {
}
public getDataAsync(): Observable<Model> {
return this.http
.get<Model>('url')
.pipe(catchError(er => throwError(er)));
}
}
and in App root component this is like below code
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
changeDetection: ChangeDetectionStrategy.Default
})
export class AppComponent implements DoCheck {
model: BehaviorSubject<Model> = new BehaviorSubject<Model>(new Model()); //with default values
subModel: BehaviorSubject<SubModel>;
constructor(private apiService: ApiService,
private zone: NgZone) {
this.apiService.getDashboard().subscribe((data) => {
this.zone.run(() => {
this.apiService.OnDataReceived.emit(data);
this.model = new BehaviorSubject<Model>(data);
});
});
this.model.subscribe((mdl) => {
this.subModel = new BehaviorSubject<SubModel>(mdl.subModel));
});
}
ngDoCheck() {
}
}
imagine the model is nested and propagated through the child components as data is loaded or changed, the structure can be like this
__ AppRootComponent
|_____ Component1
|_________SubCompoent1-1
|_________SubCompoent1-2
|_____ Component2
|_________SubCompoent2-1
|____________SubCompoent2-1-1
I receive the data changes in ngDoCheck, no need to trigger the detect changes, but the UI and child components does not get updated!
我意识到如何解决那个问题,那个组件的结构是分层的,我通过@Input() 传递了每个组件的模型,问题是初始请求是异步的,组件在接收到真正的父对象,从服务器接收到父对象后,传递的 Input 对象没有对象引用,因此它们不会得到更改。
那么,我们该如何解决这个问题呢?简单的!删除所有输入并使用事件驱动编程 如何?为每个对象创建一个事件或为所有其他对象所依赖的父(根)对象创建一个事件,在全局服务中共享事件,trigger/emit 收到根对象后的事件,并在子组件中订阅该事件.让我在下面向您展示一个简单的片段:
import { HttpClient, HttpParams, HttpErrorResponse } from '@angular/common/http';
import { Injectable, EventEmitter } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { RootDto } from 'src/app/model/root.dto.model';
@Injectable()
export class CoreApiService {
public onDataReceived: EventEmitter<RootDto> = new EventEmitter<RootDto>();
constructor(private http: HttpClient) {
}
public getRootObject(objectId: number): Observable<RootDto> {
// const _params = new HttpParams().set('objectId', objectId);
return this.http
.get<RootDto>(`${Constants.ApiUrl}/root/${objectId}`)
.pipe(catchError((err: HttpErrorResponse) => {
return throwError(err);
}));
}
}
the root componant is like below
import {
Component,
OnInit
} from '@angular/core';
import { CoreApiService } from './core/services/core-api.service';
import { RootDto } from 'src/app/model/root.dto.model';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
constructor(private apiService: CoreApiService) {
}
ngOnInit() {
this.apiService.getRootObject().subscribe((data: RootDto) => {
// todo: do something here
this.apiService.onDataReceived.emit(data);
},
(err: HttpErrorResponse) => {
if (err.status === 401 || err.status === 403) {
// not authorized
}else {
// todo: error happened!
}
}
);
}
}
the child components are like below
import {
Component,
OnInit,
NgZone
} from '@angular/core';
import { CoreApiService } from '../core/services/core-api.service';
import { RootDto } from 'src/app/model/root.dto.model';
import { ChildDto } from '../model/child.dto.model';
@Component({
selector: 'app-first-child',
templateUrl: './firstChild.component.html',
styleUrls: ['./firstChild.component.css']
})
export class FirstChildComponent implements OnInit {
dto: ChildDto;
isLoaded = false;
constructor(private apiService: CoreApiService, private zone: NgZone) {
this.apiService.onDataReceived.subscribe((rootDto: RootDto) => {
this.zone.run(() => {
this.dto = Utils.ObjectFactory.Create(rootDto.firstChildDto); // to make sure that we will have a new reference (so that change detction will be triggered) i use object instantiation
// NOTICE:
// for arrays don't simply assign or push new item to the array, because the reference is not changed the change detection is not triggered
// if the array size is small before assigning new value, you can simply empty (myArray = [];) the array otherwise don't do that
this.isLoaded = true;
});
});
}
ngOnInit() {
}
// the rest of logic
}
you can do the same for all other components and even you can create more events in share service and trigger it as you wish
让我们从一些一般性建议开始:
您通常不需要使数据从组件流向其(孙)子组件的服务。为此使用
@Input
绑定。当使用服务来管理数据流时,使用纯 RxJS,即
Observable
、Subject
、BehaviorSubject
等的实例EventEmitter
是 Angular 特定的,用于处理来自组件和指令的输出事件。如果您想使用BehaviorSubject
. 查看类似的解决方案,请选中 您不需要在默认区域中告诉 Angular 到 运行 代码。它默认执行此操作。
具体来说,您的服务可以像下面这样简单:
@Injectable({
providedIn: 'root'
})
export class ApiService {
constructor(private http: HttpClient) { }
getData(): Observable<Model> {
return this.http.get<Model>('...');
}
}
然后一个组件可以订阅它并将值存储在普通同步 属性 中。在这条路上,这个 属性 可以作为 @Input()
传递给子组件:
@Component({
selector: 'my-app',
template: `
<strong>AppComponent:</strong> {{model | json}}
<child [model]="model"></child>
`
})
export class AppComponent implements OnInit {
model: Model;
constructor(private apiService: ApiService) { }
ngOnInit(): void {
this.apiService.getData()
.subscribe(model => this.model = model);
}
}
您还可以随时更新 model
属性,更改将传播到子组件和孙组件。
Here's a Stackblitz 带有示例实现。