Angular 2 Material - How do overlay and portal work?
Asked Answered
D

1

9

I want to make an autocomplete component which makes requests to the server and renders the received values on the screen. I'm trying to understand how the portal and overlay work. Right now this is my component for autocomplete

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() { }
}

And this is the 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>

The component works and it renders the overlay as it should. The overlay container renders the options The only problem is the width. I read the core concepts of the portals and the overlay but I would like to understand how it really works with angular. Here is how it renders right now

Can anyone explain a little bit how it works ? Or at least how I control it's width ?

Decompress answered 19/1, 2017 at 18:37 Comment(0)
D
0

I managed to use the overlay component if anyone wants an example you can try to have a look here I will try to add comments about how I think it works. Also some documentation about it and how to use it will be more then welcomed from @angular/material. I think is very usable to create third party components which needs to be rendered in the dome tree to not have problems with overflow hidden or translate3d

the cdk documentation was released cdk docs also here is a usefull guide for overlay.

Decompress answered 3/2, 2017 at 8:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.