IgniteUI / igniteui-angular

Ignite UI for Angular is a complete library of Angular-native, Material-based Angular UI components with the fastest grids and charts, Pivot Grid, Dock Manager, Hierarchical Grid, and more.
https://www.infragistics.com/products/ignite-ui-angular
Other
568 stars 159 forks source link

OverlayService - Removed support for custom injector #14364

Open mrentmeister-tt opened 3 weeks ago

mrentmeister-tt commented 3 weeks ago

Description

I just recently updated from igniteui-angular v17.2.4 to 18.0.0. In the new version, I lost the ability to provide an injector when creating a component in an overlay. I use it to provide things like data and settings to the component that's created (similar to matdialog). I looked at the attach function in the OverlayService (https://github.com/IgniteUI/igniteui-angular/blob/master/projects/igniteui-angular/src/lib/services/overlay/overlay.ts#L323) to see that you're using createComponent and viewContainerRef.createComponent. These functions also support an option to provide an element injector,

This is a mix between a feature request and a bug, because it did work in previous versions, but lost support due to api changes. The feature request portion is to add injector as an optional parameter to OverlaySettings.

Excerpts from my code pre v18

// dialog-settings.ts
export type DialogSettings = OverlaySettings & {
  data?: any;
  injector?: Injector;
  dimensions?: DialogDimensions;
  showClose?: boolean;
};

// dialog.service.ts
import {
  ComponentFactoryResolver,
  ElementRef,
  Injectable,
  InjectionToken,
  Injector,
  OnDestroy,
  Type
} from '@angular/core';
import {
  GlobalPositionStrategy,
  HorizontalAlignment,
  IgxOverlayService,
  NoOpScrollStrategy,
  PositionSettings,
  VerticalAlignment
} from '@infragistics/igniteui-angular';
import { take } from 'rxjs/operators';
import { TraxDialogRef } from './dialog-ref';
import { DialogDimensions } from './models/dialog-dimensions';
import { DialogSettings } from './models/dialog-settings';

/** Injection token that can be used to access the data that was passed in to a dialog. */
export const TRAX_DIALOG_DATA_TOKEN = new InjectionToken<any>('TraxDialogDataToken');

/** Injection token that can be used to access the settings that were passed in to a dialog. */
export const TRAX_DIALOG_SETTINGS_TOKEN = new InjectionToken<any>('TraxDialogSettingsToken');

@Injectable({
  providedIn: 'root'
})
export class TraxDialogService implements OnDestroy {
  public static readonly DefaultPositionSettings: PositionSettings = {
    horizontalDirection: HorizontalAlignment.Center,
    verticalDirection: VerticalAlignment.Middle,
    horizontalStartPoint: HorizontalAlignment.Center,
    verticalStartPoint: VerticalAlignment.Middle
  };

  public static readonly DefaultDialogDimensions: DialogDimensions = {
    minWidth: '32rem',
    maxHeight: '75vh',
    maxWidth: '75vw'
  };

  public static readonly DefaultDialogSettings: DialogSettings = {
    positionStrategy: new GlobalPositionStrategy(TraxDialogService.DefaultPositionSettings),
    scrollStrategy: new NoOpScrollStrategy(),
    modal: true,
    closeOnOutsideClick: true,
    closeOnEscape: true,
    dimensions: { ...TraxDialogService.DefaultDialogDimensions }
  };

  private readonly _dialogMap = new Map<string, TraxDialogRef<any>>();

  constructor(
    private _injector: Injector,
    private _overlayService: IgxOverlayService
  ) {}

  public ngOnDestroy(): void {
    // Only close the dialogs at this level on destroy
    // since the parent service may still be active.
    this.closeDialogs([...this._dialogMap.values()]);
  }

  public get openedDialogs(): TraxDialogRef<any>[] {
    return [...this._dialogMap.values()];
  }

  public open<TC, TR = any>(
    component: Type<TC> | ElementRef<any>,
    dialogSettings?: DialogSettings
  ): TraxDialogRef<TC, TR> {
    dialogSettings = { ...TraxDialogService.DefaultDialogSettings, ...dialogSettings };
    // We need the next overlay id before it's created so that we can factory resolve the DialogRef
    const overlayId = this.getNextOverlayId();
    const dialogRef = this.registerDialogRef<TC, TR>(overlayId);
    this.attatchDialogToDom(overlayId, component, dialogSettings);
    return dialogRef;
  }

  private registerDialogRef<TC, TR>(overlayId: string): TraxDialogRef<TC, TR> {
    const dialogRef = new TraxDialogRef<TC, TR>(overlayId, this._overlayService);
    this._dialogMap.set(overlayId, dialogRef);
    dialogRef.closed$.pipe(take(1)).subscribe(e => {
      this._dialogMap.delete(overlayId);
    });

    return dialogRef;
  }

  private closeDialogs(dialogs: TraxDialogRef<any>[]): void {
    let i = dialogs.length;

    while (i--) {
      dialogs[i].close();
      this._dialogMap.delete(dialogs[i].overlayId);
    }
  }

  private createInjectorFromOverlaySettings(
    overlayId: string,
    dialogSettings: DialogSettings
  ): Injector {
    return Injector.create({
      parent: dialogSettings.injector || this._injector,
      providers: [
        { provide: TRAX_DIALOG_DATA_TOKEN, useValue: dialogSettings.data },
        { provide: TRAX_DIALOG_SETTINGS_TOKEN, useValue: dialogSettings },
        { provide: TraxDialogRef, useValue: this._dialogMap.get(overlayId) }
      ]
    });
  }

  private getNextOverlayId(): string {
    return (this._overlayService as any)._componentId.toString();
  }

  private attatchDialogToDom<TC>(
    overlayId: string,
    component: Type<TC> | ElementRef<any>,
    dialogSettings: DialogSettings
  ): void {
    const injector = this.createInjectorFromOverlaySettings(overlayId, dialogSettings);
    const factory = injector.get(ComponentFactoryResolver);
    if (component instanceof ElementRef) {
      this._overlayService.attach(component, dialogSettings);
    } else {
      this._overlayService.attach(component, dialogSettings, {
        injector,
        componentFactoryResolver: factory
      });
    }

    setTimeout(() => {
      // Delay showing to give subscribers a chance to hook into the observables
      this._overlayService.show(overlayId);
    }, 10);
  }
}

// dialog-ref.ts
import { IgxOverlayService } from '@infragistics/igniteui-angular';
import { Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { DialogCancelableEventArgs } from './models/dialog-cancelable-event-args';
import { DialogEventArgs } from './models/dialog-event-args';
import { DialogInfo } from './models/dialog-info';

export class TraxDialogRef<TComponent, TValue = any> {
  /**
   * Instance of component opened into the dialog. Will be
   * null when the dialog is opened using a `TemplateRef`.
   */
  readonly componentInstance: TComponent | null = null;

  contentAppeneded$: Observable<DialogEventArgs>;
  closing$: Observable<DialogCancelableEventArgs>;
  closed$: Observable<TValue | undefined>;
  opened$: Observable<DialogEventArgs>;
  opening$: Observable<DialogCancelableEventArgs>;

  private _result: TValue | any;

  constructor(
    readonly overlayId: string,
    private _overlayService: IgxOverlayService
  ) {
    this.contentAppeneded$ = this._overlayService.contentAppended.pipe(
      filter(event => event.id === this.overlayId)
    );
    this.opened$ = this._overlayService.opened.pipe(filter(event => event.id === this.overlayId));
    this.opening$ = this._overlayService.opening.pipe(filter(event => event.id === this.overlayId));
    this.closing$ = this._overlayService.closing.pipe(filter(event => event.id === this.overlayId));
    this.closed$ = this._overlayService.closed.pipe(
      filter(event => event.id === this.overlayId),
      map(() => this._result)
    );
  }

  get dialogInfo(): DialogInfo {
    return this._overlayService.getOverlayById(this.overlayId) as any as DialogInfo;
  }

  close(): void;
  close(result: TValue): void;
  close(result?: TValue): void {
    this._result = result;
    const dialogInfo = this.dialogInfo;
    this._overlayService.detach(this.overlayId);
    // IgniteUi is not triggering the closed event when the dialog is closed for some reason.
    this._overlayService.closed.emit({
      id: this.overlayId,
      componentRef: dialogInfo?.componentRef
    });
  }
}

Expected result

OverlayService should support providing a custom injector like before, perhaps via OverlaySettings.

Attachments

Attach a sample if available, and screenshots, if applicable.

wnvko commented 2 weeks ago

@mrentmeister-tt From the code excerpts provided I can see you have a TraxDialogService. It is look like you can show any component via this service and you provide the DialogSettings before you show the component in the overlay. The question here is do you dynamically change the DialogSettings each time the component is shown on the overlay service?

If you can provide more information about how exactly you are showing the components via TraxDialogService we will investigate how you can achieve what you need.

mrentmeister-tt commented 2 weeks ago

@wnvko No, the dialog settings are not changed after a component is opened via the dialog service. It's intended to be a short-lived modal service where I can return a result to the component that opens the modal. I have updated the source code in the description. Below is an example of how it's used:

checkForUnsavedChangesAsync(key: string): Promise<DialogResults> {
  return new Promise<DialogResults>(resolve => {
    if (!this.isDirty()) {
      return resolve(DialogResults.None);
    }

    const dialogRef = this._dialogService.open(AlertDialogComponent, {
      dimensions: { minWidth: '32rem', maxWidth: '70rem' },
      data: {
        buttons: AlertDialogButtons.SaveDiscardCancel,
        title: $localize`:@@saveChanges:Save changes`,
        description: $localize`:@@discardUnsavedChanges:There are unsaved changes. You can Discard your changes, or close this message to continue editing.`,
        type: AlertDialogType.Confirm
      }
    });

    dialogRef.closed$.pipe(first()).subscribe({
      next: (result: DialogResults = DialogResults.None) => {
        switch (result) {
          case DialogResults.Cancel:
            break;
          case DialogResults.None:
            // They clicked outside of the modal, cancel the action
            result = DialogResults.Cancel;
            break;
          case DialogResults.Save:
            // They can only get here if the form is valid
            this.manager.onSave();
            break;
          default:
            this.manager.onDiscard();
            break;
        }
        resolve(result);
      }
    });
  });
}

Another example would be displaying a table in a modal, where the user selects a row, and then the row is returned as a result.