Skip to content

Commit

Permalink
refactor: snackbar (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
njfamirm authored Dec 17, 2024
2 parents d428086 + de36218 commit 9119678
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 87 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ Here is a brief overview of the included libraries:

1. [`element`](./packages/element): Utility functions and mixins for building high-performance web components with Lit.
2. [`alpine`](./packages/alpine): Utility functions to enhance Alpine.js usage with backup support.
3. [`typescript-config`](./packages/typescript-config): Base TypeScript configuration for Nexim projects.
4. [`prettier-config`](./packages/prettier-config): Base Prettier configuration for Nexim projects.
5. [`eslint-config`](./packages/eslint-config): Base Eslint configuration for Nexim projects.
3. [`snackbar`](./packages/snackbar): Snackbar component, It includes utilities for managing the snackbar's state and animations.
4. [`service-worker`](./packages/service-worker): Utilities to simplify the usage of service workers in your web applications.
5. [`typescript-config`](./packages/typescript-config): Base TypeScript configuration for Nexim projects.
6. [`prettier-config`](./packages/prettier-config): Base Prettier configuration for Nexim projects.
7. [`eslint-config`](./packages/eslint-config): Base Eslint configuration for Nexim projects.

For more detailed information and guidelines on how to use each package, please refer to each package's README.

Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
![NPM Downloads](https://img.shields.io/npm/dm/%40nexim%2Feslint-config)
![NPM License](https://img.shields.io/npm/l/%40nexim%2Feslint-config)

## Overview

Alwatr ECMAScript Style Guide as a ESLint [shareable configurations](http://eslint.org/docs/developer-guide/shareable-configs.html).

## Installation
Expand Down
2 changes: 2 additions & 0 deletions packages/prettier-config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
![NPM Downloads](https://img.shields.io/npm/dm/%40nexim%2Fprettier-config)
![NPM License](https://img.shields.io/npm/l/%40nexim%2Fprettier-config)

## Overview

Nexim's [shareable configurations](https://prettier.io/docs/en/configuration.html#sharing-configurations) for [Prettier](https://prettier.io/).

## Installation
Expand Down
11 changes: 3 additions & 8 deletions packages/snackbar/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# @nexim/snackbar

This package provides a customizable snackbar component for displaying brief messages to users. It includes utilities for managing the snackbar's state and animations.

![NPM Version](https://img.shields.io/npm/v/%40nexim%2Fsnackbar)
![npm bundle size](https://img.shields.io/bundlephobia/min/%40nexim%2Fsnackbar)
![Build & Lint & Test](https://github.com/the-nexim/nanolib/actions/workflows/build-lint-test.yaml/badge.svg)
Expand All @@ -10,7 +8,7 @@ This package provides a customizable snackbar component for displaying brief mes

## Overview

`@nexim/snackbar` is a versatile library designed to provide a customizable snackbar component for displaying brief messages to users. It includes utilities for managing the snackbar's state and animations, ensuring efficiency and scalability in high-performance projects.
Snackbar component. It includes utilities for managing the snackbar's state and animations.

## Installation

Expand All @@ -34,14 +32,11 @@ import {snackbarSignal} from '@nexim/snackbar';

snackbarSignal.notify({
content: 'This is a snackbar message',
// The following properties are optional.
action: {
label: 'Undo',
handler: () => {
console.log('Action button clicked');
},
signalId: 'undo-handler',
},
duration: '4s',
duration: '5s',
addCloseButton: true,
});
```
2 changes: 1 addition & 1 deletion packages/snackbar/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@nexim/snackbar",
"version": "0.0.0",
"description": "A customizable snackbar component for displaying brief messages to users, with state management and animation utilities.",
"description": "Snackbar component, It includes utilities for managing the snackbar's state and animations.",
"keywords": [
"snackbar",
"notification",
Expand Down
8 changes: 4 additions & 4 deletions packages/snackbar/src/lib/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ import {LightDomMixin, LoggerMixin} from '@nexim/element';
import {html, LitElement, nothing, type PropertyValues, type TemplateResult} from 'lit';
import {customElement, property} from 'lit/decorators.js';

import {snackbarActionButtonClickedSignal} from './handler.js';
import {snackbarActionButtonClickedSignal} from './signal.js';
import {waitForNextFrame} from './utils.js';

declare global {
interface HTMLElementTagNameMap {
'snack-bar': SnackbarComponent;
'snack-bar': SnackbarElement;
}
}

@customElement('snack-bar')
export class SnackbarComponent extends LightDomMixin(LoggerMixin(LitElement)) {
export class SnackbarElement extends LightDomMixin(LoggerMixin(LitElement)) {
/**
* The content to be displayed inside the snackbar.
*/
Expand Down Expand Up @@ -52,7 +52,7 @@ export class SnackbarComponent extends LightDomMixin(LoggerMixin(LitElement)) {

this.removeAttribute('open');

await waitForTimeout(SnackbarComponent.openAndCloseAnimationDuration__);
await waitForTimeout(SnackbarElement.openAndCloseAnimationDuration__);
this.remove();
}

Expand Down
127 changes: 57 additions & 70 deletions packages/snackbar/src/lib/handler.ts
Original file line number Diff line number Diff line change
@@ -1,101 +1,79 @@
import {AlwatrSignal, AlwatrTrigger} from '@alwatr/flux';
import {createLogger} from '@alwatr/logger';
import {parseDuration, type Duration} from '@alwatr/parse-duration';
import {parseDuration} from '@alwatr/parse-duration';
import {waitForTimeout} from '@alwatr/wait';

import type {SnackbarComponent} from './element.js';
import {snackbarActionButtonClickedSignal, snackbarSignal} from './signal.js';

import type {SnackbarElement} from './element.js';
import type {SnackbarOptions} from './type.js';

const logger = /* @__PURE__ */ createLogger(`${__package_name__}/handler`);

/**
* @property content - Content to be displayed in the snackbar.
* @property {action} - The action button configuration.
* @property action.label - The label for the action button.
* @property action.handler - The handler function for the action button.
* @property duration - Duration for which the snackbar is displayed. `infinite` for infinite duration.
* Duration for which the snackbar is displayed.
* `infinite` for infinite duration.
* @property addCloseButton - Whether to add a close button to the snackbar.
* Store the function to close the last snackbar.
*/
export type SnackbarOptions = {
content: string;
action?: {
label: string;
handler: () => void;
};
duration?: Duration | 'infinite';
addCloseButton?: boolean;
};
let closeLastSnackbar: (() => Promise<void>) | null = null;

/**
* Signal for when the snackbar action button is clicked.
* Store the function to unsubscribe the action button handler after close or action button clicked.
*/
export const snackbarActionButtonClickedSignal = new AlwatrTrigger({
name: 'snackbar-action-button-clicked',
});
let unsubscribeActionButtonHandler: (() => void) | null = null;

/**
* Signal for displaying the snackbar.
* Create snackbar element with given options.
*
* @example
* import {snackbarSignal} from '@nexim/snackbar';
* @param options - Options for configuring the snackbar.
* @returns The created snackbar element.
*/
function createSnackbarElement(options: SnackbarOptions): SnackbarElement {
const element = document.createElement('snack-bar');
element.setAttribute('content', options.content);

if (options.addCloseButton === true) {
element.setAttribute('add-close-button', '');
}

if (options.action != null) {
element.setAttribute('action-button-label', options.action.label);
}

return element;
}

/**
* Handle action button click.
*
* snackbarSignal.notify({
* content: 'This is a snackbar message',
* // The following properties are optional.
* action: {
* label: 'Undo',
* handler: () => {
* console.log('Action button clicked');
* },
* },
* duration: '4s',
* addCloseButton: true,
* });
* @param options - Options for configuring the snackbar.
* @param closeSnackbar - Function to close the snackbar.
*/
export const snackbarSignal = /* @__PURE__ */ new AlwatrSignal<SnackbarOptions>({name: 'snackbar'});
function handleActionButtonClick(closeSnackbar: () => Promise<void>): void {
const actionButtonClickHandler = () => {
logger.logOther?.('Snackbar action button clicked.');

// Subscribe to the snackbar signal to show the snackbar when the signal is emitted.
snackbarSignal.subscribe((options) => {
showSnackbar(options);
});
return closeSnackbar();
};

let closeLastSnackbar: (() => Promise<void>) | null = null;
let unsubscribeActionButtonHandler: (() => void) | null = null;
// Subscribe to the action button click
unsubscribeActionButtonHandler = snackbarActionButtonClickedSignal.subscribe(actionButtonClickHandler.bind(null), {
once: true,
}).unsubscribe;
}

/**
* Displays the snackbar with the given options.
*
* @param options - Options for configuring the snackbar.
*/
async function showSnackbar(options: SnackbarOptions): Promise<void> {
logger.logMethodArgs?.('showSnackbar', {options});

// Parse the duration

// Set default duration if not provided
options.duration ??= '4s';

const element = document.createElement('snack-bar') as SnackbarComponent;
options.duration ??= '5s';

element.setAttribute('content', options.content);

if (options.addCloseButton === true) {
element.setAttribute('add-close-button', '');
}

if (options.action != null) {
element.setAttribute('action-button-label', options.action.label);

// Subscribe to the action button click
unsubscribeActionButtonHandler = snackbarActionButtonClickedSignal.subscribe(() => {
options.action!.handler();

return closeSnackbar_();
}).unsubscribe;
}
const element = createSnackbarElement(options);

let closed = false;
const closeSnackbar_ = async () => {
const closeSnackbar = async () => {
if (closed === true) return;
logger.logMethodArgs?.('closeSnackbar', {options});

Expand All @@ -104,13 +82,22 @@ async function showSnackbar(options: SnackbarOptions): Promise<void> {
closed = true;
};

if (options.action != null) {
handleActionButtonClick(closeSnackbar);
}

// Close the last snackbar if it exists
await closeLastSnackbar?.();
closeLastSnackbar = closeSnackbar_;
closeLastSnackbar = closeSnackbar;
document.body.appendChild(element);

// Set a timeout to close the snackbar if duration is not infinite
if (options.duration !== 'infinite') {
waitForTimeout(parseDuration(options.duration)).then(closeSnackbar_);
waitForTimeout(parseDuration(options.duration)).then(closeSnackbar);
}
}

// Subscribe to the snackbar signal to show the snackbar when the signal is emitted.
snackbarSignal.subscribe((options) => {
showSnackbar(options);
});
30 changes: 30 additions & 0 deletions packages/snackbar/src/lib/signal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {AlwatrSignal, AlwatrTrigger} from '@alwatr/flux';

import type {SnackbarOptions} from './type.js';

/**
* Signal triggered when the snackbar action button is clicked to close snackbar.
*/
export const snackbarActionButtonClickedSignal = /* @__PURE__ */ new AlwatrTrigger({
name: 'snackbar-action-button-clicked',
});

/**
* Signal for displaying the snackbar.
*
* @example
* import {snackbarSignal} from '@nexim/snackbar';
*
* snackbarSignal.notify({
* content: 'This is a snackbar message',
* action: {
* label: 'Undo',
* handler: () => {
* console.log('Action button clicked');
* },
* },
* duration: '5s',
* addCloseButton: true,
* });
*/
export const snackbarSignal = /* @__PURE__ */ new AlwatrSignal<SnackbarOptions>({name: 'snackbar'});
19 changes: 19 additions & 0 deletions packages/snackbar/src/lib/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type {Duration} from '@alwatr/parse-duration';

/**
* @property content - Content to be displayed in the snackbar.
* @property [action] - The action button configuration.
* @property action.label - The label for the action button.
* @property action.signalId - The signal ID to be emitted when the action button is clicked.
* @property duration - Duration for which the snackbar is displayed. `infinite` for infinite duration.
* @property addCloseButton - Whether to add a close button to the snackbar.
*/
export type SnackbarOptions = {
content: string;
action?: {
signalId: string;
label: string;
};
duration?: Duration | 'infinite';
addCloseButton?: boolean;
};
2 changes: 1 addition & 1 deletion packages/snackbar/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ import {packageTracer} from '@alwatr/package-tracer';
__dev_mode__: packageTracer.add(__package_name__, __package_version__);

export * from './lib/element.js';
export * from './lib/handler.js';
export {snackbarSignal} from './lib/signal.js';
2 changes: 2 additions & 0 deletions packages/typescript-config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
![NPM Downloads](https://img.shields.io/npm/dm/%40nexim%2Ftypescript-config)
![NPM License](https://img.shields.io/npm/l/%40nexim%2Ftypescript-config)

## Overview

This is a base TypeScript configuration for Nexim projects.

## installation
Expand Down

0 comments on commit 9119678

Please sign in to comment.