8

我想制作一个自动完成组件,它向服务器发出请求并在屏幕上呈现接收到的值。我试图了解portaland是如何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 一起工作的。 这是它现在的渲染方式

谁能解释一下它是如何工作的?或者至少我如何控制它的宽度?

4

1 回答 1

0

如果有人想要一个示例,我设法使用了覆盖组件,您可以尝试在这里查看我将尝试添加有关我认为它如何工作的评论。还有一些关于它以及如何使用它的文档将更受@angular/material 的欢迎。我认为创建需要在圆顶树中渲染的第三方组件非常有用,不会出现溢出隐藏或 translate3d 的问题

cdk 文档已发布 cdk 文档 也在这里是一个有用的覆盖指南。

于 2017-02-03T08:17:58.000 回答