Skip to content

Commit

Permalink
feat: dialog service can accept custom buttons
Browse files Browse the repository at this point in the history
  • Loading branch information
markuczy committed Jul 22, 2024
1 parent d35cf25 commit cdd3640
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,54 @@
<ng-container #container></ng-container>
<ng-content></ng-content>
</div>
<div class="w-full flex justify-content-end column-gap-2">
<button
id="buttonDialogSecondaryButton"
pButton
*ngIf="dialogData.config.secondaryButtonIncluded"
[icon]="dialogData.config.secondaryButtonDetails!.icon !== undefined ? dialogData.config.secondaryButtonDetails!.icon : ''"
(click)="secondaryButtonAction()"
[label]="dialogData.config.secondaryButtonDetails!.key | translate:dialogData.config.secondaryButtonDetails?.parameters"
[disabled]="secondaryButtonDisabled$ | async"
[pTooltip]="dialogData.config.secondaryButtonDetails!.tooltipKey ? (dialogData.config.secondaryButtonDetails!.tooltipKey | translate) : null"
[tooltipPosition]="dialogData.config.secondaryButtonDetails?.tooltipPosition ?? ''"
></button>
<button
id="buttonDialogPrimaryButton"
pButton
[icon]="dialogData.config.primaryButtonDetails!.icon !== undefined ? dialogData.config.primaryButtonDetails!.icon : ''"
(click)="primaryButtonAction()"
[label]="dialogData.config.primaryButtonDetails!.key | translate:dialogData.config.primaryButtonDetails?.parameters"
[disabled]="primaryButtonDisabled$ | async"
[pTooltip]="dialogData.config.primaryButtonDetails!.tooltipKey ? (dialogData.config.primaryButtonDetails!.tooltipKey | translate) : null"
[tooltipPosition]="dialogData.config.primaryButtonDetails?.tooltipPosition ?? ''"
></button>
<div class="w-full flex flex-row-reverse justify-content-between column-gap-2">
<div class="flex flex-row-reverse flex-wrap column-gap-2 row-gap-2">
<div>
<button
id="buttonDialogPrimaryButton"
pButton
[icon]="dialogData.config.primaryButtonDetails!.icon !== undefined ? dialogData.config.primaryButtonDetails!.icon : ''"
(click)="primaryButtonAction()"
[label]="dialogData.config.primaryButtonDetails!.key | translate:dialogData.config.primaryButtonDetails?.parameters"
[disabled]="primaryButtonDisabled$ | async"
[pTooltip]="dialogData.config.primaryButtonDetails!.tooltipKey ? (dialogData.config.primaryButtonDetails!.tooltipKey | translate) : null"
[tooltipPosition]="dialogData.config.primaryButtonDetails?.tooltipPosition ?? ''"
></button>
</div>
<div>
<button
id="buttonDialogSecondaryButton"
pButton
*ngIf="dialogData.config.secondaryButtonIncluded"
[icon]="dialogData.config.secondaryButtonDetails!.icon !== undefined ? dialogData.config.secondaryButtonDetails!.icon : ''"
(click)="secondaryButtonAction()"
[label]="dialogData.config.secondaryButtonDetails!.key | translate:dialogData.config.secondaryButtonDetails?.parameters"
[disabled]="secondaryButtonDisabled$ | async"
[pTooltip]="dialogData.config.secondaryButtonDetails!.tooltipKey ? (dialogData.config.secondaryButtonDetails!.tooltipKey | translate) : null"
[tooltipPosition]="dialogData.config.secondaryButtonDetails?.tooltipPosition ?? ''"
></button>
</div>
<div *ngFor="let button of rightCustomButtons">
<ng-container *ngTemplateOutlet="customButton; context: {button: button}"> </ng-container>
</div>
</div>
<div class="flex flex-wrap column-gap-2 row-gap-2">
<div *ngFor="let button of leftCustomButtons">
<ng-container *ngTemplateOutlet="customButton; context: {button: button}"> </ng-container>
</div>
</div>
</div>
</div>

<ng-template #customButton let-button="button">
<button
id="{{button.id}}"
pButton
[icon]="button.icon !== undefined ? button.icon : ''"
(click)="customButtonAction(button)"
[label]="button.key | translate:button.parameters"
[disabled]="resolveCustomButtonDisabled((customButtonsDisabled$ | async) ?? {}, button.id)"
[pTooltip]="button.tooltipKey ? (button.tooltipKey | translate) : null"
[tooltipPosition]="button.tooltipPosition ?? ''"
></button>
</ng-template>
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,24 @@ import {
ViewChild,
ViewContainerRef,
} from '@angular/core'
import { Observable, from, isObservable, map, of, startWith } from 'rxjs'
import { BehaviorSubject, Observable, from, isObservable, map, of, startWith, tap, withLatestFrom } from 'rxjs'
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'

import { ButtonDialogButtonDetails, ButtonDialogConfig, ButtonDialogData } from '../../../model/button-dialog'
import {
ButtonDialogButtonDetails,
ButtonDialogConfig,
ButtonDialogCustomButtonDetails,
ButtonDialogData,
} from '../../../model/button-dialog'
import { DialogMessageContentComponent } from './dialog-message-content/dialog-message-content.component'
import {
DialogButtonClicked,
DialogCustomButtonsDisabled,
DialogPrimaryButtonDisabled,
DialogResult,
DialogSecondaryButtonDisabled,
DialogState,
DialogStateButtonClicked,
} from '../../../services/portal-dialog.service'

@Component({
Expand Down Expand Up @@ -56,6 +63,10 @@ export class ButtonDialogComponent implements OnInit {
componentRef!: ComponentRef<any>
primaryButtonDisabled$: Observable<boolean | undefined> | undefined
secondaryButtonDisabled$: Observable<boolean | undefined> | undefined
customButtonsDisabled$: Observable<Record<string, boolean> | undefined> | undefined
private customButtonsDisabledSubject$: BehaviorSubject<Record<string, boolean>> = new BehaviorSubject({})
leftCustomButtons: ButtonDialogCustomButtonDetails[] = []
rightCustomButtons: ButtonDialogCustomButtonDetails[] = []

constructor(public dynamicDialogConfig: DynamicDialogConfig, public dynamicDialogRef: DynamicDialogRef) {}

Expand All @@ -64,31 +75,19 @@ export class ButtonDialogComponent implements OnInit {
}

primaryButtonAction() {
if (!this.componentRef) {
this.resultEmitter.emit('primary')
return
}

const state: DialogState<unknown> = {
button: 'primary',
result: undefined,
}

this.resolveButtonClick(state)
return this.buttonAction('primary')
}

secondaryButtonAction() {
if (!this.componentRef) {
this.resultEmitter.emit('secondary')
return
}
return this.buttonAction('secondary')
}

const state: DialogState<unknown> = {
button: 'secondary',
result: undefined,
}
customButtonAction(button: ButtonDialogCustomButtonDetails) {
return this.buttonAction(`custom`, button.id)
}

this.resolveButtonClick(state)
resolveCustomButtonDisabled(customButtonsDisabled: Record<string, boolean>, buttonId: string) {
return buttonId in customButtonsDisabled ? customButtonsDisabled[buttonId] : true
}

loadComponent() {
Expand Down Expand Up @@ -120,6 +119,8 @@ export class ButtonDialogComponent implements OnInit {
this.dialogData.componentData = dynamicConfigData.componentData
}

this.setupCustomButtons(dynamicConfigData)

const viewContainerRef = this.dialogHost
viewContainerRef.clear()

Expand All @@ -138,6 +139,23 @@ export class ButtonDialogComponent implements OnInit {
map((isEnabled: boolean) => !isEnabled)
)
}
if (this.isDialogCustomButtonDisabledImplemented(componentRef.instance)) {
const initCustomButtons: Record<string, boolean> = {}
this.rightCustomButtons.concat(this.leftCustomButtons).map((button) => {
initCustomButtons[button.id] = true
})
this.customButtonsDisabledSubject$.next(initCustomButtons)
this.customButtonsDisabled$ = componentRef.instance.customButtonEnabled.pipe(
withLatestFrom(this.customButtonsDisabledSubject$.asObservable()),
tap(([buttonEnabled, customButtonsDisabled]) => {
if (customButtonsDisabled[buttonEnabled.id] !== !buttonEnabled.enabled) {
customButtonsDisabled[buttonEnabled.id] = !buttonEnabled.enabled
this.customButtonsDisabledSubject$.next(customButtonsDisabled)
}
}),
map(([_, customButtonsDisabled]) => customButtonsDisabled)
)
}

//populate container
Object.keys(this.dialogData.componentData).forEach((k) => {
Expand All @@ -161,6 +179,27 @@ export class ButtonDialogComponent implements OnInit {
this.dialogData.config.secondaryButtonDetails = this.config.secondaryButtonDetails
}
}
this.setupCustomButtons(this.dialogData)
}

private buttonAction(resultButtonClickedName: DialogStateButtonClicked, buttonId?: string) {
if (!this.componentRef) {
this.resultEmitter.emit(resultButtonClickedName)
return
}

const state: DialogState<unknown> = {
button: resultButtonClickedName,
result: undefined,
id: buttonId,
}

this.resolveButtonClick(state)
}

private setupCustomButtons(dialogData: ButtonDialogData) {
this.leftCustomButtons = dialogData.config.customButtons?.filter((button) => button.alignment === 'left') ?? []
this.rightCustomButtons = dialogData.config.customButtons?.filter((button) => button.alignment === 'right') ?? []
}

private resolveButtonClick(state: DialogState<unknown>) {
Expand Down Expand Up @@ -221,4 +260,8 @@ export class ButtonDialogComponent implements OnInit {
private isDialogSecondaryButtonDisabledImplemented(component: any): component is DialogSecondaryButtonDisabled {
return 'secondaryButtonEnabled' in component
}

private isDialogCustomButtonDisabledImplemented(component: any): component is DialogCustomButtonsDisabled {
return 'customButtonEnabled' in component
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,16 @@ export interface ButtonDialogButtonDetails {
tooltipPosition?: 'right' | 'left' | 'top' | 'bottom' | string | undefined
}

export interface ButtonDialogCustomButtonDetails extends ButtonDialogButtonDetails {
id: string
alignment: 'right' | 'left'
}

export interface ButtonDialogConfig {
primaryButtonDetails?: ButtonDialogButtonDetails
secondaryButtonIncluded?: boolean
secondaryButtonDetails?: ButtonDialogButtonDetails
customButtons?: ButtonDialogCustomButtonDetails[]
}

export interface ButtonDialogData {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Observable, mergeMap } from 'rxjs'
import { DialogService, DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'

import { ButtonDialogComponent } from '../core/components/button-dialog/button-dialog.component'
import { ButtonDialogButtonDetails, ButtonDialogData } from '../model/button-dialog'
import { ButtonDialogButtonDetails, ButtonDialogCustomButtonDetails, ButtonDialogData } from '../model/button-dialog'
import { DialogMessageContentComponent } from '../core/components/button-dialog/dialog-message-content/dialog-message-content.component'
import { PrimeIcon } from '@onecx/angular-accelerator'

Expand Down Expand Up @@ -118,6 +118,17 @@ export interface DialogPrimaryButtonDisabled {
export interface DialogSecondaryButtonDisabled {
secondaryButtonEnabled: EventEmitter<boolean>
}

/**
* Implement via component class to be displayed by {@link PortalDialogService.openDialog}
*
* Use to control the state of custom buttons (disabled or enabled). Whenever your component wants to disable/enable any custom button it should emit an object indicating which button should be disabled/enabled. This object should contain id property (string) related to previously defined button and enabled property (boolean) equal to whether custom button should be enabled.
*
* If you implement this interface then all custom buttons will be disabled until the emitter emits true
*/
export interface DialogCustomButtonsDisabled {
customButtonEnabled: EventEmitter<{ id: string; enabled: boolean }>
}
/**
* Implement via component class to be displayed by {@link PortalDialogService.openDialog}
*
Expand Down Expand Up @@ -200,16 +211,20 @@ type Component<T extends unknown> = unknown extends T
inputs?: Record<string, unknown>
}

export type DialogStateButtonClicked = 'primary' | 'secondary' | 'custom'

/**
* Object containing information about clicked button ('primary' or 'secondary') and displayed component state captured on button click (only if component implements {@link DialogResult} interface)
*/
export type DialogState<T> = {
button: 'primary' | 'secondary'
button: DialogStateButtonClicked
result: T | undefined
id?: string
}

export type PortalDialogConfig = {
showXButton?: boolean
customButtons?: ButtonDialogCustomButtonDetails[]
ariaLabelledBy?: string
width?: string
height?: string
Expand Down Expand Up @@ -422,9 +437,12 @@ export class PortalDialogService {
const dynamicDialogDataConfig: ButtonDialogData = {
component: componentToRender.type as Type<any>,
config: {
primaryButtonDetails: this.getButtonDetails(primaryButtonTranslationKeyOrDetails),
primaryButtonDetails: this.buttonDetailsOrTranslationKey(primaryButtonTranslationKeyOrDetails),
secondaryButtonIncluded: secondaryButtonTranslationKeyOrDetails !== undefined,
secondaryButtonDetails: this.getButtonDetails(secondaryButtonTranslationKeyOrDetails),
secondaryButtonDetails: this.buttonDetailsOrTranslationKey(secondaryButtonTranslationKeyOrDetails),
customButtons: dialogOptions.customButtons?.map(
(button) => this.buttonDetailsOrTranslationKey(button) as ButtonDialogCustomButtonDetails
),
},
componentData: componentToRender.inputs,
}
Expand All @@ -447,9 +465,13 @@ export class PortalDialogService {
return title
}

private getButtonDetails(
buttonTranslationKeyOrDetails: TranslationKey | ButtonDialogButtonDetails | undefined
): ButtonDialogButtonDetails | undefined {
private buttonDetailsOrTranslationKey(
buttonTranslationKeyOrDetails:
| TranslationKey
| ButtonDialogButtonDetails
| ButtonDialogCustomButtonDetails
| undefined
): ButtonDialogButtonDetails | ButtonDialogCustomButtonDetails | undefined {
if (buttonTranslationKeyOrDetails === undefined) {
return undefined
}
Expand Down

0 comments on commit cdd3640

Please sign in to comment.