Angular 2 个组件:如何处理循环输入和输出
Angular 2 Components: How to handle circular Inputs and Outputs
现状:
我有一个 parent
和一个 child
组件。
parent
使用 @Input
初始化 child
的数据。当用户使用 @Output
编辑数据时,child
会通知父级。因为 数据是不可变的 ,child
必须连同通知一起发送数据。
当 parent
收到通知时,它将检查提交的数据是否有效,然后设置它(这会将新值传播到其他一些子组件)。
问题:
在parent
里面设置新数据的时候,当然也会给刚刚提交数据的child
组件。这将触发 child
的 ngOnChanges
,然后触发 UI.
的重绘
一些背景:
parent
有几个不同的 child
组件,它们都依赖于相同的 myItem
数据,并且可以编辑此数据,然后在更改时通知 parent
。
这是代码的简化版本,应该可以显示问题。
父组件:
template:
<child [input]="myItem" (output)="onMyItemChange($event)">
code:
ngOnInit() {
this.myItem = getDataViaHTTP();
}
onMyItemChange($event) {
if($event.myItem.isValid()) {
this.myItem = $event.myItem;
}
}
子组件:
template:
<input [(ngModel)]="myItem.name" (ngModelChange)="modelChange($event)">
code:
@Input() input;
@Output() output = new EventEmitter();
myItem;
ngOnChanges(changes) {
this.myItem = changes.input.currentValue.toMutableJS();
}
modelChange($event) {
this.output.emit(this.myItem.toImmutableJS())
}
如您所见,child
组件从 @Input
获取数据并使其可变。在将其发送回 parent
之前,它将再次使其不可变。
是否有任何模式可以防止这些循环事件?
我现在想出了一个可行的(但非常难看的解决方案):
child
将 最后发出的值 存储在 modelChange
中
当ngOnChanges
被调用时,我们可以比较那些对象的引用
如果引用相等,那么这个特定的 child
组件创建了值
简化代码示例:
@Input() input; // most recent version (immutable)
@Output() output = new EventEmitter();
myItem; // mutable version
lastMyItem; // last emitted version (immutable)
ngOnChanges(changes) {
// this will only trigger, if another component
// created the data instance
if(changes.input.currentValue !== lastMyItem) {
this.myItem = changes.input.currentValue.toMutableJS();
}
}
modelChange($event) {
this.lastMyItem = this.myItem.toImmutableJS();
this.output.emit(this.lastMyItem);
}
它按预期工作,但我仍然想知道是否有使用更少代码的更好方法。
也许可以使用 Observable
s 来解决这个问题 @Input() input
?
如果我们坚持使用 bi-directionaly 事件触发器,我想不出一种方法来打破圆圈。特别是多个 children.
方法一
我能想到的一种方法是 parent 和 children 都使用共享数据服务。数据一劳永逸,因为各方都使用相同的数据。
globaldata.service.ts
import { Injectable } from '@angular/core';
interface ShareObj {
[id: string]: any;
}
@Injectable()
export class GlobalDataService {
shareObj: ShareObj = {};
}
app.module.ts(假设这是你的根模块)
import { GlobalDataService } from './globaldata.service';
//
// skip ..
//
@NgModule({
//
// skip ..
//
provider:[GlobalDataService]
})
export class AppModule {}
parent.component.ts(假设non-root,多个实例,app.module的一部分)
template:
<child [parent]="myId"></child>
code:
import { GlobalDataService } from './globaldata.service';
//
// skip ..
//
// use uuid to generate unique id
private uuid = require('node-uuid');
myId = this.uuid.v1();
constructor(private gd: GlobalDataService){
// This can be string, array or object
this.gd.shareObj[myId]='data';
}
child.component.ts
template:
<input [(ngModel)]="gd.shareObj[parent]">
code:
import { GlobalDataService } from './globaldata.service';
//
// skip ..
//
constructor(private gd: GlobalDataService){}
@Input() parent;
方法二——广播队列
使用 RxJs 主题订阅,就像广播队列一样。我实际上已经用示例创建了一个包:
https://github.com/J-Siu/ng2-simple-mq
https://github.com/J-Siu/ng2-simple-mq-example
想法:
- Parent 和所有 children 将订阅同一个队列
- 广播到队列时包含一个发送者id,如果你使用我的包,你可以使用订阅id作为发送者id,因为它是一个uuid。
- 回调将检查发件人 ID,如果消息来自自己则不执行任何操作
Parent(假设non-root,多个实例,app.module的一部分)
import {Component, OnInit} from '@angular/core';
import {SimpleMQ} from 'ng2-simple-mq';
template:
<child [parent]="myId"></child>
code:
export class SomeComponent implements OnInit {
title = 'Some Component';
// use uuid to generate unique id
private uuid = require('node-uuid');
myId = this.uuid.v1();
myItem = {};
constructor(private smq: SimpleMQ) { }
ngOnInit() {
this.smq.subscribe(this.myId, e => this.receiveBroadcast(e));
}
broadcast() {
let msg = {
id: this.myId,
msg: 'some messages or object go here'
};
// Publish to queue name 'this.myId'
this.smq.publish(this.myId, msg);
}
receiveBroadcast(m) {
if (m.id !== this.myId) {
// msg from soneone else, lets do something
this.myItem = m.msg; // Update local data
console.log(m.Id + ' received: ' + m.msg);
}
}
}
Child
import {Component, Input, OnInit} from '@angular/core';
import {SimpleMQ} from 'ng2-simple-mq';
template:
<input [(ngModel)]="myItem.name" (ngModelChange)="broadcast()">
code:
export class SomeComponent implements OnInit {
title = 'Some Component';
@Input() parent;
// use uuid to generate unique id
private uuid = require('node-uuid');
myId = this.uuid.v1();
myItem = {};
constructor(private smq: SimpleMQ) { }
ngOnInit() {
this.smq.subscribe(parent, e => this.receiveBroadcast(e));
}
broadcast() {
let msg = {
id: this.myId,
msg: this.myItem // send the whole object
};
// Publish to queue name = parent id
this.smq.publish(parent, msg);
}
receiveBroadcast(m) {
if (m.id !== this.myId) {
// msg from soneone else, lets do something
this.myItem = m.msg; // Update local data
console.log(m.Id + ' received: ' + m.msg);
}
}
}
现状:
我有一个 parent
和一个 child
组件。
parent
使用 @Input
初始化 child
的数据。当用户使用 @Output
编辑数据时,child
会通知父级。因为 数据是不可变的 ,child
必须连同通知一起发送数据。
当 parent
收到通知时,它将检查提交的数据是否有效,然后设置它(这会将新值传播到其他一些子组件)。
问题:
在parent
里面设置新数据的时候,当然也会给刚刚提交数据的child
组件。这将触发 child
的 ngOnChanges
,然后触发 UI.
一些背景:
parent
有几个不同的 child
组件,它们都依赖于相同的 myItem
数据,并且可以编辑此数据,然后在更改时通知 parent
。
这是代码的简化版本,应该可以显示问题。
父组件:
template:
<child [input]="myItem" (output)="onMyItemChange($event)">
code:
ngOnInit() {
this.myItem = getDataViaHTTP();
}
onMyItemChange($event) {
if($event.myItem.isValid()) {
this.myItem = $event.myItem;
}
}
子组件:
template:
<input [(ngModel)]="myItem.name" (ngModelChange)="modelChange($event)">
code:
@Input() input;
@Output() output = new EventEmitter();
myItem;
ngOnChanges(changes) {
this.myItem = changes.input.currentValue.toMutableJS();
}
modelChange($event) {
this.output.emit(this.myItem.toImmutableJS())
}
如您所见,child
组件从 @Input
获取数据并使其可变。在将其发送回 parent
之前,它将再次使其不可变。
是否有任何模式可以防止这些循环事件?
我现在想出了一个可行的(但非常难看的解决方案):
child
将 最后发出的值 存储在modelChange
中
当
ngOnChanges
被调用时,我们可以比较那些对象的引用如果引用相等,那么这个特定的
child
组件创建了值
简化代码示例:
@Input() input; // most recent version (immutable)
@Output() output = new EventEmitter();
myItem; // mutable version
lastMyItem; // last emitted version (immutable)
ngOnChanges(changes) {
// this will only trigger, if another component
// created the data instance
if(changes.input.currentValue !== lastMyItem) {
this.myItem = changes.input.currentValue.toMutableJS();
}
}
modelChange($event) {
this.lastMyItem = this.myItem.toImmutableJS();
this.output.emit(this.lastMyItem);
}
它按预期工作,但我仍然想知道是否有使用更少代码的更好方法。
也许可以使用 Observable
s 来解决这个问题 @Input() input
?
如果我们坚持使用 bi-directionaly 事件触发器,我想不出一种方法来打破圆圈。特别是多个 children.
方法一
我能想到的一种方法是 parent 和 children 都使用共享数据服务。数据一劳永逸,因为各方都使用相同的数据。
globaldata.service.ts
import { Injectable } from '@angular/core';
interface ShareObj {
[id: string]: any;
}
@Injectable()
export class GlobalDataService {
shareObj: ShareObj = {};
}
app.module.ts(假设这是你的根模块)
import { GlobalDataService } from './globaldata.service';
//
// skip ..
//
@NgModule({
//
// skip ..
//
provider:[GlobalDataService]
})
export class AppModule {}
parent.component.ts(假设non-root,多个实例,app.module的一部分)
template:
<child [parent]="myId"></child>
code:
import { GlobalDataService } from './globaldata.service';
//
// skip ..
//
// use uuid to generate unique id
private uuid = require('node-uuid');
myId = this.uuid.v1();
constructor(private gd: GlobalDataService){
// This can be string, array or object
this.gd.shareObj[myId]='data';
}
child.component.ts
template:
<input [(ngModel)]="gd.shareObj[parent]">
code:
import { GlobalDataService } from './globaldata.service';
//
// skip ..
//
constructor(private gd: GlobalDataService){}
@Input() parent;
方法二——广播队列
使用 RxJs 主题订阅,就像广播队列一样。我实际上已经用示例创建了一个包:
https://github.com/J-Siu/ng2-simple-mq
https://github.com/J-Siu/ng2-simple-mq-example
想法:
- Parent 和所有 children 将订阅同一个队列
- 广播到队列时包含一个发送者id,如果你使用我的包,你可以使用订阅id作为发送者id,因为它是一个uuid。
- 回调将检查发件人 ID,如果消息来自自己则不执行任何操作
Parent(假设non-root,多个实例,app.module的一部分)
import {Component, OnInit} from '@angular/core';
import {SimpleMQ} from 'ng2-simple-mq';
template:
<child [parent]="myId"></child>
code:
export class SomeComponent implements OnInit {
title = 'Some Component';
// use uuid to generate unique id
private uuid = require('node-uuid');
myId = this.uuid.v1();
myItem = {};
constructor(private smq: SimpleMQ) { }
ngOnInit() {
this.smq.subscribe(this.myId, e => this.receiveBroadcast(e));
}
broadcast() {
let msg = {
id: this.myId,
msg: 'some messages or object go here'
};
// Publish to queue name 'this.myId'
this.smq.publish(this.myId, msg);
}
receiveBroadcast(m) {
if (m.id !== this.myId) {
// msg from soneone else, lets do something
this.myItem = m.msg; // Update local data
console.log(m.Id + ' received: ' + m.msg);
}
}
}
Child
import {Component, Input, OnInit} from '@angular/core';
import {SimpleMQ} from 'ng2-simple-mq';
template:
<input [(ngModel)]="myItem.name" (ngModelChange)="broadcast()">
code:
export class SomeComponent implements OnInit {
title = 'Some Component';
@Input() parent;
// use uuid to generate unique id
private uuid = require('node-uuid');
myId = this.uuid.v1();
myItem = {};
constructor(private smq: SimpleMQ) { }
ngOnInit() {
this.smq.subscribe(parent, e => this.receiveBroadcast(e));
}
broadcast() {
let msg = {
id: this.myId,
msg: this.myItem // send the whole object
};
// Publish to queue name = parent id
this.smq.publish(parent, msg);
}
receiveBroadcast(m) {
if (m.id !== this.myId) {
// msg from soneone else, lets do something
this.myItem = m.msg; // Update local data
console.log(m.Id + ' received: ' + m.msg);
}
}
}