https://github.com/nabeelalihashmi/nthjs
Signals Based Minimal Modular JavaScript Reactivity
https://github.com/nabeelalihashmi/nthjs
blade blade-template dom frontend javascript javascript-library laravel library reactive reactivity signals
Last synced: 2 months ago
JSON representation
Signals Based Minimal Modular JavaScript Reactivity
- Host: GitHub
- URL: https://github.com/nabeelalihashmi/nthjs
- Owner: nabeelalihashmi
- Created: 2025-03-13T15:38:26.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2025-03-14T07:27:04.000Z (over 1 year ago)
- Last Synced: 2025-10-24T11:47:22.845Z (9 months ago)
- Topics: blade, blade-template, dom, frontend, javascript, javascript-library, laravel, library, reactive, reactivity, signals
- Language: JavaScript
- Homepage: https://nabeelalihashmi.github.io/nthjs/
- Size: 19.5 KB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# NthJS - Minimal Modular JavaScript Reactivity Starter Kit Library
NthJS is a lightweight, dependency-free javascript reactivity and reactive dom library. It provides fine-grained reactivity without a build step, making it an minimal alternative to frameworks like Vue or Svelte for smaller projects. It is great companion for PHP projects. It is ~1.4kb minified+gzipped.
## Main Idea
NthJS provides a minimal yet solid base to make your own reactive library according to your needs. Just edit `nthjs.js` file, This is <250 loc. Remove functions/features you don't want or add functions/features you want, such as automatic binding of data attributes etc.
## Motivation for NthJS
I needed simple reactivity many time in frontend, but didn’t want the overhead of a build step. Neither I wanted to use Vue or other those have many features I never needed. I wanted something lightweight, yet powerful - something that took inspiration from Vue and Svelte, including features like watchers and group bindings, without unnecessary bloat. That’s why nthJS is born. It’s minimal, modular, and designed to keep reactivity purely in JavaScript, avoiding extra markup in the DOM. While it focuses on a JS-first approach, it can be easily extended to support data-attribute-based bindings if needed.
## Features
- **Signals & Effects**: Reactive state with automatic dependency tracking.
- **Computed Values**: Derived state that updates when dependencies change.
- **Watchers**: Listen for state changes and trigger callbacks.
- **DOM Bindings**: Bind state to the DOM with `createBinding`, `createListBinding`, and `createGroupBinding`.
- **Async Support**: Fetch data reactively with `createAsyncSignal`.
## Installation
Simply include the `nthjs.js` file in your project via cdn.
```
```
or download and use.
## Complete Example
```
NthJS Demo
body {
font-family: Arial, sans-serif;
margin: 20px;
background: #f4f4f4;
text-align: center;
}
h1 {
color: #333;
}
.container {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
display: inline-block;
}
.counter {
font-size: 24px;
margin: 10px 0;
}
button {
padding: 10px 15px;
margin: 5px;
font-size: 16px;
cursor: pointer;
border: none;
border-radius: 5px;
transition: 0.3s;
}
button:hover {
background: #ddd;
}
ul {
list-style: none;
padding: 0;
}
li {
padding: 8px;
background: #ddd;
margin: 5px 0;
border-radius: 5px;
}
.loading {
color: orange;
}
.error {
color: red;
}
NthJS Demo
Counter
0
Increment
Decrement
Name Input
Hello, !
Sum of Two Numbers
+
Sum:
Dynamic List
Add Item
Favorite Color
Red
Blue
Green
Selected Color:
Hobbies
Reading
Gaming
Cooking
Selected Hobbies:
Fetch Data
Load Data
Loading...
// Counter
const count = createSignal(0);
createBinding(document.getElementById("count"), count);
document.getElementById("inc").addEventListener("click", () => count.value++);
document.getElementById("dec").addEventListener("click", () => count.value--);
// Name Input
const name = createSignal("User");
createBinding(document.getElementById("nameInput"), name, "value");
createBinding(document.getElementById("nameDisplay"), name);
// Computed Sum
const num1 = createSignal(0);
const num2 = createSignal(0);
const sum = createComputed(() => parseInt(num1.value) + parseInt(num2.value));
createBinding(document.getElementById("sum"), sum);
document.getElementById("num1").addEventListener("input", e => num1.value = e.target.value);
document.getElementById("num2").addEventListener("input", e => num2.value = e.target.value);
// Dynamic List
const items = createSignal([]);
createListBinding(document.getElementById("list"), items, item => {
const li = document.createElement("li");
li.textContent = item.text;
return li;
});
document.getElementById("addItem").addEventListener("click", () => {
items.value = [...items.value, { key: Date.now(), text: "Item " + (items.value.length + 1) }];
});
// Group Binding (Radio)
const selectedColor = createSignal("red");
createGroupBinding(document.querySelectorAll("input[name=color]"), selectedColor);
createBinding(document.getElementById("selectedColor"), selectedColor);
// Group Binding (Checkbox)
const hobbies = createSignal([]);
createGroupBinding(document.querySelectorAll("input[type=checkbox]"), hobbies);
createBinding(document.getElementById("hobbies"), hobbies);
// Async Signal (Fixed)
const [dataSignal, fetchData, isLoading, errorSignal] = createAsyncSignal(null, async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/todos/1');
if (!res.ok) throw new Error("Failed to fetch data!");
return res.json();
});
createEffect(() => {
document.getElementById("loading").style.display = isLoading.value ? "block" : "none";
document.getElementById("error").textContent = errorSignal.value || "";
document.getElementById("dataDisplay").textContent = dataSignal.value ?
`To-Do: ${dataSignal.value.title}` : "";
});
document.getElementById("fetchData").addEventListener("click", fetchData);
```
## API Documentation
### `createSignal(initialValue)`
Creates a reactive signal.
#### Parameters:
- `initialValue`: Any value (primitive or object).
#### Returns:
A signal object with:
- `.value`: **Getter & setter** for the reactive state.
#### Usage:
```js
const count = createSignal(0);
console.log(count.value); // 0
count.value = 5;
console.log(count.value); // 5
```
---
### `createEffect(effectFn)`
Runs an effect whenever its dependencies change.
#### Parameters:
- `effectFn`: A function that depends on reactive signals.
#### Usage:
```js
const count = createSignal(0);
createEffect(() => {
console.log("Count changed:", count.value);
});
count.value = 10; // Logs "Count changed: 10"
```
---
### `createComputed(computeFn)`
Creates a computed value that updates when dependencies change.
#### Parameters:
- `computeFn`: A function returning a derived value.
#### Returns:
A signal containing the computed value.
#### Usage:
```js
const a = createSignal(2);
const b = createSignal(3);
const sum = createComputed(() => a.value + b.value);
console.log(sum.value); // 5
a.value = 10;
console.log(sum.value); // 13
```
---
### `watch(signal, callback)`
Watches a signal and executes a callback when its value changes.
#### Parameters:
- `signal`: The signal to watch.
- `callback`: Function `(newValue, oldValue) => {}`.
#### Usage:
```js
const name = createSignal("Alice");
watch(name, (newVal, oldVal) => {
console.log(`Name changed from ${oldVal} to ${newVal}`);
});
name.value = "Bob"; // Logs: "Name changed from Alice to Bob"
```
---
### `createBinding(element, signal, attribute?)`
Binds a signal to a DOM element.
#### Parameters:
- `element`: The target DOM element.
- `signal`: The reactive signal.
- `attribute?`: Optional attribute to bind.
#### Usage:
```js
const title = createSignal("Hello");
const h1 = document.querySelector("h1");
createBinding(h1, title); // Updates text content
const input = document.querySelector("input");
createBinding(input, title, "value"); // Two-way binding
```
---
### `createListBinding(container, signal, renderItem)`
Binds a signal array to a container element, rendering child elements.
#### Parameters:
- `container`: Parent element.
- `signal`: Reactive array signal.
- `renderItem`: Function `(item) => HTMLElement`.
#### Usage:
```js
const items = createSignal([{ key: "1", text: "Item 1" }]);
const ul = document.querySelector("ul");
createListBinding(ul, items, (item) => {
const li = document.createElement("li");
li.textContent = item.text;
return li;
});
items.value = [...items.value, { key: "2", text: "Item 2" }];
```
---
### `createGroupBinding(elements, signal)`
Binds a group of radio buttons or checkboxes to a signal.
#### Parameters:
- `elements`: Array of input elements.
- `signal`: Reactive signal.
#### Usage:
**For radio buttons:**
```js
const selected = createSignal("option1");
const radios = document.querySelectorAll("input[type=radio]");
createGroupBinding(radios, selected);
```
**For checkboxes:**
```js
const selectedValues = createSignal(["a"]);
const checkboxes = document.querySelectorAll("input[type=checkbox]");
createGroupBinding(checkboxes, selectedValues);
```
---
## Async Support
### `createAsyncSignal(initialValue, fetcher)`
Creates a reactive signal that updates from an async function.
#### Parameters:
- `initialValue`: Default value.
- `fetcher`: Async function `(signal) => Promise`.
#### Usage:
```js
const [data, fetchData, loading, error] = createAsyncSignal(null, async () => {
const response = await fetch("https://api.example.com/data");
return response.json();
});
createEffect(() => {
if (loading.value) console.log("Loading...");
if (data.value) console.log("Data loaded:", data.value);
if (error.value) console.log("Error:", error.value);
});
// Trigger fetch
fetchData();
```
---
## Why Use NthJS?
- **No build step**: Works directly in the browser.
- **Tiny & fast**: Minimal overhead, designed for simplicity.
- **Fine-grained reactivity**: Updates only the necessary parts of the DOM.
---
## License
MIT