kui-framework
v1.0.9
Published
A lightweight framework for building web applications.
Downloads
2
Readme
UI Framework
A lightweight and flexible UI framework for building modern web applications.
Features
You can create all kinds of interfaces quickly with this framework.
- 🚀 Lightweight and fast
- 🧩 Modular component system
- 🎨 Customizable styling
- 🔌 Extensible plugin architecture
- 📱 Responsive design support
- ✨ Animation support for UI elements
- 📝 Templating system for rendering dynamic content
How it Works
The code in src/lib
is implementing a lightweight UI framework for building web applications. It provides a set of reusable components and utilities to create interactive user interfaces. Here's a brief overview of its functionality:
- Core components: View, Container, Button, Form, List, and Modal.
- Templating system for rendering dynamic content.
- Animation support for UI elements.
- Styling utilities for consistent appearance.
- Plugin system for extensibility.
Simple Example:
import { Container, Button, Form, Animation } from "../index";
export function loginexample()
{
const app = new Container();
const login = new Button({
text: "Login",
hoverAnimation: Animation("grow", 300),
});
login.setOnClick(() => alert("Hello!"));
const loginForm = new Form({
fields: [
{ type: "text", name: "username", label: "Username" },
{ type: "password", name: "password", label: "Password" },
],
onSubmit: (data) => console.log("Login:", data),
});
app.addChild(loginForm);
app.addChild(login);
app.mount(document.body);
}
This example creates a simple app with a greeting button and a login form.
In-depth explanation
The framework is built around the concept of Views, which are the basic building blocks of the UI. The View class provides core functionality for creating and managing UI elements.
// view.ts
import { AnimationOptions } from "./animations";
export interface ViewOptions {
id?: string;
className?: string;
template?: string;
}
export class View {
protected element: HTMLElement;
protected children: View[] = [];
private mountedPromise: Promise<void>;
private mountedResolve!: () => void;
constructor(options: ViewOptions = {}) {
this.element = document.createElement("div");
if (options.id) this.element.id = options.id;
if (options.className) this.element.className = options.className;
if (options.template) this.setTemplate(options.template);
this.mountedPromise = new Promise((resolve) => {
this.mountedResolve = resolve;
});
}
setTemplate(template: string): void {
this.element.innerHTML = template;
}
appendChild(child: View): void {
this.children.push(child);
this.element.appendChild(child.getElement());
}
getElement(): HTMLElement {
return this.element;
}
async onMount(): Promise<void> {
// To be overridden by subclasses
}
async onUnmount(): Promise<void> {
// To be overridden by subclasses
}
mounted(): Promise<void> {
return this.mountedPromise;
}
mount(parent: HTMLElement): void {
parent.appendChild(this.element);
this.mountedResolve();
this.onMount();
}
unmount(): void {
this.element.remove();
this.onUnmount();
}
centerInRow(): this {
this.element.style.justifySelf = "center";
return this;
}
centerInColumn(): this {
this.element.style.alignSelf = "center";
return this;
}
}
The Container class extends View and allows for more complex layouts, including grid systems and flexible positioning of child elements.
// container.ts
import { View, ViewOptions } from "./view";
import { Button, ButtonOptions } from "./button";
import { Form, FormOptions } from "./form";
import { framework } from "./framework";
// Initialize the framework
framework.init();
export interface GridOptions {
columns: number | string;
rows?: number | string;
gap?: string;
}
export type RowAlignment =
| "left"
| "center"
| "right"
| "space-between"
| "space-around"
| "space-evenly";
export interface ContainerOptions extends ViewOptions {
gap?: string;
padding?: string;
grid?: GridOptions;
scrollable?: boolean;
rowAlignment?: RowAlignment;
equalWidthChildren?: boolean;
columns?: number | string;
rows?: number | string;
}
Other components like Button, Form, and List build upon these base classes to provide specific functionality. For example, the Button class adds click handling and hover effects.
// button.ts
import { View, ViewOptions } from "./view";
import { renderTemplate } from "./templating";
import { AnimationOptions, applyAnimation } from "./animations";
export interface ButtonOptions extends ViewOptions {
text: string;
onClick?: () => void;
color?: string;
hoverColor?: string;
hoverAnimation?: AnimationOptions;
clickAnimation?: AnimationOptions;
}
const buttonTemplate = `
<button class="ui-button" style="background-color: {{color}}">
{{text}}
</button>
`;
export class Button extends View {
private buttonElement: HTMLButtonElement;
constructor(options: ButtonOptions) {
super({
...options,
template: renderTemplate(buttonTemplate, {
text: options.text,
color: options.color || "#007bff",
}),
});
this.buttonElement = this.element.querySelector(
"button"
) as HTMLButtonElement;
if (options.onClick) {
this.buttonElement.addEventListener("click", () => {
if (options.clickAnimation) {
applyAnimation(this.buttonElement, options.clickAnimation);
}
// Add null check before invoking onClick
options.onClick?.();
});
}
if (options.hoverAnimation || options.hoverColor) {
this.buttonElement.addEventListener("mouseenter", () => {
if (options.hoverAnimation) {
applyAnimation(this.buttonElement, options.hoverAnimation);
}
if (options.hoverColor) {
this.buttonElement.style.backgroundColor = options.hoverColor;
}
});
this.buttonElement.addEventListener("mouseleave", () => {
if (options.hoverAnimation) {
this.buttonElement.style.animation = "none";
}
if (options.hoverColor) {
this.buttonElement.style.backgroundColor = options.color || "#007bff";
}
});
}
}
setText(text: string): void {
this.buttonElement.textContent = text;
}
setColor(color: string): void {
this.buttonElement.style.backgroundColor = color;
}
}
The framework also includes a templating system for rendering dynamic content, and an animation system (referenced in button.ts) for adding visual effects to UI elements.
// templating.ts
export function renderTemplate(
template: string,
data: { [key: string]: any }
): string {
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => data[key] || "");
}
Complex Example:
// emailClientExample.ts
import { Container, View, Button, Form, List } from "../index";
import { initStyles, appendStyles } from "../styles";
interface Email {
id: number;
from: string;
subject: string;
body: string;
read: boolean;
}
class EmailItem extends View {
constructor(email: Email, onSelect: (id: number) => void) {
super({
className: `email-item ${email.read ? "read" : "unread"}`,
template: `
<div class="email-sender">${email.from}</div>
<div class="email-subject">${email.subject}</div>
`,
});
this.element.addEventListener("click", () => onSelect(email.id));
}
}
class EmailView extends View {
constructor(email: Email | null) {
super({
className: "email-view",
template: email
? `
<h2>${email.subject}</h2>
<p><strong>From:</strong> ${email.from}</p>
<div class="email-body">${email.body}</div>
`
: "<p>Select an email to view</p>",
});
}
}
export function createEmailClientExample(): View {
initStyles();
appendStyles(emailClientCSS);
const mainContainer = new Container({
className: "email-client",
gap: "20px",
});
// Header
const header = new View({
className: "header",
template: "<h1>Simple Email Client</h1>",
});
mainContainer.addChild(header);
// Main content area
const content = new Container({
className: "content",
gap: "20px",
columns: "150px 300px 1fr", // Define three columns
});
mainContainer.addChild(content);
// Sidebar (Folders)
const sidebar = new Container({
className: "sidebar",
gap: "10px",
equalWidthChildren: true,
});
["Inbox", "Sent", "Drafts", "Trash"].forEach((folder) => {
sidebar.addChild(
new Button({
text: folder,
hoverColor: "#0056b3",
hoverAnimation: { type: "grow", duration: 300 },
clickAnimation: { type: "push", duration: 200 },
onClick: () => console.log(`Switched to ${folder}`),
})
);
});
content.addChild(sidebar);
// Email list
const emailListContainer = new Container({
className: "email-list-container",
gap: "10px",
});
content.addChild(emailListContainer);
const emailList = new List({ className: "email-list" });
emailListContainer.addChild(emailList);
// Compose button
const composeButton = new Button({
text: "Compose",
onClick: () => showComposeForm(),
});
emailListContainer.addChild(composeButton);
// Email view
const emailViewContainer = new Container({
className: "email-view-container",
gap: "10px",
});
content.addChild(emailViewContainer);
let currentEmailView = new EmailView(null);
emailViewContainer.addChild(currentEmailView);
// Sample emails
const emails: Email[] = [
{
id: 1,
from: "[email protected]",
subject: "Hello",
body: "Hi there!",
read: false,
},
{
id: 2,
from: "[email protected]",
subject: "Meeting tomorrow",
body: "Don't forget our meeting.",
read: true,
},
{
id: 3,
from: "[email protected]",
subject: "Urgent: Report needed",
body: "Please send the report ASAP.",
read: false,
},
];
function renderEmails() {
emailList.clear();
emails.forEach((email) => {
emailList.addItem(new EmailItem(email, selectEmail));
});
}
function selectEmail(id: number) {
const email = emails.find((e) => e.id === id);
if (email) {
email.read = true;
currentEmailView = new EmailView(email);
emailViewContainer.clear();
emailViewContainer.addChild(currentEmailView);
renderEmails(); // Re-render to update read/unread status
}
}
function showComposeForm() {
const composeForm = new Form({
fields: [
{ type: "text", name: "to", label: "To:", required: true },
{ type: "text", name: "subject", label: "Subject:", required: true },
{ type: "textarea", name: "body", label: "Message:", required: true },
],
onSubmit: (data) => {
console.log("Sending email:", data);
// Here you would typically send the email and update the sent folder
emailViewContainer.clear();
emailViewContainer.addChild(new EmailView(null));
},
});
emailViewContainer.clear();
emailViewContainer.addChild(composeForm);
}
renderEmails();
return mainContainer;
}
const emailClientCSS = `
.email-client {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.content {
display: grid;
grid-template-columns: 150px 300px 1fr;
gap: 20px;
}
.sidebar .ui-button button {
width: 100%;
text-align: left;
padding: 10px;
}
.email-list-container {
overflow-y: auto;
max-height: 80vh;
}
.email-item {
padding: 10px;
border-bottom: 1px solid #40444B;
cursor: pointer;
}
.email-item:hover {
background-color: #40444B;
}
.email-item.unread {
font-weight: bold;
}
.email-sender {
font-size: 0.9em;
}
.email-subject {
font-size: 1em;
}
.email-view {
background-color: #40444B;
padding: 20px;
border-radius: 4px;
}
.email-body {
margin-top: 20px;
white-space: pre-wrap;
}
`;
This example creates a more complex task management application using various components and demonstrating their interactions.
Adding and Using Plugins
The framework includes a plugin system that allows for extending its functionality.
// plugin-system.ts
/**
* Interface for plugins that can be registered with the PluginManager.
*/
interface Plugin {
readonly name: string;
install(framework: any): void;
}
To add a plugin:
- Create a plugin object that implements the
Plugin
interface:
const myPlugin: Plugin = {
name: 'myPlugin',
install(framework: any) {
// Add new functionality to the framework
framework.newMethod = () => console.log('New method added by plugin');
}
};
- Register the plugin with the framework:
import { framework } from './lib/framework';
framework.registerPlugin(myPlugin);
- Use the new functionality provided by the plugin:
framework.newMethod(); // Outputs: "New method added by plugin"
This plugin system allows for modular extension of the framework's capabilities without modifying its core code. Here is a state management plugin:
import { Plugin } from '../plugin-system';
type Reducer<S, A> = (state: S, action: A) => S;
type Listener = () => void;
type Unsubscribe = () => void;
interface Store<S, A> {
getState: () => S;
dispatch: (action: A) => void;
subscribe: (listener: Listener) => Unsubscribe;
}
const stateManagementPlugin: Plugin = {
name: 'StateManagementPlugin',
install(framework) {
framework.createStore = function<S, A>(reducer: Reducer<S, A>, initialState: S): Store<S, A> {
let state = initialState;
const listeners: Listener[] = [];
const store: Store<S, A> = {
getState: () => state,
dispatch: (action: A) => {
state = reducer(state, action);
listeners.forEach(listener => listener());
},
subscribe: (listener: Listener): Unsubscribe => {
listeners.push(listener);
return () => {
const index = listeners.indexOf(listener);
if (index > -1) listeners.splice(index, 1);
};
}
};
return store;
};
framework.View.prototype.connectToStore = function<S, P>(store: Store<S, any>, mapStateToProps: (state: S) => P) {
const updateView = () => {
const stateProps = mapStateToProps(store.getState());
Object.assign(this, stateProps);
this.render();
};
const unsubscribe = store.subscribe(updateView);
updateView();
// Clean up subscription when view is unmounted
const originalUnmount = this.unmount;
this.unmount = () => {
unsubscribe();
originalUnmount.call(this);
};
};
}
};
export default stateManagementPlugin;
This plugin adds a Redux-like state management system to the framework:
- It defines a createStore function that creates a store with getState, dispatch, and subscribe methods.
- The connectToStore method is added to the View prototype, allowing views to easily connect to the store and update when the state changes.
- It uses the reducer pattern to update the state immutably.
- The plugin handles subscription cleanup when a view is unmounted.