NoUI is a lightweight, pure JavaScript framework for building traditional web applications (non-SPA) with server-side routing. It leverages Web Components, internationalization (i18n), and two state management solutions: a local reactive createState and a global centralized createStore (inspired by Zustand). Designed for simplicity and readability, NoUI requires no build tools and works by including two scripts in your HTML.
- Server-side routing: Clean URLs (
/,/about,/contact) without.html, noCannot GETerrors on refresh. - Web Components: Reusable, encapsulated UI components.
- Internationalization (i18n): Multi-language support with configurable JSON-based translations.
- Dual state management:
createState: Local, reactive state for simple, isolated components.createStore: Global, centralized state with actions and middleware support, inspired by Zustand.
- No build required: Pure JavaScript, works with
<script>tags. - Readable code: Full, descriptive variable and method names (e.g.,
registerComponent,translations). - English comments: All code comments are in English for international accessibility.
Get started with NoUI in 5 minutes.
- A web server (e.g., Python's
http.server, Apache, or any static file server). - A modern browser with Web Components support (polyfill included via CDN).
-
Create project structure:
noui/ ├── assets/ │ └── locale/ │ ├── en.json │ ├── az.json │ ├── ru.json ├── noui.js ├── components.js ├── index.html ├── about.html ├── contact.html ├── server.py -
Download files:
- Copy
noui.js,components.js,index.html,about.html,contact.html, andassets/locale/*.jsonfrom the provided code (see project repository or code snippets). - Configure translations in each HTML file (e.g.,
index.html):<script> // Initialize NoUI with translation configuration window.noUI = new NoUI({ langs: ["en", "az", "ru"], localePath: "assets/locale", defaultLang: "en" }); </script>
- Create
server.pyfor local testing:import http.server import socketserver PORT = 8000 class Handler(http.server.SimpleHTTPRequestHandler): def do_GET(self): if self.path == "/": self.path = "/index.html" elif self.path == "/about": self.path = "/about.html" elif self.path == "/contact": self.path = "/contact.html" return http.server.SimpleHTTPRequestHandler.do_GET(self) with socketserver.TCPServer(("", PORT), Handler) as httpd: print(f"Serving at http://localhost:{PORT}") httpd.serve_forever()
- Copy
-
Run the server:
python3 server.py
-
Open in browser:
- Visit
http://localhost:8000/. - Navigate to
/aboutand/contact. - Test features: click buttons, switch languages, check global state persistence in
localStorage.
- Visit
-
Verify:
- Home page (
/): Shows "Welcome to Home". - About page (
/about): Shows "About Us". - Contact page (
/contact): Shows global state (count, message) with middleware logging and persistence. - Refresh any page → No
Cannot GETerrors. - Language switcher: Changes text across pages.
- Console: Logs state changes (via
loggerMiddleware). localStorage: Stores global state (viapersistMiddleware).
- Home page (
noui.js: Core framework (classNoUI,createState,createStore, middleware).components.js: Web Components and page logic.index.html,about.html,contact.html: HTML files for each route, with translation configuration.assets/locale/*.json: Translation files (en.json,az.json,ru.json).server.py: Optional Python server for local testing.
Configure translations in each HTML file via <script> before loading components.js:
<script src="noui.js"></script>
<script>
// Initialize NoUI with translation configuration
window.noUI = new NoUI({
langs: ["en", "az", "ru"], // List of languages
localePath: "assets/locale", // Path to translation files
defaultLang: "en" // Default language
});
</script>
<script src="components.js"></script>langs: Array of language codes (e.g.,["en", "fr"]).localePath: Directory or URL for.jsonfiles (e.g.,assets/locale/en.json).defaultLang: Fallback language if none saved inlocalStorage.
To support clean URLs (/about instead of /about.html), configure your server:
Use server.py (see Quick Start) to map:
/→index.html/about→about.html/contact→contact.html
Run:
python3 server.pyCreate .htaccess in the noui/ folder:
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^about$ about.html [L]
RewriteRule ^contact$ contact.html [L]
RewriteRule ^$ index.html [L]
Ensure Apache has mod_rewrite enabled.
- Nginx:
server { listen 80; root /path/to/noui; index index.html; location / { try_files $uri $uri.html /index.html; } }
- Node.js (Express):
const express = require('express'); const app = express(); app.use(express.static('noui')); app.get('/about', (req, res) => res.sendFile('about.html', { root: 'noui' })); app.get('/contact', (req, res) => res.sendFile('contact.html', { root: 'noui' })); app.get('/', (req, res) => res.sendFile('index.html', { root: 'noui' })); app.listen(8000);
- Web Components polyfill (loaded via CDN in HTML):
<script src="https://unpkg.com/@webcomponents/webcomponentsjs@2.8.0/webcomponents-bundle.js"></script>
- No other dependencies required.
Each page in NoUI corresponds to an HTML file (index.html, about.html, etc.) and a component in components.js.
-
Create HTML file (e.g.,
blog.html):<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>NoUI Demo - Blog</title> <script src="https://unpkg.com/@webcomponents/webcomponentsjs@2.8.0/webcomponents-bundle.js"></script> </head> <body> <div id="app"></div> <script src="noui.js"></script> <script> // Initialize NoUI with translation configuration window.noUI = new NoUI({ langs: ["en", "az", "ru"], localePath: "assets/locale", defaultLang: "en" }); </script> <script src="components.js"></script> </body> </html>
-
Add component in
components.js:// Blog page component const BlogComponent = { render(element) { element.innerHTML = ` <no-lang-switcher></no-lang-switcher> <no-header></no-header> <div> <h2>${noUI.t("blog.title")}</h2> <p>${noUI.t("blog.description")}</p> <a href="/">Go to Home</a> </div> <no-footer></no-footer>`; } };
-
Update routes in
components.js(inrenderPage):// Route configuration const routes = { "/": HomeComponent, "/about": AboutComponent, "/contact": ContactComponent, "/blog": BlogComponent };
-
Update translations (e.g.,
en.json):{ "blog.title": "Blog", "blog.description": "This is the blog page." } -
Update server:
- In
server.py:elif self.path == "/blog": self.path = "/blog.html"
- In
.htaccess:RewriteRule ^blog$ blog.html [L]
- In
-
Test:
- Visit
http://localhost:8000/blog. - Verify content, components, and translations.
- Visit
NoUI uses Web Components for reusable UI elements.
-
Define component in
components.js:// Sidebar component const SidebarComponent = { render(element) { element.innerHTML = ` <div> <h3>Sidebar</h3> <p>${noUI.t("sidebar.text")}</p> </div>`; } };
-
Register component:
// Register Sidebar component noUI.registerComponent("no-sidebar", SidebarComponent);
-
Use in pages (e.g., in
HomeComponent):// Home page component const HomeComponent = { render(element) { element.innerHTML = ` <no-lang-switcher></no-lang-switcher> <no-header></no-header> <no-sidebar></no-sidebar> <div> <h2>${noUI.t("home.title")}</h2> <p>${noUI.t("home.description")}</p> </div> <no-footer></no-footer>`; } };
-
Add translations (e.g.,
en.json):{ "sidebar.text": "This is the sidebar." } -
Test:
- Open a page using the component.
- Verify rendering and translations.
Use createState for local state:
// Counter component with local state
const CounterComponent = {
render(element) {
const state = noUI.constructor.createState(0);
const update = () => {
element.innerHTML = `
<div>
<p>Count: ${state.value}</p>
<button id="increment">Increment</button>
</div>`;
element.querySelector("#increment")?.addEventListener("click", () => state.value++);
};
state.subscribe(update);
update();
}
};
// Register Counter component
noUI.registerComponent("no-counter", CounterComponent);NoUI offers two state management solutions: createState (local, reactive) and createStore (global, centralized, with middleware support).
-
Purpose: Simple, isolated state for individual components.
-
API:
createState(initialValue): Creates a reactive state object.state.value: Get or set the current value.state.subscribe(listener): Subscribe to changes.
-
Example:
// Create a reactive state const state = noUI.constructor.createState(0); state.subscribe((value) => console.log(`Count: ${value}`)); state.value = 1; // Logs: Count: 1
-
Use case:
// Header component with local state const HeaderComponent = { render(element) { const state = noUI.constructor.createState(0); const update = () => { element.innerHTML = ` <p>Count: ${state.value}</p> <button id="increment">Increment</button>`; element.querySelector("#increment")?.addEventListener("click", () => state.value++); }; state.subscribe(update); update(); } };
-
Purpose: Centralized state for sharing data across components, with actions and middleware.
-
API:
createStore(createState, middlewares = []): Creates a store with state, actions, and optional middleware.store.getState(): Returns current state.store.setState(partial): Updates state (object or function).store.subscribe(listener): Subscribes to state changes.store.actionName(): Calls an action (defined increateState).
-
Example:
// Create a global store with middleware const useStore = createStore( (set, get) => ({ state: { count: 0, text: "Hello" }, actions: { increment: () => set({ count: get().count + 1 }), updateText: (text) => set({ text }) } }), [noUIMiddlewares.loggerMiddleware] ); useStore.subscribe((state) => console.log(state)); useStore.increment(); // Logs previous and next state, then: { count: 1, text: "Hello" } useStore.updateText("Hi"); // Logs previous and next state, then: { count: 1, text: "Hi" }
-
Use case:
// Contact component with global state const ContactComponent = { render(element) { const update = () => { const { count, message } = useGlobalStore.getState(); element.innerHTML = ` <p>Global Count: ${count}</p> <button id="increment">Increment</button>`; element.querySelector("#increment")?.addEventListener("click", () => useGlobalStore.increment()); }; useGlobalStore.subscribe(update); update(); } };
-
Purpose: Intercept and modify state updates (e.g., logging, persistence, async handling).
-
API:
- Middleware is a function that receives
{ getState, setState, nextState }and returnsnextStateor nothing. - Passed as an array to
createStore(createState, middlewares).
- Middleware is a function that receives
-
Built-in Middleware:
noUIMiddlewares.loggerMiddleware: Logs previous and next state to console.noUIMiddlewares.persistMiddleware: Saves state tolocalStorage(default key:noUIState).
-
Example:
// Create a store with logging and persistence const useStore = createStore( (set, get) => ({ state: { count: 0 }, actions: { increment: () => set({ count: get().count + 1 }) } }), [noUIMiddlewares.loggerMiddleware, noUIMiddlewares.persistMiddleware] ); // Logs state changes and saves to localStorage useStore.increment();
-
Custom Middleware:
// Middleware for async actions const asyncMiddleware = ({ getState, setState, nextState }) => { if (nextState.asyncAction) { fetch('/api/data').then((data) => setState({ data })); return null; // Prevent immediate state update } return nextState; }; // Create a store with async middleware const useStore = createStore( (set, get) => ({ state: { data: null }, actions: { fetchData: () => set({ asyncAction: true }) } }), [asyncMiddleware] );
NoUI uses server-side routing with clean URLs (/, /about, /contact).
- Each route corresponds to an HTML file (
index.html,about.html,contact.html). - Server maps URLs to files (e.g.,
/about→about.html). components.jsrenders the appropriate component based onlocation.pathname:// Route configuration const routes = { "/": HomeComponent, "/about": AboutComponent, "/contact": ContactComponent }; noUI.renderPage(path, routes[path] || routes["/"]);
See "Creating Pages" section.
See "Setup" section for Python, Apache, Nginx, or Express configurations.
-
Constructor:
new NoUI(config)- Initializes components, translations, and MutationObserver.
- Stored in
window.noUI. config:langs: Array of language codes (default:[]).localePath: Path to translation files (default:assets/locale).defaultLang: Default language (default:en).
- Example:
// Initialize NoUI with translation configuration window.noUI = new NoUI({ langs: ["en", "fr"], localePath: "assets/translations", defaultLang: "en" });
-
Methods:
init(): Loads translations and sets up component scanning.loadTranslations(langs, localePath): Loads JSON translation files.- Example:
noUI.loadTranslations(["fr"], "assets/translations").
- Example:
setLanguage(lang): Sets the active language and saves tolocalStorage.- Example:
noUI.setLanguage("az").
- Example:
t(key): Returns translated string for the current language.- Example:
noUI.t("home.title")→ "Welcome to Home".
- Example:
subscribeToLanguageChange(callback): Subscribes to language changes.- Example:
noUI.subscribeToLanguageChange(() => render()).
- Example:
registerComponent(name, component): Registers a Web Component.- Example:
noUI.registerComponent("no-header", HeaderComponent).
- Example:
scanComponents(): Scans DOM for registered components and renders them.renderPage(path, component): Renders a component into#app.- Example:
noUI.renderPage("/about", AboutComponent).
- Example:
NoUI.createState(initialValue): Creates a local reactive state.- See "State Management" for details.
createStore(createState, middlewares = []): Creates a global state store with optional middleware.- See "State Management" for details.
noUIMiddlewares.loggerMiddleware: Logs state changes.noUIMiddlewares.persistMiddleware: Persists state tolocalStorage.- See "Middleware for
createStore" for details.
Load additional languages dynamically:
// Load French translations dynamically
noUI.loadTranslations(["fr"], "assets/translations").then(() => {
noUI.setLanguage("fr");
});Add fr.json to assets/translations/:
{
"home.title": "Bienvenue"
}Share useGlobalStore across all pages:
// Create a global store with persistence
const useGlobalStore = createStore(
(set, get) => ({
state: { theme: "light" },
actions: {
toggleTheme: () => set({ theme: get().theme === "light" ? "dark" : "light" })
}
}),
[noUIMiddlewares.persistMiddleware]
);
// Theme component using global state
const ThemeComponent = {
render(element) {
const update = () => {
const { theme } = useGlobalStore.getState();
element.innerHTML = `
<p>Theme: ${theme}</p>
<button id="toggle">Toggle Theme</button>`;
element.querySelector("#toggle")?.addEventListener("click", () => useGlobalStore.toggleTheme());
};
useGlobalStore.subscribe(update);
update();
}
};Avoid redundant renders by checking state changes:
// Optimized counter component
const CounterComponent = {
render(element) {
let lastValue = null;
const state = noUI.constructor.createState(0);
const update = () => {
if (state.value !== lastValue) {
lastValue = state.value;
element.innerHTML = `<p>Count: ${state.value}</p>`;
}
};
state.subscribe(update);
update();
}
};Create a middleware for async actions:
// Middleware for async actions
const asyncMiddleware = ({ getState, setState, nextState }) => {
if (nextState.asyncAction) {
fetch('/api/data').then((response) => response.json()).then((data) => setState({ data }));
return null;
}
return nextState;
};
// Create a store with async middleware
const useStore = createStore(
(set, get) => ({
state: { data: null },
actions: { fetchData: () => set({ asyncAction: true }) }
}),
[asyncMiddleware]
);Use a CDN for translations:
<script>
// Initialize NoUI with CDN translations
window.noUI = new NoUI({
langs: ["en", "fr"],
localePath: "https://cdn.example.com/translations",
defaultLang: "en"
});
</script>- Keep components small and focused.
- Group related components in
components.jswith comments:// UI Components const HeaderComponent = { ... }; const FooterComponent = { ... }; // Page Components const HomeComponent = { ... };
- Use
createStatefor local, isolated state (e.g., form inputs, counters). - Use
createStorefor shared state (e.g., user data, theme). - Define clear action names in
createStore:// Define clear actions actions: { incrementCount: () => set({ count: get().count + 1 }), setUserData: (user) => set({ user }) }
- Use middleware for cross-cutting concerns (logging, persistence).
- Minimize DOM updates by checking state changes (see "Optimizing Component Rendering").
- Use
noUI.scanComponents()only when necessary (automatically called inrenderPage). - Cache translation lookups:
// Cache translation function const t = noUI.t.bind(noUI); const title = t("home.title");
- Organize translations in nested objects for large apps:
Access:
{ "home": { "title": "Welcome to Home", "description": "This is the home page." } }noUI.t("home.title"). - Split
components.jsinto multiple files if it grows large (requires a build tool).
- Handle translation errors:
// Handle translation loading errors noUI.loadTranslations(["en"], "assets/locale").catch(() => console.error("Failed to load translations"));
- Check for missing
#app:// Verify #app element exists const main = document.querySelector("#app"); if (!main) throw new Error("No #app element found");
// Counter component with local state
const CounterComponent = {
render(element) {
const state = noUI.constructor.createState(0);
const update = () => {
element.innerHTML = `
<p>Count: ${state.value}</p>
<button id="increment">Increment</button>`;
element.querySelector("#increment")?.addEventListener("click", () => state.value++);
};
state.subscribe(update);
update();
}
};
// Register Counter component
noUI.registerComponent("no-counter", CounterComponent);// Create a global store with middleware
const useThemeStore = createStore(
(set, get) => ({
state: { theme: "light" },
actions: {
toggleTheme: () => set({ theme: get().theme === "light" ? "dark" : "light" })
}
}),
[noUIMiddlewares.loggerMiddleware, noUIMiddlewares.persistMiddleware]
);
// Theme component using global state
const ThemeComponent = {
render(element) {
const update = () => {
const { theme } = useThemeStore.getState();
element.innerHTML = `
<p>Theme: ${theme}</p>
<button id="toggle">Toggle Theme</button>`;
element.querySelector("#toggle")?.addEventListener("click", () => useThemeStore.toggleTheme());
};
useThemeStore.subscribe(update);
update();
}
};
// Register Theme component
noUI.registerComponent("no-theme", ThemeComponent);See "Creating Pages" section.
// Middleware for async actions
const asyncMiddleware = ({ getState, setState, nextState }) => {
if (nextState.asyncAction) {
fetch('/api/data').then((response) => response.json()).then((data) => setState({ data }));
return null;
}
return nextState;
};
// Create a store with async middleware
const useStore = createStore(
(set, get) => ({
state: { data: null },
actions: { fetchData: () => set({ asyncAction: true }) }
}),
[asyncMiddleware]
);- Page not found (
Cannot GET /contact):- Ensure server is configured (e.g.,
server.pyor.htaccess). - Verify HTML files exist (
contact.html).
- Ensure server is configured (e.g.,
- Translations not loading:
- Check
localePathandlangsin HTML configuration. - Verify
.jsonfiles exist inlocalePath. - Use an HTTP server (not
file://) to avoid CORS issues.
- Check
- Components not rendering:
- Verify
<div id="app">in HTML. - Check console for errors (e.g.,
customElements.define). - Ensure Web Components polyfill is loaded.
- Verify
- State not persisting:
- Check
localStoragefornoUIState. - Ensure
persistMiddlewareis included increateStore.
- Check
- Report issues or suggest features via the project repository.
- Keep code readable with full variable names.
- Use English for all comments and documentation.
- Avoid build tools to maintain simplicity.
MIT License. Use NoUI freely in your projects.