# bridgey

## jQuery のまま、モダンに。

**保守する人のためのライブラリです。**
書き直す予算はなくても、事故だけは止められます。

```html
<script src="https://cdn.jsdelivr.net/npm/bridgey@2"></script>
```

これだけ。ビルド不要、依存ゼロ、gzip 18kB、`eval` 不使用（CSP セーフ）。
**HTML は1文字も変えなくていい**し、<br>いま動いている jQuery のコードも1行も変えなくていい。

---

## 30秒で試す

`demo.html` という名前で保存して、ブラウザで開くだけです。サーバもビルドも要りません。

```html
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>demo</title>
</head>
<body>

<p>合計 <span id="total">0</span> 円</p>
<button id="add">追加する</button>

<script src="https://cdn.jsdelivr.net/npm/bridgey@2"></script>
<script>
  const count = $$.state(0);
  const total = $$.computed(() => count() * 1100);

  $$("#total").text(total, $$.money);        // 状態が変われば画面が勝手に追いつく
  $$("#add").click(() => count(n => n + 1)); // ここは jQuery のまま
</script>

</body>
</html>
```

`$` を `$$` にするだけ。増えた概念は**1つだけ**です。

```js
$$("#total").text("1,100");   // 値を渡す      → その瞬間だけ入る（jQuery と同じ）
$$("#total").text(total);     // state を渡す  → 以後ずっと自動で追いかける ← これだけが新しい
```

---

## まず4語

```
state      状態を作る
computed   状態から計算する（合計・小計・表示するか）
when       条件で表示を出し入れする
repeat     一覧を描く（行の DOM を使い回すので入力が消えない）
```

これで8割書けます。残りは**知らなくても詰まりません**（必要になった場面で bridgey が教えます）。

```js
const count = $$.state(0);

count()             // 読む   ← .val() と同じ手癖
count(5)            // 書く   ← .val(5) と同じ手癖
count(n => n + 1)   // 更新
`${count}`          // 補間もできる
```

---

## 何を解決するのか

実際の現場で起きていた事故を、そのまま止めます。

| 事故 | bridgey では |
|---|---|
| **数値計算が合わない**（change の中で合計を計算して代入していた） | 合計は導出値。**代入する場所が存在しない**ので更新漏れが起きない |
| **非表示の必須項目で送信できず、理由も出ない**（CV が消える） | 見えているフィールドだけを検証する。エラー0件で送信不能にならない |
| **オートフィルで入力しても未入力扱い** | `value` の代入を検知する（既存 jQuery の `.val()` 経由でも追従） |
| **HTML に要素を1つ挟むと静かに壊れる**（`next()` / `children()` / `eq()`） | 名前で引く。壊れたら**0件で警告**する（jQuery は黙る） |
| **1箇所の undefined で全機能が死ぬ**（common.js の `$(function(){})`） | `component()` 単位でエラーを隔離。壊れた部品だけが止まる |
| **class 名のタイポで、隠すべきものが見えてしまう** | CSS に無い class 名を検出し、似た名前を提示する |
| **Ajax で追加した要素にプラグインが効かない** | `component()` が自動で適用。消えたら自動で片付ける |
| **二重送信でレコードが2件できる** | `action()` / `form()` が構造的に防ぐ |
| **リストを `.html()` で作り直して入力が消える** | `repeat()` がキー付き差分。行の DOM を使い回す |
| **通信の後着追い越しで古い金額が残る** | `resource()` が前のリクエストを中断し、古い応答を捨てる |

---

## 段階移行 — 全部書き直さなくていい

**1機能だけ置き換えられます。**予算が取れない現場のために、これが最優先の設計です。

```
第1週  金額計算だけ bridgey に（20行）。他は jQuery のまま。同じ DOM に共存OK
第2週  フォームの検証だけ移す。CV 事故が止まる
第3週  タブとアコーディオンを component に。undefined の全滅が止まる
       …残りは触らなくてよい
```

### そして、そのまま卒業できます

同じ UI を4段階で書けます。**①②③はいま全部動きます。**

```js
// ① いまのやり方（そのまま動く）
$(".tab").on("click", function () {
  $(".tab").removeClass("active");
  $(this).addClass("active");
});

// ② bridgey の JS API（事故が減る）
const current = $$.state("a");
$$(".tab").each((el) => $$(el)
  .click(() => current(el.dataset.tab))
  .selected(() => current() === el.dataset.tab));

// ③ ディレクティブ（宣言的な考え方が身につく・完全に任意）
//    <li data-brg-selected="isCurrent" data-brg-on-click="pick">
$$.bind("#tabs", { isCurrent, pick });

// ④ Svelte / Vue（卒業）
//    <li class:active={isCurrent} on:click={pick}>
//    <li :class="{active: isCurrent}" @click="pick">
```

③の記法は④と**1対1で対応**しています。

| bridgey | Vue | Svelte |
|---|---|---|
| `data-brg-text="total"` | `{{ total }}` | `{total}` |
| `data-brg-when="isFree"` | `v-if="isFree"` | `{#if isFree}` |
| `data-brg-repeat="todos"` | `v-for="t in todos"` | `{#each todos as t}` |
| `data-brg-class-active="isOn"` | `:class="{active: isOn}"` | `class:active={isOn}` |
| `data-brg-attr-src="url"` | `:src="url"` | `src={url}` |
| `data-brg-value="keyword"` | `v-model="keyword"` | `bind:value={keyword}` |
| `data-brg-on-click="save"` | `@click="save"` | `on:click={save}` |

**ディレクティブに式は書けません。**

```html
<p data-brg-when="qty > 3">   <!-- ✗ 書けない（警告が出ます） -->
```
```js
const isFree = $$.computed(() => qty() >= 3);   // ロジックは JS 側
```
```html
<p data-brg-when="isFree">    <!-- ✓ -->
```

式を評価しないので `new Function` が不要 = **CSP セーフのまま**。`{{ }}` を作らないので **Blade / Twig / Smarty と衝突しません**。

---

## 使えるもの

### 知っている jQuery のメソッドは、そのまま使えます

`text` `html` `val` `attr` `removeAttr` `prop` `css` `addClass` `removeClass` `toggleClass` `hasClass` `data`
`find` `closest` `parent` `children` `first` `last` `is` `each` `append` `empty` `remove`
`on` `off` `click` `change` `submit` `input` `keydown` `focus` … `$$.fn`（プラグイン拡張口）

**そこに state を渡せるようになっただけです。**

```js
$$("#label").text(total);                    // 表示
$$("#btn").prop("disabled", busy);           // 活性
$$("#box").toggleClass("open", isOpen);      // 見た目
$$("#img").attr("src", url);                 // 属性
$$("#q").val(keyword);                       // 双方向
$$("#bar").css("width", () => `${pct()}%`);  // 関数を渡せば依存を自動追跡
```

表示の形はここで整えられます。

```js
$$("#n").text(count, "件");                  // 3件            後ろに付ける
$$("#n").text(count, "残り{}件");            // 残り3件        {} の位置に入れる
$$("#t").text(total, $$.money, "円");        // 23,800円       3桁区切り＋単位
```

### 困ったら足す4語

```js
$$("#signup").form({ … })   // フォームの事故を止める（非表示・オートフィル・二重送信）
$$("#plan input").group()   // ラジオ/チェックボックスの選択を1つの状態に
$$.component("tabs", fn)    // common.js を安全にする（要素がある頁でだけ動く・エラー隔離）
$$.resource(src, fetcher)   // 通信の競合を止める（自動中断・後着追い越しなし）
```

### 知らなくても書けるもの（出会ったときに覚える）

`mode` `vars` `toggleAttr` `selected` `expanded` `invalid` `busy` `disabled`
`action` `scope` `effect` `tick` `batch` `untrack` `bind` `money`

`.addClass("active")` と書くと、bridgey がこう教えます。

```
[bridgey] addClass("active") で状態を表しているようです。
          → .selected(state) が使えます: aria-selected も一緒に付き、
            他の要素から消し忘れることもありません
          (このヒントは $$.hints = false で止められます)
```

---

## セレクタ

id も class も**今まで通り**使えます。`@` は `data-brg` 属性の**省略記法**です。

```js
$$("#total")     // id="total"
$$(".total")     // class="total"
$$("@total")     // data-brg="total"   ← [data-brg="total"] の略記
```

`@` を使うと、**class 名や DOM 構造が変わっても JS が壊れません**（デザイナーが自由に見た目を変えられる）。必須ではありません。0件になったときは、こう教えます。

```
[bridgey] $$(".price") が0件でした。text() を結びつけられません。
  HTML が変わった可能性があります(class 名の変更・要素の削除・読み込み順)。
  → 壊れにくくするには data 属性で結びつけてください:
     <span data-brg="price"></span>  →  $$("@price")
```

属性名は `$$.attr = "data-ui"` で変えられます。

---

## 名前は自由に変えられます

既定は `$$`。**`<script>` タグの1箇所**だけで変えられます。

```html
<script src="…/bridgey@2"></script>                     <!-- $$ -->
<script src="…/bridgey@2" data-global="$"></script>      <!-- $ -->
<script src="…/bridgey@2" data-global="$$$"></script>    <!-- $$$ -->
<script src="…/bridgey@2" data-global="$_$"></script>    <!-- $_$ -->
<script src="…/bridgey@2" data-global="brg,$$"></script> <!-- 両方 -->
```

`$$` が既に使われていたら（Prototype.js など）、**勝手に別名へ逃がさず**警告します。サイトによって名前が変わると、コピーしたコードが動かない原因が分からなくなるからです。`window.bridgey` からは常に使えます。

---

## npm でも使えます（型が付きます）

```bash
npm i bridgey
```
```ts
import { $$ } from "bridgey";
import { $$ as $ } from "bridgey";   // 好きな名前に（言語機能で足りる）
```

TypeScript で書かれているので、型定義は実装と乖離しません。

```ts
$$("input#name").prop("checked", flag);   // OK
const count = $$("#n").state(0);
count("あ");                               // ❌ number ではない
```

---

## 持たないもの、その代わりに

| 持たないもの | 代わりに |
|---|---|
| `fadeIn` / `slideToggle` | CSS の `transition` ＋ `.toggleClass("is-open", state)` |
| `$.ajax` / `$.get` / `$.post` | `fetch()`。競合制御が要るときは `$$.resource()` |
| `$.extend` / `$.each` / `$.map` | `{...a, ...b}` / `for...of` / `Array#map` |
| `serialize()` | `new FormData(form)`。検証は `form()` |
| `next` / `prev` / `siblings` / `eq` / `index` | `closest("@row").find("@price")`（構造ではなく名前で引く） |
| `width` / `height` / `offset` | `getBoundingClientRect()` |
| `show` / `hide` / `toggle` | `when(state)` |
| ルーター / SPA 化 | URL の主権はサーバーに |
| スコープ付き CSS / 仮想DOM / SSR | 入れません |

**呼ぶと、代わりの書き方を教えて例外になります。**

```js
$$("#row").next();
// Error: [bridgey] next() は使えません。HTML の構造が変わると壊れるため v2 では
//        提供していません(要素を1つ挟むだけで、例外も出ないまま静かに動かなくなります)。
//        → 構造ではなく名前で引いてください: $$(this).closest("@row").find("@price")
```

---

## デモを動かす

```bash
npm install
npm run build
```

あとは `demo/index.html` をブラウザで開くだけです（サーバー不要）。

```bash
start demo/index.html      # Windows
open demo/index.html       # macOS
```

デモでは、実際に作者がレガシーな現場で遭遇した事故を**再現して止めてみせます**。

- 「HTML を1段ラップする」 → 従来のやり方（`next()`）だけが静かに壊れる
- 「オートフィルを再現」 → イベントが飛ばなくても検証が追従する
- 「送信を5連打」 → 1回しか送信されない
- 「式を書いてみる」 → ディレクティブが警告を出す
- わざと壊した component → 他の部品は動き続ける
- 同じ画面を ②JS API と ③ディレクティブで並べて比較

コンソールを開いておくと、警告とヒントが読めます。

---

## 動作環境

- モダンブラウザ（ES2020 / `AbortController` / `MutationObserver`）
- **`eval` / `new Function` を使いません** → `script-src 'self'` の厳格な CSP でも動きます
- jQuery と同じページ・同じ要素で共存できます（`$` は奪いません）
- jQuery プラグイン（slick / DataTables 等）は bridgey では動きません。jQuery 本体と併用してください

---

## v1 からの移行

v2 は破壊的変更です。v1 は `1.x` ブランチで凍結しています。

| v1 | v2 |
|---|---|
| `useEngine(svelteEngine)` / `mount(Component)` | 削除。Svelte / Vue への依存をやめました |
| `state(0)` → `count.value` | `$$.state(0)` → `count()` / `count(5)` |
| `bindText` / `bindClass` / `bindAttr` / `bindValue` | `.text(state)` / `.toggleClass(n, state)` / `.attr(n, state)` / `.val(state)` |
| `computed(deps, fn)` | `$$.computed(fn)`（依存は自動追跡） |
| `.val()` が String のサブクラス | 普通の文字列（`=== ""` が期待通り動く） |
| jQuery 共存は非サポート | **公式サポート** |

---

## 開発

```bash
npm test          # jsdom で DOM も dist も検査
npm run typecheck # tsc（型検査 + .d.ts 生成）
npm run build     # esbuild で dist を作る
```

テストは「事故の名前」で書いてあります。

```
✔ ★①見えていないフィールドは検証しない
✔ ★②JS からの値の代入も検知する(オートフィル・既存 jQuery の .val() 相当)
✔ ★行の DOM は使い回される(入力中の文字が消えない)
✔ ★後着追い越しが起きない(遅い応答が後で届いても上書きしない)
✔ ★1つの初期化が失敗しても、他の部品は動き続ける
✔ ★数値計算が合わない: 合計を「書き込む場所」を無くす
✔ dist: eval / new Function を使っていない(CSPセーフ)
```

---

## English

**bridgey** — modern state management with jQuery's ergonomics. Zero dependencies, no build step, ~18kB gzipped, and **no `eval`** (works under a strict CSP).

Built for people who *maintain* legacy jQuery rather than start new apps: adopt it one feature at a time, keep jQuery on the same DOM node, and never edit your HTML if you can't. Where jQuery fails silently, bridgey tells you what broke — empty selector matches, class names missing from your CSS, duplicate list keys, components that failed to initialize.

Four words to start: `state`, `computed`, `when`, `repeat`. Optional directives (`data-brg-text`, `data-brg-when`, …) map one-to-one onto Vue and Svelte template syntax, so this becomes a stepping stone rather than a dead end.

```html
<script src="https://cdn.jsdelivr.net/npm/bridgey@2"></script>
<script>
  const count = $$.state(0);
  $$("#total").text($$.computed(() => count() * 1100), $$.money);
  $$("#add").click(() => count(n => n + 1));
</script>
```

---

MIT License
