Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ All notable changes to the **NoJS Skill** will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased](https://github.com/no-js-dev/nojs-skill/compare/v1.17.0...HEAD)
## [Unreleased](https://github.com/no-js-dev/nojs-skill/compare/v1.18.0...HEAD)

## [1.18.0](https://github.com/no-js-dev/nojs-skill/compare/v1.17.0...v1.18.0) — 2026-07-07

### Added

- Directive compatibility section in SKILL.md documenting directive interaction rules, if-gate semantics, and use priority changes
- Limitation notes in directive reference docs for known incompatible combinations
- 6 validation diagnostics documented (switch×loop, if+loop, ref+loop, bind-value+model, watch+on:change, t+bind)

## [1.17.0](https://github.com/no-js-dev/nojs-skill/compare/v1.16.1...v1.17.0) — 2026-07-06

Expand Down
16 changes: 15 additions & 1 deletion nojs/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: nojs
metadata:
version: 1.17.0
version: 1.18.0
description: Provides expert-level knowledge of the No.JS HTML-first reactive framework for building dynamic web applications using only HTML attributes. Activates when the user explicitly mentions No.JS, NoJS, no-js.dev, cdn.no-js.dev, @no-js-dev/nojs, or the NoJS LSP. Also activates when HTML files use NoJS-specific directive combinations on plain HTML elements — bind (text binding attribute), foreach/each/for (loop attributes on elements), on:click/on:submit (colon-syntax event attributes), model (two-way binding attribute), state (reactive state attribute), store (global store attribute), computed/watch (reactive derivation attributes), show/hide (visibility toggle attributes), bind-html, bind-*, class-*, style-* (attribute-binding patterns), route/route-view (client-side routing attributes), validate (form validation attribute), or use/include (template composition attributes). Does NOT activate for generic HTML/CSS questions, React/Vue/Angular/Svelte/Alpine.js/HTMX development, or JavaScript framework questions unrelated to No.JS.
---

Expand Down Expand Up @@ -469,6 +469,20 @@ Browser globals (`window`, `document`, `location`, `history`, `navigator`) avail
- Statement write-back: in `on:*`/`watch`, mutated variables auto-write-back to the owning context
- AST caching: LRU-cached (configurable: `exprCacheSize`, default 500)


## 6.5. Directive Compatibility

Certain directive combinations on the same element produce unexpected behavior. The NoJS LSP warns about these during development.

| Combination | Problem | Fix |
|------------|---------|-----|
| `case`/`default` + `each`/`foreach`/`for` | Switch becomes inert; all branches render | Move loop inside the case branch |
| `if` + `each`/`foreach`/`for` | Condition cannot filter individual items | Use `filter` attribute or wrap in container with `if` |
| `ref` + `each`/`foreach`/`for` | Every clone re-registers; `$refs` points to last clone | Remove `ref` or access via loop context |
| `bind-value` + `model` | Redundant two-way bindings; duplicate listeners | Use one: `model` (preferred) or `bind-value` |
| `watch` + `on:change` (on form controls) | Both claim the change event; may conflict | Use `watch` with its `on:change` companion OR a standalone `on:change` |
| `t` + `bind` | Both write text content; last-processed wins silently | Use one text source per element |

## 7. Config Reference

| Option | Default | Description |
Expand Down
4 changes: 4 additions & 0 deletions nojs/references/directives/binding.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Replaces the element's text content with the evaluated expression. Supports pipe
- When the expression evaluates to `null` or `undefined`, the text content is set to an empty string.
- Pipe expressions are evaluated left to right: `bind="value | filterA | filterB"` passes the result of `filterA` into `filterB`.
- HTML entities in the expression result are rendered as literal text (not HTML). Use `bind-html` for HTML output.
- **Incompatible with `t` on the same element:** Both `bind` and `t` write text content. The last-processed directive wins silently. Use only one text source per element.

### Complete Example

Expand Down Expand Up @@ -105,6 +106,8 @@ Works with any attribute: `src`, `href`, `alt`, `title`, `disabled`, `checked`,

On `<input>`, `<textarea>`, and `<select>`, `bind-value` is **two-way** -- it also attaches an `input` event listener that writes the element's value back to the expression. For `type="number"` inputs, the value is coerced to `Number`.

> **Limitation:** Do not use `bind-value` and `model` on the same element -- both create two-way bindings with duplicate listeners. For number inputs, they may fight over the value due to different coercion policies. Use one or the other.

### Boolean Attributes

The following attributes receive special boolean handling -- truthy values set the attribute (empty string), falsy values remove it, and the corresponding DOM property is also toggled:
Expand Down Expand Up @@ -177,6 +180,7 @@ Creates automatic two-way data binding between a form input and a state property
- `model` is only valid on `<input>`, `<select>`, and `<textarea>` elements. Using it on non-input elements produces a validation warning.
- For radio buttons, multiple radios with the same `model` property share the value -- selecting one updates the property for all.
- For `type="number"`, the value is coerced via `Number()`. If the input is empty, the value becomes `NaN` -- use `0` as the initial state to avoid this.
- **Incompatible with `bind-value` on the same element:** Both `model` and `bind-value` create two-way bindings with separate listeners. Remove one to avoid duplicate pipelines and potential value conflicts.

### Complete Example

Expand Down
6 changes: 6 additions & 0 deletions nojs/references/directives/conditionals.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Conditionally render element. Removes from DOM when condition is false; re-creat
- When `if` is false, the element and all its children are removed from the DOM entirely. Event listeners and component state are lost.
- When `if` becomes true again, the element is re-created from scratch (directives re-initialize).
- Expressions that evaluate to `0`, `""`, `null`, `undefined`, or `false` are all falsy.
- **Incompatible with loops on the same element:** Do not place `if` and a loop (`each`/`foreach`/`for`) on the same element — the condition cannot remove individual items. Use the loop's `filter` attribute for per-item filtering, or wrap the loop in a container element with `if` for conditional rendering of the entire list.

### Complete Example

Expand Down Expand Up @@ -307,6 +308,7 @@ Supports multi-value matching with comma separation.

- Multi-value matching uses comma separation inside the attribute value, not multiple `case` attributes.
- Values are compared using loose equality (`==`), so `case="'1'"` matches the number `1`.
- **Incompatible with loops on the same element:** Do not place `case` or `default` on an element that also has `each`/`foreach`/`for` — the switch/case logic becomes inert and all branches render regardless of the switch value. Move the loop inside the case branch or restructure.

---

Expand All @@ -330,3 +332,7 @@ Default case inside a `switch` block. Renders when no `case` matches.
<span default>Unknown</span>
</div>
```

### Edge Cases

- **Incompatible with loops on the same element:** Do not place `default` on an element that also has `each`/`foreach`/`for` — the switch/case logic becomes inert. See `case` edge cases.
1 change: 1 addition & 0 deletions nojs/references/directives/i18n.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Translate element content using an i18n key.
- If the key is not found, the key string itself is rendered as the text content.
- Keys are dot-separated for nested lookup: `t="section.subsection.key"`.
- When the locale changes, all `t` elements automatically re-render with the new locale's translation.
- **Incompatible with `bind` on the same element:** Both `t` and `bind` write text content. The last-processed directive wins silently. Use only one text source per element -- either `t` for translated text or `bind` for expression-based text.

### Complete Example

Expand Down
12 changes: 12 additions & 0 deletions nojs/references/directives/loops.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,3 +345,15 @@ Child loops can access parent scope variables. Each loop element is self-repeati
</ul>
<template id="noTasksTpl"><li>You have no tasks yet. Create one to get started.</li></template>
```

---

## Directive Compatibility Notes

The following directive combinations on the same loop element produce unexpected behavior:

- **`if` + loop:** Do not place `if` and a loop on the same element -- the condition cannot remove individual items. Use the loop's `filter` attribute for per-item filtering, or wrap the loop in a container element with `if`.
- **`ref` on a looped element:** Every clone re-registers the same ref name in `$refs`, so `$refs.name` will point to the last clone only. Deterministic but almost never the intended behavior.
- **`case`/`default` + loop:** Do not place `case` or `default` on a looped element -- the switch/case logic becomes inert and all branches render. Move the loop inside the case branch.

See the [validation reference](../validation.md) for the full list of LSP diagnostics that detect these combinations.
1 change: 1 addition & 0 deletions nojs/references/directives/state.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ Executes the `on:change` handler whenever the watched property changes. The hand
- The `on:change` handler fires asynchronously after the state update completes.
- `$old` and `$new` are only available inside the `on:change` handler, not in other expressions.
- Watching a nested property path (e.g. `watch="user.name"`) is not supported -- watch the top-level property instead.
- **Incompatible with `on:change` on form controls:** On `<input>`, `<textarea>`, or `<select>`, the `watch` directive's `on:change` companion and an explicit `on:change` event handler both claim the change event and may conflict. Use one approach -- either `watch` with its `on:change` companion, or a standalone `on:change`/`on:input` event handler.

### Complete Example -- Debounced Search

Expand Down
111 changes: 105 additions & 6 deletions nojs/references/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,20 @@ Rules, common mistakes, and a checklist for validating No.JS templates. Based on
- [3. Event Modifier Validation](#3-event-modifier-validation) — Valid and invalid event modifiers
- [Valid Modifiers](#valid-modifiers) — Supported event modifier list
- [Invalid Modifiers](#invalid-modifiers) — Unsupported modifiers
- [4. Deprecation Warnings](#4-deprecation-warnings) — Deprecated features and alternatives
- [4. Directive Incompatibility Diagnostics](#4-directive-incompatibility-diagnostics) — Incompatible directive combinations
- [`case`/`default` + loop](#casedefault--loop) — switch becomes inert
- [`if` + loop](#if--loop) — condition cannot filter items
- [`ref` + loop](#ref--loop) — last clone wins
- [`bind-value` + `model`](#bind-value--model) — redundant two-way bindings
- [`watch` + `on:change`](#watch--onchange) — event conflict on form controls
- [`t` + `bind`](#t--bind) — double text-writer
- [5. Deprecation Warnings](#5-deprecation-warnings) — Deprecated features and alternatives
- [Router `mode` is deprecated](#router-mode-is-deprecated) — Migration from mode attribute
- [5. Security](#5-security) — Template security considerations
- [6. Security](#6-security) — Template security considerations
- [`bind-html` sanitization warning](#bind-html-sanitization-warning) — XSS risk with raw HTML binding
- [Expression evaluator security](#expression-evaluator-security) — Safe expression evaluation
- [CSRF protection](#csrf-protection) — Cross-site request forgery setup
- [6. Quick Validation Checklist](#6-quick-validation-checklist) — Categorized checklist for reviews
- [7. Quick Validation Checklist](#7-quick-validation-checklist) — Categorized checklist for reviews
- [Directives & Syntax](#directives--syntax) — Directive usage checks
- [Events](#events) — Event handler checks
- [Security](#security) — Security verification items
Expand Down Expand Up @@ -185,7 +192,99 @@ Note that `debounce` and `throttle` are also supported by the framework runtime

---

## 4. Deprecation Warnings

## 4. Directive Incompatibility Diagnostics

The NoJS LSP detects directive combinations that produce unexpected behavior at runtime. These are emitted as warnings during development.

### `case`/`default` + loop

Placing `case` or `default` on an element that also has `each`/`foreach`/`for` causes the switch/case logic to become inert -- all branches render regardless of the switch value.

```html
<!-- WARNING: switch becomes inert -->
<div switch="status">
<span each="item in items" case="'active'" bind="item.name"></span>
</div>

<!-- CORRECT: loop inside the case branch -->
<div switch="status">
<div case="'active'">
<span each="item in items" bind="item.name"></span>
</div>
</div>
```

### `if` + loop

Placing `if` and a loop on the same element is unreliable -- the condition cannot remove individual items. Use the loop's `filter` attribute for per-item filtering, or wrap the loop in a container with `if`.

```html
<!-- WARNING: condition cannot filter items -->
<li if="showActive" each="item in items" bind="item.name"></li>

<!-- CORRECT: filter attribute -->
<li each="item in items" filter="item.active" bind="item.name"></li>

<!-- CORRECT: wrapper element -->
<div if="showItems">
<li each="item in items" bind="item.name"></li>
</div>
```

### `ref` + loop

Using `ref` on a looped element means every clone re-registers the same ref name. `$refs.name` will point to the last clone only.

```html
<!-- WARNING: $refs.card points to last clone only -->
<div each="item in items" ref="card" bind="item.name"></div>
```

### `bind-value` + `model`

Both `bind-value` and `model` create two-way bindings with separate input listeners. Using both is redundant and they may conflict, especially on `type="number"` inputs where they use different coercion policies.

```html
<!-- WARNING: redundant two-way bindings -->
<input bind-value="name" model="name">

<!-- CORRECT: use one or the other -->
<input model="name">
<input bind-value="name">
```

### `watch` + `on:change`

On form controls (`input`, `textarea`, `select`), both `watch` (via its `on:change` companion) and an explicit `on:change` event handler claim the change event and may conflict.

```html
<!-- WARNING: both claim change event -->
<input watch="value" on:change="save()">

<!-- CORRECT: use watch with its companion -->
<input watch="value" on:change="console.log($old, $new)">

<!-- CORRECT: use standalone event handler -->
<input on:change="save()">
```

### `t` + `bind`

Both `t` and `bind` write text content to the element. The last-processed directive wins silently, which depends on attribute order.

```html
<!-- WARNING: double text-writer -->
<span t="greeting" bind="name"></span>

<!-- CORRECT: use one text source -->
<span t="greeting" t-name="name"></span>
<span bind="name"></span>
```

---

## 5. Deprecation Warnings

### Router `mode` is deprecated

Expand All @@ -209,7 +308,7 @@ The `mode="hash"` and `mode="history"` router attributes are deprecated. Use the

---

## 5. Security
## 6. Security

### `bind-html` sanitization warning

Expand Down Expand Up @@ -253,7 +352,7 @@ For mutating requests, configure CSRF tokens globally:

---

## 6. Quick Validation Checklist
## 7. Quick Validation Checklist

Use this checklist when reviewing No.JS templates:

Expand Down