angular – 从ng-content向父级发送输出

我正在尝试使用按钮组件和下拉组件为Bootstrap拆分式下拉按钮创建一个包装器.我需要在点击和文档上发出输出:从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 {}

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

请参阅//注释我添加到原始组件的内容,以使它们与服务一起使用.

另见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
    }
  }
}
点赞