Angular 8 - 如何识别(和订阅)传递给组件的模板中的元素
Angular 8 - how to identify (and subscribe to) an element in a template passed into a component
我有一个呈现 "item" 组件的 parent 组件,使用模板来定义项目的标记(我已将我的场景精简到最简单的骨架):
// parent component template
<item [model]="itemModel" [template]="itemTemplate"></item>
<ng-template #itemTemplate let-model="model">
<div>AAA {{model.property1}}</div>
<div>BBB {{model.property2}}</div>
<div>CCC {{model.property3}}</div>
</ng-template>
// "item" component template
<ng-container [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{model: model}"></ng-container>
在 "item" 组件代码中,我需要订阅 #itemTemplate 中元素之一的点击事件(例如 "AAA" div)。
要注意的是 parent 组件定义了要订阅#itemTemplate 中的哪些元素。这可以由开发人员在 parent 组件中进行硬编码(具体如何做到这一点是此处问题的一部分)。
同时,点击处理程序需要位于 "item" 组件内部(以封装点击处理功能 - parent 组件无需关心)。
我尝试了三种方法来做到这一点,但我都不喜欢,我想知道是否有更好的方法。这3次尝试是相互独立的。
1。使用模板引用变量
思路:正如#itemTemplate 引用用于标识模板一样,使用相同的语法创建对所需组件(订阅)的引用。
然后通过新的 @Input.
将引用传递给 "item" 组件
// parent component template
// the "item" component now takes an additional input "subscribeToThis"
<item [model]="itemModel" [template]="itemTemplate" [subscribeToThis]="subscribeToThis"></item>
<ng-template #itemTemplate let-model="model">
// the #subscribeToThis reference on the div below is used above as the value
// for the subscribeToThis input of the child component;
// but it's always undefined ...
<div #subscribeToThis >AAA {{model.property1}}</div>
<div>BBB {{model.property2}}</div>
<div>CCC {{model.property3}}</div>
</ng-template>
Result/problem: 在 "item" 组件中,subscribeToThis 未定义。 (由于 #subscribeToThis 引用的范围,我认为它未定义 - 我说得对吗?)因此 "item" 组件不能使用它来查找要订阅的元素。
与此类似,结果相同 - 我尝试在 "item" 组件 class 中找到 #subscribeToThis 引用,如下所示:
// bits from child component class
@ViewChild('subscribeToThis', {static: false}) elementToSubscribeTo: TemplateRef<any>;
ngAfterViewInit() {
// this.elementToSubscribeTo is undefined here;
// if "things worked", this.elementToSubscribeTo would refer to the element to whose click event I need to subscibe to
}
但是 elementToSubscribeTo 又是未定义的。
2。使用css class 标记并找到元素
想法: 将特殊的 class(下例中的 "subscribe-to-this")分配给所需的 #itemTemplate 元素:
// parent component template
<ng-template #itemTemplate let-model="model">
// note the class on this div; you can use low-level tools to find the element
// by its class, but that's not "clean angular"
<div class="subscribe-to-this">AAA {{model.property1}}</div>
<div>BBB {{model.property2}}</div>
<div>CCC {{model.property3}}</div>
</ng-template>
然后,在"item"组件中,注入ElementRef和Renderer2。使用它们,我可以通过 class 找到元素,然后订阅它的点击事件。
Result/success: "item" 组件将找到该元素并且订阅该元素的点击事件确实会触发 [=83] 中的点击处理程序=] 组件.
这种方法的问题:有充分的理由不鼓励这种低级方法。
3。直接在模板中的元素中分配点击处理程序
思路:
// parent component template
<ng-template #itemTemplate let-model="model">
// the click handler is assigned right here in the template;
// note that the event handler is a function on the model
// that is passed to the child component; that function needs to be
// added to the model "artificially" since the model shouldn't need to be aware
// of how it's handled in the "item" component; that's one of the bad points
// in this attempt
<div (click)="model.clickHandler()">AAA {{model.property1}}</div>
<div>BBB {{model.property2}}</div>
<div>CCC {{model.property3}}</div>
</ng-template>
为此,必须将 clickHandler() 函数添加到模型中(例如,在 "item" 组件的 onNgInit 中)。之所以做这种体操,是因为#itemTemplate 中的用法相对简单。
Result/success: 单击正确的元素时将调用 clickHandler()。
这种方法的问题:
parent 组件必须知道魔术函数名称。这不是一个交易破坏者,但离一个漂亮干净的 black-box 组件只有一步之遥。
如果消费者 (parent) 之后需要使用 itemModel 数据,则
"add-on" 函数 clickHandler() 放在那里并不好。当 parent 返回数据并添加一些 "garbage" 时,这并不好。我知道我可以从 object 中删除它,但这感觉就像是兔子洞的开始。
结论
我是 Angular 的新手,我希望我只是遗漏了一些技术,这些技术可以让我在保持 "item" 组件尽可能 "black-boxy" 的同时干净地实现它。
感谢您的任何想法或见解。
在 Angular 中,通知父级事件发生的方法是在子组件中使用 @Ouput
装饰器。
例如。
子组件
@Component({
selector: 'hello',
template: `<button (click)='mycustomEvent.emit('foo')'>Click me</button>`,
})
export class HelloComponent {
@Output() mycustomEvent = new EventEmitter();
}
父组件
@Component({
selector: 'my-app',
template: `<hello (mycustomEvent)="handleEvent($event)"></hello>`,
})
export class AppComponent {
handlEvent(event) {
console.log(event);
}
}
现在您可以将点击功能封装在子项中,并且可以通过 @Output
通知父项
以下是如何使用服务执行此操作的示例:
Communicate via Service
希望对您有所帮助
为了尽可能保持项目组件为 "black-boxy",我建议使用 ng-content / content-projection 来解决这个问题。
我们将不再使用 ng-template,相反,您的 item 组件将在其模板中包含 ng-content(创建内容投影槽),父组件将使用此内容投影槽来呈现内容之前在 ng-template 中。
父组件class:
export class ParentComponent {
numberOfClicks = 0;
onClick() {
this.numberOfClicks++;
}
}
父组件模板:
<p>Some parent html</p>
<app-item [clickEvent]="numberOfClicks">
<div>AAA</div>
<div (click)="onClick()">BBB</div>
<div>CCC</div>
</app-item>
<p>Some other parent html</p>
项目组件class:
export class ItemComponent implements OnChanges {
@Input() clickEvent: any;
ngOnChanges(changes: SimpleChanges) {
if (changes.clickEvent.currentValue) {
console.log('Write whatever code you want to run on the click event here');
}
}
}
项目组件模板
<p>Some item HTML</p>
<ng-content></ng-content>
<p>Some more item HTML</p>
我知道这不是您所要求的,但我相信这将是解决您问题的好方法。可以在这里阅读一篇关于内容投影的好文章:https://blog.angular-university.io/angular-ng-content/
你只需要在上下文中提供点击方法,例如
// "item" component template
<ng-container [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{model: model, onClickItem: doStuffWhenClick}"></ng-container>
// "item" component ts file
public doStuffWhenClick(...) { ... }
// parent component template
<item [model]="itemModel" [template]="itemTemplate"></item>
<ng-template #itemTemplate let-model="model" let-onClickItem="onClickItem">
<div (click)="onClickItem()">AAA {{model.property1}}</div>
<div>BBB {{model.property2}}</div>
<div>CCC {{model.property3}}</div>
</ng-template>
我有一个呈现 "item" 组件的 parent 组件,使用模板来定义项目的标记(我已将我的场景精简到最简单的骨架):
// parent component template
<item [model]="itemModel" [template]="itemTemplate"></item>
<ng-template #itemTemplate let-model="model">
<div>AAA {{model.property1}}</div>
<div>BBB {{model.property2}}</div>
<div>CCC {{model.property3}}</div>
</ng-template>
// "item" component template
<ng-container [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{model: model}"></ng-container>
在 "item" 组件代码中,我需要订阅 #itemTemplate 中元素之一的点击事件(例如 "AAA" div)。 要注意的是 parent 组件定义了要订阅#itemTemplate 中的哪些元素。这可以由开发人员在 parent 组件中进行硬编码(具体如何做到这一点是此处问题的一部分)。 同时,点击处理程序需要位于 "item" 组件内部(以封装点击处理功能 - parent 组件无需关心)。
我尝试了三种方法来做到这一点,但我都不喜欢,我想知道是否有更好的方法。这3次尝试是相互独立的。
1。使用模板引用变量
思路:正如#itemTemplate 引用用于标识模板一样,使用相同的语法创建对所需组件(订阅)的引用。 然后通过新的 @Input.
将引用传递给 "item" 组件// parent component template
// the "item" component now takes an additional input "subscribeToThis"
<item [model]="itemModel" [template]="itemTemplate" [subscribeToThis]="subscribeToThis"></item>
<ng-template #itemTemplate let-model="model">
// the #subscribeToThis reference on the div below is used above as the value
// for the subscribeToThis input of the child component;
// but it's always undefined ...
<div #subscribeToThis >AAA {{model.property1}}</div>
<div>BBB {{model.property2}}</div>
<div>CCC {{model.property3}}</div>
</ng-template>
Result/problem: 在 "item" 组件中,subscribeToThis 未定义。 (由于 #subscribeToThis 引用的范围,我认为它未定义 - 我说得对吗?)因此 "item" 组件不能使用它来查找要订阅的元素。
与此类似,结果相同 - 我尝试在 "item" 组件 class 中找到 #subscribeToThis 引用,如下所示:
// bits from child component class
@ViewChild('subscribeToThis', {static: false}) elementToSubscribeTo: TemplateRef<any>;
ngAfterViewInit() {
// this.elementToSubscribeTo is undefined here;
// if "things worked", this.elementToSubscribeTo would refer to the element to whose click event I need to subscibe to
}
但是 elementToSubscribeTo 又是未定义的。
2。使用css class 标记并找到元素
想法: 将特殊的 class(下例中的 "subscribe-to-this")分配给所需的 #itemTemplate 元素:
// parent component template
<ng-template #itemTemplate let-model="model">
// note the class on this div; you can use low-level tools to find the element
// by its class, but that's not "clean angular"
<div class="subscribe-to-this">AAA {{model.property1}}</div>
<div>BBB {{model.property2}}</div>
<div>CCC {{model.property3}}</div>
</ng-template>
然后,在"item"组件中,注入ElementRef和Renderer2。使用它们,我可以通过 class 找到元素,然后订阅它的点击事件。
Result/success: "item" 组件将找到该元素并且订阅该元素的点击事件确实会触发 [=83] 中的点击处理程序=] 组件.
这种方法的问题:有充分的理由不鼓励这种低级方法。
3。直接在模板中的元素中分配点击处理程序
思路:
// parent component template
<ng-template #itemTemplate let-model="model">
// the click handler is assigned right here in the template;
// note that the event handler is a function on the model
// that is passed to the child component; that function needs to be
// added to the model "artificially" since the model shouldn't need to be aware
// of how it's handled in the "item" component; that's one of the bad points
// in this attempt
<div (click)="model.clickHandler()">AAA {{model.property1}}</div>
<div>BBB {{model.property2}}</div>
<div>CCC {{model.property3}}</div>
</ng-template>
为此,必须将 clickHandler() 函数添加到模型中(例如,在 "item" 组件的 onNgInit 中)。之所以做这种体操,是因为#itemTemplate 中的用法相对简单。
Result/success: 单击正确的元素时将调用 clickHandler()。
这种方法的问题:
parent 组件必须知道魔术函数名称。这不是一个交易破坏者,但离一个漂亮干净的 black-box 组件只有一步之遥。
如果消费者 (parent) 之后需要使用 itemModel 数据,则 "add-on" 函数 clickHandler() 放在那里并不好。当 parent 返回数据并添加一些 "garbage" 时,这并不好。我知道我可以从 object 中删除它,但这感觉就像是兔子洞的开始。
结论
我是 Angular 的新手,我希望我只是遗漏了一些技术,这些技术可以让我在保持 "item" 组件尽可能 "black-boxy" 的同时干净地实现它。 感谢您的任何想法或见解。
在 Angular 中,通知父级事件发生的方法是在子组件中使用 @Ouput
装饰器。
例如。 子组件
@Component({
selector: 'hello',
template: `<button (click)='mycustomEvent.emit('foo')'>Click me</button>`,
})
export class HelloComponent {
@Output() mycustomEvent = new EventEmitter();
}
父组件
@Component({
selector: 'my-app',
template: `<hello (mycustomEvent)="handleEvent($event)"></hello>`,
})
export class AppComponent {
handlEvent(event) {
console.log(event);
}
}
现在您可以将点击功能封装在子项中,并且可以通过 @Output
以下是如何使用服务执行此操作的示例: Communicate via Service
希望对您有所帮助
为了尽可能保持项目组件为 "black-boxy",我建议使用 ng-content / content-projection 来解决这个问题。
我们将不再使用 ng-template,相反,您的 item 组件将在其模板中包含 ng-content(创建内容投影槽),父组件将使用此内容投影槽来呈现内容之前在 ng-template 中。
父组件class:
export class ParentComponent {
numberOfClicks = 0;
onClick() {
this.numberOfClicks++;
}
}
父组件模板:
<p>Some parent html</p>
<app-item [clickEvent]="numberOfClicks">
<div>AAA</div>
<div (click)="onClick()">BBB</div>
<div>CCC</div>
</app-item>
<p>Some other parent html</p>
项目组件class:
export class ItemComponent implements OnChanges {
@Input() clickEvent: any;
ngOnChanges(changes: SimpleChanges) {
if (changes.clickEvent.currentValue) {
console.log('Write whatever code you want to run on the click event here');
}
}
}
项目组件模板
<p>Some item HTML</p>
<ng-content></ng-content>
<p>Some more item HTML</p>
我知道这不是您所要求的,但我相信这将是解决您问题的好方法。可以在这里阅读一篇关于内容投影的好文章:https://blog.angular-university.io/angular-ng-content/
你只需要在上下文中提供点击方法,例如
// "item" component template
<ng-container [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{model: model, onClickItem: doStuffWhenClick}"></ng-container>
// "item" component ts file
public doStuffWhenClick(...) { ... }
// parent component template
<item [model]="itemModel" [template]="itemTemplate"></item>
<ng-template #itemTemplate let-model="model" let-onClickItem="onClickItem">
<div (click)="onClickItem()">AAA {{model.property1}}</div>
<div>BBB {{model.property2}}</div>
<div>CCC {{model.property3}}</div>
</ng-template>