From 881e16bbe21bf48888a0c1b416122c851cebeb77 Mon Sep 17 00:00:00 2001 From: Ed Carroll Date: Thu, 10 Aug 2017 18:43:55 +0100 Subject: [PATCH 1/5] feat(select): Renamed `value` to `option` on select options Value was often unclear --- .../localization/localization.page.ts | 8 ++-- .../modules/datepicker/datepicker.page.ts | 2 +- .../src/app/pages/modules/modal/modal.page.ts | 2 +- .../app/pages/modules/popup/popup.page.html | 2 +- .../app/pages/modules/select/select.page.ts | 38 +++++++++---------- .../modules/transition/transition.page.ts | 2 +- src/modules/select/classes/select-base.ts | 6 +-- .../select/components/multi-select-label.ts | 8 ++-- src/modules/select/components/multi-select.ts | 8 ++-- .../select/components/select-option.ts | 8 ++-- src/modules/select/components/select.ts | 6 +-- 11 files changed, 45 insertions(+), 45 deletions(-) diff --git a/demo/src/app/pages/behaviors/localization/localization.page.ts b/demo/src/app/pages/behaviors/localization/localization.page.ts index 310e54d30..b862752dc 100644 --- a/demo/src/app/pages/behaviors/localization/localization.page.ts +++ b/demo/src/app/pages/behaviors/localization/localization.page.ts @@ -15,7 +15,7 @@ const exampleTemplate = ` #lang> + [option]="l">
@@ -26,9 +26,9 @@ const exampleTemplate = `
- - - + + +
diff --git a/demo/src/app/pages/modules/datepicker/datepicker.page.ts b/demo/src/app/pages/modules/datepicker/datepicker.page.ts index 909d43636..25b60f8bb 100644 --- a/demo/src/app/pages/modules/datepicker/datepicker.page.ts +++ b/demo/src/app/pages/modules/datepicker/datepicker.page.ts @@ -18,7 +18,7 @@ const exampleStandardTemplate = `
- +
diff --git a/demo/src/app/pages/modules/modal/modal.page.ts b/demo/src/app/pages/modules/modal/modal.page.ts index 3107925da..1a6f6cfc6 100644 --- a/demo/src/app/pages/modules/modal/modal.page.ts +++ b/demo/src/app/pages/modules/modal/modal.page.ts @@ -44,7 +44,7 @@ const exampleComponentTemplate = `
- +
diff --git a/demo/src/app/pages/modules/popup/popup.page.html b/demo/src/app/pages/modules/popup/popup.page.html index 134749e8f..6f2d6af78 100644 --- a/demo/src/app/pages/modules/popup/popup.page.html +++ b/demo/src/app/pages/modules/popup/popup.page.html @@ -40,7 +40,7 @@

Placement


- +
diff --git a/demo/src/app/pages/modules/select/select.page.ts b/demo/src/app/pages/modules/select/select.page.ts index 84db911aa..9c7b49ce9 100644 --- a/demo/src/app/pages/modules/select/select.page.ts +++ b/demo/src/app/pages/modules/select/select.page.ts @@ -13,7 +13,7 @@ const exampleStandardTemplate = ` [isDisabled]="disabled" #select> + [option]="option"> @@ -28,7 +28,7 @@ const exampleStandardTemplate = ` [hasLabels]="!hideLabels" #multiSelect> + [option]="option">

@@ -52,10 +52,10 @@ const exampleVariationsTemplate = `

Basic

- - - - + + + +
@@ -66,9 +66,9 @@ const exampleVariationsTemplate = ` Trending repos
Adjust time span
- - - + + +
@@ -85,7 +85,7 @@ const exampleVariationsTemplate = ` Filter by tag - + @@ -108,7 +108,7 @@ const exampleInMenuSearchTemplate = ` Options `; @@ -128,7 +128,7 @@ const exampleTemplateTemplate = ` [optionTemplate]="optionTemplate" [isSearchable]="true" #templated> - +
@@ -138,7 +138,7 @@ const exampleTemplateTemplate = ` [options]="options" [optionFormatter]="formatter" #formatted> - +
@@ -152,10 +152,10 @@ const exampleSearchLookupTemplate = ` [(ngModel)]="selectedOption" [optionsLookup]="optionsLookup" labelField="name" - valueField="id" + optionField="id" [isSearchable]="true" #searchSelect> - +

Currently selected: {{ selectedOption | json }}

@@ -257,7 +257,7 @@ export class SelectPage { }, { name: "localeOverrides", - type: "RecursivePartial", + type: "RecursivePartial", description: "Overrides the values from the localization service." } ], @@ -374,7 +374,7 @@ export class SelectPage { }, { name: "localeOverrides", - type: "Partial", + type: "Partial", description: "Overrides the values from the localization service." } ], @@ -395,9 +395,9 @@ export class SelectPage { selector: "", properties: [ { - name: "value", + name: "option", type: "T", - description: "Sets the value of the option.", + description: "Sets the option the component represents.", required: true } ] diff --git a/demo/src/app/pages/modules/transition/transition.page.ts b/demo/src/app/pages/modules/transition/transition.page.ts index 427c0df27..645411637 100644 --- a/demo/src/app/pages/modules/transition/transition.page.ts +++ b/demo/src/app/pages/modules/transition/transition.page.ts @@ -7,7 +7,7 @@ const exampleStandardTemplate = `
- + `; diff --git a/src/modules/select/classes/select-base.ts b/src/modules/select/classes/select-base.ts index 041bc9d69..4714c6dde 100644 --- a/src/modules/select/classes/select-base.ts +++ b/src/modules/select/classes/select-base.ts @@ -305,12 +305,12 @@ export abstract class SuiSelectBase implements AfterContentInit { // Slightly delay initialisation to avoid change after checked errors. TODO - look into avoiding this! setTimeout(() => this.initialiseRenderedOption(ro)); - this._renderedSubscriptions.push(ro.onSelected.subscribe(() => this.selectOption(ro.value))); + this._renderedSubscriptions.push(ro.onSelected.subscribe(() => this.selectOption(ro.option))); }); // If no options have been provided, autogenerate them from the rendered ones. if (this.searchService.options.length === 0 && !this.searchService.optionsLookup) { - this.options = this._renderedOptions.map(ro => ro.value); + this.options = this._renderedOptions.map(ro => ro.option); } } @@ -319,7 +319,7 @@ export abstract class SuiSelectBase implements AfterContentInit { option.formatter = this.configuredFormatter; if (option.usesTemplate) { - this.drawTemplate(option.templateSibling, option.value); + this.drawTemplate(option.templateSibling, option.option); } } diff --git a/src/modules/select/components/multi-select-label.ts b/src/modules/select/components/multi-select-label.ts index 0d59b03e2..291df420d 100644 --- a/src/modules/select/components/multi-select-label.ts +++ b/src/modules/select/components/multi-select-label.ts @@ -13,7 +13,7 @@ const templateRef = TemplateRef; selector: "sui-multi-select-label", template: ` - + ` }) @@ -27,7 +27,7 @@ export class SuiMultiSelectLabel extends SuiTransition { private _transitionController:TransitionController; @Input() - public value:T; + public option:T; @Input() public query?:string; @@ -49,7 +49,7 @@ export class SuiMultiSelectLabel extends SuiTransition { this._template = template; if (this.template) { this.componentFactory.createView(this.templateSibling, this.template, { - $implicit: this.value, + $implicit: this.option, query: this.query }); } @@ -82,7 +82,7 @@ export class SuiMultiSelectLabel extends SuiTransition { this._transitionController.animate( new Transition("scale", 100, TransitionDirection.Out, () => - this.onDeselected.emit(this.value))); + this.onDeselected.emit(this.option))); } @HostListener("click", ["$event"]) diff --git a/src/modules/select/components/multi-select.ts b/src/modules/select/components/multi-select.ts index 91f914853..b6f7dd408 100644 --- a/src/modules/select/components/multi-select.ts +++ b/src/modules/select/components/multi-select.ts @@ -13,7 +13,7 @@ import { ISelectRenderedOption } from "./select-option"; extends SuiSelectBase implements ICustom } } - protected initialiseRenderedOption(option:ISelectRenderedOption):void { - super.initialiseRenderedOption(option); + protected initialiseRenderedOption(rendered:ISelectRenderedOption):void { + super.initialiseRenderedOption(rendered); // Boldens the item so it appears selected in the dropdown. - option.isActive = !this.hasLabels && this.selectedOptions.indexOf(option.value) !== -1; + rendered.isActive = !this.hasLabels && this.selectedOptions.indexOf(rendered.option) !== -1; } public selectOption(option:T):void { diff --git a/src/modules/select/components/select-option.ts b/src/modules/select/components/select-option.ts index 717901429..44fd642d3 100644 --- a/src/modules/select/components/select-option.ts +++ b/src/modules/select/components/select-option.ts @@ -6,7 +6,7 @@ import { SuiDropdownMenuItem } from "../../dropdown"; import { HandledEvent } from "../../../misc/util"; export interface ISelectRenderedOption { - value:T; + option:T; isActive?:boolean; formatter:(o:T) => string; usesTemplate:boolean; @@ -26,7 +26,7 @@ export class SuiSelectOption extends SuiDropdownMenuItem implements ISelectRe private _optionClasses:boolean; @Input() - public value:T; + public option:T; // Fires when the option is selected, whether by clicking or by keyboard. @Output() @@ -39,7 +39,7 @@ export class SuiSelectOption extends SuiDropdownMenuItem implements ISelectRe public set formatter(formatter:(obj:T) => string) { if (!this.usesTemplate) { - this.renderedText = formatter(this.value); + this.renderedText = formatter(this.option); } else { this.renderedText = undefined; } @@ -70,6 +70,6 @@ export class SuiSelectOption extends SuiDropdownMenuItem implements ISelectRe public onClick(e:HandledEvent):void { e.eventHandled = true; - setTimeout(() => this.onSelected.emit(this.value)); + setTimeout(() => this.onSelected.emit(this.option)); } } diff --git a/src/modules/select/components/select.ts b/src/modules/select/components/select.ts index f7f175b51..6a09ccea3 100644 --- a/src/modules/select/components/select.ts +++ b/src/modules/select/components/select.ts @@ -122,11 +122,11 @@ export class SuiSelect extends SuiSelectBase implements ICustomValue } } - protected initialiseRenderedOption(option:ISelectRenderedOption):void { - super.initialiseRenderedOption(option); + protected initialiseRenderedOption(rendered:ISelectRenderedOption):void { + super.initialiseRenderedOption(rendered); // Boldens the item so it appears selected in the dropdown. - option.isActive = option.value === this.selectedOption; + rendered.isActive = rendered.option === this.selectedOption; } private drawSelectedOption():void { From 2b1722366aa2c8572f522abb36dd25e976318e6d Mon Sep 17 00:00:00 2001 From: Ed Carroll Date: Thu, 10 Aug 2017 19:39:12 +0100 Subject: [PATCH 2/5] feat(select): First attempt at a container component --- .../app/pages/development/test/test.page.html | 2 +- .../app/pages/modules/select/select.page.ts | 12 +-------- src/modules/select/classes/select-base.ts | 26 ++++++++++++++++--- src/modules/select/components/multi-select.ts | 1 + .../select/components/select-option.ts | 11 +++++++- .../select/components/select-options.ts | 13 ++++++++++ src/modules/select/components/select.ts | 1 + src/modules/select/select.module.ts | 3 +++ 8 files changed, 52 insertions(+), 17 deletions(-) create mode 100644 src/modules/select/components/select-options.ts diff --git a/demo/src/app/pages/development/test/test.page.html b/demo/src/app/pages/development/test/test.page.html index 011d256d2..848bac258 100644 --- a/demo/src/app/pages/development/test/test.page.html +++ b/demo/src/app/pages/development/test/test.page.html @@ -7,6 +7,6 @@

Examples

- +
\ No newline at end of file diff --git a/demo/src/app/pages/modules/select/select.page.ts b/demo/src/app/pages/modules/select/select.page.ts index 9c7b49ce9..f283d6ce8 100644 --- a/demo/src/app/pages/modules/select/select.page.ts +++ b/demo/src/app/pages/modules/select/select.page.ts @@ -12,9 +12,6 @@ const exampleStandardTemplate = ` [isSearchable]="searchable" [isDisabled]="disabled" #select> - -
@@ -27,9 +24,6 @@ const exampleStandardTemplate = ` [isDisabled]="disabled" [hasLabels]="!hideLabels" #multiSelect> - -

Hide labels? @@ -85,7 +79,6 @@ const exampleVariationsTemplate = ` Filter by tag
- @@ -108,7 +101,7 @@ const exampleInMenuSearchTemplate = ` Options `; @@ -128,7 +121,6 @@ const exampleTemplateTemplate = ` [optionTemplate]="optionTemplate" [isSearchable]="true" #templated> -
@@ -138,7 +130,6 @@ const exampleTemplateTemplate = ` [options]="options" [optionFormatter]="formatter" #formatted> -
@@ -155,7 +146,6 @@ const exampleSearchLookupTemplate = ` optionField="id" [isSearchable]="true" #searchSelect> -

Currently selected: {{ selectedOption | json }}

diff --git a/src/modules/select/classes/select-base.ts b/src/modules/select/classes/select-base.ts index 4714c6dde..9d2ff3578 100644 --- a/src/modules/select/classes/select-base.ts +++ b/src/modules/select/classes/select-base.ts @@ -1,6 +1,6 @@ import { - ViewChild, HostBinding, ElementRef, HostListener, Input, ContentChildren, QueryList, - AfterContentInit, TemplateRef, ViewContainerRef, ContentChild, EventEmitter, Output + ViewChild, HostBinding, ElementRef, HostListener, Input, ContentChildren, QueryList, AfterViewInit, + AfterContentInit, TemplateRef, ViewContainerRef, ContentChild, EventEmitter, Output, ViewChildren } from "@angular/core"; import { Subscription } from "rxjs/Subscription"; import { DropdownService, SuiDropdownMenu } from "../../dropdown"; @@ -9,6 +9,7 @@ import { Util, ITemplateRefContext, HandledEvent, KeyCode } from "../../../misc/ import { ISelectLocaleValues, RecursivePartial, SuiLocalizationService } from "../../../behaviors/localization"; import { SuiSelectOption, ISelectRenderedOption } from "../components/select-option"; import { SuiSelectSearch } from "../directives/select-search"; +import { SuiSelectOptions } from "../components/select-options"; export interface IOptionContext extends ITemplateRefContext { query?:string; @@ -16,7 +17,7 @@ export interface IOptionContext extends ITemplateRefContext { // We use generic type T to specify the type of the options we are working with, // and U to specify the type of the property of the option used as the value. -export abstract class SuiSelectBase implements AfterContentInit { +export abstract class SuiSelectBase implements AfterContentInit, AfterViewInit { public dropdownService:DropdownService; public searchService:SearchService; @@ -24,12 +25,22 @@ export abstract class SuiSelectBase implements AfterContentInit { protected _menu:SuiDropdownMenu; // Keep track of all of the rendered select options. (Rendered by the user using *ngFor). - @ContentChildren(SuiSelectOption, { descendants: true }) + @ContentChildren(SuiSelectOption) protected _renderedOptions:QueryList>; // Keep track of all of the subscriptions to the selected events on the rendered options. private _renderedSubscriptions:Subscription[]; + @ViewChild(SuiSelectOptions) + private _internalOptions?:SuiSelectOptions; + + @ContentChild(SuiSelectOptions) + private _manualOptions?:SuiSelectOptions; + + public get optionsContainer():SuiSelectOptions | undefined { + return this._manualOptions || this._internalOptions; + } + // Sets the Semantic UI classes on the host element. @HostBinding("class.ui") @HostBinding("class.dropdown") @@ -249,7 +260,9 @@ export abstract class SuiSelectBase implements AfterContentInit { this._menu.service = this.dropdownService; // We manually specify the menu items to the menu because the @ContentChildren doesn't pick up our dynamically rendered items. this._menu.items = this._renderedOptions; + } + public ngAfterViewInit():void { if (this._manualSearch) { this.isSearchable = true; this.isSearchExternal = true; @@ -260,6 +273,10 @@ export abstract class SuiSelectBase implements AfterContentInit { this.searchInput.onQueryKeyDown.subscribe((e:KeyboardEvent) => this.onQueryInputKeydown(e)); } + if (this.optionsContainer) { + this.optionsContainer.options = this.availableOptions; + } + // We must call this immediately as changes doesn't fire when you subscribe. this.onAvailableOptionsRendered(); this._renderedOptions.changes.subscribe(() => this.onAvailableOptionsRendered()); @@ -297,6 +314,7 @@ export abstract class SuiSelectBase implements AfterContentInit { } protected onAvailableOptionsRendered():void { + console.log(this._renderedOptions); // Unsubscribe from all previous subscriptions to avoid memory leaks on large selects. this._renderedSubscriptions.forEach(rs => rs.unsubscribe()); this._renderedSubscriptions = []; diff --git a/src/modules/select/components/multi-select.ts b/src/modules/select/components/multi-select.ts index b6f7dd408..8fe385d70 100644 --- a/src/modules/select/components/multi-select.ts +++ b/src/modules/select/components/multi-select.ts @@ -45,6 +45,7 @@ import { ISelectRenderedOption } from "./select-option"; [menuAutoSelectFirst]="true"> +
{{ localeValues.noResultsMessage }}
{{ maxSelectedMessage }}
diff --git a/src/modules/select/components/select-option.ts b/src/modules/select/components/select-option.ts index 44fd642d3..b1f7c1af1 100644 --- a/src/modules/select/components/select-option.ts +++ b/src/modules/select/components/select-option.ts @@ -18,7 +18,16 @@ export interface ISelectRenderedOption { template: ` -` +`, + styles: [` +:host { + display: none !important; +} + +:host-context(sui-select-options) { + display: block; +} +`] }) export class SuiSelectOption extends SuiDropdownMenuItem implements ISelectRenderedOption { // Sets the Semantic UI classes on the host element. diff --git a/src/modules/select/components/select-options.ts b/src/modules/select/components/select-options.ts new file mode 100644 index 000000000..03e390d97 --- /dev/null +++ b/src/modules/select/components/select-options.ts @@ -0,0 +1,13 @@ +import { Component, Input } from "@angular/core"; + +@Component({ + selector: "sui-select-options", + template: `` +}) +export class SuiSelectOptions { + public options:T[]; + + constructor() { + this.options = []; + } +} diff --git a/src/modules/select/components/select.ts b/src/modules/select/components/select.ts index 6a09ccea3..57e70cc04 100644 --- a/src/modules/select/components/select.ts +++ b/src/modules/select/components/select.ts @@ -29,6 +29,7 @@ import { ISelectRenderedOption } from "./select-option"; [menuAutoSelectFirst]="isSearchable"> +
{{ localeValues.noResultsMessage }}
diff --git a/src/modules/select/select.module.ts b/src/modules/select/select.module.ts index 24fdfe55d..0ad8dc72f 100644 --- a/src/modules/select/select.module.ts +++ b/src/modules/select/select.module.ts @@ -6,6 +6,7 @@ import { SuiUtilityModule } from "../../misc/util"; import { SuiLocalizationModule } from "../../behaviors/localization"; import { SuiSelect, SuiSelectValueAccessor } from "./components/select"; import { SuiSelectOption } from "./components/select-option"; +import { SuiSelectOptions } from "./components/select-options"; import { SuiSelectSearch } from "./directives/select-search"; import { SuiMultiSelect, SuiMultiSelectValueAccessor } from "./components/multi-select"; import { SuiMultiSelectLabel } from "./components/multi-select-label"; @@ -21,6 +22,7 @@ import { SuiMultiSelectLabel } from "./components/multi-select-label"; declarations: [ SuiSelect, SuiSelectOption, + SuiSelectOptions, SuiSelectSearch, SuiSelectValueAccessor, SuiMultiSelect, @@ -30,6 +32,7 @@ import { SuiMultiSelectLabel } from "./components/multi-select-label"; exports: [ SuiSelect, SuiSelectOption, + SuiSelectOptions, SuiSelectSearch, SuiSelectValueAccessor, SuiMultiSelect, From 3730636b7129aaa6342d9dda67a5bac001fb33d1 Mon Sep 17 00:00:00 2001 From: Ed Carroll Date: Thu, 10 Aug 2017 20:00:19 +0100 Subject: [PATCH 3/5] feat(select): Attempt at improving rendering --- src/modules/select/classes/select-base.ts | 4 +- src/modules/select/components/multi-select.ts | 7 +- .../select/components/select-option.ts | 69 ++++++++++--------- .../select/components/select-options.ts | 30 +++++++- src/modules/select/components/select.ts | 13 ++-- 5 files changed, 80 insertions(+), 43 deletions(-) diff --git a/src/modules/select/classes/select-base.ts b/src/modules/select/classes/select-base.ts index 9d2ff3578..a86ad2d85 100644 --- a/src/modules/select/classes/select-base.ts +++ b/src/modules/select/classes/select-base.ts @@ -7,7 +7,7 @@ import { DropdownService, SuiDropdownMenu } from "../../dropdown"; import { SearchService, LookupFn, FilterFn } from "../../search"; import { Util, ITemplateRefContext, HandledEvent, KeyCode } from "../../../misc/util"; import { ISelectLocaleValues, RecursivePartial, SuiLocalizationService } from "../../../behaviors/localization"; -import { SuiSelectOption, ISelectRenderedOption } from "../components/select-option"; +import { SuiSelectOption } from "../components/select-option"; import { SuiSelectSearch } from "../directives/select-search"; import { SuiSelectOptions } from "../components/select-options"; @@ -332,7 +332,7 @@ export abstract class SuiSelectBase implements AfterContentInit, AfterView } } - protected initialiseRenderedOption(option:ISelectRenderedOption):void { + protected initialiseRenderedOption(option:any):void { option.usesTemplate = !!this.optionTemplate; option.formatter = this.configuredFormatter; diff --git a/src/modules/select/components/multi-select.ts b/src/modules/select/components/multi-select.ts index 8fe385d70..29f68d83a 100644 --- a/src/modules/select/components/multi-select.ts +++ b/src/modules/select/components/multi-select.ts @@ -2,7 +2,6 @@ import { Component, HostBinding, ElementRef, EventEmitter, Output, Input, Direct import { ICustomValueAccessorHost, KeyCode, customValueAccessorFactory, CustomValueAccessor } from "../../../misc/util"; import { SuiLocalizationService } from "../../../behaviors/localization"; import { SuiSelectBase } from "../classes/select-base"; -import { ISelectRenderedOption } from "./select-option"; @Component({ selector: "sui-multi-select", @@ -12,7 +11,7 @@ import { ISelectRenderedOption } from "./select-option"; - - +
{{ localeValues.noResultsMessage }}
{{ maxSelectedMessage }}
@@ -159,7 +158,7 @@ export class SuiMultiSelect extends SuiSelectBase implements ICustom } } - protected initialiseRenderedOption(rendered:ISelectRenderedOption):void { + protected initialiseRenderedOption(rendered:any):void { super.initialiseRenderedOption(rendered); // Boldens the item so it appears selected in the dropdown. diff --git a/src/modules/select/components/select-option.ts b/src/modules/select/components/select-option.ts index b1f7c1af1..c29ec0254 100644 --- a/src/modules/select/components/select-option.ts +++ b/src/modules/select/components/select-option.ts @@ -1,35 +1,31 @@ import { Component, Input, HostBinding, HostListener, EventEmitter, ViewContainerRef, - ViewChild, Renderer2, ElementRef, Output, ChangeDetectorRef + ViewChild, Renderer2, ElementRef, Output, ChangeDetectorRef, TemplateRef } from "@angular/core"; import { SuiDropdownMenuItem } from "../../dropdown"; -import { HandledEvent } from "../../../misc/util"; - -export interface ISelectRenderedOption { - option:T; - isActive?:boolean; - formatter:(o:T) => string; - usesTemplate:boolean; - templateSibling:ViewContainerRef; -} +import { HandledEvent, SuiComponentFactory } from "../../../misc/util"; +import { IOptionContext } from "../classes/select-base"; + +// See https://github.com/Microsoft/TypeScript/issues/13449. +const templateRef = TemplateRef; @Component({ selector: "sui-select-option", template: ` - + `, styles: [` -:host { - display: none !important; +:host.item { + display: none; } -:host-context(sui-select-options) { +:host-context(sui-select-options).item { display: block; } `] }) -export class SuiSelectOption extends SuiDropdownMenuItem implements ISelectRenderedOption { +export class SuiSelectOption extends SuiDropdownMenuItem { // Sets the Semantic UI classes on the host element. @HostBinding("class.item") private _optionClasses:boolean; @@ -37,48 +33,59 @@ export class SuiSelectOption extends SuiDropdownMenuItem implements ISelectRe @Input() public option:T; + @Input() + public query?:string; + // Fires when the option is selected, whether by clicking or by keyboard. - @Output() + @Output("selected") public onSelected:EventEmitter; @HostBinding("class.active") public isActive:boolean; - public renderedText?:string; + @Input() + public formatter:(obj:T) => string; - public set formatter(formatter:(obj:T) => string) { - if (!this.usesTemplate) { - this.renderedText = formatter(this.option); - } else { - this.renderedText = undefined; - } + private _template?:TemplateRef>; + + @Input() + public get template():TemplateRef> | undefined { + return this._template; } - public usesTemplate:boolean; + public set template(template:TemplateRef> | undefined) { + this._template = template; + if (this.template) { + this.componentFactory.createView(this.templateSibling, this.template, { + $implicit: this.option, + query: this.query + }); + } + } // Placeholder to draw template beside. @ViewChild("templateSibling", { read: ViewContainerRef }) public templateSibling:ViewContainerRef; - constructor(renderer:Renderer2, element:ElementRef) { + constructor(renderer:Renderer2, + element:ElementRef, + changeDetector:ChangeDetectorRef, + public componentFactory:SuiComponentFactory) { + // We inherit SuiDropdownMenuItem to automatically gain all keyboard navigation functionality. // This is not done via adding the .item class because it isn't supported by Angular. super(renderer, element); - this._optionClasses = true; this.isActive = false; this.onSelected = new EventEmitter(); - // By default we make this function return an empty string, for the brief moment when it isn't displaying the correct label. - this.formatter = o => ""; - - this.usesTemplate = false; + this._optionClasses = true; } @HostListener("click", ["$event"]) public onClick(e:HandledEvent):void { e.eventHandled = true; - setTimeout(() => this.onSelected.emit(this.option)); + this.onSelected.emit(this.option); } } diff --git a/src/modules/select/components/select-options.ts b/src/modules/select/components/select-options.ts index 03e390d97..efda8be5d 100644 --- a/src/modules/select/components/select-options.ts +++ b/src/modules/select/components/select-options.ts @@ -1,13 +1,39 @@ -import { Component, Input } from "@angular/core"; +import { Component, Input, TemplateRef, EventEmitter, Output } from "@angular/core"; +import { IOptionContext } from "../classes/select-base"; + +// See https://github.com/Microsoft/TypeScript/issues/13449. +const templateRef = TemplateRef; @Component({ selector: "sui-select-options", - template: `` + template: ` + +` }) export class SuiSelectOptions { + @Input() public options:T[]; + @Input() + public query?:string; + + @Input() + public optionFormatter?:(obj:T) => string; + + @Input() + public optionTemplate?:TemplateRef>; + + @Output("optionSelected") + public onOptionSelected:EventEmitter; + constructor() { this.options = []; + + this.onOptionSelected = new EventEmitter(); } } diff --git a/src/modules/select/components/select.ts b/src/modules/select/components/select.ts index 57e70cc04..6be6f7fb0 100644 --- a/src/modules/select/components/select.ts +++ b/src/modules/select/components/select.ts @@ -2,7 +2,6 @@ import { Component, ViewContainerRef, ViewChild, Output, EventEmitter, ElementRe import { ICustomValueAccessorHost, customValueAccessorFactory, CustomValueAccessor } from "../../../misc/util"; import { SuiLocalizationService } from "../../../behaviors/localization"; import { SuiSelectBase } from "../classes/select-base"; -import { ISelectRenderedOption } from "./select-option"; @Component({ selector: "sui-select", @@ -29,8 +28,14 @@ import { ISelectRenderedOption } from "./select-option"; [menuAutoSelectFirst]="isSearchable"> - -
+ + + +
{{ localeValues.noResultsMessage }}
@@ -123,7 +128,7 @@ export class SuiSelect extends SuiSelectBase implements ICustomValue } } - protected initialiseRenderedOption(rendered:ISelectRenderedOption):void { + protected initialiseRenderedOption(rendered:any):void { super.initialiseRenderedOption(rendered); // Boldens the item so it appears selected in the dropdown. From 89c763178944ee7979613208e414299da034c5b8 Mon Sep 17 00:00:00 2001 From: Ed Carroll Date: Thu, 10 Aug 2017 20:00:56 +0100 Subject: [PATCH 4/5] revert: feat(select): Attempt at improving rendering This reverts commit 3730636b7129aaa6342d9dda67a5bac001fb33d1. --- src/modules/select/classes/select-base.ts | 4 +- src/modules/select/components/multi-select.ts | 7 +- .../select/components/select-option.ts | 69 +++++++++---------- .../select/components/select-options.ts | 30 +------- src/modules/select/components/select.ts | 13 ++-- 5 files changed, 43 insertions(+), 80 deletions(-) diff --git a/src/modules/select/classes/select-base.ts b/src/modules/select/classes/select-base.ts index a86ad2d85..9d2ff3578 100644 --- a/src/modules/select/classes/select-base.ts +++ b/src/modules/select/classes/select-base.ts @@ -7,7 +7,7 @@ import { DropdownService, SuiDropdownMenu } from "../../dropdown"; import { SearchService, LookupFn, FilterFn } from "../../search"; import { Util, ITemplateRefContext, HandledEvent, KeyCode } from "../../../misc/util"; import { ISelectLocaleValues, RecursivePartial, SuiLocalizationService } from "../../../behaviors/localization"; -import { SuiSelectOption } from "../components/select-option"; +import { SuiSelectOption, ISelectRenderedOption } from "../components/select-option"; import { SuiSelectSearch } from "../directives/select-search"; import { SuiSelectOptions } from "../components/select-options"; @@ -332,7 +332,7 @@ export abstract class SuiSelectBase implements AfterContentInit, AfterView } } - protected initialiseRenderedOption(option:any):void { + protected initialiseRenderedOption(option:ISelectRenderedOption):void { option.usesTemplate = !!this.optionTemplate; option.formatter = this.configuredFormatter; diff --git a/src/modules/select/components/multi-select.ts b/src/modules/select/components/multi-select.ts index 29f68d83a..8fe385d70 100644 --- a/src/modules/select/components/multi-select.ts +++ b/src/modules/select/components/multi-select.ts @@ -2,6 +2,7 @@ import { Component, HostBinding, ElementRef, EventEmitter, Output, Input, Direct import { ICustomValueAccessorHost, KeyCode, customValueAccessorFactory, CustomValueAccessor } from "../../../misc/util"; import { SuiLocalizationService } from "../../../behaviors/localization"; import { SuiSelectBase } from "../classes/select-base"; +import { ISelectRenderedOption } from "./select-option"; @Component({ selector: "sui-multi-select", @@ -11,7 +12,7 @@ import { SuiSelectBase } from "../classes/select-base"; - - +
{{ localeValues.noResultsMessage }}
{{ maxSelectedMessage }}
@@ -158,7 +159,7 @@ export class SuiMultiSelect extends SuiSelectBase implements ICustom } } - protected initialiseRenderedOption(rendered:any):void { + protected initialiseRenderedOption(rendered:ISelectRenderedOption):void { super.initialiseRenderedOption(rendered); // Boldens the item so it appears selected in the dropdown. diff --git a/src/modules/select/components/select-option.ts b/src/modules/select/components/select-option.ts index c29ec0254..b1f7c1af1 100644 --- a/src/modules/select/components/select-option.ts +++ b/src/modules/select/components/select-option.ts @@ -1,31 +1,35 @@ import { Component, Input, HostBinding, HostListener, EventEmitter, ViewContainerRef, - ViewChild, Renderer2, ElementRef, Output, ChangeDetectorRef, TemplateRef + ViewChild, Renderer2, ElementRef, Output, ChangeDetectorRef } from "@angular/core"; import { SuiDropdownMenuItem } from "../../dropdown"; -import { HandledEvent, SuiComponentFactory } from "../../../misc/util"; -import { IOptionContext } from "../classes/select-base"; - -// See https://github.com/Microsoft/TypeScript/issues/13449. -const templateRef = TemplateRef; +import { HandledEvent } from "../../../misc/util"; + +export interface ISelectRenderedOption { + option:T; + isActive?:boolean; + formatter:(o:T) => string; + usesTemplate:boolean; + templateSibling:ViewContainerRef; +} @Component({ selector: "sui-select-option", template: ` - + `, styles: [` -:host.item { - display: none; +:host { + display: none !important; } -:host-context(sui-select-options).item { +:host-context(sui-select-options) { display: block; } `] }) -export class SuiSelectOption extends SuiDropdownMenuItem { +export class SuiSelectOption extends SuiDropdownMenuItem implements ISelectRenderedOption { // Sets the Semantic UI classes on the host element. @HostBinding("class.item") private _optionClasses:boolean; @@ -33,59 +37,48 @@ export class SuiSelectOption extends SuiDropdownMenuItem { @Input() public option:T; - @Input() - public query?:string; - // Fires when the option is selected, whether by clicking or by keyboard. - @Output("selected") + @Output() public onSelected:EventEmitter; @HostBinding("class.active") public isActive:boolean; - @Input() - public formatter:(obj:T) => string; - - private _template?:TemplateRef>; - - @Input() - public get template():TemplateRef> | undefined { - return this._template; - } + public renderedText?:string; - public set template(template:TemplateRef> | undefined) { - this._template = template; - if (this.template) { - this.componentFactory.createView(this.templateSibling, this.template, { - $implicit: this.option, - query: this.query - }); + public set formatter(formatter:(obj:T) => string) { + if (!this.usesTemplate) { + this.renderedText = formatter(this.option); + } else { + this.renderedText = undefined; } } + public usesTemplate:boolean; + // Placeholder to draw template beside. @ViewChild("templateSibling", { read: ViewContainerRef }) public templateSibling:ViewContainerRef; - constructor(renderer:Renderer2, - element:ElementRef, - changeDetector:ChangeDetectorRef, - public componentFactory:SuiComponentFactory) { - + constructor(renderer:Renderer2, element:ElementRef) { // We inherit SuiDropdownMenuItem to automatically gain all keyboard navigation functionality. // This is not done via adding the .item class because it isn't supported by Angular. super(renderer, element); + this._optionClasses = true; this.isActive = false; this.onSelected = new EventEmitter(); - this._optionClasses = true; + // By default we make this function return an empty string, for the brief moment when it isn't displaying the correct label. + this.formatter = o => ""; + + this.usesTemplate = false; } @HostListener("click", ["$event"]) public onClick(e:HandledEvent):void { e.eventHandled = true; - this.onSelected.emit(this.option); + setTimeout(() => this.onSelected.emit(this.option)); } } diff --git a/src/modules/select/components/select-options.ts b/src/modules/select/components/select-options.ts index efda8be5d..03e390d97 100644 --- a/src/modules/select/components/select-options.ts +++ b/src/modules/select/components/select-options.ts @@ -1,39 +1,13 @@ -import { Component, Input, TemplateRef, EventEmitter, Output } from "@angular/core"; -import { IOptionContext } from "../classes/select-base"; - -// See https://github.com/Microsoft/TypeScript/issues/13449. -const templateRef = TemplateRef; +import { Component, Input } from "@angular/core"; @Component({ selector: "sui-select-options", - template: ` - -` + template: `` }) export class SuiSelectOptions { - @Input() public options:T[]; - @Input() - public query?:string; - - @Input() - public optionFormatter?:(obj:T) => string; - - @Input() - public optionTemplate?:TemplateRef>; - - @Output("optionSelected") - public onOptionSelected:EventEmitter; - constructor() { this.options = []; - - this.onOptionSelected = new EventEmitter(); } } diff --git a/src/modules/select/components/select.ts b/src/modules/select/components/select.ts index 6be6f7fb0..57e70cc04 100644 --- a/src/modules/select/components/select.ts +++ b/src/modules/select/components/select.ts @@ -2,6 +2,7 @@ import { Component, ViewContainerRef, ViewChild, Output, EventEmitter, ElementRe import { ICustomValueAccessorHost, customValueAccessorFactory, CustomValueAccessor } from "../../../misc/util"; import { SuiLocalizationService } from "../../../behaviors/localization"; import { SuiSelectBase } from "../classes/select-base"; +import { ISelectRenderedOption } from "./select-option"; @Component({ selector: "sui-select", @@ -28,14 +29,8 @@ import { SuiSelectBase } from "../classes/select-base"; [menuAutoSelectFirst]="isSearchable"> - - - -
+ +
{{ localeValues.noResultsMessage }}
@@ -128,7 +123,7 @@ export class SuiSelect extends SuiSelectBase implements ICustomValue } } - protected initialiseRenderedOption(rendered:any):void { + protected initialiseRenderedOption(rendered:ISelectRenderedOption):void { super.initialiseRenderedOption(rendered); // Boldens the item so it appears selected in the dropdown. From b5875232b3b7d02856a94d109b2918aefe2d288e Mon Sep 17 00:00:00 2001 From: Ed Carroll Date: Thu, 10 Aug 2017 21:12:37 +0100 Subject: [PATCH 5/5] feat(select): Options now manually rendered at specified location --- package-lock.json | 36 +++--- package.json | 2 +- .../services/component-factory.service.ts | 4 +- src/modules/search/services/search.service.ts | 16 ++- src/modules/select/classes/select-base.ts | 116 +++++++++++------- .../select/components/multi-select-label.ts | 2 +- src/modules/select/components/multi-select.ts | 28 ++--- .../select/components/select-option.ts | 71 +++++------ .../select/components/select-options.ts | 15 ++- src/modules/select/components/select.ts | 21 ++-- src/modules/select/select.module.ts | 3 + 11 files changed, 170 insertions(+), 144 deletions(-) diff --git a/package-lock.json b/package-lock.json index a2315a2d0..d23e09b67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,7 +52,7 @@ "raw-loader": "0.5.1", "resolve": "1.3.3", "rsvp": "3.6.0", - "rxjs": "5.4.1", + "rxjs": "5.4.2", "sass-loader": "6.0.6", "script-loader": "0.7.0", "semver": "5.3.0", @@ -427,7 +427,7 @@ "aproba": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.1.2.tgz", - "integrity": "sha1-RcZikJTeTpb2k+9+q3SuB5wkD8E=", + "integrity": "sha512-ZpYajIfO0j2cOFTO955KUMIKNmj6zhX8kVztMAxFsDaMwz+9Z9SV0uou2pC9HJqcfpffOsjnbrDMvkNy+9RXPw==", "dev": true }, "are-we-there-yet": { @@ -4170,7 +4170,7 @@ "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "dev": true, "requires": { "fs.realpath": "1.0.0", @@ -4203,7 +4203,7 @@ "globals": { "version": "9.18.0", "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha1-qjiWs+abSH8X4x7SFD1pqOMMLYo=", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", "dev": true }, "globby": { @@ -5395,7 +5395,7 @@ "istanbul-lib-coverage": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz", - "integrity": "sha1-c7+5mIhSmUFck9OKPprfeEp3qdo=", + "integrity": "sha512-0+1vDkmzxqJIn5rcoEqapSB4DmPxE31EtI2dF2aCkV5esN9EWHxZ0dwgDClivMXJqE7zaYQxq30hj5L0nlTN5Q==", "dev": true }, "istanbul-lib-hook": { @@ -6249,7 +6249,7 @@ "lru-cache": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", - "integrity": "sha1-Yi4y6CSItJJ5EUpPns9F581rulU=", + "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", "dev": true, "optional": true, "requires": { @@ -6426,7 +6426,7 @@ "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "requires": { "brace-expansion": "1.1.8" @@ -8025,7 +8025,7 @@ "randomatic": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", - "integrity": "sha1-x6vpzIuHwLqodrGf3oP9RkeX44w=", + "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", "dev": true, "requires": { "is-number": "3.0.0", @@ -8066,7 +8066,7 @@ "randombytes": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.5.tgz", - "integrity": "sha1-3ACaJGuNCaF3tLegrne8Vw9LG3k=", + "integrity": "sha512-8T7Zn1AhMsQ/HI1SjcCfT/t4ii3eAqco3yOcSzS4mozsOz69lHLsoMXmF9nZgnFanYscnSlUSgs8uZyKzpE6kg==", "dev": true, "requires": { "safe-buffer": "5.1.1" @@ -8758,9 +8758,9 @@ } }, "rxjs": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.4.1.tgz", - "integrity": "sha1-ti91fyeURdJloYpY+wpw3JDpFiY=", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.4.2.tgz", + "integrity": "sha1-KjI2/L8D31e64G/Wly/ZnlwI/Pc=", "requires": { "symbol-observable": "1.0.4" } @@ -9470,7 +9470,7 @@ "stream-http": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.2.tgz", - "integrity": "sha1-QKBQ7I3DtTsz2ZCUFcAsC/Gr+60=", + "integrity": "sha512-c0yTD2rbQzXtSsFSVhtpvY/vS6u066PcXOX9kBB3mSO76RiUQzL340uJkGBWnlBg4/HZzqiUXtaVA7wcRcJgEw==", "dev": true, "requires": { "builtin-status-codes": "3.0.0", @@ -10059,7 +10059,7 @@ "url-loader": { "version": "0.5.9", "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-0.5.9.tgz", - "integrity": "sha1-zI/qgse5Bud3cBklCGnlaemVwpU=", + "integrity": "sha512-B7QYFyvv+fOBqBVeefsxv6koWWtjmHaMFT6KZWti4KRw8YUD/hOU+3AECvXuzyVawIBx3z7zQRejXCDSO5kk1Q==", "dev": true, "requires": { "loader-utils": "1.1.0", @@ -10269,7 +10269,7 @@ "walk-sync": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-0.3.2.tgz", - "integrity": "sha1-SCcoCvxC0OA1NnxKTjHurA0Tb3U=", + "integrity": "sha512-FMB5VqpLqOCcqrzA9okZFc0wq0Qbmdm396qJxvQZhDpyu0W95G9JCmp74tx7iyYnyOcBtUuKJsgIKAqjozvmmQ==", "dev": true, "requires": { "ensure-posix-path": "1.0.2", @@ -10634,7 +10634,7 @@ "webpack-sources": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.0.1.tgz", - "integrity": "sha1-xzVkNqTRMSO+LiQmoF0drZy+Zc8=", + "integrity": "sha512-05tMxipUCwHqYaVS8xc7sYPTly8PzXayRCB4dTxLhWTqlKUiwH6ezmEe0OSreL1c30LAuA3Zqmc+uEBUGFJDjw==", "dev": true, "requires": { "source-list-map": "2.0.0", @@ -10644,7 +10644,7 @@ "source-list-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", - "integrity": "sha1-qqR0A/eyRakvvJfqCPJQ1gh+0IU=", + "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==", "dev": true } } @@ -10694,7 +10694,7 @@ "wide-align": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", - "integrity": "sha1-Vx4PGwYEY268DfwhsDObvjE0FxA=", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", "dev": true, "requires": { "string-width": "1.0.2" diff --git a/package.json b/package.json index 84a10edb8..1c25f70c1 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "element-closest": "^2.0.2", "extend": "^3.0.1", "popper.js": "~1.10.6", - "rxjs": "^5.0.1" + "rxjs": "^5.4.2" }, "devDependencies": { "@angular/common": "^4.3.1", diff --git a/src/misc/util/services/component-factory.service.ts b/src/misc/util/services/component-factory.service.ts index 2796e79b3..ef6cbb7dc 100644 --- a/src/misc/util/services/component-factory.service.ts +++ b/src/misc/util/services/component-factory.service.ts @@ -34,8 +34,8 @@ export class SuiComponentFactory { } // Inserts the component into the specified view container. - public attachToView(componentRef:ComponentRef, viewContainer:ViewContainerRef):void { - viewContainer.insert(componentRef.hostView, 0); + public attachToView(componentRef:ComponentRef, viewContainer:ViewContainerRef, index:number = 0):void { + viewContainer.insert(componentRef.hostView, index); } // Inserts the component in the root application node. diff --git a/src/modules/search/services/search.service.ts b/src/modules/search/services/search.service.ts index 9b54754fb..1c4e7d2fd 100644 --- a/src/modules/search/services/search.service.ts +++ b/src/modules/search/services/search.service.ts @@ -1,3 +1,4 @@ +import { BehaviorSubject } from "rxjs/BehaviorSubject"; import { Util } from "../../../misc/util"; import { LookupFn, LookupFnResult, FilterFn } from "../helpers/lookup-fn"; @@ -59,6 +60,9 @@ export class SearchService { return this._results; } + // Results in observable form. + public results$:BehaviorSubject; + private _query:string; // Allows the empty query to produce results. public allowEmptyQuery:boolean; @@ -97,6 +101,8 @@ export class SearchService { return false; }; + this.results$ = new BehaviorSubject([]); + // Set default values and reset. this.allowEmptyQuery = allowEmptyQuery; this.searchDelay = 0; @@ -128,7 +134,7 @@ export class SearchService { if (this._resultsCache.hasOwnProperty(this._query)) { // If the query is already cached, make use of it. - this._results = this._resultsCache[this._query]; + this.updateResults(this._resultsCache[this._query], false); return callback(); } @@ -163,9 +169,12 @@ export class SearchService { } // Updates & caches the new set of results. - private updateResults(results:T[]):void { - this._resultsCache[this._query] = results; + private updateResults(results:T[], cache:boolean = true):void { + if (cache) { + this._resultsCache[this._query] = results; + } this._results = results; + this.results$.next(results); } // tslint:disable-next-line:promise-function-async @@ -201,6 +210,7 @@ export class SearchService { // Resets the search back to a pristine state. private reset():void { this._results = []; + this.results$.next([]); this._resultsCache = {}; this._isSearching = false; this.updateQuery(""); diff --git a/src/modules/select/classes/select-base.ts b/src/modules/select/classes/select-base.ts index 9d2ff3578..07ac69c57 100644 --- a/src/modules/select/classes/select-base.ts +++ b/src/modules/select/classes/select-base.ts @@ -1,13 +1,16 @@ import { ViewChild, HostBinding, ElementRef, HostListener, Input, ContentChildren, QueryList, AfterViewInit, - AfterContentInit, TemplateRef, ViewContainerRef, ContentChild, EventEmitter, Output, ViewChildren + AfterContentInit, TemplateRef, ViewContainerRef, ContentChild, EventEmitter, Output, ViewChildren, ComponentRef } from "@angular/core"; import { Subscription } from "rxjs/Subscription"; +import { BehaviorSubject } from "rxjs/BehaviorSubject"; +import { Observable } from "rxjs/Observable"; +import "rxjs/add/observable/merge"; import { DropdownService, SuiDropdownMenu } from "../../dropdown"; import { SearchService, LookupFn, FilterFn } from "../../search"; -import { Util, ITemplateRefContext, HandledEvent, KeyCode } from "../../../misc/util"; +import { Util, ITemplateRefContext, HandledEvent, KeyCode, SuiComponentFactory } from "../../../misc/util"; import { ISelectLocaleValues, RecursivePartial, SuiLocalizationService } from "../../../behaviors/localization"; -import { SuiSelectOption, ISelectRenderedOption } from "../components/select-option"; +import { SuiSelectOption } from "../components/select-option"; import { SuiSelectSearch } from "../directives/select-search"; import { SuiSelectOptions } from "../components/select-options"; @@ -26,19 +29,16 @@ export abstract class SuiSelectBase implements AfterContentInit, AfterView // Keep track of all of the rendered select options. (Rendered by the user using *ngFor). @ContentChildren(SuiSelectOption) - protected _renderedOptions:QueryList>; + protected _manualOptions:QueryList>; - // Keep track of all of the subscriptions to the selected events on the rendered options. - private _renderedSubscriptions:Subscription[]; + @ViewChild(SuiSelectOptions, { read: ViewContainerRef }) + private _internalOptionsContainer:ViewContainerRef; - @ViewChild(SuiSelectOptions) - private _internalOptions?:SuiSelectOptions; + @ContentChild(SuiSelectOptions, { read: ViewContainerRef }) + private _manualOptionsContainer?:ViewContainerRef; - @ContentChild(SuiSelectOptions) - private _manualOptions?:SuiSelectOptions; - - public get optionsContainer():SuiSelectOptions | undefined { - return this._manualOptions || this._internalOptions; + public get optionsContainer():ViewContainerRef { + return this._manualOptionsContainer || this._internalOptionsContainer; } // Sets the Semantic UI classes on the host element. @@ -143,14 +143,12 @@ export abstract class SuiSelectBase implements AfterContentInit, AfterView } } - public get filteredOptions():T[] { - return this.searchService.results; + public get filteredOptions$():BehaviorSubject { + return this.searchService.results$; } - // Deprecated - public get availableOptions():T[] { - return this.filteredOptions; - } + private _renderedOptions:ComponentRef>[]; + private _renderedSubscription:Subscription; public get query():string | undefined { return this.isSearchable ? this.searchService.query : undefined; @@ -161,7 +159,7 @@ export abstract class SuiSelectBase implements AfterContentInit, AfterView this.queryUpdateHook(); this.updateQuery(query); // Update the rendered text as query has changed. - this._renderedOptions.forEach(ro => ro.formatter = this.configuredFormatter); + this._manualOptions.forEach(ro => ro.formatter = this.configuredFormatter); if (this.searchInput) { this.searchInput.query = query; @@ -236,16 +234,22 @@ export abstract class SuiSelectBase implements AfterContentInit, AfterView @Output("touched") public onTouched:EventEmitter; - constructor(private _element:ElementRef, protected _localizationService:SuiLocalizationService) { + constructor(private _element:ElementRef, + private _componentFactory:SuiComponentFactory, + protected _localizationService:SuiLocalizationService) { + this.dropdownService = new DropdownService(); // We do want an empty query to return all results. this.searchService = new SearchService(true); + this._renderedOptions = []; + this.isSearchable = false; this.onLocaleUpdate(); this._localizationService.onLanguageUpdate.subscribe(() => this.onLocaleUpdate()); - this._renderedSubscriptions = []; + + this.searchService.results$.subscribe(rs => this.renderOptions(rs)); this.icon = "dropdown"; this.transition = "slide down"; @@ -259,7 +263,7 @@ export abstract class SuiSelectBase implements AfterContentInit, AfterView public ngAfterContentInit():void { this._menu.service = this.dropdownService; // We manually specify the menu items to the menu because the @ContentChildren doesn't pick up our dynamically rendered items. - this._menu.items = this._renderedOptions; + this._menu.items = this._manualOptions; } public ngAfterViewInit():void { @@ -273,13 +277,9 @@ export abstract class SuiSelectBase implements AfterContentInit, AfterView this.searchInput.onQueryKeyDown.subscribe((e:KeyboardEvent) => this.onQueryInputKeydown(e)); } - if (this.optionsContainer) { - this.optionsContainer.options = this.availableOptions; - } - // We must call this immediately as changes doesn't fire when you subscribe. - this.onAvailableOptionsRendered(); - this._renderedOptions.changes.subscribe(() => this.onAvailableOptionsRendered()); + this.onManualOptionsRendered(); + this._manualOptions.changes.subscribe(() => this.onManualOptionsRendered()); } private onLocaleUpdate():void { @@ -313,31 +313,53 @@ export abstract class SuiSelectBase implements AfterContentInit, AfterView } } - protected onAvailableOptionsRendered():void { - console.log(this._renderedOptions); - // Unsubscribe from all previous subscriptions to avoid memory leaks on large selects. - this._renderedSubscriptions.forEach(rs => rs.unsubscribe()); - this._renderedSubscriptions = []; + private renderOptions(options:T[]):void { + if (this.optionsContainer) { + this.optionsContainer.clear(); + } - this._renderedOptions.forEach(ro => { - // Slightly delay initialisation to avoid change after checked errors. TODO - look into avoiding this! - setTimeout(() => this.initialiseRenderedOption(ro)); + if (this._renderedSubscription) { + this._renderedSubscription.unsubscribe(); + } - this._renderedSubscriptions.push(ro.onSelected.subscribe(() => this.selectOption(ro.option))); - }); + this._renderedOptions.forEach(ro => ro.destroy()); + this._renderedOptions = []; - // If no options have been provided, autogenerate them from the rendered ones. - if (this.searchService.options.length === 0 && !this.searchService.optionsLookup) { - this.options = this._renderedOptions.map(ro => ro.option); - } + options + .slice() + .reverse() + .forEach(option => { + const component = this._componentFactory.createComponent(SuiSelectOption); + component.instance.option = option; + + this._componentFactory.attachToView(component, this.optionsContainer); + this._renderedOptions.push(component); + }); + + this._renderedSubscription = Observable + .merge(...this._renderedOptions.map(ro => ro.instance.onSelected)) + .subscribe((o:T) => { + this.selectOption(o); + this.updateRenderedOptions(); + }); + + this.updateRenderedOptions(); } - protected initialiseRenderedOption(option:ISelectRenderedOption):void { - option.usesTemplate = !!this.optionTemplate; + private updateRenderedOptions():void { + this._renderedOptions.forEach(ro => this.updateRenderedOption(ro.instance)); + } + + protected updateRenderedOption(option:SuiSelectOption):void { + option.query = this.query; option.formatter = this.configuredFormatter; + option.template = this.optionTemplate; + } - if (option.usesTemplate) { - this.drawTemplate(option.templateSibling, option.option); + protected onManualOptionsRendered():void { + // If no options have been provided, autogenerate them from the rendered ones. + if (this.searchService.options.length === 0 && !this.searchService.optionsLookup) { + this.options = this._manualOptions.map(ro => ro.option); } } diff --git a/src/modules/select/components/multi-select-label.ts b/src/modules/select/components/multi-select-label.ts index 291df420d..9412e631c 100644 --- a/src/modules/select/components/multi-select-label.ts +++ b/src/modules/select/components/multi-select-label.ts @@ -36,7 +36,7 @@ export class SuiMultiSelectLabel extends SuiTransition { public onDeselected:EventEmitter; @Input() - public formatter:(obj:T) => string; + public formatter?:(obj:T) => string; private _template?:TemplateRef>; diff --git a/src/modules/select/components/multi-select.ts b/src/modules/select/components/multi-select.ts index 8fe385d70..b5aecc81e 100644 --- a/src/modules/select/components/multi-select.ts +++ b/src/modules/select/components/multi-select.ts @@ -1,8 +1,11 @@ import { Component, HostBinding, ElementRef, EventEmitter, Output, Input, Directive } from "@angular/core"; -import { ICustomValueAccessorHost, KeyCode, customValueAccessorFactory, CustomValueAccessor } from "../../../misc/util"; +import { + ICustomValueAccessorHost, KeyCode, customValueAccessorFactory, + CustomValueAccessor, SuiComponentFactory +} from "../../../misc/util"; import { SuiLocalizationService } from "../../../behaviors/localization"; import { SuiSelectBase } from "../classes/select-base"; -import { ISelectRenderedOption } from "./select-option"; +import { SuiSelectOption } from "./select-option"; @Component({ selector: "sui-multi-select", @@ -46,7 +49,7 @@ import { ISelectRenderedOption } from "./select-option"; - +
{{ localeValues.noResultsMessage }}
{{ maxSelectedMessage }}
@@ -135,8 +138,11 @@ export class SuiMultiSelect extends SuiSelectBase implements ICustom @HostBinding("class.multiple") private _multiSelectClasses:boolean; - constructor(element:ElementRef, localizationService:SuiLocalizationService) { - super(element, localizationService); + constructor(element:ElementRef, + componentFactory:SuiComponentFactory, + localizationService:SuiLocalizationService) { + + super(element, componentFactory, localizationService); this.selectedOptions = []; this.selectedOptionsChange = new EventEmitter(); @@ -159,8 +165,8 @@ export class SuiMultiSelect extends SuiSelectBase implements ICustom } } - protected initialiseRenderedOption(rendered:ISelectRenderedOption):void { - super.initialiseRenderedOption(rendered); + protected updateRenderedOption(rendered:SuiSelectOption):void { + super.updateRenderedOption(rendered); // Boldens the item so it appears selected in the dropdown. rendered.isActive = !this.hasLabels && this.selectedOptions.indexOf(rendered.option) !== -1; @@ -178,10 +184,6 @@ export class SuiMultiSelect extends SuiSelectBase implements ICustom // Automatically refocus the search input for better keyboard accessibility. this.focus(); - - if (!this.hasLabels) { - this.onAvailableOptionsRendered(); - } } public writeValue(values:U[]):void { @@ -217,10 +219,6 @@ export class SuiMultiSelect extends SuiSelectBase implements ICustom // Automatically refocus the search input for better keyboard accessibility. this.focus(); - - if (!this.hasLabels) { - this.onAvailableOptionsRendered(); - } } public onQueryInputKeydown(event:KeyboardEvent):void { diff --git a/src/modules/select/components/select-option.ts b/src/modules/select/components/select-option.ts index b1f7c1af1..43ab1ea38 100644 --- a/src/modules/select/components/select-option.ts +++ b/src/modules/select/components/select-option.ts @@ -1,35 +1,19 @@ import { Component, Input, HostBinding, HostListener, EventEmitter, ViewContainerRef, - ViewChild, Renderer2, ElementRef, Output, ChangeDetectorRef + ViewChild, Renderer2, ElementRef, Output, ChangeDetectorRef, TemplateRef } from "@angular/core"; import { SuiDropdownMenuItem } from "../../dropdown"; -import { HandledEvent } from "../../../misc/util"; - -export interface ISelectRenderedOption { - option:T; - isActive?:boolean; - formatter:(o:T) => string; - usesTemplate:boolean; - templateSibling:ViewContainerRef; -} +import { HandledEvent, SuiComponentFactory } from "../../../misc/util"; +import { IOptionContext } from "../classes/select-base"; @Component({ selector: "sui-select-option", template: ` - -`, - styles: [` -:host { - display: none !important; -} - -:host-context(sui-select-options) { - display: block; -} -`] + +` }) -export class SuiSelectOption extends SuiDropdownMenuItem implements ISelectRenderedOption { +export class SuiSelectOption extends SuiDropdownMenuItem { // Sets the Semantic UI classes on the host element. @HostBinding("class.item") private _optionClasses:boolean; @@ -37,48 +21,55 @@ export class SuiSelectOption extends SuiDropdownMenuItem implements ISelectRe @Input() public option:T; - // Fires when the option is selected, whether by clicking or by keyboard. - @Output() - public onSelected:EventEmitter; + @Input() + public query?:string; @HostBinding("class.active") public isActive:boolean; - public renderedText?:string; + @Input() + public formatter?:(obj:T) => string; - public set formatter(formatter:(obj:T) => string) { - if (!this.usesTemplate) { - this.renderedText = formatter(this.option); - } else { - this.renderedText = undefined; - } + private _template?:TemplateRef>; + + @Input() + public get template():TemplateRef> | undefined { + return this._template; } - public usesTemplate:boolean; + public set template(template:TemplateRef> | undefined) { + this._template = template; + if (this.template) { + this.componentFactory.createView(this.templateSibling, this.template, { + $implicit: this.option, + query: this.query + }); + } + } // Placeholder to draw template beside. @ViewChild("templateSibling", { read: ViewContainerRef }) public templateSibling:ViewContainerRef; - constructor(renderer:Renderer2, element:ElementRef) { + // Fires when the option is selected, whether by clicking or by keyboard. + @Output() + public onSelected:EventEmitter; + + constructor(renderer:Renderer2, element:ElementRef, public componentFactory:SuiComponentFactory) { // We inherit SuiDropdownMenuItem to automatically gain all keyboard navigation functionality. // This is not done via adding the .item class because it isn't supported by Angular. super(renderer, element); - this._optionClasses = true; this.isActive = false; this.onSelected = new EventEmitter(); - // By default we make this function return an empty string, for the brief moment when it isn't displaying the correct label. - this.formatter = o => ""; - - this.usesTemplate = false; + this._optionClasses = true; } @HostListener("click", ["$event"]) public onClick(e:HandledEvent):void { e.eventHandled = true; - setTimeout(() => this.onSelected.emit(this.option)); + this.onSelected.emit(this.option); } } diff --git a/src/modules/select/components/select-options.ts b/src/modules/select/components/select-options.ts index 03e390d97..4b99113de 100644 --- a/src/modules/select/components/select-options.ts +++ b/src/modules/select/components/select-options.ts @@ -2,12 +2,11 @@ import { Component, Input } from "@angular/core"; @Component({ selector: "sui-select-options", - template: `` -}) -export class SuiSelectOptions { - public options:T[]; - - constructor() { - this.options = []; - } + template: ``, + styles: [` +:host { + display: none; } +`] +}) +export class SuiSelectOptions {} diff --git a/src/modules/select/components/select.ts b/src/modules/select/components/select.ts index 57e70cc04..8155703e4 100644 --- a/src/modules/select/components/select.ts +++ b/src/modules/select/components/select.ts @@ -1,8 +1,8 @@ import { Component, ViewContainerRef, ViewChild, Output, EventEmitter, ElementRef, Directive, Input } from "@angular/core"; -import { ICustomValueAccessorHost, customValueAccessorFactory, CustomValueAccessor } from "../../../misc/util"; +import { ICustomValueAccessorHost, customValueAccessorFactory, CustomValueAccessor, SuiComponentFactory } from "../../../misc/util"; import { SuiLocalizationService } from "../../../behaviors/localization"; import { SuiSelectBase } from "../classes/select-base"; -import { ISelectRenderedOption } from "./select-option"; +import { SuiSelectOption } from "./select-option"; @Component({ selector: "sui-select", @@ -30,7 +30,7 @@ import { ISelectRenderedOption } from "./select-option"; -
+
{{ localeValues.noResultsMessage }}
@@ -58,8 +58,11 @@ export class SuiSelect extends SuiSelectBase implements ICustomValue this._placeholder = placeholder; } - constructor(element:ElementRef, localizationService:SuiLocalizationService) { - super(element, localizationService); + constructor(element:ElementRef, + componentFactory:SuiComponentFactory, + localizationService:SuiLocalizationService) { + + super(element, componentFactory, localizationService); this.selectedOptionChange = new EventEmitter(); } @@ -123,8 +126,8 @@ export class SuiSelect extends SuiSelectBase implements ICustomValue } } - protected initialiseRenderedOption(rendered:ISelectRenderedOption):void { - super.initialiseRenderedOption(rendered); + protected updateRenderedOption(rendered:SuiSelectOption):void { + super.updateRenderedOption(rendered); // Boldens the item so it appears selected in the dropdown. rendered.isActive = rendered.option === this.selectedOption; @@ -132,8 +135,8 @@ export class SuiSelect extends SuiSelectBase implements ICustomValue private drawSelectedOption():void { // Updates the active class on the newly selected option. - if (this._renderedOptions) { - this.onAvailableOptionsRendered(); + if (this._manualOptions) { + this.onManualOptionsRendered(); } if (this.selectedOption != undefined && this.optionTemplate) { diff --git a/src/modules/select/select.module.ts b/src/modules/select/select.module.ts index 0ad8dc72f..2d4ed6cad 100644 --- a/src/modules/select/select.module.ts +++ b/src/modules/select/select.module.ts @@ -37,6 +37,9 @@ import { SuiMultiSelectLabel } from "./components/multi-select-label"; SuiSelectValueAccessor, SuiMultiSelect, SuiMultiSelectValueAccessor + ], + entryComponents: [ + SuiSelectOption ] }) export class SuiSelectModule {}