Skip to content

场景 Recipes

这些示例用于固定 Kupola 的推荐使用方式:HTML 描述结构和绑定,JavaScript controller 负责复杂逻辑、副作用和清理。

搜索输入

输入框用 k-model.debounce 降低状态写入频率,副作用放到 watch()

html
<section k-data="usersSearch">
  <input k-model.trim.debounce.300="keyword" placeholder="搜索用户" />
  <p k-show="loading">搜索中...</p>

  <ul>
    <template k-for="user in users" :key="user.id">
      <li k-text="user.name"></li>
    </template>
  </ul>
</section>
js
import { defineScope } from '@kupola/platform/directives'

defineScope('usersSearch', () => ({
  keyword: '',
  users: [],
  loading: false,
  mounted({ watch }) {
    watch(() => this.keyword, () => this.search())
  },
  async search() {
    if (!this.keyword) {
      this.users = []
      return
    }

    this.loading = true
    try {
      this.users = await api.searchUsers(this.keyword)
    } finally {
      this.loading = false
    }
  },
}))

表单提交

提交事件放在 <form>,用 @submit.prevent 保证点击按钮和按 Enter 走同一条路径:

html
<form k-data="profileForm" @submit.prevent="save()">
  <input k-model.trim="name" name="name" />
  <input k-model.trim="email" name="email" />
  <button :disabled="saving || !name || !email">保存</button>
</form>
js
defineScope('profileForm', () => ({
  name: '',
  email: '',
  saving: false,
  async save() {
    this.saving = true
    try {
      await api.saveProfile({ name: this.name, email: this.email })
    } finally {
      this.saving = false
    }
  },
}))

列表更新

列表项有稳定 id 时始终写 :key。数组更新优先重新赋值,不依赖深层对象原地修改:

html
<section k-data="todosPage">
  <input k-model.trim="newTodo" @keydown.enter="addTodo()" />
  <button @click="addTodo()">新增</button>

  <template k-for="todo in todos" :key="todo.id">
    <label k-class="{ done: todo.done }">
      <input
        type="checkbox"
        :checked="todo.done"
        @change="toggleTodo(todo.id, event.target.checked)"
      />
      <span k-text="todo.text"></span>
    </label>
  </template>
</section>
js
defineScope('todosPage', () => ({
  todos: [],
  newTodo: '',
  addTodo() {
    if (!this.newTodo) { return }
    this.todos = [
      ...this.todos,
      { id: Date.now(), text: this.newTodo, done: false },
    ]
    this.newTodo = ''
  },
  toggleTodo(id, done) {
    this.todos = this.todos.map(todo => (
      todo.id === id ? { ...todo, done } : todo
    ))
  },
}))

动态片段

局部刷新、弹窗内容、后端片段替换时,先销毁旧实例,再替换 HTML,最后 walk() 新片段:

js
import { walk } from '@kupola/platform/directives'

let panel

async function reloadPanel() {
  panel?.destroy()

  const root = document.querySelector('#users-panel')
  root.innerHTML = await fetch('/users/fragment').then(r => r.text())
  panel = walk(root)
}

function removePanel() {
  panel?.destroy()
  panel = null
  document.querySelector('#users-panel')?.replaceChildren()
}

同一段 DOM 不要重复 walk()。重复初始化会让事件、effects、watch() 和 refs 叠加。

如果初始化入口可能被重复调用,或者不方便保存实例引用,可以使用生命周期工具组:

js
import { destroyWalk, walkOnce } from '@kupola/platform/directives'

async function reloadPanel() {
  const root = document.querySelector('#users-panel')
  destroyWalk(root)
  root.innerHTML = await fetch('/users/fragment').then(r => r.text())
  walkOnce(root)
}

后端模板项目可以参考 后端模板集成,把整页增强、交互岛和动态片段分开管理。

条件与过渡

频繁切换且要保留 DOM 状态,用 k-show。只在条件满足时存在的 DOM,用 k-if

html
<section k-data="{ open: false }">
  <button @click="open = !open">切换</button>
  <div k-show="open" k-transition>
    面板内容
  </div>
</section>
css
.kp-enter-active,
.kp-leave-active {
  transition: opacity 150ms ease;
}

.kp-enter-from,
.kp-leave-to {
  opacity: 0;
}

k-transition 只提供 class 生命周期,不内置动画 CSS。它需要配合 k-showk-if 使用。

DOM 引用

稳定关键节点用 k-ref,临时查询用 $ / $$

html
<section k-data="dialogForm">
  <input k-ref="firstInput" k-model.trim="name" />
  <button @click="submit()">提交</button>
</section>
js
defineScope('dialogForm', () => ({
  name: '',
  mounted({ refs, $, on }) {
    refs.firstInput.focus()
    on('click', '[data-close]', () => this.close())
    this.submitButton = $('[type=submit]')
  },
  submit() {
    // ...
  },
}))

ctx.on() 返回清理函数,也会随当前 walk() 实例销毁。只有需要提前解绑时,才手动调用它。