将输出从ng-content发送到parent

时间:2017-02-16 15:44:49

标签: angular

我尝试使用按钮组件和下拉组件为Bootstrap拆分式下拉按钮创建包装器。我需要在clickdocument:click上从ng-content<button perf-btn>)向父DropdownComponent发出一个输出。

有几个类似的问题,但似乎都没有适合我的用例。

用法(app.component.html)

<perf-drop [data]="items">
  <button perf-btn>Default Dropdown Button</button> // click doesn't open dropdown
  <button perf-btn dropdown="true"></button>        // click opens dropdown
</perf-drop>

dropdown.component.html

<ng-content select="[perf-btn]" (notify)='onNotify($event)')></ng-content>
<ul class="dropdown-menu">
  <template ngFor let-item [ngForOf]="data">
    <li *ngIf="item.separator" role="separator" class="divider"></li>
    <li *ngIf="!item.separator" [class.disabled]="item.disabled">
      <a [routerLink]="item.path" [ngClass]="getItemColor(item.color)">
        {{item.label}}
      </a>
    </li>
  </template>
</ul>

dropdown.component.ts

import { Component, Input, ElementRef} from '@angular/core';

@Component({
  selector: 'perf-drop',
  host: {
    '[attr.disabled]': 'disabled',
    '[class.open]': 'isOpen'
  },
  templateUrl: 'dropdown.component.html',
  styleUrls: ['dropdown.component.scss']
})
export class DropdownComponent {
  private _data: any[] = [];
  private _isOpen: boolean = false;

  @Input()
  get isOpen() { return this._isOpen; }
  set isOpen(value: boolean) { this._isOpen = value ? true : null; }

  @Input()
  get data(): any[] { return this._data; }
  set data(value: any[]) {
    this._data = value;
  }

  constructor(private _elementRef: ElementRef) { }

  private toggle(): void {
    this._isOpen = !this._isOpen;
  }

  private close(event): void {
    if (!this._elementRef.nativeElement.contains(event.target) && this._isOpen)
      this._isOpen = false;
  }

  private getItemColor(color) {
    if (color) return `text--${color}`;
  }
}

btn.component.ts

import { Component, ViewEncapsulation, Input, HostBinding, ChangeDetectionStrategy,
  ElementRef, Renderer, EventEmitter, Output } from '@angular/core';

@Component({
  selector: 'button[perf-btn], input[perf-btn], a[perf-btn], div[perf-btn], perf-btn',
  host: {
    [snip conditional classes],
    "(click)": "_toggle()",
    "(document:click)": "_close($event)"
  },
  templateUrl: './btn.component.html',
  styleUrls: ['./btn.component.scss']
})
export class BtnComponent {
  [snip irrelevant fields]
  private _dropdown: boolean;
  private _state: boolean = false;

  @Input()
  get dropdown() { return this._dropdown; }
  set dropdown(value: boolean) { this._dropdown = value ? true : null; }

  get state() { return this._state; }
  set state(value: boolean) { this._state = value ? true : null; }

  [snip irrelevant getters/setters]

  @Output() notify: EventEmitter<boolean> = new EventEmitter<boolean>();

  constructor(private _elementRef: ElementRef, private _renderer: Renderer) { }

  _toggle() {
    console.log("notifying " + this._state);
    this._state = !this._state;
    this.notify.emit(this._state);
  }

  _close(event) {
    if (!this._elementRef.nativeElement.contains(event.target) && this._state) {
      this._state = false;
      this.notify.emit(this._state);
    }
  }

  [snip irrelevant functions]
}

btn.component.html

<ng-content></ng-content>
<span class="caret" *ngIf="dropdown"></span>

dropdown.directive.ts

import { Directive } from '@angular/core';
import { BtnDirective } from './btn.directive';

@Directive({
  selector: `button[perf-drop], button[perf-drop], a[perf-drop], input[perf-drop],
            div[perf-drop], perf-drop`,
  host: {
    '[class.btn-group]': 'true',
    '[attr.disabled]': '[disabled]'
  }
})
export class DropdownDirective extends BtnDirective {}

btn.directive.ts

import { Directive } from '@angular/core';

@Directive({
  selector: `button[perf-btn], button[perf-btn], a[perf-btn], input[perf-btn],
            div[perf-btn], perf-btn`,
  host: {
    '[class.btn]': 'true'
  }
})
export class BtnDirective {}

3 个答案:

答案 0 :(得分:1)

感谢@AngularFrance,我了解到ng-content无法发出。但是,服务可以在父组件和子组ng-content之间进行通信。

请参阅// comments了解我添加到原始组件的内容,以使其与服务配合使用。

另见Bidirectional Communication食谱食谱。

btn.service.ts

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class BtnService {
  private _stateSource: Subject<boolean> = new Subject<boolean>();
  public state$: Observable<Subject<boolean>> = this._stateSource.asObservable();

  public toggle(state: boolean): void {
    console.log("toggling");
    this._stateSource.next(state);
  }

  public close(): void {
    this._stateSource.next(false);
  }

  public open(): void {
    this._stateSource.next(true);
  }
}

dropdown.component.ts

import { Component, Input, ElementRef, OnDestroy } from '@angular/core';
import { Subscription }   from 'rxjs/Subscription';    // import subscription

import { BtnService } from './btn.service';            // import service

@Component({
  selector: 'perf-drop',
  host: {
    '[attr.disabled]': 'disabled',
    '[class.open]': '_isOpen'
  },
  templateUrl: 'dropdown.component.html',
  styleUrls: ['dropdown.component.scss'],
  providers: [BtnService]                              // add service as provider to parent
})
export class DropdownComponent implements OnDestroy {
  private _data: any[] = [];
  private _isOpen: boolean = false;
  private _subscription: Subscription;

  get isOpen() { return this._isOpen; }
  set isOpen(value: boolean) { this._isOpen = value ? true : null; }

  @Input()
  get data(): any[] { return this._data; }
  set data(value: any[]) {
    this._data = value;
  }

  constructor(private _elementRef: ElementRef,
    private _service: BtnService) {                      // add to constructor
      this._subscription = _service.state$.subscribe(    // subscribe to service
        state => {
          this._isOpen = state;
        })
    }

  ngOnDestroy() {
    // prevent memory leak when component destroyed
    this._subscription.unsubscribe();
  }
}

btn.component.ts

import { Component, ViewEncapsulation, Input, HostBinding, ChangeDetectionStrategy,
  ElementRef, Renderer, EventEmitter } from '@angular/core';

import { BtnService } from './btn.service';                   // import service

@Component({
  selector: 'button[perf-btn], input[perf-btn], a[perf-btn], div[perf-btn], perf-btn',
  host: {
    "(click)": "_dropdown && _toggle()",
    "(document:click)": "_dropdown && _close()"
  },
  templateUrl: './btn.component.html',
  styleUrls: ['./btn.component.scss']
})
export class BtnComponent {
  private _dropdown: boolean;
  private _state: boolean;

  @Input()
  get dropdown() { return this._dropdown; }
  set dropdown(value: boolean) { this._dropdown = value ? true : null; }

  @Input()
  get state() { return this._state; }
  set state(value: boolean) { this._state = value; }

  constructor(private _elementRef: ElementRef, private _renderer: Renderer,
    private _service: BtnService) {                      // add service to constructor
  }

  _toggle() {
    this._state = !this._state;
    this._service.toggle(this._state);                   // call service
  }

  _close() {
    if (!this._elementRef.nativeElement.contains(event.target) && this._state) {
      this._state = false;
      this._service.close();                             // call service
    }
  }
}

答案 1 :(得分:0)

另一个答案是依靠包含组件进行布线,例如在名为ParentComponent的组件中

<app-filter-bar #filterBar>
  <app-some-contents (someEvent)="callContainingComponent()"></app-some-contents>
</app-filter-bar>

someEvent触发,在ParentComponent上调用callContainingComponent,ParentComponent通过ViewChild在filterBar上调用方法

不太干净,您每次都必须明确地进行接线,但是更简单,父级现在可以在必要时拦截呼叫

答案 2 :(得分:0)

尽管<ng-content>本身不能发出,但内容后面的实际组成部分可以发出。

实际上,嵌套组件可以具有如下的EventEmitter:

@Output()
nestedComponentChange: EventEmitter<number> = new EventEmitter<number>();

父组件随后可以监听:


@ContentChildren(MyNestedComponent) templates: QueryList<MyNestedComponent>;

ngAfterContentInit() {
    this.templates.forEach((template) => {
      template.nestedComponentChange.subscribe(() => doSomething());
    });
}

在上述情况下无需服务。