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()。

这种方法的问题:

结论

我是 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);
  }
}

Stackblitz Example

现在您可以将点击功能封装在子项中,并且可以通过 @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>