Skip to content

Repository files navigation

NS Framework

Language: English | Tiếng Việt | 日本語 | 中文

NS Framework is a tiny, lightweight mini-framework for Chrome Extensions, with a syntax close to Vue but with no build step required.

Key features

  • ⚡ No external dependencies
  • 🔄 Realtime binding via Proxy
  • 🛡️ Manifest V3 / CSP friendly
  • 🧩 Component support via ES Modules, props, slots (default + named)
  • 🔒 Scope isolation per component
  • 🧮 Computed properties (cached, auto-invalidated, setter support)
  • 👀 Watch, lifecycle hooks close to Vue
  • 🧠 Many directives close to Vue's syntax

Installation

npm

npm i ns-framework

Plain JS

Copy lib/ns.js into your extension's lib folder and load it in your HTML:

<script src="./lib/ns.js"></script>

Basic usage

<div id="app"></div>
<script src="./lib/ns.js"></script>
<script src="./popup.js"></script>
// popup.js
new NS({
  el: "#app",
  data: {
    title: "Hello NS",
    count: 0,
    isVisible: true
  },
  methods: {
    increment() {
      this.count += 1;
    }
  },
  mounted() {
    console.log("Ready");
  },
  template: `
    <div>
      <h1 ns-text="title"></h1>
      <p>Count: <span ns-text="count"></span></p>
      <button ns-click="increment">Increment</button>
      <div ns-show="isVisible">Visible block</div>
    </div>
  `
});

Supported directives

Text / HTML

<span ns-text="name"></span>
<p ns-html="message"></p>

The expression in a directive is only a path to a property (which can be nested), not an arbitrary JS expression:

<span ns-text="user.profile.name"></span>
<div ns-if="user?.profile?.name"></div> <!-- optional chaining "?." is supported, same as "." -->

ns-text / ns-if / ns-show / etc. cannot run a ternary or comparison like a ? b : c, count > 5 directly in the template - precompute that value with computed and bind to the resulting property instead.

Props and components

⚠️ Important: HTML always lowercases attribute names when parsed (this applies to .html files and to template strings assigned via innerHTML in .js files alike). Multi-word (camelCase) props declared in props must be written as kebab-case attributes when bound with :, otherwise the attribute name loses its casing and won't match the prop.

<ns-header title="WebBlock | MLight"></ns-header>
<ns-footer :app-config-info="appConfigInfo"></ns-footer>
// footer.js
export default {
  props: {
    title: {
      type: String,      // "type" is documentation only, it is NOT validated or coerced
      default: "MLight"  // "default" is applied when the parent doesn't pass a value
    },
    appConfigInfo: {
      type: Object
    }
  },
  template: `
    <div>
      <h1 ns-text="title"></h1>
    </div>
  `
};

Props can also be declared as an array of names (no default/type): props: ["title", "appConfigInfo"].

Object/array props share a reference with the parent: if you mutate a field inside the object directly (this.appConfigInfo.author.name = "x"), the change automatically propagates and re-renders correctly in the child component (including any computed that depends on it). But if the parent reassigns the whole prop to a new value (this.appConfigInfo = {...} with a brand-new object, or changes a string/number/boolean prop), the child component does not resync automatically - props are currently only read once, at mount time.

Computed

computed: {
  // simple getter form - cached, automatically recomputed when a dependency changes
  // (including nested paths and props shared by reference from a parent)
  isTextEmpty() {
    return !this.input.text.value;
  },
  // form with a setter - assigning to this.fullName calls set()
  fullName: {
    get() {
      return `${this.first} ${this.last}`;
    },
    set(value) {
      const [first, last] = value.split(" ");
      this.first = first;
      this.last = last;
    }
  }
}
<div ns-show="isTextEmpty">Empty</div>
<span ns-text="fullName"></span>

Watch

watch: {
  // fires when property "count" changes
  count(newValue, oldValue) {
    console.log(newValue, oldValue);
  },
  // "*" catches every property change (top-level and nested)
  "*": (newValue, oldValue) => {
    console.log("changed:", newValue, oldValue);
  }
}

Slots (default + named)

// child
template: `
  <div class="input-group">
    <slot name="before"></slot>
    <slot></slot>
    <slot name="after"></slot>
  </div>
`
<!-- parent -->
<my-input>
  <i slot="before" class="fa fa-search"></i>
  <span>Default content</span>
  <button slot="after">Clear</button>
</my-input>

ns-slot="before" can be used instead of <slot name="before"> if you want the slot outlet to be a regular element instead of a <slot> tag.

Emit and custom events on components

// child component
methods: {
  save() {
    this.$emit("saved", { ok: true });
  }
}
<!-- parent: bind @event="handler" (recommended) -->
<child-component @saved="onSaved"></child-component>
// parent
methods: {
  onSaved(payload) {
    console.log(payload);
  }
}

If you don't bind @event, a method with the same name as the event on the parent (saved()) still fires automatically (back-compat) - but prefer @event="handler" so you can name the method differently, avoiding collisions when several instances emit the same event name.

v-model on a component (two-way custom component binding)

<child-component v-model="value"></child-component>

The child component receives the value through a modelValue prop and reports changes back with this.$emit("update:modelValue", newValue).

Two-way binding

<input ns-model="name" />
<textarea ns-model="description"></textarea>

Model modifiers

<input ns-model="name.trim" />
<input ns-model="age.number" />
<input ns-model="search.lazy" />
<input ns-model="query.debounce300" />
<input ns-model="title.capitalize" />
<input ns-model="email.lowercase" />

Supported on:

  • checkbox
  • radio
  • select
  • select multiple

Events

<button ns-click="save"></button>
<button ns-click="save.prevent.stop"></button>
<button @click="save"></button>
<div ns-on="input:updateValue"></div>
<div @input="updateValue"></div>

Conditional rendering

<div ns-if="isLoggedIn">Logged in</div>
<div ns-else-if="isLoading">Loading...</div>
<div ns-else>Not logged in</div>

List rendering

Both arrays and plain objects (item/key) are supported, with or without Vue-style parentheses:

<ul>
  <li ns-for="item in items" ns-text="item.name"></li>
  <li ns-for="(item, index) in items" ns-text="item.name"></li>
  <li ns-for="(value, key) in someObject" ns-text="key"></li>
</ul>

Class / Style / Attribute binding

<div ns-class="className"></div>
<div ns-class="{ active: isActive, disabled: isDisabled }"></div>
<div ns-style="styleObject"></div>
<a ns-bind="href:linkUrl"></a>
<a :href="linkUrl"></a>

Options

Option Description
el a selector or a DOM element
data an object or a function (recommended as a function for components to avoid sharing state between instances)
template an HTML string that overrides the content of el
methods an object of methods; this inside a method points to the reactive data
computed an object of getters (or {get, set}), cached and auto-invalidated
watch an object watching property changes (by key or "*")
components a registry of child components ({ "tag-name": ComponentConfig })
props prop declarations for a component (array of names, or an object with type/default)
stateChanged (prop, value) => {}, called every time a property's value changes
beforeCreate / created hooks before/after reactive data is initialized
beforeMount / mounted hooks before/after the first render and DOM mount
beforeUpdate / updated hooks before/after every re-render triggered by a state change

Lifecycle order

beforeCreate → (reactive proxy created) → created → (methods bound)
  → (template loaded, slots rendered, child components mounted, first render, events bound)
  → beforeMount → mounted

On every property change: beforeUpdate → (the whole component subtree re-renders) → updated → stateChanged.

Component example

// Header.js
export default {
  template: `
    <header>
      <h1 ns-text="title"></h1>
    </header>
  `,
  data() {
    return {
      title: "Header Component"
    };
  }
};
import Header from "./Header.js";

new NS({
  el: "#app",
  components: {
    "app-header": Header
  }
});
<div id="app">
  <app-header></app-header>
</div>
<script type="module" src="./main.js"></script>

Notes

  • When using ES Modules, serve the extension via a local server or as an unpacked extension.
  • For Chrome Extensions, keep logic in a separate JS file rather than inline scripts.
  • A custom element (<my-component>) cannot self-close with <my-component /> in HTML (HTML doesn't honor self-closing on regular elements) - always write it out explicitly as <my-component></my-component>.
  • :key on ns-for is just a plain HTML attribute; the framework doesn't use it to diff/reconcile the DOM yet - the whole list is rebuilt from scratch on every render.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages