Angular 8 - как идентифицировать (и подписаться) элемент в шаблоне, переданном в компонент - PullRequest
1 голос
/ 01 февраля 2020

У меня есть родительский компонент, который визуализирует компонент «item», используя шаблон для определения разметки элемента (я разобрал свой сценарий до пустых костей):

// 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", мне нужно подписаться на событие click одного из элементов внутри #itemTemplate (например, div "AAA"). Подвох в том, что именно родительский компонент определяет, на какой из элементов в #itemTemplate следует подписаться. Это может быть жестко задано разработчиком в родительском компоненте (как именно это сделать, является частью вопроса здесь). В то же время обработчик кликов должен находиться внутри компонента item (для инкапсуляции функциональности обработки кликов - родительский компонент не должен этим заниматься).

Я попробовал три способа, как это сделать. это, но я тоже не люблю, и мне интересно, есть ли лучший способ. Эти 3 попытки не зависят друг от друга.

1. используйте переменную шаблона ref

Идея: , так же как ссылка #itemTemplate используется для идентификации шаблона, используйте тот же синтаксис, чтобы создать ссылку на нужный компонент (для подписки). Затем передайте ссылку на компонент "item" через новый @ Input.

// 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>

Результат / проблема: в компоненте "item" значение subscribeToThis не определено. (Я думаю, что это было неопределено из-за объема ссылки #subscribeToThis - я прав?) Поэтому компонент "item" не может использовать это, чтобы найти элемент для подписки.

Аналогично этому, с тем же результатом - я попытался найти ссылку #subscribeToThis в классе компонентов "item" следующим образом:

// 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 класс, чтобы пометить и найти элемент

Идея: назначить специальный класс («подписаться на это» в приведенном ниже примере) на нужный элемент #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. Используя их, я могу найти элемент по классу, а затем подписаться на его событие click.

Результат / успех: компонент "item" найдет элемент и подписку на щелчок элемента событие вызывает триггер обработчика щелчка в компоненте "item".

Проблема с этим подходом: этот низкоуровневый подход не рекомендуется по веским причинам.

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 () в модель (например, в onNgInit компонента "item"). Причиной такой гимнастики является относительно простое использование в # itemTemplate.

Результат / успех: clickHandler () вызывается при нажатии на правильный элемент.

Проблемы с этим подходом:

  • Родительский компонент должен знать имя функции magi c. Это не прерыватель сделки, но это шаг от красивого чистого черного ящика.

  • Если потребитель (родитель) должен работать с данными itemModel впоследствии, " функция надстройки clickHandler () не очень удобна. Неприятно, когда родитель возвращает данные с добавленным «мусором». Я знаю, что могу удалить его из объекта, но это похоже на начало кроличьей норы.

Заключение

Я новичок в Angular, и я надеясь, что мне просто не хватает какой-то техники, которая позволила бы мне реализовать это чисто, сохраняя компонент "item" как можно более "черным". Спасибо за любые идеи или идеи.

Ответы [ 3 ]

1 голос
/ 01 февраля 2020

В целях сохранения как можно большей степени компонента элемента я собираюсь предложить использовать ng-content / content-projection для решения этой проблемы.

Мы больше не будем использовать ng-шаблон, вместо этого ваш элемент item будет иметь ng-контент (создает слот проекции контента) внутри своего шаблона, а родительский компонент будет использовать этот слот проекции контента для рендеринга того, что было ранее в ng-шаблоне.

Класс родительского компонента:

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>

Класс компонента элемента:

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/

1 голос
/ 01 февраля 2020

Вам просто нужно предоставить метод щелчка в контексте, например,

// "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>
1 голос
/ 01 февраля 2020

В 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

Теперь вы можете сохранить функциональность щелчка внутри дочернего и родительского элементов можно уведомить ребенка с помощью @Output

Вот пример того, как сделать это с помощью службы: Связь через службу

Надеюсь, это поможет

...