Skip to content

Commit

Permalink
feat(modal): handle Observable values for modal/sidepanel labels
Browse files Browse the repository at this point in the history
  • Loading branch information
GuillaumeNury committed Sep 14, 2022
1 parent 6b79bec commit e13fbd7
Show file tree
Hide file tree
Showing 12 changed files with 266 additions and 75 deletions.
8 changes: 4 additions & 4 deletions packages/ng/modal/src/lib/modal-panel.component.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<div class="lu-modal-panel-inner" cdkTrapFocus="true" cdkTrapFocusAutoCapture="true" role="dialog" aria-modal="true">
<div class="lu-modal-header" tabindex="-1" cdkFocusInitial>
<h3 class="lu-modal-header-title">{{ title }}</h3>
<h3 class="lu-modal-header-title">{{ title$ | async }}</h3>
<button type="button" class="lu-modal-header-close" (click)="dismiss()" [luTooltip]="closeLabel">
<span aria-hidden="true" class="lucca-icon icon-cross"></span>
<span class="u-mask">{{ closeLabel }}</span>
</button>
</div>
<div class="lu-modal-content">
<ng-container cdkPortalOutlet #outlet></ng-container>
<ng-container #container></ng-container>
</div>
<div class="lu-modal-footer">
<button
Expand All @@ -19,9 +19,9 @@ <h3 class="lu-modal-header-title">{{ title }}</h3>
[ngClass]="submitClass$ | async"
(click)="submit()"
>
{{ submitLabel }}
{{ submitLabel$ | async }}
<label class="button-counter" *ngIf="hasSubmitCounter">{{ submitCounter }}</label>
</button>
<button type="button" class="button mod-text" (click)="dismiss()">{{ cancelLabel }}</button>
<button type="button" class="button mod-text" (click)="dismiss()">{{ cancelLabel$ | async }}</button>
</div>
</div>
72 changes: 34 additions & 38 deletions packages/ng/modal/src/lib/modal-panel.component.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,25 @@
import { CdkPortalOutlet, Portal, PortalOutlet } from '@angular/cdk/portal';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentRef, Directive, HostBinding, Inject, OnDestroy, ViewChild } from '@angular/core';
import { Observable, Subject, Subscription, timer } from 'rxjs';
import { delay, tap } from 'rxjs/operators';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Directive, DoCheck, HostBinding, Inject, Injector, OnDestroy, Type, ViewChild, ViewContainerRef } from '@angular/core';
import { Observable, of, ReplaySubject, Subject, Subscription, timer } from 'rxjs';
import { delay, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators';
import { ALuModalRef } from './modal-ref.model';
import { LuModalIntl } from './modal.intl';
import { ILuModalContent } from './modal.model';
import { LU_MODAL_TRANSLATIONS } from './modal.token';
import { ILuModalLabel } from './modal.translate';

@Directive()
export abstract class ALuModalPanelComponent<T extends ILuModalContent> implements PortalOutlet, OnDestroy {
@ViewChild('outlet', { read: CdkPortalOutlet, static: true })
protected _outlet: PortalOutlet;
protected _componentInstance: T;
get title() {
return this._componentInstance.title;
}
get submitLabel() {
return this._componentInstance.submitLabel || this.intl.submit;
}
get cancelLabel() {
return this._componentInstance.cancelLabel || this.intl.cancel;
}
get closeLabel() {
return this.intl.close;
}
export abstract class ALuModalPanelComponent<T extends ILuModalContent> implements OnDestroy, DoCheck {
@ViewChild('container', { read: ViewContainerRef, static: true })
protected _containerRef: ViewContainerRef;
protected _componentInstance: ILuModalContent;
protected doCheck$ = new ReplaySubject<void>(1);

protected title$ = this.listenComponentValue(() => this._componentInstance.title);
protected submitLabel$ = this.listenComponentValue(() => this._componentInstance.submitLabel || this.intl.submit);
protected cancelLabel$ = this.listenComponentValue(() => this._componentInstance.cancelLabel || this.intl.cancel);

protected closeLabel = this.intl.close;

get isSubmitDisabled() {
return this._componentInstance.submitDisabled;
}
Expand All @@ -46,25 +41,18 @@ export abstract class ALuModalPanelComponent<T extends ILuModalContent> implemen

private _subs = new Subscription();

constructor(protected _ref: ALuModalRef<LuModalPanelComponent<T>>, protected _cdr: ChangeDetectorRef, @Inject(LU_MODAL_TRANSLATIONS) public intl: ILuModalLabel) {}
attach<U extends T = T>(portal: Portal<U>) {
const ref = this._outlet.attach(portal) as ComponentRef<U>;
constructor(protected _ref: ALuModalRef<T>, protected _cdr: ChangeDetectorRef, @Inject(LU_MODAL_TRANSLATIONS) public intl: ILuModalLabel) {}
ngDoCheck(): void {
this.doCheck$.next();
}
attachInnerComponent(componentType: Type<T>, injector: Injector) {
const ref = this._containerRef.createComponent(componentType, { injector });
this._componentInstance = ref.instance;
return ref;
}
detach() {
this._outlet.detach();
}
dispose() {
return this._outlet.dispose();
}
hasAttached() {
return this._outlet.hasAttached();
}
ngOnDestroy() {
this.doCheck$.complete();
this._subs.unsubscribe();
this.detach();
this.dispose();
}
dismiss() {
this._ref.dismiss();
Expand Down Expand Up @@ -99,6 +87,14 @@ export abstract class ALuModalPanelComponent<T extends ILuModalContent> implemen
this._ref.close(result);
}
}

private listenComponentValue(selector: () => string | Observable<string>): Observable<string> {
return this.doCheck$.pipe(
map(selector),
distinctUntilChanged(),
switchMap((value) => (typeof value === 'string' ? of(value) : value)),
);
}
}

@Component({
Expand All @@ -107,9 +103,9 @@ export abstract class ALuModalPanelComponent<T extends ILuModalContent> implemen
styleUrls: ['./modal-panel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LuModalPanelComponent<T extends ILuModalContent> extends ALuModalPanelComponent<T> {
export class LuModalPanelComponent<T extends ILuModalContent = ILuModalContent> extends ALuModalPanelComponent<T> {
@HostBinding('class.lu-modal-panel') class = true;
constructor(_ref: ALuModalRef<LuModalPanelComponent<T>>, _cdr: ChangeDetectorRef, @Inject(LuModalIntl) intl: ILuModalLabel) {
constructor(_ref: ALuModalRef<T>, _cdr: ChangeDetectorRef, @Inject(LuModalIntl) intl: ILuModalLabel) {
super(_ref, _cdr, intl);
}
}
Expand All @@ -120,9 +116,9 @@ export class LuModalPanelComponent<T extends ILuModalContent> extends ALuModalPa
changeDetection: ChangeDetectionStrategy.Default,
})
// eslint-disable-next-line @angular-eslint/component-class-suffix
export class LuModalPanelComponentDefaultCD<T extends ILuModalContent> extends ALuModalPanelComponent<T> {
export class LuModalPanelComponentDefaultCD<T extends ILuModalContent = ILuModalContent> extends ALuModalPanelComponent<T> {
@HostBinding('class.lu-modal-panel') class = true;
constructor(_ref: ALuModalRef<LuModalPanelComponent<T>>, _cdr: ChangeDetectorRef, @Inject(LuModalIntl) intl: ILuModalLabel) {
constructor(_ref: ALuModalRef<T>, _cdr: ChangeDetectorRef, @Inject(LuModalIntl) intl: ILuModalLabel) {
super(_ref, _cdr, intl);
}
}
8 changes: 3 additions & 5 deletions packages/ng/modal/src/lib/modal-ref.factory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ComponentType, Overlay } from '@angular/cdk/overlay';
import { ComponentPortal, PortalOutlet } from '@angular/cdk/portal';
import { ComponentPortal } from '@angular/cdk/portal';
import { ChangeDetectionStrategy, ComponentRef, Injectable, Injector } from '@angular/core';
import { ALuPopupRef, ILuPopupRefFactory } from '@lucca-front/ng/popup';
import { ILuModalConfig } from './modal-config.model';
Expand All @@ -10,7 +10,6 @@ import { LU_MODAL_DATA } from './modal.token';

class LuModalRef<T extends ILuModalContent = ILuModalContent, D = unknown, R = unknown> extends ALuPopupRef<T, D, R> implements ILuModalRef<T, D, R> {
protected _containerRef: ComponentRef<ALuModalPanelComponent<T>>;
protected _containerOutlet: PortalOutlet;
constructor(protected override _overlay: Overlay, protected override _injector: Injector, protected override _component: ComponentType<T>, protected override _config: ILuModalConfig) {
super(_overlay, _injector, _component, _config);
}
Expand All @@ -29,9 +28,8 @@ class LuModalRef<T extends ILuModalContent = ILuModalContent, D = unknown, R = u
const containerPortal = new ComponentPortal<ALuModalPanelComponent<T>>(LuModalPanelComponentDefaultCD, undefined, injector);
this._containerRef = this._overlayRef.attach<ALuModalPanelComponent<T>>(containerPortal);
}
this._containerOutlet = this._containerRef.instance;
const portal = new ComponentPortal(this._component, undefined, injector);
this._componentRef = this._containerOutlet.attach(portal) as ComponentRef<T>;
const panel = this._containerRef.instance;
this._componentRef = panel.attachInnerComponent(this._component, injector);
}
protected override _closePopup() {
this._componentRef.destroy();
Expand Down
6 changes: 3 additions & 3 deletions packages/ng/modal/src/lib/modal.model.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Observable } from 'rxjs';

export interface ILuModalContent<T = unknown> {
title: string;
title: string | Observable<string>;
submitAction?: () => T | Observable<T>;
submitLabel?: string;
submitLabel?: string | Observable<string>;
submitPalette?: string;
submitDisabled?: boolean;
submitCounter?: number;
cancelLabel?: string;
cancelLabel?: string | Observable<string>;
}
3 changes: 1 addition & 2 deletions packages/ng/modal/src/lib/modal.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { A11yModule } from '@angular/cdk/a11y';
import { OverlayModule } from '@angular/cdk/overlay';
import { PortalModule } from '@angular/cdk/portal';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { LuTooltipModule } from '@lucca-front/ng/tooltip';
Expand All @@ -13,7 +12,7 @@ import { LU_MODAL_CONFIG, LU_MODAL_REF_FACTORY, LU_MODAL_TRANSLATIONS } from './
import { luModalTranslations } from './modal.translate';

@NgModule({
imports: [OverlayModule, PortalModule, CommonModule, A11yModule, LuTooltipModule],
imports: [OverlayModule, CommonModule, A11yModule, LuTooltipModule],
declarations: [LuModalPanelComponent, LuModalPanelComponentDefaultCD],
exports: [LuModalPanelComponent, LuModalPanelComponentDefaultCD],
providers: [
Expand Down
70 changes: 70 additions & 0 deletions packages/ng/modal/src/lib/modal.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { Component, Type } from '@angular/core';
import { By } from '@angular/platform-browser';
import { LuModalModule } from 'dist/ng/modal';
import { BehaviorSubject } from 'rxjs';
import { ILuModalContent } from './modal.model';
import { LuModal } from './modal.service';

@Component({
selector: 'lu-test-modal',
standalone: true,
template: `Content`
})
export class ModalContentComponent implements ILuModalContent {
title = new BehaviorSubject('OriginalTitle');
}

@Component({
selector: 'lu-test-opener',
template: `Content`
})
export class ModalOpenerComponent {
constructor(
public modal: LuModal,
) { }
}

describe('LuModal', () => {
let opener: ModalOpenerComponent;
let fixture: ComponentFixture<ModalOpenerComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ModalOpenerComponent],
imports: [LuModalModule],
}).compileComponents();

fixture = TestBed.createComponent(ModalOpenerComponent);
opener = fixture.componentInstance;
fixture.detectChanges();
});

function openModal<T>(type: Type<T>): T {
opener.modal.open(type);
return fixture.debugElement.parent.query(By.directive(type)).componentInstance;
}

function getModalTitle(): string {
return fixture.debugElement.parent.query(By.css('.lu-modal-header-title')).nativeElement.innerHTML;
}

it('should update modal title when ILuModalContent\'s title emits a new value', () => {
// Arrange
const contentComponent = openModal(ModalContentComponent);
fixture.detectChanges();

// Act
const beforeTitle = getModalTitle();

contentComponent.title.next('UpdatedTitle');
fixture.detectChanges();

const afterTitle = getModalTitle();

// Assert
expect(beforeTitle).toBe('OriginalTitle')
expect(afterTitle).toBe('UpdatedTitle')
});
});
8 changes: 4 additions & 4 deletions packages/ng/sidepanel/src/lib/sidepanel-panel.component.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<div class="lu-sidepanel-panel-inner" cdkTrapFocus="true" cdkTrapFocusAutoCapture="true" role="dialog" aria-modal="true">
<div class="lu-modal-header" tabindex="-1" cdkFocusInitial>
<h3 class="lu-modal-header-title">{{ title }}</h3>
<h3 class="lu-modal-header-title">{{ title$ | async }}</h3>
<button type="button" class="lu-modal-header-close" (click)="dismiss()" [luTooltip]="closeLabel">
<span aria-hidden="true" class="lucca-icon icon-cross"></span>
<span class="u-mask">{{ closeLabel }}</span>
</button>
</div>
<div class="lu-modal-content">
<ng-container cdkPortalOutlet #outlet></ng-container>
<ng-container #container></ng-container>
</div>
<div class="lu-modal-footer">
<button
Expand All @@ -19,9 +19,9 @@ <h3 class="lu-modal-header-title">{{ title }}</h3>
[ngClass]="submitClass$ | async"
(click)="submit()"
>
{{ submitLabel }}
{{ submitLabel$ | async }}
<label class="button-counter" *ngIf="hasSubmitCounter">{{ submitCounter }}</label>
</button>
<button type="button" class="button mod-text" (click)="dismiss()">{{ cancelLabel }}</button>
<button type="button" class="button mod-text" (click)="dismiss()">{{ cancelLabel$ | async }}</button>
</div>
</div>
4 changes: 2 additions & 2 deletions packages/ng/sidepanel/src/lib/sidepanel-panel.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { ILuSidepanelLabel } from './sidepanel.translate';
export class LuSidepanelPanelComponent<T extends ILuSidepanelContent = ILuSidepanelContent> extends ALuModalPanelComponent<T> {
@HostBinding('class.lu-sidepanel-panel') public class = true;

constructor(_ref: ALuSidepanelRef<LuSidepanelPanelComponent<T>>, _cdr: ChangeDetectorRef, @Inject(LuSidepanelIntl) intl: ILuSidepanelLabel) {
constructor(_ref: ALuSidepanelRef<T>, _cdr: ChangeDetectorRef, @Inject(LuSidepanelIntl) intl: ILuSidepanelLabel) {
super(_ref, _cdr, intl);
}
}
Expand All @@ -29,7 +29,7 @@ export class LuSidepanelPanelComponent<T extends ILuSidepanelContent = ILuSidepa
// eslint-disable-next-line @angular-eslint/component-class-suffix
export class LuSidepanelPanelComponentDefaultCD<T extends ILuSidepanelContent = ILuSidepanelContent> extends ALuModalPanelComponent<T> {
@HostBinding('class.lu-sidepanel-panel') public class = true;
constructor(_ref: ALuSidepanelRef<LuSidepanelPanelComponent<T>>, _cdr: ChangeDetectorRef, @Inject(LuSidepanelIntl) intl: ILuSidepanelLabel) {
constructor(_ref: ALuSidepanelRef<T>, _cdr: ChangeDetectorRef, @Inject(LuSidepanelIntl) intl: ILuSidepanelLabel) {
super(_ref, _cdr, intl);
}
}
18 changes: 8 additions & 10 deletions packages/ng/sidepanel/src/lib/sidepanel-ref.factory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ComponentType, Overlay } from '@angular/cdk/overlay';
import { ComponentPortal, PortalOutlet } from '@angular/cdk/portal';
import { ComponentPortal } from '@angular/cdk/portal';
import { ChangeDetectionStrategy, ComponentRef, Injectable, Injector } from '@angular/core';
import { ILuPopupRefFactory } from '@lucca-front/ng/popup';
import { ILuSidepanelConfig } from './sidepanel-config.model';
Expand All @@ -9,8 +9,7 @@ import { ILuSidepanelContent } from './sidepanel.model';
import { LU_SIDEPANEL_DATA } from './sidepanel.token';

class LuSidepanelRef<T extends ILuSidepanelContent<unknown> = ILuSidepanelContent<unknown>, D = unknown, R = unknown> extends ALuSidepanelRef<T, D, R> implements ILuSidepanelRef<T, D, R> {
protected _containerRef: ComponentRef<LuSidepanelPanelComponent>;
protected _containerOutlet: PortalOutlet;
protected _containerRef: ComponentRef<LuSidepanelPanelComponent<T>>;
constructor(protected override _overlay: Overlay, protected override _injector: Injector, protected override _component: ComponentType<T>, protected override _config: ILuSidepanelConfig) {
super(_overlay, _injector, _component, _config);
}
Expand All @@ -23,15 +22,14 @@ class LuSidepanelRef<T extends ILuSidepanelContent<unknown> = ILuSidepanelConten
parent: this._injector,
});
if (this._config.changeDetection === ChangeDetectionStrategy.OnPush) {
const containerPortal = new ComponentPortal(LuSidepanelPanelComponent, undefined, injector);
this._containerRef = this._overlayRef.attach<LuSidepanelPanelComponent>(containerPortal);
const containerPortal = new ComponentPortal<LuSidepanelPanelComponent<T>>(LuSidepanelPanelComponent, undefined, injector);
this._containerRef = this._overlayRef.attach(containerPortal);
} else {
const containerPortal = new ComponentPortal(LuSidepanelPanelComponentDefaultCD, undefined, injector);
this._containerRef = this._overlayRef.attach<LuSidepanelPanelComponent>(containerPortal);
const containerPortal = new ComponentPortal<LuSidepanelPanelComponentDefaultCD<T>>(LuSidepanelPanelComponentDefaultCD, undefined, injector);
this._containerRef = this._overlayRef.attach(containerPortal);
}
this._containerOutlet = this._containerRef.instance as unknown as PortalOutlet;
const portal = new ComponentPortal(this._component, undefined, injector);
this._componentRef = this._containerOutlet.attach(portal) as ComponentRef<T>;
const panel = this._containerRef.instance;
this._componentRef = panel.attachInnerComponent(this._component, injector);
}
protected override _closePopup() {
this._componentRef.destroy();
Expand Down
3 changes: 1 addition & 2 deletions packages/ng/sidepanel/src/lib/sidepanel.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { A11yModule } from '@angular/cdk/a11y';
import { OverlayModule } from '@angular/cdk/overlay';
import { PortalModule } from '@angular/cdk/portal';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { LuTooltipModule } from '@lucca-front/ng/tooltip';
Expand All @@ -13,7 +12,7 @@ import { LU_SIDEPANEL_CONFIG, LU_SIDEPANEL_REF_FACTORY, LU_SIDEPANEL_TRANSLATION
import { luSidepanelTranslations } from './sidepanel.translate';

@NgModule({
imports: [OverlayModule, PortalModule, CommonModule, A11yModule, LuTooltipModule],
imports: [OverlayModule, CommonModule, A11yModule, LuTooltipModule],
declarations: [LuSidepanelPanelComponent, LuSidepanelPanelComponentDefaultCD],
exports: [LuSidepanelPanelComponent, LuSidepanelPanelComponentDefaultCD],
providers: [
Expand Down
Loading

0 comments on commit e13fbd7

Please sign in to comment.