From d072852f3d331dbbdd3a98652532ca432d39c880 Mon Sep 17 00:00:00 2001 From: Milko Venkov Date: Tue, 14 Mar 2023 16:42:00 +0200 Subject: [PATCH] feat(overlay): use ViewContainerRef to create components #11671 (#11685) --- CHANGELOG.md | 2 + .../calendar-multi-view.component.spec.ts | 7 +- .../date-picker/date-picker.component.spec.ts | 23 +++-- .../lib/date-picker/date-picker.component.ts | 99 +++++++++++++------ .../date-range-picker.component.spec.ts | 29 +++--- .../date-range-picker.component.ts | 37 +++---- .../src/lib/dialog/dialog.component.spec.ts | 6 +- .../src/lib/grids/grid-base.directive.ts | 8 +- .../grid/grid-filtering-advanced.spec.ts | 9 ++ .../src/lib/services/overlay/overlay.spec.ts | 26 ++++- .../src/lib/services/overlay/overlay.ts | 88 +++++++++++++---- 11 files changed, 235 insertions(+), 99 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c150df8672d..a8e559b99af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ All notable changes for each version of this project will be documented in this - Added `shape` property that controls the shape of the badge and can be either `square` or `rounded`. The default shape of the badge is rounded. - `IgxAvatar` - **Breaking Change** The `roundShape` property has been deprecated and will be removed in a future version. Users can control the shape of the avatar by the newly added `shape` attribute that can be `square`, `rounded` or `circle`. The default shape of the avatar is `square`. +- `IgxOverlayService` + - `attach` method overload accepting `ComponentFactoryResolver` (trough `NgModuleRef`-like object) is now deprecated in line with API deprecated in Angular 13. New overload is added accepting `ViewComponentRef` that should be used instead. ## 15.0.1 diff --git a/projects/igniteui-angular/src/lib/calendar/calendar-multi-view.component.spec.ts b/projects/igniteui-angular/src/lib/calendar/calendar-multi-view.component.spec.ts index cbc4fac9f10..812d47c9d53 100644 --- a/projects/igniteui-angular/src/lib/calendar/calendar-multi-view.component.spec.ts +++ b/projects/igniteui-angular/src/lib/calendar/calendar-multi-view.component.spec.ts @@ -1317,6 +1317,9 @@ describe('Multi-View Calendar - ', () => { overlay = document.querySelector(HelperTestFunctions.OVERLAY_CSSCLASS); HelperTestFunctions.verifyMonthsViewNumber(overlay, 2); HelperTestFunctions.verifyCalendarSubHeaders(overlay, [new Date('2019-09-16'), new Date('2019-10-16')]); + + // clean up test + tick(350); })); it('Verify setting hideOutsideDays and monthsViewNumber from datepicker', fakeAsync(() => { @@ -1349,8 +1352,10 @@ describe('Multi-View Calendar - ', () => { expect(HelperTestFunctions.getHiddenDays(overlay, 0).length).toBe(0); expect(HelperTestFunctions.getHiddenDays(overlay, 1).length).toBe(0); expect(HelperTestFunctions.getHiddenDays(overlay, 2).length).toBe(0); - })); + // clean up test + tick(350); + })); }); }); diff --git a/projects/igniteui-angular/src/lib/date-picker/date-picker.component.spec.ts b/projects/igniteui-angular/src/lib/date-picker/date-picker.component.spec.ts index ecfe51d9f5f..13e028a8842 100644 --- a/projects/igniteui-angular/src/lib/date-picker/date-picker.component.spec.ts +++ b/projects/igniteui-angular/src/lib/date-picker/date-picker.component.spec.ts @@ -114,13 +114,14 @@ describe('IgxDatePicker', () => { fixture.detectChanges(); datePicker.close(); - tick(); + tick(350); fixture.detectChanges(); expect(datePicker.collapsed).toBeFalsy(); expect(datePicker.closing.emit).toHaveBeenCalled(); expect(datePicker.closed.emit).not.toHaveBeenCalled(); closingSub.unsubscribe(); + (datePicker as any)._overlayService.detachAll(); })); }); @@ -176,6 +177,9 @@ describe('IgxDatePicker', () => { expect(datePicker.collapsed).toBeFalsy(); expect(datePicker.opening.emit).toHaveBeenCalledTimes(1); expect(datePicker.opened.emit).toHaveBeenCalledTimes(1); + + // wait datepicker to get destroyed and test to cleanup + tick(350); })); it('should close the calendar with ESC', fakeAsync(() => { @@ -455,7 +459,7 @@ describe('IgxDatePicker', () => { let mockDateEditor: any; let mockCalendar: Partial; let mockInputDirective: any; - const mockModuleRef = {} as any; + const viewsContainerRef = {} as any; const mockOverlayId = '1'; const today = new Date(); const elementRef = { @@ -625,10 +629,11 @@ describe('IgxDatePicker', () => { }, focus: () => { } }; - datePicker = new IgxDatePickerComponent(elementRef, 'en-US', overlay, mockModuleRef, mockInjector, renderer2, null, mockCdr); + datePicker = new IgxDatePickerComponent(elementRef, 'en-US', overlay, mockInjector, renderer2, null, mockCdr); (datePicker as any).inputGroup = mockInputGroup; (datePicker as any).inputDirective = mockInputDirective; (datePicker as any).dateTimeEditor = mockDateEditor; + (datePicker as any).viewContainerRef = viewsContainerRef; // TODO: TEMP workaround for afterViewInit call in unit tests: datePicker.clearComponents = new QueryList(); datePicker.toggleComponents = new QueryList(); @@ -886,19 +891,19 @@ describe('IgxDatePicker', () => { const isDropdownSpy = spyOnProperty(datePicker, 'isDropdown', 'get'); isDropdownSpy.and.returnValue(false); datePicker.open(); - expect(overlay.attach).toHaveBeenCalledWith(IgxCalendarContainerComponent, baseDialogSettings, mockModuleRef); + expect(overlay.attach).toHaveBeenCalledWith(IgxCalendarContainerComponent, viewsContainerRef, baseDialogSettings); expect(overlay.show).toHaveBeenCalledWith(mockOverlayId); isDropdownSpy.and.returnValue(true); datePicker.open(); - expect(overlay.attach).toHaveBeenCalledWith(IgxCalendarContainerComponent, baseDropdownSettings, mockModuleRef); + expect(overlay.attach).toHaveBeenCalledWith(IgxCalendarContainerComponent, viewsContainerRef, baseDropdownSettings); expect(overlay.show).toHaveBeenCalledWith(mockOverlayId); const mockOutlet = {} as any; datePicker.outlet = mockOutlet; datePicker.open(); expect(overlay.attach).toHaveBeenCalledWith( IgxCalendarContainerComponent, + viewsContainerRef, Object.assign({}, baseDropdownSettings, { outlet: mockOutlet }), - mockModuleRef ); expect(overlay.show).toHaveBeenCalledWith(mockOverlayId); let mockSettings: OverlaySettings = { @@ -910,8 +915,8 @@ describe('IgxDatePicker', () => { datePicker.open(mockSettings); expect(overlay.attach).toHaveBeenCalledWith( IgxCalendarContainerComponent, + viewsContainerRef, Object.assign({}, baseDropdownSettings, mockSettings), - mockModuleRef ); expect(overlay.show).toHaveBeenCalledWith(mockOverlayId); isDropdownSpy.and.returnValue(false); @@ -923,8 +928,8 @@ describe('IgxDatePicker', () => { datePicker.open(mockSettings); expect(overlay.attach).toHaveBeenCalledWith( IgxCalendarContainerComponent, + viewsContainerRef, Object.assign({}, baseDialogSettings, mockSettings), - mockModuleRef ); expect(overlay.show).toHaveBeenCalledWith(mockOverlayId); isDropdownSpy.and.returnValue(true); @@ -937,8 +942,8 @@ describe('IgxDatePicker', () => { datePicker.open(mockSettings); expect(overlay.attach).toHaveBeenCalledWith( IgxCalendarContainerComponent, + viewsContainerRef, Object.assign({}, baseDropdownSettings, { modal: true }), - mockModuleRef ); }); diff --git a/projects/igniteui-angular/src/lib/date-picker/date-picker.component.ts b/projects/igniteui-angular/src/lib/date-picker/date-picker.component.ts index 6bd1a30873e..26ef5dd5300 100644 --- a/projects/igniteui-angular/src/lib/date-picker/date-picker.component.ts +++ b/projects/igniteui-angular/src/lib/date-picker/date-picker.component.ts @@ -1,43 +1,77 @@ import { - Component, ContentChild, EventEmitter, HostBinding, Input, - OnDestroy, Output, ViewChild, ElementRef, Inject, HostListener, - NgModuleRef, OnInit, AfterViewInit, Injector, AfterViewChecked, ContentChildren, - QueryList, LOCALE_ID, Renderer2, Optional, PipeTransform, ChangeDetectorRef + AfterViewChecked, + AfterViewInit, + ChangeDetectorRef, + Component, + ContentChild, + ContentChildren, + ElementRef, + EventEmitter, + HostBinding, + HostListener, + Inject, + Injector, + Input, + LOCALE_ID, + OnDestroy, + OnInit, + Optional, + Output, + PipeTransform, + QueryList, + Renderer2, + ViewChild, + ViewContainerRef } from '@angular/core'; import { - ControlValueAccessor, NG_VALUE_ACCESSOR, NgControl, AbstractControl, - NG_VALIDATORS, ValidationErrors, Validator + AbstractControl, + ControlValueAccessor, + NgControl, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator } from '@angular/forms'; -import { - IgxCalendarComponent, IgxCalendarHeaderTemplateDirective, IgxCalendarSubheaderTemplateDirective, - isDateInRanges, IFormattingViews, IFormattingOptions -} from '../calendar/public_api'; -import { - IgxInputDirective, IgxInputGroupComponent, - IgxLabelDirective, IGX_INPUT_GROUP_TYPE, IgxInputGroupType, IgxInputState -} from '../input-group/public_api'; -import { fromEvent, Subscription, noop, MonoTypeOperatorFunction } from 'rxjs'; +import { fromEvent, MonoTypeOperatorFunction, noop, Subscription } from 'rxjs'; import { filter, takeUntil } from 'rxjs/operators'; -import { IgxOverlayOutletDirective } from '../directives/toggle/toggle.directive'; +import { fadeIn, fadeOut } from '../animations/fade'; import { - OverlaySettings, IgxOverlayService, AbsoluteScrollStrategy, - AutoPositionStrategy, - OverlayCancelableEventArgs, - OverlayEventArgs -} from '../services/public_api'; -import { CurrentResourceStrings } from '../core/i18n/resources'; -import { IDatePickerResourceStrings } from '../core/i18n/date-picker-resources'; + IFormattingOptions, + IFormattingViews, + IgxCalendarComponent, + IgxCalendarHeaderTemplateDirective, + IgxCalendarSubheaderTemplateDirective, + isDateInRanges +} from '../calendar/public_api'; import { DateRangeDescriptor, DateRangeType } from '../core/dates/dateRange'; -import { IBaseCancelableBrowserEventArgs, PlatformUtil, isDate } from '../core/utils'; +import { DisplayDensityToken, IDisplayDensityOptions } from '../core/density'; +import { IDatePickerResourceStrings } from '../core/i18n/date-picker-resources'; +import { CurrentResourceStrings } from '../core/i18n/resources'; +import { IBaseCancelableBrowserEventArgs, isDate, PlatformUtil } from '../core/utils'; import { IgxCalendarContainerComponent } from '../date-common/calendar-container/calendar-container.component'; -import { fadeIn, fadeOut } from '../animations/fade'; import { PickerBaseDirective } from '../date-common/picker-base.directive'; -import { DisplayDensityToken, IDisplayDensityOptions } from '../core/density'; -import { DatePart, DatePartDeltas, IgxDateTimeEditorDirective } from '../directives/date-time-editor/public_api'; +import { IgxPickerActionsDirective, IgxPickerClearComponent } from '../date-common/public_api'; +import { PickerHeaderOrientation } from '../date-common/types'; import { DateTimeUtil } from '../date-common/util/date-time.util'; -import { PickerHeaderOrientation as PickerHeaderOrientation } from '../date-common/types'; +import { DatePart, DatePartDeltas, IgxDateTimeEditorDirective } from '../directives/date-time-editor/public_api'; +import { IgxOverlayOutletDirective } from '../directives/toggle/toggle.directive'; +import { + IgxInputDirective, + IgxInputGroupComponent, + IgxInputGroupType, + IgxInputState, + IgxLabelDirective, + IGX_INPUT_GROUP_TYPE +} from '../input-group/public_api'; +import { + AbsoluteScrollStrategy, + AutoPositionStrategy, + IgxOverlayService, + OverlayCancelableEventArgs, + OverlayEventArgs, + OverlaySettings +} from '../services/public_api'; import { IDatePickerValidationFailedEventArgs } from './date-picker.common'; -import { IgxPickerClearComponent, IgxPickerActionsDirective } from '../date-common/public_api'; let NEXT_ID = 0; @@ -378,6 +412,9 @@ export class IgxDatePickerComponent extends PickerBaseDirective implements Contr @ViewChild(IgxInputGroupComponent) private inputGroup: IgxInputGroupComponent; + @ViewChild(IgxInputGroupComponent, { read: ViewContainerRef }) + private viewContainerRef: ViewContainerRef; + @ViewChild(IgxLabelDirective) private labelDirective: IgxLabelDirective; @@ -467,7 +504,6 @@ export class IgxDatePickerComponent extends PickerBaseDirective implements Contr constructor(public element: ElementRef, @Inject(LOCALE_ID) protected _localeId: string, @Inject(IgxOverlayService) private _overlayService: IgxOverlayService, - private _moduleRef: NgModuleRef, private _injector: Injector, private _renderer: Renderer2, private platform: PlatformUtil, @@ -541,9 +577,8 @@ export class IgxDatePickerComponent extends PickerBaseDirective implements Contr if (this.outlet) { overlaySettings.outlet = this.outlet; } - this._overlayId = this._overlayService - .attach(IgxCalendarContainerComponent, overlaySettings, this._moduleRef); + .attach(IgxCalendarContainerComponent, this.viewContainerRef, overlaySettings); this._overlayService.show(this._overlayId); } diff --git a/projects/igniteui-angular/src/lib/date-range-picker/date-range-picker.component.spec.ts b/projects/igniteui-angular/src/lib/date-range-picker/date-range-picker.component.spec.ts index 199598fb5b7..0fb33f94a3a 100644 --- a/projects/igniteui-angular/src/lib/date-range-picker/date-range-picker.component.spec.ts +++ b/projects/igniteui-angular/src/lib/date-range-picker/date-range-picker.component.spec.ts @@ -54,7 +54,6 @@ describe('IgxDateRangePicker', () => { let mockPlatformUtil: any; let overlay: IgxOverlayService; let mockInjector; - let ngModuleRef: any; let mockCalendar: IgxCalendarComponent; let mockDaysView: any; let mockAnimationService: AnimationService; @@ -77,13 +76,6 @@ describe('IgxDateRangePicker', () => { }) }) }; - ngModuleRef = ({ - injector: (...args: any[]) => { }, - componentFactoryResolver: mockFactoryResolver, - instance: () => { }, - destroy: () => { }, - onDestroy: (fn: any) => { } - }); mockElement = { style: { visibility: '', cursor: '', transitionDuration: '' }, classList: { add: () => { }, remove: () => { } }, @@ -123,7 +115,14 @@ describe('IgxDateRangePicker', () => { getPosition: () => 0, parentPlayer: {}, totalTime: 0, - beforeDestroy: () => { } + beforeDestroy: () => { }, + _renderer: { + engine: { + players: [ + {} + ] + } + } }) }) }; @@ -255,7 +254,7 @@ describe('IgxDateRangePicker', () => { }); it('should disable calendar dates when min and/or max values as dates are provided', () => { - const dateRange = new IgxDateRangePickerComponent(elementRef, 'en-US', platform, mockInjector, ngModuleRef, null, overlay); + const dateRange = new IgxDateRangePickerComponent(elementRef, 'en-US', platform, mockInjector, null, overlay); dateRange.ngOnInit(); spyOnProperty((dateRange as any), 'calendar').and.returnValue(mockCalendar); @@ -775,6 +774,9 @@ describe('IgxDateRangePicker', () => { fixture.detectChanges(); expect((dateRange as any).calendar.selectedDates.length).toBeGreaterThan(0); + + // clean up test + tick(350); })); it('should set initial validity state when the form group is disabled', () => { @@ -1036,12 +1038,12 @@ describe('IgxDateRangePicker', () => { const fix = TestBed.createComponent(DateRangeReactiveFormComponent); fix.detectChanges(); const dateRangePicker = fix.componentInstance.dateRangeWithTwoInputs; - + fix.componentInstance.markAsTouched(); fix.detectChanges(); expect(dateRangePicker.projectedInputs.first.inputDirective.valid).toBe(IgxInputState.INVALID); expect(dateRangePicker.projectedInputs.last.inputDirective.valid).toBe(IgxInputState.INVALID); - + fix.componentInstance.disableForm(); fix.detectChanges(); expect(dateRangePicker.projectedInputs.first.inputDirective.valid).toBe(IgxInputState.INITIAL); @@ -1349,6 +1351,9 @@ describe('IgxDateRangePicker', () => { dateRange.select(startDate, endDate); fixture.detectChanges(); expect(singleInputElement.nativeElement.getAttribute('placeholder')).toEqual(''); + + // clean up test + tick(350); })); it('should render custom label', () => { diff --git a/projects/igniteui-angular/src/lib/date-range-picker/date-range-picker.component.ts b/projects/igniteui-angular/src/lib/date-range-picker/date-range-picker.component.ts index 16e0a22e78f..9f0e048163a 100644 --- a/projects/igniteui-angular/src/lib/date-range-picker/date-range-picker.component.ts +++ b/projects/igniteui-angular/src/lib/date-range-picker/date-range-picker.component.ts @@ -1,13 +1,12 @@ import { - AfterViewInit, ChangeDetectorRef, Component, ContentChild, ContentChildren, ElementRef, - EventEmitter, HostBinding, HostListener, Inject, Injector, Input, LOCALE_ID, - NgModuleRef, - OnChanges, OnDestroy, OnInit, Optional, Output, QueryList, - SimpleChanges, TemplateRef, ViewChild + AfterViewInit, ChangeDetectorRef, Component, ContentChild, ContentChildren, ElementRef, + EventEmitter, HostBinding, HostListener, Inject, Injector, Input, LOCALE_ID, + OnChanges, OnDestroy, OnInit, Optional, Output, QueryList, + SimpleChanges, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core'; import { - AbstractControl, ControlValueAccessor, NgControl, - NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator + AbstractControl, ControlValueAccessor, NgControl, + NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms'; import { fromEvent, merge, MonoTypeOperatorFunction, noop, Subscription } from 'rxjs'; import { filter, takeUntil } from 'rxjs/operators'; @@ -15,25 +14,25 @@ import { fadeIn, fadeOut } from '../animations/fade'; import { CalendarSelection, IgxCalendarComponent } from '../calendar/public_api'; import { DateRangeType } from '../core/dates'; import { DisplayDensityToken, IDisplayDensityOptions } from '../core/density'; -import { CurrentResourceStrings } from '../core/i18n/resources'; import { IDateRangePickerResourceStrings } from '../core/i18n/date-range-picker-resources'; -import { DateTimeUtil } from '../date-common/util/date-time.util'; +import { CurrentResourceStrings } from '../core/i18n/resources'; import { IBaseCancelableBrowserEventArgs, isDate, parseDate, PlatformUtil } from '../core/utils'; import { IgxCalendarContainerComponent } from '../date-common/calendar-container/calendar-container.component'; -import { IgxPickerActionsDirective } from '../date-common/picker-icons.common'; import { PickerBaseDirective } from '../date-common/picker-base.directive'; +import { IgxPickerActionsDirective } from '../date-common/picker-icons.common'; +import { DateTimeUtil } from '../date-common/util/date-time.util'; import { IgxOverlayOutletDirective } from '../directives/toggle/toggle.directive'; import { - IgxInputDirective, IgxInputGroupComponent, IgxInputGroupType, IgxInputState, - IgxLabelDirective, IGX_INPUT_GROUP_TYPE + IgxInputDirective, IgxInputGroupComponent, IgxInputGroupType, IgxInputState, + IgxLabelDirective, IGX_INPUT_GROUP_TYPE } from '../input-group/public_api'; import { - AutoPositionStrategy, IgxOverlayService, OverlayCancelableEventArgs, OverlayEventArgs, - OverlaySettings, PositionSettings + AutoPositionStrategy, IgxOverlayService, OverlayCancelableEventArgs, OverlayEventArgs, + OverlaySettings, PositionSettings } from '../services/public_api'; import { - DateRange, IgxDateRangeEndComponent, IgxDateRangeInputsBaseComponent, - IgxDateRangeSeparatorDirective, IgxDateRangeStartComponent + DateRange, IgxDateRangeEndComponent, IgxDateRangeInputsBaseComponent, + IgxDateRangeSeparatorDirective, IgxDateRangeStartComponent } from './date-range-picker-inputs.common'; const SingleInputDatesConcatenationString = ' - '; @@ -276,6 +275,9 @@ export class IgxDateRangePickerComponent extends PickerBaseDirective @ViewChild(IgxInputGroupComponent) public inputGroup: IgxInputGroupComponent; + @ViewChild(IgxInputGroupComponent, { read: ViewContainerRef }) + private viewContainerRef: ViewContainerRef; + /** @hidden @internal */ @ViewChild(IgxInputDirective) public inputDirective: IgxInputDirective; @@ -416,7 +418,6 @@ export class IgxDateRangePickerComponent extends PickerBaseDirective @Inject(LOCALE_ID) protected _localeId: string, protected platform: PlatformUtil, private _injector: Injector, - private _moduleRef: NgModuleRef, private _cdr: ChangeDetectorRef, @Inject(IgxOverlayService) private _overlayService: IgxOverlayService, @Optional() @Inject(DisplayDensityToken) protected _displayDensityOptions?: IDisplayDensityOptions, @@ -464,7 +465,7 @@ export class IgxDateRangePickerComponent extends PickerBaseDirective , overlaySettings); this._overlayId = this._overlayService - .attach(IgxCalendarContainerComponent, settings, this._moduleRef); + .attach(IgxCalendarContainerComponent, this.viewContainerRef, settings); this.subscribeToOverlayEvents(); this._overlayService.show(this._overlayId); } diff --git a/projects/igniteui-angular/src/lib/dialog/dialog.component.spec.ts b/projects/igniteui-angular/src/lib/dialog/dialog.component.spec.ts index 1353481f406..b46cbe2f2e3 100644 --- a/projects/igniteui-angular/src/lib/dialog/dialog.component.spec.ts +++ b/projects/igniteui-angular/src/lib/dialog/dialog.component.spec.ts @@ -11,8 +11,8 @@ import { slideOutBottom, slideInTop } from '../animations/main'; import { IgxToggleDirective } from '../directives/toggle/toggle.directive'; const OVERLAY_MAIN_CLASS = 'igx-overlay'; -const OVERLAY_WRAPPER_CLASS = `${OVERLAY_MAIN_CLASS}__wrapper`; -const OVERLAY_MODAL_WRAPPER_CLASS = `${OVERLAY_WRAPPER_CLASS}--modal`; +const OVERLAY_WRAPPER_CLASS = `${OVERLAY_MAIN_CLASS}__wrapper--flex`; +const OVERLAY_MODAL_WRAPPER_CLASS = `${OVERLAY_MAIN_CLASS}__wrapper--modal`; const CLASS_OVERLAY_CONTENT_MODAL = `${OVERLAY_MAIN_CLASS}__content--modal`; describe('Dialog', () => { @@ -414,7 +414,7 @@ describe('Dialog', () => { overlaydiv = document.getElementsByClassName(OVERLAY_MAIN_CLASS)[0]; overlayWrapper = overlaydiv.children[0]; expect(overlayWrapper.classList.contains(OVERLAY_MODAL_WRAPPER_CLASS)).toBe(true); - expect(overlayWrapper.classList.contains(OVERLAY_WRAPPER_CLASS)).toBe(false); + expect(overlayWrapper.classList.contains(OVERLAY_WRAPPER_CLASS)).toBe(true); })); it('Default button of the dialog is focused after opening the dialog and can be closed with keyboard.', fakeAsync(() => { diff --git a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts index fc85b9fbc80..0785ac3f580 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts @@ -3819,6 +3819,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements } private createComponentInstance(component: any) { + // TODO: create component instance view viewContainerRef.createComponent(Component, Settings) overload let dynamicFactory: ComponentFactory; const factoryResolver = this.moduleRef ? this.moduleRef.componentFactoryResolver @@ -6262,11 +6263,8 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements this._advancedFilteringOverlayId = this.overlayService.attach( IgxAdvancedFilteringDialogComponent, - settings, - { - injector: this.viewRef.injector, - componentFactoryResolver: this.resolver - }); + this.viewRef, + settings); this.overlayService.show(this._advancedFilteringOverlayId); } } diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-advanced.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-advanced.spec.ts index 97fe6efe1d8..c782b15a766 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-advanced.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-advanced.spec.ts @@ -193,6 +193,7 @@ describe('IgxGrid - Advanced Filtering #grid - ', () => { // Discard the new group and verify that the initial buttons are visible. GridFunctions.clickAdvancedFilteringExpressionCloseButton(fix); + fix.detectChanges(); expect(GridFunctions.getAdvancedFilteringInitialAddGroupButtons(fix).length).toBe(2); // Click the initial 'Add Or Group' button. @@ -287,6 +288,8 @@ describe('IgxGrid - Advanced Filtering #grid - ', () => { const input = GridFunctions.getAdvancedFilteringValueInput(fix).querySelector('input'); UIInteractions.clickAndSendInputElementValue(input, 'ign', fix); // Type filter value. + tick(); + fix.detectChanges(); verifyEditModeExpressionInputStates(fix, true, true, true, true); // Commit the populated expression. @@ -1140,9 +1143,11 @@ describe('IgxGrid - Advanced Filtering #grid - ', () => { const expressionItem = fix.nativeElement.querySelectorAll(`.${ADVANCED_FILTERING_EXPRESSION_ITEM_CLASS}`)[9]; expressionItem.dispatchEvent(new MouseEvent('mouseenter')); tick(); + fix.detectChanges(); // Click the add icon to display the adding buttons. GridFunctions.clickAdvancedFilteringTreeExpressionChipAddIcon(fix, [9]); + fix.detectChanges(); tick(50); // Verify the adding buttons are in view. @@ -1176,6 +1181,7 @@ describe('IgxGrid - Advanced Filtering #grid - ', () => { const expressionItem = fix.nativeElement.querySelectorAll(`.${ADVANCED_FILTERING_EXPRESSION_ITEM_CLASS}`)[8]; expressionItem.dispatchEvent(new MouseEvent('mouseenter')); tick(); + fix.detectChanges(); // Click the add icon to display the adding buttons. GridFunctions.clickAdvancedFilteringTreeExpressionChipAddIcon(fix, [8]); @@ -1214,6 +1220,7 @@ describe('IgxGrid - Advanced Filtering #grid - ', () => { const expressionItem = fix.nativeElement.querySelectorAll(`.${ADVANCED_FILTERING_EXPRESSION_ITEM_CLASS}`)[9]; expressionItem.dispatchEvent(new MouseEvent('mouseenter')); tick(); + fix.detectChanges(); // Click the edit icon to enter edit mode of the expression. GridFunctions.clickAdvancedFilteringTreeExpressionChipEditIcon(fix, [9]); @@ -2725,6 +2732,7 @@ describe('IgxGrid - Advanced Filtering #grid - ', () => { const keyboardEvent = new KeyboardEvent('keydown', { key: 'Enter' }); rootOperatorLine.dispatchEvent(keyboardEvent); tick(); + fix.detectChanges(); // Simulate end of chip selection animation const chipSelect = fix.nativeElement.querySelector(CHIP_SELECT_CLASS); @@ -2845,6 +2853,7 @@ describe('IgxGrid - Advanced Filtering #grid - ', () => { GridFunctions.clickAdvancedFilteringTreeExpressionChip(fix, [0]); GridFunctions.clickAdvancedFilteringTreeExpressionChip(fix, [1]); tick(200); + fix.detectChanges(); // Simulate end of chip selection animation const chipSelectHidden = fix.nativeElement.querySelector(CHIP_SELECT_CLASS); diff --git a/projects/igniteui-angular/src/lib/services/overlay/overlay.spec.ts b/projects/igniteui-angular/src/lib/services/overlay/overlay.spec.ts index 0237630d005..06fcc4beb3d 100644 --- a/projects/igniteui-angular/src/lib/services/overlay/overlay.spec.ts +++ b/projects/igniteui-angular/src/lib/services/overlay/overlay.spec.ts @@ -7,6 +7,7 @@ import { Inject, NgModule, ViewChild, + ViewContainerRef, ViewEncapsulation } from '@angular/core'; import { fakeAsync, inject, TestBed, tick, waitForAsync } from '@angular/core/testing'; @@ -1097,6 +1098,27 @@ describe('igxOverlay', () => { overlay.detachAll(); })); + it('#3988 - Should use viewContainerRef to create component', () => { + const fixture = TestBed.createComponent(EmptyPageComponent); + const overlay = fixture.componentInstance.overlay; + const viewContainerRef = fixture.componentInstance.viewContainerRef; + fixture.detectChanges(); + + const mockNativeElement = document.createElement('div'); + const mockComponent = { + hostView: fixture.componentRef.hostView, + changeDetectorRef: { detectChanges: () => { } }, + location: { nativeElement: mockNativeElement }, + destroy: () => { } + }; + spyOn(viewContainerRef, 'createComponent').and.returnValue(mockComponent as any); + const id = overlay.attach(SimpleDynamicComponent, viewContainerRef); + expect(viewContainerRef.createComponent).toHaveBeenCalledWith(SimpleDynamicComponent as any); + expect(overlay.getOverlayById(id).componentRef as any).toBe(mockComponent); + + overlay.detachAll(); + }); + // it('##6474 - should calculate correctly position', () => { // const elastic: ElasticPositionStrategy = new ElasticPositionStrategy(); // const targetRect: ClientRect = { @@ -4404,7 +4426,9 @@ export class EmptyPageComponent { @ViewChild('button', { static: true }) public buttonElement: ElementRef; @ViewChild('div', { static: true }) public divElement: ElementRef; - constructor(@Inject(IgxOverlayService) public overlay: IgxOverlayService) { } + constructor( + @Inject(IgxOverlayService) public overlay: IgxOverlayService, + public viewContainerRef: ViewContainerRef) { } public click() { this.overlay.show(this.overlay.attach(SimpleDynamicComponent)); diff --git a/projects/igniteui-angular/src/lib/services/overlay/overlay.ts b/projects/igniteui-angular/src/lib/services/overlay/overlay.ts index 3b549a276a6..79d64dc9fdc 100644 --- a/projects/igniteui-angular/src/lib/services/overlay/overlay.ts +++ b/projects/igniteui-angular/src/lib/services/overlay/overlay.ts @@ -10,7 +10,10 @@ import { Inject, Injectable, Injector, - NgZone, OnDestroy, Type + NgZone, + OnDestroy, + Type, + ViewContainerRef } from '@angular/core'; import { fromEvent, Subject, Subscription } from 'rxjs'; import { filter, takeUntil } from 'rxjs/operators'; @@ -33,8 +36,8 @@ import { } from '../../animations/main'; import { PlatformUtil } from '../../core/utils'; import { IgxOverlayOutletDirective } from '../../directives/toggle/toggle.directive'; -import { AnimationService } from '../animation/animation'; import { IgxAngularAnimationService } from '../animation/angular-animation-service'; +import { AnimationService } from '../animation/animation'; import { AutoPositionStrategy } from './position/auto-position-strategy'; import { ConnectedPositioningStrategy } from './position/connected-positioning-strategy'; import { ContainerPositionStrategy } from './position/container-position-strategy'; @@ -316,12 +319,25 @@ export class IgxOverlayService implements OnDestroy { * @param moduleRef Optional reference to an object containing Injector and ComponentFactoryResolver * that can resolve the component's factory * @returns Id of the created overlay. Valid until `detach` is called. + * @deprecated deprecated in 14.0.0. Use the `attach(component, viewContainerRef, settings)` overload */ - public attach(component: Type, settings?: OverlaySettings, + public attach( + component: Type, + settings?: OverlaySettings, moduleRef?: { injector: Injector, componentFactoryResolver: ComponentFactoryResolver }): string; - public attach(component: ElementRef | Type, settings?: OverlaySettings, - moduleRef?: { injector: Injector, componentFactoryResolver: ComponentFactoryResolver }): string { - const info: OverlayInfo = this.getOverlayInfo(component, moduleRef); + /** + * Generates an Id. Provide this Id when calling the `show(id)` method + * + * @param component Component Type to show in overlay + * @param viewContainerRef Reference to the container where created component's host view will be inserted + * @param settings Display settings for the overlay, such as positioning and scroll/close behavior. + */ + public attach(component: Type, viewContainerRef: ViewContainerRef, settings?: OverlaySettings): string; + public attach( + componentOrElement: ElementRef | Type, + viewContainerRefOrSettings?: ViewContainerRef | OverlaySettings, + moduleRefOrSettings?: { injector: Injector, componentFactoryResolver: ComponentFactoryResolver } | OverlaySettings): string { + const info: OverlayInfo = this.getOverlayInfo(componentOrElement, this.getUserViewContainerOrModuleRef(viewContainerRefOrSettings, moduleRefOrSettings)); if (!info) { console.warn('Overlay was not able to attach provided component!'); @@ -330,7 +346,7 @@ export class IgxOverlayService implements OnDestroy { info.id = (this._componentId++).toString(); info.visible = false; - settings = Object.assign({}, this._defaultSettings, settings); + const settings = Object.assign({}, this._defaultSettings, this.getUserOverlaySettings(viewContainerRefOrSettings, moduleRefOrSettings)); info.settings = settings; this._overlayInfos.push(info); info.hook = this.placeElementHook(info.elementRef.nativeElement); @@ -542,21 +558,58 @@ export class IgxOverlayService implements OnDestroy { } } - private getOverlayInfo(component: any, moduleRef?: { injector: Injector, componentFactoryResolver: ComponentFactoryResolver }): OverlayInfo { + private getUserOverlaySettings( + viewContainerRefOrSettings?: ViewContainerRef | OverlaySettings, + moduleRefOrSettings?: { injector: Injector, componentFactoryResolver: ComponentFactoryResolver } | OverlaySettings): OverlaySettings { + let result: OverlaySettings | undefined; + if (viewContainerRefOrSettings && !(viewContainerRefOrSettings instanceof ViewContainerRef)) { + result = viewContainerRefOrSettings; + return result; + } + if (moduleRefOrSettings && !('injector' in moduleRefOrSettings && 'componentFactoryResolver' in moduleRefOrSettings)) { + result = moduleRefOrSettings; + } + return result; + } + + + private getUserViewContainerOrModuleRef( + viewContainerRefOrSettings?: ViewContainerRef | OverlaySettings, + moduleRefOrSettings?: { injector: Injector, componentFactoryResolver: ComponentFactoryResolver } | OverlaySettings + ): ViewContainerRef | { injector: Injector, componentFactoryResolver: ComponentFactoryResolver } | undefined { + let result: ViewContainerRef | { injector: Injector, componentFactoryResolver: ComponentFactoryResolver } | undefined; + if (viewContainerRefOrSettings instanceof ViewContainerRef) { + result = viewContainerRefOrSettings; + } + if (moduleRefOrSettings && 'injector' in moduleRefOrSettings && 'componentFactoryResolver' in moduleRefOrSettings) { + result = moduleRefOrSettings; + } + return result; + } + + private getOverlayInfo( + component: ElementRef | Type, + viewContainerRef?: { injector: Injector, componentFactoryResolver: ComponentFactoryResolver } | ViewContainerRef): OverlayInfo | null { const info: OverlayInfo = { ngZone: this._zone, transformX: 0, transformY: 0 }; if (component instanceof ElementRef) { info.elementRef = component; } else { - let dynamicFactory: ComponentFactory; - const factoryResolver = moduleRef ? moduleRef.componentFactoryResolver : this._factoryResolver; - try { - dynamicFactory = factoryResolver.resolveComponentFactory(component); - } catch (error) { - console.error(error); - return null; + let dynamicComponent: ComponentRef; + if (viewContainerRef instanceof ViewContainerRef) { + dynamicComponent = viewContainerRef.createComponent(component); + } else { + let dynamicFactory: ComponentFactory; + const factoryResolver = viewContainerRef ? viewContainerRef.componentFactoryResolver : this._factoryResolver; + try { + dynamicFactory = factoryResolver.resolveComponentFactory(component); + } catch (error) { + console.error(error); + return null; + } + const injector = viewContainerRef ? viewContainerRef.injector : this._injector; + dynamicComponent = dynamicFactory.create(injector); + this._appRef.attachView(dynamicComponent.hostView); } - const injector = moduleRef ? moduleRef.injector : this._injector; - const dynamicComponent: ComponentRef = dynamicFactory.create(injector); if (dynamicComponent.onDestroy) { dynamicComponent.onDestroy(() => { if (!info.detached && this._overlayInfos.indexOf(info) !== -1) { @@ -564,7 +617,6 @@ export class IgxOverlayService implements OnDestroy { } }) } - this._appRef.attachView(dynamicComponent.hostView); // If the element is newly created from a Component, it is wrapped in 'ng-component' tag - we do not want that. const element = dynamicComponent.location.nativeElement;