@commonmodule/app v0.3.6
@commonmodule/app
A TypeScript/ES module that builds on top of
@commonmodule/ts to provide
utilities for:
- DOM manipulation (e.g. virtual element creation, node trees)
- Window and DOM events (e.g. scroll, resize, custom events)
- View management (e.g. base
Viewclass,Routerfor single-page apps) - Internationalization (
I18nMessageManager,msg) - Storage and Auth (
Store,AuthTokenManager) - Theming (
ThemeManager,Themeenum) - Browser environment detection (
Browserutility) - Utility helpers for DOM, images, styles, and more
Table of Contents
- Installation
- API Reference
- Usage Examples
- Contributing
- License
Installation
npm install @commonmodule/app
# or
yarn add @commonmodule/appNote: This library depends on
@commonmodule/ts. Make sure it is also installed.
API Reference
DOM
DomNode
DomNode is a specialized node class for creating and managing DOM elements as
a tree structure. It extends WindowEventTreeNode from
@commonmodule/ts—therefore it can dispatch and listen to events, and is also
organized in a tree-like hierarchy.
export default class DomNode<
H extends HTMLElement = HTMLElement,
E extends EventRecord = {},
> extends WindowEventTreeNode<DomNode, E & { visible: () => void }> {
public htmlElement: H;
// ...
}Key Points:
- Maintains an internal
htmlElement: H. - Can be appended to other
DomNodes, forming a nested structure that mirrors the DOM tree. - Extends the event system from
WindowEventTreeNode, which provides:onWindow,offWindowfor window-level events.- Also inherits a normal
on,offfor custom or node-specific events.
Constructor:
constructor(elementOrSelector?: H | DomSelector, ...children: DomChild<H>[])elementOrSelectorcan be either a raw DOM element (HTMLElement) or a CSS-like selector in string form (e.g."div#my-id.my-class").childrencan be:- Another
DomNode - An object describing element properties and styles
- A string (appended as text)
undefined(skipped)
- Another
Core Methods:
| Method | Description |
|---|---|
append(...children: DomChild<H>[]) | Appends children to the current node. |
prepend(...children: DomChild<H>[]) | Prepends children to the current node. |
appendTo(parent: DomNode, index?: number): this | Attaches the current node to a parent DomNode at a specified index in the parent’s child list. |
remove() | Removes the current node (and its subtree) from the DOM and from its parent. |
clear(...except: (DomNode \| undefined)[]) | Removes all child DomNodes except those specified. |
style<T extends Partial<CSSStyleDeclaration> \| string>(styles: T) | If styles is a string, returns the style value for that CSS property. If it’s an object, applies each key-value pair to the node’s htmlElement.style. |
addClass(...classNames: string[]): this | Adds one or more CSS classes. |
removeClass(...classNames: string[]): this | Removes one or more CSS classes. |
hasClass(className: string): boolean | Checks if the node has the specified CSS class. |
onDom<K extends keyof HTMLElementEventMap>(type: K, listener, options?) | Adds a native DOM event listener for this node’s htmlElement. |
offDom<K extends keyof HTMLElementEventMap>(type: K, listener, options?) | Removes a native DOM event listener from this node’s htmlElement. |
calculateRect(): DOMRect | Returns the bounding client rect of htmlElement. |
clone(): DomNode<H, E> | Creates a new DomNode by cloning htmlElement. (Note: The cloned node does not preserve event listeners or children from the original DomNode structure—just the DOM.) |
text: string (getter and setter) | Returns or sets the node’s text content. Setting text clears existing children first. |
BodyNode
A convenience subclass of DomNode<HTMLBodyElement> that automatically
references document.body.
class BodyNode extends DomNode<HTMLBodyElement> {
constructor() {
super(document.body as HTMLBodyElement);
}
}Usage:
import BodyNode from "@commonmodule/app/dom/BodyNode.js";
const body = new BodyNode();
body.append("Hello, world!");QueriedDomNode
A DomNode subclass that creates itself from an existing element in the
document, selected by a CSS selector string.
export default class QueriedDomNode extends DomNode {
constructor(selector: string) {
super(document.querySelector(selector) as HTMLElement);
}
protected isVisible(): boolean {
return true;
}
}Usage:
import QueriedDomNode from "@commonmodule/app/dom/QueriedDomNode.js";
const existingDom = new QueriedDomNode("#already-existing-element");
existingDom.append("This is appended to the existing DOM element.");el() & html.impl
A helper function for concise DOM creation, similar to other “hyperscript”
helpers (el() is set as UniversalEl.impl in the code).
export default function el<EOS extends ElementOrSelector>(
elementOrSelector: EOS,
...children: DomChild<EOS>[]
): DomNode<InferElementType<EOS>>;- Takes a string selector or element type (e.g.
"div","section#header","button.my-button") and optional children to append. - Returns a
DomNode.
There’s also an html helper in
@commonmodule/universal-page, which gets its impl
set here. It can parse an HTML string, take the first node from it, and wrap it
in a DomNode.
Example:
import el from "@commonmodule/app/dom/el.js";
import { html } from "@commonmodule/universal-page";
// Using the "el" function:
const myDiv = el(
"div.my-container",
"Hello, ",
el("span.highlighted", "World!"),
);
document.body.appendChild(myDiv.htmlElement);
// Using the "html" function:
const someHtml = html("<p>Parsed from raw HTML</p>");
document.body.appendChild(someHtml.htmlElement);WindowEventTreeNode
WindowEventTreeNode extends EventTreeNode (from @commonmodule/ts) to allow
easy subscription to global window events that automatically unsubscribe when
the node is removed.
export default class WindowEventTreeNode<
T extends EventTreeNode<T, E>,
E extends EventRecord,
> extends EventTreeNode<T, E> {
// ...
}Key Methods:
| Method | Description |
|---|---|
onWindow<K extends keyof WindowEventMap>(type: K, listener, options?) | Binds a window-level event listener. Automatically removed when node’s remove() is called. |
offWindow<K extends keyof WindowEventMap>(type: K, listener, options?) | Unbinds a previously attached window-level event listener. |
remove() | Removes all window listeners (and calls super.remove()). |
Inherited from EventTreeNode: on, off, emit, appendTo, etc. | Because it extends EventTreeNode, you can also manage custom events and hierarchical structure as usual for tree nodes. |
Internationalization
I18nMessageManager
Manages a collection of localized messages. Allows adding messages in bulk and retrieving them by language key and message key.
class I18nMessageManager {
public addMessage(language: string, key: string, message: string): void;
public addMessages(
language: string,
messages: { [key: string]: string },
): void;
public addMessagesBulk(
messages: { [language: string]: { [key: string]: string } },
): void;
public getMessage(language: string, key: string): string;
}Usage:
- Typically used alongside the
msg()helper for templating. - Allows nested keys via
key.split("."), e.g."home.welcome".
msg()
A utility function that retrieves a message from I18nMessageManager in the
current browser language and performs parameter substitution with
%{paramName}.
export default function msg(
key: string,
params?: Record<string, string | number>,
): string;Example:
import msg from "@commonmodule/app/i18n/msg.js";
import I18nMessageManager from "@commonmodule/app/i18n/I18nMessageManager.js";
// Add some messages
I18nMessageManager.addMessages("en", {
greeting: "Hello, %{name}!",
});
I18nMessageManager.addMessages("ko", {
greeting: "안녕하세요, %{name}!",
});
// Suppose the current Browser.languageCode = "en"
console.log(msg("greeting", { name: "Alice" }));
// => "Hello, Alice!"Resource Loading
FontLoader
FontLoader extends ResourceLoader<boolean> from @commonmodule/ts. It
manages loading and verifying that a specified font is available to the document
(either via document.fonts or fallback measurement technique).
export default class FontLoader extends ResourceLoader<boolean> {
protected async loadResource(fontName: string): Promise<boolean | undefined>;
// ...
}Key Features:
- Reference-counted loading: Once you
load(fontName), the loader ensures it’s fetched. Additional calls toload()for the samefontNamewill reuse the existing load or resource. - Fallback: If the browser does not support
document.fonts, it uses a manual technique to detect font availability.
View
An abstract class to represent a UI “view” or screen, with built-in event management and cleanup.
export default abstract class View<DT = {}, CT extends DomNode = DomNode> {
protected container!: CT;
public changeData(data: DT): void {}
public close(): void {}
}Key Points:
Viewhas an internalcontainer(aDomNodeor subclass), typically the root node of the view’s DOM structure.changeData(data: DT)can be overridden to handle dynamic updates.close()should remove thecontainerand clean up event listeners.
addViewManagedEvent:
This protected utility method helps you attach events to the container or other
EventContainers, ensuring they’re all removed on close().
protected addViewManagedEvent<T extends EventContainer<E>, E extends EventRecord, K extends keyof E>(
target: T,
eventName: K,
listener: E[K],
): thisRouting
Router
A minimal Single-Page Application (SPA) router that uses the History API. Routes
are tracked with
URLPattern
(or a polyfill if needed).
class Router extends EventContainer<{
routeChanged: (pathname: `/${string}`, data: any) => void;
}> {
public prefix = "";
// ...
}Key Methods:
| Method | Description |
|---|---|
add(pathname, View, exclude?) | Registers a route (or multiple pathnames) to render a certain View class. Optional exclude path(s) can be provided to skip this route if matched. |
go(pathname, data?) | Pushes a new history state and updates active views. |
goWithoutHistory(pathname, data?) | Replaces the current history state (i.e. no “back” step) and updates active views. |
updateActiveViews(data?) | Internal method that checks the current location.pathname against all routes to see which should be active or removed. |
Events: routeChanged(pathname, data) | Emitted after a new route is pushed/replaced. |
| View Lifecycle | When a route matches, Router instantiates that route’s View (if not already active). Once the route no longer matches, the View is closed. If a user revisits the route, a new instance is created (unless you store and re-use view instances somewhere). |
| Matching | Uses URLPattern to match prefix + pathname. For named segments in the pattern (e.g. "/user/:id"), the matched values are passed into view.changeData(params). If an excludePattern is matched, the route is ignored. |
| Dynamic URL segments | The example code uses URLPattern({ pathname: "/some/:param" }); matched segment groups are passed to the View. |
| Polyfill | If window.URLPattern is not defined, urlpattern-polyfill is conditionally loaded. |
Authentication
AuthTokenManager
AuthTokenManager extends EventContainer to manage an auth token in a
persistent Store. It emits a tokenChanged event whenever the token changes.
class AuthTokenManager<E extends EventRecord = {}> extends EventContainer<
E & { tokenChanged: (token: string | undefined) => void }
> {
public get token(): string | undefined;
public set token(value: string | undefined);
}- Internally uses
Storeto store the token. - On each assignment to
token, fires atokenChangedevent.
Usage:
import AuthTokenManager from "@commonmodule/app/store/AuthTokenManager.js";
AuthTokenManager.on("tokenChanged", (newToken) => {
console.log("Token updated:", newToken);
});
AuthTokenManager.token = "my-jwt-token";
// => "Token updated: my-jwt-token"Storage
Store
A thin wrapper around localStorage and sessionStorage with a consistent key
prefix and fallback clearing for quota errors.
export default class Store {
constructor(name: string);
public setTemporary<T>(key: string, value: T): void;
public setPermanent<T>(key: string, value: T): void;
public get<T>(key: string): T | undefined;
public getAll<T>(): Record<string, T>;
public remove(...keys: string[]): void;
public clear(): void;
public isPermanent(key: string): boolean;
}Key Details:
name: a prefix in kebab-case appended to all keys.- E.g., if
name = "my-app", a call tosetPermanent("theme", "light")stores the key as"my-app/theme".
- E.g., if
setTemporary()usessessionStorage,setPermanent()useslocalStorage.- If a quota-exceeded error occurs, it clears the storage entirely and reloads the page.
isStorageAvailable()is a static method to detect if storage is accessible.
Theme Management
ThemeManager
Uses a Store to remember the current theme choice. Toggles and applies a
data-theme attribute to the <html> element.
class ThemeManager {
public init(): void;
public get theme(): Theme; // returns the user-preferred theme or Theme.Auto
public set theme(theme: Theme); // sets and persists the theme
public getShowingTheme(): Theme;
public toggleTheme(): void;
}Key Points:
Theme.Autouses the OS-level dark/light preference.- The manager calls
document.documentElement.setAttribute("data-theme", "dark" | "light"). - You can style your app via CSS attribute selectors (e.g.
[data-theme="dark"] { ... }).
Theme (enum)
Represents a tri-state theme mode:
enum Theme {
Auto = "auto",
Dark = "dark",
Light = "light",
}Browser Info
Browser
A collection of platform-related checks, full screen methods, sharing, downloading, and language preference.
class Browser {
public isAndroid(): boolean;
public isIOS(): boolean;
public isMobileDevice(): boolean;
public isPageVisible(): boolean;
public hasPageFocus(): boolean;
public isDarkMode(): boolean;
public get languageCode(): string;
public set languageCode(lang: string);
public async share(data: { title: string; url: string }): Promise<void>;
public async download(url: string): Promise<void>;
public enterFullscreen(domNode: DomNode): void;
public exitFullscreen(): void;
public isFullscreen(): boolean;
}Notes:
- Stores language preference in a
Storeby default, falling back tonavigator.language. - On mobile, calls
navigator.shareif available; otherwise, copies the URL to clipboard. enterFullscreenandexitFullscreenrely on the modern fullscreen API.
DOM Utilities
DomUtils
Provides helper methods for certain device quirks, such as simulating a
contextmenu event on iOS.
class DomUtils {
public enhanceWithContextMenu(
dom: DomNode,
handler: (event: MouseEvent) => void,
): void;
}- On iOS Safari, there's no native “long-press to show context menu.” This
method replicates it by detecting a long press (
touchstart+setTimeout) and firing the handler as if it werecontextmenu.
Image Optimization
ImageOptimizer
A utility for resizing and recompressing single-frame images client-side, converting them to JPEG by default. It also checks whether a GIF is animated and, if so, refuses to compress it.
class ImageOptimizer {
public async optimizeImage(
file: File,
maxWidth: number,
maxHeight: number,
): Promise<File>;
}Features:
- If the new (compressed) blob is bigger than original, it returns the original.
- Rejects animated GIFs with an error (
"Animated GIFs are not compressed.").
Style Utilities
StyleUtils
A small helper for advanced CSS styling:
class StyleUtils {
public applyTextStroke(target: DomNode, width: number, color: string): void;
}applyTextStrokecreates multipletext-shadowoffsets for a “text stroke” effect, since actual-webkit-text-strokeis unsupported in some browsers.
WebSocketClient
An example WebSocketClient that extends EventContainer (from
@commonmodule/ts) and implements the RealtimeClient interface. Reconnects
automatically on close.
export default class WebSocketClient
extends EventContainer<{ connect: () => void; disconnect: () => void }>
implements RealtimeClient {
constructor(private url: string);
public send(data: string): void;
public onMessage(handler: (message: string) => void): void;
public isConnected(): boolean;
}Usage:
import WebSocketClient from "@commonmodule/app/network/WebSocketClient.js";
const client = new WebSocketClient("wss://example.com/socket");
client.on("connect", () => {
console.log("Connected");
client.send("Hello from client!");
});
client.onMessage((data) => {
console.log("Received:", data);
});SPAInitializer
An optional helper that checks for an “initial path” in sessionStorage and
redirects the router to that path if it exists. Useful for certain post-redirect
flows.
class SPAInitializer {
public init(): void;
}- Looks for
sessionStorage["initialPath"], callsRouter.goWithoutHistory, then clears it.
Usage Examples
1. Simple App Setup
// main.ts
import el from "@commonmodule/app/dom/el.js";
import BodyNode from "@commonmodule/app/dom/BodyNode.js";
import Router from "@commonmodule/app/route/Router.js";
import SPAInitializer from "@commonmodule/app/SPAInitializer.js";
import View from "@commonmodule/app/view/View.js";
// 1) Initialize Body
const body = new BodyNode();
// 2) Create a simple View
class HomeView extends View {
constructor() {
super();
this.container = el("div.home-view", "Hello, home view!");
body.append(this.container);
}
}
// 3) Set up Router
Router.add("/home", HomeView);
// 4) Initialize SPA (if you want to handle stored initial path)
SPAInitializer.init();
// 5) Start your app
Router.go("/home");2. Using DomNode for Complex UI
import el from "@commonmodule/app/dom/el.js";
const card = el(
"div.card",
{ style: { border: "1px solid #ccc", padding: "1em" } },
el("h2", "Title"),
el("p", "Some paragraph text..."),
el("button", "Click me!"),
);
document.body.appendChild(card.htmlElement);3. Storing and Retrieving Data
import Store from "@commonmodule/app/store/Store.js";
const store = new Store("my-app");
store.setTemporary("sessionKey", "temp-value");
store.setPermanent("accessToken", "xyz123");
console.log(store.get("sessionKey")); // "temp-value"
console.log(store.isPermanent("accessToken")); // true4. Theming
import ThemeManager from "@commonmodule/app/theme/ThemeManager.js";
import Theme from "@commonmodule/app/theme/Theme.js";
// Ensure the manager syncs with DOM
ThemeManager.init();
// Toggle
ThemeManager.toggleTheme();
// Or set a specific theme
ThemeManager.theme = Theme.Dark;5. Internationalization
import I18nMessageManager from "@commonmodule/app/i18n/I18nMessageManager.js";
import msg from "@commonmodule/app/i18n/msg.js";
import Browser from "@commonmodule/app/utils/Browser.js";
// Add some sample translations
I18nMessageManager.addMessages("en", {
welcome: "Welcome, %{name}!",
});
I18nMessageManager.addMessages("ko", {
welcome: "환영합니다, %{name}!",
});
// Switch language
Browser.languageCode = "ko";
console.log(msg("welcome", { name: "Alice" }));
// => "환영합니다, Alice!"Contributing
- Fork the repository.
- Create a new branch:
git checkout -b feature/my-feature. - Make your changes and commit:
git commit -m 'Add my feature'. - Push the changes:
git push origin feature/my-feature. - Create a pull request.
License
This module is provided under the MIT License. See the LICENSE file for details.
Author: yj.gaia\ Based on: @commonmodule/ts
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
11 months ago
11 months ago