我想制作一个自动完成组件,它向服务器发出请求并在屏幕上呈现接收到的值。我试图了解portal
and是如何overlay
工作的。现在这是我的自动完成组件
import {
Component, OnInit, Input, Output, EventEmitter, OnDestroy, ViewChild, ViewContainerRef,
ElementRef, Optional
} from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';
import { MdOption, ConnectedOverlayDirective, Dir, transformPlaceholder, transformPanel, fadeInContent } from '@angular/material';
import { Subscription } from 'rxjs/Subscription';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Subject } from 'rxjs/Subject';
import { AutocompleteConfiguration } from './autocomplete-config.model';
import { SearchState, SearchingService, IBackend } from './../../services/searching.service';
import { ISearchConfig } from './../../models/iSearch-config';
import { IHint } from './../generic-form/generic-form.service';
import { getValueAccessorProviders } from './../../models/custom-value-accessor.builder';
@Component({
selector: 'autocomplete',
templateUrl: './autocomplete.html',
providers: [
SearchingService,
getValueAccessorProviders(AutocompleteComponent)
],
animations: [
transformPlaceholder, transformPanel, fadeInContent
]
})
export class AutocompleteComponent implements OnInit, OnDestroy, ControlValueAccessor {
/** Placeholder to be shown if no value has been selected. */
@Input()
get placeholder() { return this._placeholder; }
set placeholder(value: string) {
this._placeholder = value;
// Must wait to record the trigger width to ensure placeholder width is included.
Promise.resolve(null).then(() => this._triggerWidth = this._getWidth());
}
@Input() hint: IHint = null;
@Input() iconPosition: string = ''; // 'prefix', 'suffix' or 'placeholder-prefix', 'placeholder-suffix'
@Input() autocompleteConfigurtation: AutocompleteConfiguration;
@Input() backend: IBackend;
@ViewChild(ConnectedOverlayDirective) overlayDir: ConnectedOverlayDirective;
/** Trigger that opens the select. */
@ViewChild('triggerRef', { read: ElementRef }) trigger: ElementRef;
@Output() blur: EventEmitter<FocusEvent> = new EventEmitter<FocusEvent>();
@Output() focus: EventEmitter<FocusEvent> = new EventEmitter<FocusEvent>();
get value() {
return this._inputValue;
}
set value(value: any) {
if (String(value) !== String(this._inputValue)) {
this._inputValue = String(value);
}
}
/** Whether or not the overlay panel is open. */
private _panelOpen = false;
/** The currently selected option. */
private _selected: MdOption;
private _placeholder: string;
/**
* The width of the trigger. Must be saved to set the min width of the overlay panel
* and the width of the selected value.
*/
private _triggerWidth: number;
private _inputValue: string = '';
private _focused: boolean = false;
private _disabled: boolean = false;
private onSearchStateChange: BehaviorSubject<SearchState>;
private onModelChangeSubject: Subject<any> = new Subject<any>();
private subscriptions: Subscription[] = [];
private searchState: SearchState = null;
/**
* The x-offset of the overlay panel in relation to the trigger's top start corner.
* This must be adjusted to align the selected option text over the trigger text when
* the panel opens. Will change based on LTR or RTL text direction.
*/
_offsetX = 0;
/**
* The y-offset of the overlay panel in relation to the trigger's top start corner.
* This must be adjusted to align the selected option text over the trigger text.
* when the panel opens. Will change based on the y-position of the selected option.
*/
_offsetY = 0;
/** The value of the select panel's transform-origin property. */
_transformOrigin: string = 'top';
/** The animation state of the placeholder. */
_placeholderState = '';
/**
* This position config ensures that the top "start" corner of the overlay
* is aligned with with the top "start" of the origin by default (overlapping
* the trigger completely). If the panel cannot fit below the trigger, it
* will fall back to a position above the trigger.
*/
_positions = [
{
originX: 'start',
originY: 'top',
overlayX: 'start',
overlayY: 'top',
},
{
originX: 'start',
originY: 'bottom',
overlayX: 'start',
overlayY: 'bottom',
},
];
/** The scroll position of the overlay panel, calculated to center the selected option. */
private _scrollTop = 0;
constructor(
private searchBuilder: SearchingService,
@Optional() private _dir: Dir
) { }
ngOnInit() {
this.onSearchStateChange = this.searchBuilder
.createSearchObservable(this.onModelChangeSubject, this.autocompleteConfigurtation.searchConfig, this.backend);
this.setSearchStateChangeSubscription();
}
setSearchStateChangeSubscription() {
this.subscriptions.push(
this.onSearchStateChange.subscribe(newState => {
debugger
this.searchState = newState;
// this._calculateOverlayPosition();
this._placeholderState = this._isRtl() ? 'floating-rtl' : 'floating-ltr';
this._panelOpen = newState && newState.responseObject && newState.responseObject.length > 0;
})
);
}
onModelChange(inputValue) {
this.value = inputValue;
this._onChangeCallback(this._inputValue);
this._onTouchedCallback();
this._activateSearch(this.value);
}
// From ControlValueAccessor interface
// the ngModel init or form write value
writeValue(value: any) {
this.value = value;
}
// From ControlValueAccessor interface
registerOnChange(fn: any) {
this._onChangeCallback = fn;
}
// From ControlValueAccessor interface
registerOnTouched(fn: any) {
this._onTouchedCallback = fn;
}
setDisabledState(isDisabled: boolean) {
this._disabled = isDisabled;
}
close() {
}
ngOnDestroy() {
this.subscriptions.forEach(val => val.unsubscribe());
this.subscriptions = [];
this.onModelChangeSubject.unsubscribe();
}
/**
* Sets the scroll position of the scroll container. This must be called after
* the overlay pane is attached or the scroll container element will not yet be
* present in the DOM.
*/
_setScrollTop(): void {
const scrollContainer =
this.overlayDir.overlayRef.overlayElement.querySelector('.md-select-panel');
scrollContainer.scrollTop = this._scrollTop;
}
/** The width of the trigger element. This is necessary to match
* the overlay width to the trigger width.
*/
_getWidth(): number {
return this._getTriggerRect().width;
}
_isRtl(): boolean {
return this._dir ? this._dir.value === 'rtl' : false;
}
_onPanelDone($event) {
console.log($event);
}
private _getTriggerRect(): ClientRect {
return this.trigger.nativeElement.getBoundingClientRect();
}
private _activateSearch(value) {
if (this._disabled || !this.focus) {
return;
}
this.onModelChangeSubject.next(value);
}
private _handleFocus($event) {
this._focused = true;
if (this.autocompleteConfigurtation.activateOnFocus) {
this._activateSearch(this.value);
}
this.focus.emit($event);
}
private _handleBlur($event) {
this._focused = false;
this._onTouchedCallback();
this.blur.emit($event);
}
private _onChangeCallback(_: any) { }
private _onTouchedCallback() { }
}
这是html
<md-input type="text"
#triggerRef
#origin="cdkOverlayOrigin"
cdk-overlay-origin
[disabled]="_disabled"
[(ngModel)]="value"
(blur)="_handleBlur($event)"
(focus)="_handleFocus($event)"
(ngModelChange)="onModelChange($event)">
<md-placeholder *ngIf="placeholder || iconPosition.indexOf('placeholder') !== -1">
<i *ngIf="iconPosition === 'placeholder-prefix'" class="material-icons app-input-icon">{{field.icon}}</i>
{{placeholder}}
<i *ngIf="iconPosition === 'placeholder-suffix'" class="material-icons app-input-icon">{{field.icon}}</i>
</md-placeholder>
<span md-prefix>
<md-icon *ngIf="iconPosition === 'prefix'">field.icon</md-icon>
</span>
<span md-suffix>
<md-icon *ngIf="iconPosition === 'suffix'">field.icon</md-icon>
</span>
<md-hint *ngIf="hint" [align]="hint.align">
{{hint.value}}
</md-hint>
</md-input>
<template
cdk-connected-overlay
hasBackdrop
backdropClass="cdk-overlay-transparent-backdrop"
[origin]="origin"
[open]="_panelOpen"
[positions]="_positions"
[minWidth]="_triggerWidth"
[offsetY]="_offsetY"
[offsetX]="_offsetX"
(backdropClick)="close()"
(attach)="_setScrollTop()">
<div class="md-select-panel"
[@transformPanel]="'showing'"
[style.transformOrigin]="_transformOrigin"
[class.md-select-panel-done-animating]="_panelDoneAnimating"
(@transformPanel.done)="_onPanelDone()"
(keydown)="log($event)">
<div class="md-select-content" [@fadeInContent]="'showing'">
<md-option *ngFor="let option of searchState?.responseObject"
[value]="option.value">
{{ option?.text }}
</md-option>
</div>
</div>
</template>
该组件可以正常工作并按应有的方式呈现叠加层。覆盖容器呈现选项唯一的问题是宽度。我阅读了门户和覆盖的核心概念,但我想了解它是如何与 Angular 一起工作的。
谁能解释一下它是如何工作的?或者至少我如何控制它的宽度?