Introduction
MelodiJS is a progressive, ultra-lightweight JavaScript framework for building user interfaces. It combines the intuitive Options API of Vue.js with a fine-grained reactivity system inspired by SolidJS.
- No Virtual DOM: Updates are surgical and target exact DOM nodes.
- Tiny Footprint: ~3KB gzipped core.
- Familiar API: If you know Vue, you know MelodiJS.
- Reactive Arrays: Array mutations automatically trigger updates.
- Built-in Router & Store: Everything you need out of the box.
Quick Start
Get started with MelodiJS in seconds using a CDN or npm.
Via CDN
<script type="module">
import { createApp } from 'https://unpkg.com/melodijs';
createApp({
data: () => ({ message: 'Hello Melodi!' })
}).mount('#app')
</script>
Via NPM
npm install melodijs
import { createApp } from 'melodijs';
const app = createApp({
data: () => ({ count: 0 }),
methods: {
increment() { this.count++ }
}
});
app.mount('#app');
Creating an Application
MelodiJS supports two ways to create applications:
Root Component API (Recommended)
Define your app as a single root component with data, methods, computed, watch, etc.
const app = createApp({
data: () => ({
message: 'Hello World',
count: 0
}),
computed: {
doubleCount() {
return this.count * 2;
}
},
methods: {
increment() {
this.count++;
}
}
});
app.mount('#app');
Component Registration (Legac y)
Register components that will automatically mount to matching HTML elements.
createApp({
components: {
'my-component': {
data: () => ({ message: 'Hello' }),
template: '<div>{{ message }}</div>'
}
}
}).mount('#app');
Template Syntax
Text Interpolation
<span>Message: {{ msg }}</span>
Attribute Binding
<div v-bind:id="dynamicId"></div>
<!-- Shorthand -->
<div :id="dynamicId"></div>
JavaScript Expressions
{{ number + 1 }}
{{ ok ? 'YES' : 'NO' }}
{{ message.split('').reverse().join('') }}
Template Options
MelodiJS provides three ways to define component templates:
1. Inline String
{
template: '<div>{{ message }}</div>'
}
2. From Element (ID Selector)
<script type="text/template" id="my-template">
<div>{{ message }}</div>
</script>
{
template: { el: '#my-template' }
}
3. From URL
{
template: { url: '/templates/my-component.html' }
}
When using the root component API without a template property, MelodiJS uses the innerHTML of the mount target as the template.
Reactivity Fundamentals
Declare reactive state using the data function:
data: () => ({
count: 0,
message: 'Hello',
items: [1, 2, 3]
})
Array Reactivity
MelodiJS automatically tracks array mutations:
this.items.push(4); // ✅ Reactive
this.items.pop(); // ✅ Reactive
this.items.splice(0, 1); // ✅ Reactive
this.items[0] = 99; // ✅ Reactive
Computed Properties
Computed properties are cached and only re-evaluate when dependencies change:
computed: {
fullName() {
return `${this.firstName} ${this.lastName}`;
},
total() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
Watchers
React to data changes with watchers:
Simple Watcher
watch: {
username(newVal, oldVal) {
console.log(`Username changed: ${oldVal} → ${newVal}`);
}
}
Watcher with Options
watch: {
userId: {
handler(newVal) {
this.loadUserData(newVal);
},
immediate: true // Run on mount
}
}
Class & Style Bindings
Dynamic Classes
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
Dynamic Styles
<div :style="{ color: textColor, fontSize: fontSize + 'px' }"></div>
Conditional Rendering
v-if
<h1 v-if="awesome">MelodiJS is awesome!</h1>
v-show
<h1 v-show="ok">Hello!</h1>
The difference: v-show toggles CSS display, v-if removes/adds
elements.
List Rendering
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index }}: {{ item.name }}
</li>
</ul>
Providing a unique
:key improves performance and prevents rendering bugs.
Event Handling
Inline Handlers
<button @click="count++">Increment</button>
Method Handlers
<button @click="handleClick">Click Me</button>
Event with Parameters
<button @click="say('Hello')">Say Hello</button>
Form Input Bindings
Use v-model for two-way data binding:
Text Input
<input v-model="message" />
Checkbox
<input type="checkbox" v-model="checked" />
Select
<select v-model="selected">
<option value="a">A</option>
<option value="b">B</option>
</select>
Component Basics
createApp({
components: {
'button-counter': {
data: () => ({ count: 0 }),
template: `
<button @click="count++">
Clicked {{ count }} times
</button>`
}
}
}).mount('#app');
Props
// Child component
{
props: ['title', 'likes'],
template: '<h3>{{ title }} ({{ likes }}))</h3>'
}
// Parent usage
<blog-post title="My Post" :likes="42"></blog-post>
Component Events
// Child emits event
methods: {
submit() {
this.$emit('submit', this.formData);
}
}
// Parent listens
<my-form @submit="handleSubmit"></my-form>
Slots
// Component template
<button class="btn">
<slot></slot>
</button>
// Usage
<my-button>Click Me</my-button>
Lifecycle Hooks
beforeMount: Before mounting beginsmounted: After component is mountedunmounted: After component is unmounted
hooks: {
mounted() {
console.log('Component mounted!');
}
}
Router
MelodiJS includes a built-in router for Single Page Applications:
Setup
import { createApp } from 'melodijs';
import { MelodiRouter } from 'melodijs/router';
const router = new MelodiRouter({
routes: [
{ path: '/', component: Home },
{ path: '/about', component: About }
]
});
const app = createApp({ /* ... */ });
app.use(router);
app.mount('#app');
Navigation
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
<router-view></router-view>
Dynamic Route Matching
We often need to map routes with the same component to different URLs. In MelodiJS, we can use a dynamic segment in the path to achieve that.
const router = new MelodiRouter({
routes: [
// dynamic segments start with a colon
{ path: '/user/:id', component: User }
]
})
Now, URLs like /user/foo and /user/bar will both map to the same route.
A dynamic segment is denoted by a colon :. When a route is matched, the value of the dynamic
segments will be exposed as this.$router.params() in every component.
const User = {
template: '<div>User {{ $router.params().id }}</div>',
hooks: {
mounted() {
console.log(this.$router.params().id)
}
}
}
Programmatic Navigation
Aside from using <router-link> for declarative navigation, you can do this
programmatically using the router instance.
// literal string path
this.$router.push('/home')
// with params
this.$router.push('/user/123')
Nested Routes
Real applications often have multiple levels of nesting. MelodiJS router supports nested routes with the
children option:
const router = new MelodiRouter({
routes: [
{
path: '/user/:id',
component: User,
children: [
{ path: 'profile', component: UserProfile },
{ path: 'posts', component: UserPosts }
]
}
]
})
The User component will need to include a <router-view> for the nested
routes:
const User = {
template: `
<div>
<h2>User {{ $router.params().id }}</h2>
<router-view></router-view>
</div>
`
}
Navigation Guards
Use navigation guards to control navigation flow, such as authentication checks:
router.beforeEach((to, from, next) => {
// Check authentication
if (to === '/admin' && !isAuthenticated()) {
next('/login'); // Redirect to login
} else {
next(); // Proceed with navigation
}
});
Accessing Route Information
Components can access current route information:
// Current path
this.$router.currentRoute() // => '/user/123'
// Route parameters
this.$router.params() // => { id: '123' }
// Query parameters
this.$router.query() // => { tab: 'profile' }
Transitions
Add CSS transitions to elements with v-if:
CSS Classes
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.5s;
}
Usage
<transition name="fade">
<div v-if="show">Hello</div>
</transition>
State Management (MelodiStore)
Centralized state management with MelodiStore:
Setup
import { MelodiStore } from 'melodijs/store';
const store = new MelodiStore({
state: () => ({
count: 0,
user: null,
items: []
}),
actions: {
increment() {
this.state.count++;
},
setUser(user) {
this.state.user = user;
},
addItem(item) {
this.state.items.push(item);
}
},
getters: {
double: (state) => state.count * 2,
itemCount: (state) => state.items.length
}
});
app.use(store);
Using in Components
// Access state
{{ $store.state.count }}
// Use getters
{{ $store.getters.double }}
// Dispatch actions
this.$store.dispatch('increment');
Actions with Parameters
Actions can accept parameters and access other actions via this.dispatch:
actions: {
incrementBy(amount) {
this.state.count += amount;
},
async fetchUser(userId) {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
this.dispatch('setUser', user);
}
}
Deep Reactivity
MelodiStore supports deep reactivity for nested objects and arrays:
const store = new MelodiStore({
state: () => ({
user: {
profile: {
name: 'John',
age: 30
}
},
todos: [
{ id: 1, text: 'Learn MelodiJS', done: false }
]
}),
actions: {
updateUserName(name) {
// Deep reactivity - UI updates automatically
this.state.user.profile.name = name;
},
toggleTodo(id) {
const todo = this.state.todos.find(t => t.id === id);
if (todo) todo.done = !todo.done;
}
}
});
All array mutations (
push, pop, splice, etc.) are tracked automatically
and trigger reactive updates.
Plugin System
Extend MelodiJS with plugins:
Using Plugins
import { createApp } from 'melodijs';
import { MelodiRouter } from 'melodijs/router';
const app = createApp({ /* ... */ });
app.use(myPlugin, { /* options */ });
app.mount('#app');
Creating Plugins
const myPlugin = {
install(app, options) {
// Add global components
app.components['my-component'] = { /* ... */ };
// Add to app instance
app.myProperty = 'value';
}
};