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.

Why MelodiJS?
  • 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' }
}
Root Component Templates
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>
Always use :key
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 begins
  • mounted: After component is mounted
  • unmounted: 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;
    }
  }
});
Array Mutations
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';
  }
};