关键词:dagger.js / HTML-First / 指令驱动 / 表单交互 / 前端范式
适读人群:前端开发者、对低样板代码有强需求的同学
在线示例(CodePen


📌 导读

这篇文章带你拆解一个超轻量计算器示例:仅用一份 HTML + 少量脚本,通过 dagger.js 的几条指令就能完成状态初始化、双向绑定、事件拦截、列表渲染等工作。文末附上 React/Vue 等价实现,便于横向对比开发心智与样板代码量。


在这里插入图片描述

01|先看成品:结构像 HTML,行为写在模板上

在示例里,几个关键 dagger.js 指令把“行为”直接声明在 HTML 上:

<form dg-cloak name="calculator" class="calc" +load +reset="value = ''" +submit#prevent="value = eval(value)">
  <p>
    <input type="textfield" placeholder="0" *value>
    <input type="reset" value="C">
  </p>
  <p *each#item:button-Row="buttonRows">
    <input *each#item:button="resolve(buttonRow)" *type="button.type" *value="button.showValue" +click="value += button.realValue">
  </p>
</form>
  • +load:载入初始状态(如 valuebuttonRows)。
  • *value:输入框与作用域变量双向绑定。
  • *each:把 buttonRows 展成按钮矩阵。
  • +submit#prevent+reset+click:表单提交/重置/点击事件在模板上完成声明。

读者要点:dagger.js 倾向于“HTML 优先”,让你在模板属性层就把 80% 的交互说清楚,减少 JS 样板与组件胶水。


02|状态从哪里来?用 +load 一步到位

示例中用 +load="load()"load() 返回对象注入到模板作用域,因此模板里可以直接使用返回的字段与函数(如 valuebuttonRowsresolve):

const load = () => ({
  value: '',
  buttonRows: [
    [7, 8, 9, ['/', '÷']],
    [4, 5, 6, ['*', '×']],
    [1, 2, 3, '-'],
    [0, '.', ['', '='], '+']
  ]
});

const resolve = buttonRow => buttonRow.map(item => ({
  type: item[0] === '' ? 'submit' : 'button',
  showValue: item[1] || item,
  realValue: item[1] ? item[0] : item + ''
}));

对比传统做法:在很多框架中,状态初始化需要 Hook/Store/组件数据等;在 dagger.js,把“数据边界”声明回模板即可。


03|绑定与事件:一眼看懂的“指令语义”

  • 双向绑定:*value="value" (阻止默认提交并计算)
  • 表单提交:+submit#prevent="value = eval(value)"(阻止默认提交并计算)
  • 重置按钮:+reset="value = ''"
  • 按钮点击:+click="value += button.realValue"

🔒 安全提醒:示例中 eval 仅用于演示。生产请改用受限表达式解析(白名单字符/运算符或专用解析器)。


04|命名小细节:更贴近直觉

外层循环形如 *each#item:button-Row="buttonRows",模板里可用驼峰形式 buttonRow 继续传参(如 resolve(buttonRow))。这种直观的一致性降低了心智负担,让模板更可读。


05|同题双解:React / Vue 等价实现

下面给出“功能对齐”的极简版本(同样仅作演示,求值方式请替换为安全解析)。

✅ React(Hooks)版

import { useState } from "react";

const rows = [
  [7, 8, 9, ["/", "÷"]],
  [4, 5, 6, ["*", "×"]],
  [1, 2, 3, "-"],
  [0, ".", ["", "="], "+"],
];

export default function Calc() {
  const [value, setValue] = useState("");

  const resolve = (row) =>
    row.map((it) => ({
      type: Array.isArray(it) && it[0] === "" ? "submit" : "button",
      showValue: Array.isArray(it) ? it[1] : it,
      realValue: Array.isArray(it) ? it[0] : String(it),
    }));

  const onSubmit = (e) => {
    e.preventDefault();
    try { setValue(String(Function(`return (${value})`)())); } catch {}
  };

  return (
    <form onSubmit={onSubmit} onReset={() => setValue("")} className="calc">
      <p>
        <input value={value} onChange={(e) => setValue(e.target.value)} />
        <input type="reset" value="C" />
      </p>
      {rows.map((r, i) => (
        <p key={i}>
          {resolve(r).map((b, j) =>
            b.type === "submit" ? (
              <input key={j} type="submit" value={b.showValue} />
            ) : (
              <input
                key={j}
                type="button"
                value={b.showValue}
                onClick={() => setValue((v) => v + b.realValue)}
              />
            )
          )}
        </p>
      ))}
    </form>
  );
}

观察:UI 结构与交互主要由 JSX/JS 表达,需要 useState、事件回调、mapkey 等样板。


✅ Vue 3(组合式)版

<form class="calc" @submit.prevent="onSubmit" @reset="value = ''">
  <p>
    <input v-model="value" placeholder="0" />
    <input type="reset" value="C" />
  </p>
  <p v-for="(row, i) in rows" :key="i">
    <input
      v-for="(b, j) in resolve(row)"
      :key="j"
      :type="b.type"
      :value="b.showValue"
      @click="b.type==='button' && (value += b.realValue)"
    />
  </p>
</form>

<script setup>
import { ref } from "vue";
const value = ref("");
const rows = [
  [7, 8, 9, ["/", "÷"]],
  [4, 5, 6, ["*", "×"]],
  [1, 2, 3, "-"],
  [0, ".", ["", "="], "+"],
];
const resolve = (row) =>
  row.map((it) => ({
    type: Array.isArray(it) && it[0] === "" ? "submit" : "button",
    showValue: Array.isArray(it) ? it[1] : it,
    realValue: Array.isArray(it) ? it[0] : String(it),
  }));
const onSubmit = () => {
  try { value.value = String(Function(`return (${value.value})`)()); } catch {}
};
</script>

观察v-model、事件修饰符与dagger.js的指令范式接近,但仍需组件化上下文与实例化。


06|对照清单:三家同题的“心智与样板”

  • 状态初始化
    • dagger.js:+load 返回对象 → 直接成为模板作用域
    • React:useState / 上层状态管理
    • Vue:ref / reactive
  • 双向绑定
    • dagger.js:*value
    • React:受控组件 + onChange
    • Vue:v-model
  • 事件拦截
    • dagger.js:+submit#prevent
    • React:手写 e.preventDefault()
    • Vue:@submit.prevent
  • 列表渲染
    • dagger.js:*each
    • React:array.map
    • Vue:v-for
  • 模板位置与语义
    • dagger.js:原生 HTML 属性层表意
    • React:JSX(JS 主导)
    • Vue:模板/单文件组件

一句话总结:dagger.js 让你“少写样板,模板能动”,尤其适合简单到中等复杂的表单/工具型页面与后台面板。


07|进阶练习(自行扩展)

  • 🔒 替换 eval:使用受限表达式解析(仅允许 0–9 . + - * / ())。
  • ⌨️ 键盘输入:在容器上加 +keydown,过滤无效按键并 #prevent
  • 🧾 历史记录:在 +load 增加 history: [],提交后 history.push(...) 并渲染。
  • 📱 移动端体验:按钮尺寸/间距、触控反馈与误触容错。

08|结语

这个小计算器例子展示了 dagger.js 的HTML-First、指令即行为:把交互与数据边界写回模板,让页面更易读、更易改、更易落地。当需求增长时,再引入模块化与生命周期钩子(如 +loaded+unload+unloaded)也很顺滑。

本文内容就到这里,后续文章将为大家带来更多案例和讲解。

如果对dagger.js感兴趣的话,请您点赞收藏、分享本系列文章,也欢迎留言或者私信作者提出问题和建议,您的关注是对我最大的支持和鼓励。感谢您的阅读,祝工作学习顺利!

👉 在线示例(CodePen


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐