src/components/sq-modal/sq-modal.component.ts
Represents a modal component with customizable options and event handling.
Look the link about the component in original framework and the appearance
See https://css.squidit.com.br/components/modal
Example :<sq-modal [open]="isModalOpen" (modalClose)="onModalClose()">
<ng-template #headerModal>
<h2>Title</h2>
</ng-template>
<div>
<!-- Your content here -->
</div>
<ng-template #footerModal>
Footer
</ng-template>
</sq-modal>
<button (click)='isModalOpen = true'>Open Modal</button>
OnChanges
OnDestroy
| selector | sq-modal |
| standalone | true |
| imports |
NgClass
NgStyle
NgTemplateOutlet
SqClickOutsideDirective
SqDataTestDirective
|
| styleUrls | ./sq-modal.component.scss |
| templateUrl | ./sq-modal.component.html |
Properties |
|
Methods |
Inputs |
Outputs |
constructor(documentImported: Document, router: Router, getWindow: GetWindow)
|
||||||||||||||||
|
Creates an instance of
Parameters :
|
| backdrop | |
Type : string
|
|
Default value : 'static'
|
|
|
Determines whether clicking outside the modal closes it. Options: 'static' (no close), 'true' (close). |
|
| backdropClass | |
Type : string
|
|
|
Additional CSS classes to apply to the modal backdrop element. |
|
| bodyBackgroundColor | |
Type : string
|
|
Default value : ''
|
|
|
Determines the body background color. |
|
| bodyPadding | |
Type : string
|
|
Default value : '0 1rem'
|
|
|
Determines the body padding. |
|
| buttonClose | |
Type : boolean
|
|
Default value : true
|
|
|
Determines whether to display the close button. |
|
| footerBackgroundColor | |
Type : string
|
|
Default value : ''
|
|
|
Determines the footer background color. |
|
| footerPadding | |
Type : string
|
|
Default value : ''
|
|
|
Determines the footer padding. |
|
| headerBackgroundColor | |
Type : string
|
|
Default value : ''
|
|
|
Determines the header background color. |
|
| headerPadding | |
Type : string
|
|
Default value : ''
|
|
|
Determines the header padding. |
|
| id | |
Type : string
|
|
Default value : `modal-random-id-${(1 + Date.now() + Math.random()).toString().replace('.', '')}`
|
|
|
A unique identifier for the modal component. |
|
| modalClass | |
Type : string
|
|
|
Additional CSS classes to apply to the modal element. |
|
| modalDataTest | |
Type : string
|
|
Default value : 'sq-modal-element'
|
|
|
The data-test attribute value for the modal element. |
|
| modalSize | |
Type : "sm" | "md" | "lg" | "xl" | string
|
|
Default value : 'md'
|
|
|
The size of the modal, which can be 'sm' (small), 'md' (medium), 'lg' (large), or 'xl' (extra large). |
|
| open | |
Type : boolean
|
|
|
Indicates whether the modal should be open or closed. |
|
| leftPress | |
Type : EventEmitter<void>
|
|
|
Event emitted when the left arrow key is pressed while the modal is open. |
|
| modalClose | |
Type : EventEmitter<void>
|
|
|
Event emitted when the modal is closed. |
|
| rightPress | |
Type : EventEmitter<void>
|
|
|
Event emitted when the right arrow key is pressed while the modal is open. |
|
| events | ||||||||
events(key: string)
|
||||||||
|
Handles specific keyboard events.
Parameters :
Returns :
void
|
| Async ngOnChanges | ||||||||
ngOnChanges(changes: SimpleChanges)
|
||||||||
|
Lifecycle hook that detects changes to the 'open' input property and handles modal behavior accordingly.
Parameters :
Returns :
any
|
| ngOnDestroy |
ngOnDestroy()
|
|
Performs actions before the component is destroyed.
Returns :
void
|
| observeRouter |
observeRouter()
|
|
Function that init the routerObservable.
Returns :
void
|
| onKeydown | ||||||||
onKeydown(event: KeyboardEvent)
|
||||||||
|
Handles keyboard events for the modal component.
Parameters :
Returns :
void
|
| removeModalFromBody |
removeModalFromBody()
|
|
Removes the modal element from document body.
Returns :
void
|
| document |
Type : Document
|
|
Reference to the Document object for interacting with the DOM. |
| Public documentImported |
Type : Document
|
Decorators :
@Inject(DOCUMENT)
|
|
- The injected Document object for DOM manipulation.
|
| Optional footerTemplate |
Type : TemplateRef<ElementRef> | null
|
Default value : null
|
Decorators :
@ContentChild('footerModal')
|
|
Reference to the footer template provided in the component's content. |
| Public getWindow |
Type : GetWindow
|
|
- The GetWindow service for safely accessing the window object.
|
| hasHeader |
Default value : false
|
|
Indicates whether the modal has a header template. |
| Optional headerTemplate |
Type : TemplateRef<ElementRef> | null
|
Default value : null
|
Decorators :
@ContentChild('headerModal')
|
|
Reference to the header template provided in the component's content. |
| localized |
Type : URL
|
|
Indicates the origin path from modal. |
| modal |
Type : ElementRef | null
|
Default value : null
|
Decorators :
@ViewChild('modal')
|
|
Reference to the modal element in the component's template. |
| modalNumber |
Type : number
|
Default value : 0
|
|
The number of open modals in the document. |
| modals |
Type : HTMLCollectionOf<Element> | undefined
|
|
HTML collection of modal elements in the document. |
| Public router |
Type : Router
|
|
- The Router service for programmatic navigation.
|
| routerObservable |
Type : Subscription
|
|
A subscription to the router change url. |
| scrollY |
Default value : this.getWindow?.window()?.scrollY
|
|
Indicates the scroll position of the window. |
import { SqDataTestDirective } from './../../directives/sq-data-test/sq-data-test.directive';
import { DOCUMENT, NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
import {
Component,
ContentChild,
ElementRef,
EventEmitter,
Inject,
Input,
OnChanges,
OnDestroy,
Output,
SimpleChanges,
TemplateRef,
ViewChild,
} from '@angular/core';
import { sleep } from '../../helpers/sleep.helper';
import { NavigationStart, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { GetWindow } from '../../helpers/window.helper';
import { SqClickOutsideDirective } from '../../directives/sq-click-outside/sq-click-outside.directive';
/**
* Represents a modal component with customizable options and event handling.
*
* Look the link about the component in original framework and the appearance
*
* @see {@link https://css.squidit.com.br/components/modal}
*
* @example
* <sq-modal [open]="isModalOpen" (modalClose)="onModalClose()">
* <ng-template #headerModal>
* <h2>Title</h2>
* </ng-template>
* <div>
* <!-- Your content here -->
* </div>
* <ng-template #footerModal>
* Footer
* </ng-template>
* </sq-modal>
* <button (click)='isModalOpen = true'>Open Modal</button>
*
* @implements {OnChanges}
* @implements {OnDestroy}
*/
@Component({
selector: 'sq-modal',
templateUrl: './sq-modal.component.html',
styleUrls: ['./sq-modal.component.scss'],
standalone: true,
imports: [NgClass, NgStyle, NgTemplateOutlet, SqClickOutsideDirective, SqDataTestDirective],
})
export class SqModalComponent implements OnChanges, OnDestroy {
/**
* A unique identifier for the modal component.
*/
@Input() id = `modal-random-id-${(1 + Date.now() + Math.random()).toString().replace('.', '')}`;
/**
* Indicates whether the modal should be open or closed.
*/
@Input() open?: boolean;
/**
* The size of the modal, which can be 'sm' (small), 'md' (medium), 'lg' (large), or 'xl' (extra large).
*/
@Input() modalSize: 'sm' | 'md' | 'lg' | 'xl' | '' = 'md';
/**
* Additional CSS classes to apply to the modal element.
*/
@Input() modalClass?: string;
/**
* Additional CSS classes to apply to the modal backdrop element.
*/
@Input() backdropClass?: string;
/**
* Determines whether clicking outside the modal closes it. Options: 'static' (no close), 'true' (close).
*/
@Input() backdrop = 'static';
/**
* Determines whether to display the close button.
*/
@Input() buttonClose = true;
/**
* Determines the header padding.
*/
@Input() headerPadding = '';
/**
* Determines the body padding.
*/
@Input() bodyPadding = '0 1rem';
/**
* Determines the footer padding.
*/
@Input() footerPadding = '';
/**
* Determines the header background color.
*/
@Input() headerBackgroundColor = '';
/**
* Determines the body background color.
*/
@Input() bodyBackgroundColor = '';
/**
* Determines the footer background color.
*/
@Input() footerBackgroundColor = '';
/**
* The data-test attribute value for the modal element.
*
* @default 'sq-modal-element'
*/
@Input() modalDataTest = 'sq-modal-element';
/**
* Event emitted when the modal is closed.
*/
@Output() modalClose: EventEmitter<void> = new EventEmitter();
/**
* Event emitted when the left arrow key is pressed while the modal is open.
*/
@Output() leftPress: EventEmitter<void> = new EventEmitter();
/**
* Event emitted when the right arrow key is pressed while the modal is open.
*/
@Output() rightPress: EventEmitter<void> = new EventEmitter();
/**
* Reference to the modal element in the component's template.
*/
@ViewChild('modal') modal: ElementRef | null = null;
/**
* Reference to the header template provided in the component's content.
*/
@ContentChild('headerModal') headerTemplate?: TemplateRef<ElementRef> | null = null;
/**
* Reference to the footer template provided in the component's content.
*/
@ContentChild('footerModal') footerTemplate?: TemplateRef<ElementRef> | null = null;
/**
* HTML collection of modal elements in the document.
*/
modals: HTMLCollectionOf<Element> | undefined;
/**
* The number of open modals in the document.
*/
modalNumber = 0;
/**
* Indicates whether the modal has a header template.
*/
hasHeader = false;
/**
* Reference to the Document object for interacting with the DOM.
*/
document: Document;
/**
* Indicates the origin path from modal.
*
*/
localized: URL;
/**
* A subscription to the router change url.
*/
routerObservable!: Subscription;
/**
* Indicates the scroll position of the window.
*/
scrollY = this.getWindow?.window()?.scrollY;
/**
* Creates an instance of `SqModalComponent`.
* @constructor
* @param {Document} documentImported - The injected Document object for DOM manipulation.
* @param {Router} router - The Router service for programmatic navigation.
* @param {GetWindow} getWindow - The GetWindow service for safely accessing the window object.
*/
constructor(
@Inject(DOCUMENT) public documentImported: Document,
public router: Router,
public getWindow: GetWindow
) {
this.onKeydown = this.onKeydown.bind(this);
this.document = documentImported || document;
this.localized = new URL(this.getWindow.href());
}
/**
* Lifecycle hook that detects changes to the 'open' input property and handles modal behavior accordingly.
*
* @param changes - The changes detected in the component's input properties.
*/
async ngOnChanges(changes: SimpleChanges) {
if (changes.hasOwnProperty('open')) {
const modal = this.modal;
if (modal) {
const body = this.document.getElementsByTagName('body')[0];
const backdrop = this.document.getElementById('modal-backdrop') || this.document.createElement('div');
if (this.open) {
this.scrollY = this.getWindow?.window()?.scrollY;
body.appendChild(modal.nativeElement);
this.observeRouter();
this.hasHeader = !!this.headerTemplate;
body.classList.add('block');
modal.nativeElement.style.display = 'flex';
this.getWindow?.window()?.addEventListener('keydown', this.onKeydown);
this.modals = this.document.getElementsByClassName('modal open');
await sleep(10);
this.modalNumber = this.modals?.length || 0;
if (this.modalNumber <= 1) {
backdrop.setAttribute('id', 'modal-backdrop');
backdrop.setAttribute('class', 'modal-backdrop show');
body.appendChild(backdrop);
} else if (this.modalNumber > 1) {
modal.nativeElement.style.zIndex = 1060 + this.modalNumber + 1;
backdrop.setAttribute('style', `z-index: ${1060 + this.modalNumber};`);
}
} else {
this.removeModalFromBody();
}
}
}
}
/**
* Performs actions before the component is destroyed.
*/
ngOnDestroy(): void {
this.routerObservable?.unsubscribe();
}
/**
* Function that init the routerObservable.
*/
observeRouter() {
this.routerObservable = this.router.events.subscribe(async event => {
if (event instanceof NavigationStart) {
const destinationRoute = new URL(event.url, this.localized.origin);
if (this.localized.origin + this.localized.pathname !== destinationRoute.origin + destinationRoute.pathname) {
this.removeModalFromBody();
await sleep(1000);
}
}
});
}
/**
* Removes the modal element from document body.
*/
removeModalFromBody() {
const body = this.document.getElementsByTagName('body')[0];
if (this.modalNumber <= 1) {
body?.classList?.remove('block');
if (this.getWindow?.window()?.scrollY !== this.scrollY) {
if (this.scrollY) this.getWindow?.window()?.scrollTo(0, this.scrollY);
}
}
const backdrop = this.document.getElementById('modal-backdrop');
const modal = this.document.getElementById(this.id);
this.modalClose.emit();
modal?.parentNode?.removeChild(modal);
if (this.modalNumber === 2) {
backdrop?.removeAttribute('style');
} else if (this.modalNumber <= 1) {
backdrop?.parentNode?.removeChild(backdrop);
}
this.getWindow?.window()?.removeEventListener('keydown', this.onKeydown);
}
/**
* Handles keyboard events for the modal component.
*
* @param event - The keyboard event object.
*/
onKeydown(event: KeyboardEvent) {
if (this.open) {
this.modals = this.document.getElementsByClassName('modal open');
if (this.modals?.length && this.modals[this.modals.length - 1]?.id === this.id) {
this.events(event.key);
}
}
}
/**
* Handles specific keyboard events.
*
* @param key - The key code of the pressed key.
*/
events(key: string) {
switch (key) {
case 'Escape':
this.modalClose.emit();
break;
case 'ArrowLeft':
this.leftPress.emit();
break;
case 'ArrowRight':
this.rightPress.emit();
break;
}
}
}
<div
class="modal align-items-center {{ backdropClass }}"
[id]="id"
[ngClass]="{
open: open,
}"
#modal
[dataTest]="modalDataTest"
>
<div
class="modal-dialog modal-{{ modalSize }} {{ modalClass }}"
[clickOutsideEnabled]="!!(open && backdrop !== 'static' && modal)"
(clickOutside)="modalClose.emit()"
>
<div [dataTest]="modalDataTest + '-content'" class="modal-content modal-sq">
<div
[dataTest]="modalDataTest + '-header'"
class="modal-header"
[ngClass]="{
'without-header': !hasHeader,
}"
[ngStyle]="{
background: headerBackgroundColor,
padding: headerPadding,
}"
>
@if (headerTemplate) {
<ng-container *ngTemplateOutlet="headerTemplate"></ng-container>
}
@if (buttonClose) {
<button type="button" class="close button-close" aria-label="Close" (click)="modalClose.emit()">
<i class="fa-solid fa-xmark fa-lg"></i>
</button>
}
</div>
<div
[dataTest]="modalDataTest + '-body'"
class="modal-body scrollbar"
[ngStyle]="{ background: bodyBackgroundColor, padding: bodyPadding }"
>
<ng-content></ng-content>
</div>
@if (footerTemplate) {
<div
[dataTest]="modalDataTest + '-footer'"
class="modal-footer"
[ngStyle]="{
background: footerBackgroundColor,
padding: footerPadding,
}"
>
<ng-container *ngTemplateOutlet="footerTemplate"></ng-container>
</div>
}
</div>
</div>
</div>
./sq-modal.component.scss
.modal {
min-height: auto;
&.need-priority {
z-index: 1081;
}
.modal-sq {
border-radius: 4px;
.modal-header {
display: flex;
justify-content: space-between;
}
.without-header {
padding: 0 0.08rem;
border: none;
justify-content: flex-end;
button.close {
z-index: 1;
position: relative;
top: 5px;
left: -4px;
margin: 0;
padding: 0;
}
}
}
}