我尝试使用按钮组件和下拉组件为Bootstrap拆分式下拉按钮创建包装器。我需要在click
和document: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 {}
答案 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());
});
}
在上述情况下无需服务。