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/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/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..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? @@ -52,10 +46,10 @@ const exampleVariationsTemplate = `

Basic

- - - - + + + +
@@ -66,9 +60,9 @@ const exampleVariationsTemplate = ` Trending repos
Adjust time span
- - - + + +
@@ -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> -
@@ -152,10 +143,9 @@ const exampleSearchLookupTemplate = ` [(ngModel)]="selectedOption" [optionsLookup]="optionsLookup" labelField="name" - valueField="id" + optionField="id" [isSearchable]="true" #searchSelect> -

Currently selected: {{ selectedOption | json }}

@@ -257,7 +247,7 @@ export class SelectPage { }, { name: "localeOverrides", - type: "RecursivePartial", + type: "RecursivePartial", description: "Overrides the values from the localization service." } ], @@ -374,7 +364,7 @@ export class SelectPage { }, { name: "localeOverrides", - type: "Partial", + type: "Partial", description: "Overrides the values from the localization service." } ], @@ -395,9 +385,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/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 041bc9d69..07ac69c57 100644 --- a/src/modules/select/classes/select-base.ts +++ b/src/modules/select/classes/select-base.ts @@ -1,14 +1,18 @@ 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, 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"; export interface IOptionContext extends ITemplateRefContext { query?:string; @@ -16,7 +20,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,11 +28,18 @@ 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 }) - protected _renderedOptions:QueryList>; + @ContentChildren(SuiSelectOption) + 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; + + @ContentChild(SuiSelectOptions, { read: ViewContainerRef }) + private _manualOptionsContainer?:ViewContainerRef; + + public get optionsContainer():ViewContainerRef { + return this._manualOptionsContainer || this._internalOptionsContainer; + } // Sets the Semantic UI classes on the host element. @HostBinding("class.ui") @@ -132,14 +143,12 @@ export abstract class SuiSelectBase implements AfterContentInit { } } - 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; @@ -150,7 +159,7 @@ export abstract class SuiSelectBase implements AfterContentInit { 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; @@ -225,16 +234,22 @@ export abstract class SuiSelectBase implements AfterContentInit { @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"; @@ -248,8 +263,10 @@ export abstract class SuiSelectBase implements AfterContentInit { 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 { if (this._manualSearch) { this.isSearchable = true; this.isSearchExternal = true; @@ -261,8 +278,8 @@ export abstract class SuiSelectBase implements AfterContentInit { } // 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 { @@ -296,30 +313,53 @@ export abstract class SuiSelectBase implements AfterContentInit { } } - protected onAvailableOptionsRendered():void { - // 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.value))); - }); + 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.value); - } + 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.value); + 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 0d59b03e2..9412e631c 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; @@ -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>; @@ -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..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", @@ -13,7 +16,7 @@ import { ISelectRenderedOption } from "./select-option"; - + +
{{ localeValues.noResultsMessage }}
{{ maxSelectedMessage }}
@@ -134,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(); @@ -158,11 +165,11 @@ export class SuiMultiSelect extends SuiSelectBase implements ICustom } } - protected initialiseRenderedOption(option:ISelectRenderedOption):void { - super.initialiseRenderedOption(option); + protected updateRenderedOption(rendered:SuiSelectOption):void { + super.updateRenderedOption(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 { @@ -177,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 { @@ -216,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 717901429..43ab1ea38 100644 --- a/src/modules/select/components/select-option.ts +++ b/src/modules/select/components/select-option.ts @@ -1,75 +1,75 @@ 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 { - value: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: ` - + ` }) -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; @Input() - public value:T; + 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.value); - } 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.value)); + this.onSelected.emit(this.option); } } diff --git a/src/modules/select/components/select-options.ts b/src/modules/select/components/select-options.ts new file mode 100644 index 000000000..4b99113de --- /dev/null +++ b/src/modules/select/components/select-options.ts @@ -0,0 +1,12 @@ +import { Component, Input } from "@angular/core"; + +@Component({ + selector: "sui-select-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 f7f175b51..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", @@ -29,7 +29,8 @@ import { ISelectRenderedOption } from "./select-option"; [menuAutoSelectFirst]="isSearchable"> -
+ +
{{ localeValues.noResultsMessage }}
@@ -57,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(); } @@ -122,17 +126,17 @@ export class SuiSelect extends SuiSelectBase implements ICustomValue } } - protected initialiseRenderedOption(option:ISelectRenderedOption):void { - super.initialiseRenderedOption(option); + protected updateRenderedOption(rendered:SuiSelectOption):void { + super.updateRenderedOption(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 { // 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 24fdfe55d..2d4ed6cad 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,10 +32,14 @@ import { SuiMultiSelectLabel } from "./components/multi-select-label"; exports: [ SuiSelect, SuiSelectOption, + SuiSelectOptions, SuiSelectSearch, SuiSelectValueAccessor, SuiMultiSelect, SuiMultiSelectValueAccessor + ], + entryComponents: [ + SuiSelectOption ] }) export class SuiSelectModule {}