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.
Can anyone explain a little bit how it works ? Or at least how I control it's width ?