kitsui
kitsui是一个dom优先的UI库 Component 和 State 对象。一个 Component 包裹一个真实的 HTMLElement;一个 State 是一个自有的无功值。
把喜水想象成反应,但没有那么神奇。每个可能的效果都由您显式声明和控制。直接构建和移动真正的组件;Kitsui不会在代码背后运行应用程序范围的渲染周期。
pnpm add @inthedark/kitsui
典型组件
import { Component, State, Style } from "@inthedark/kitsui"
const counterStyle = Style.Class("counter", {
alignItems: "center",
display: "flex",
gap: 8,
})
function Counter (): Component {
const root = Component("section")
.class.add(counterStyle)
const count = State(root, 0)
Component("output")
.text.set(count.map(value => `Count: ${value}`))
.appendTo(root)
Component("button")
.attribute.set("type", "button")
.text.set("Increment")
.event.owned.on.click(() => {
count.update(value => value + 1)
})
.appendTo(root)
return root
}
Counter().appendTo(document.body)
这种风格使所有权保持简单: root 拥有状态、事件清理和子组件,因此可以删除 root 处理整个子树。
最佳实践
- 返回
Component来自小型工厂函数的实例。 - 创建
State应该处理它的组件或所有者。 - 使用
Owner()为了一个没有归属的活镜Component,然后将其传递给该范围内的每个感知所有者的API。 - 在手动之前使用派生状态、操纵符和条件放置
subscribe加上DOM突变。 - 在一个流畅的链中构建大多数静态结构。
- 使用
component.class,component.style,component.attribute,component.aria,component.text,component.event的DOM效果。 - 使用
event.owned对于应该与组件一起清理的处理程序。 - 使用
Style.Class用于可重用、可选择或共享的样式。使用component.style用于动态或反应性内联属性。 - 在事件处理程序或真正的自定义行为中保持直接的DOM访问。
可重用组件
可重用组件文件应该与组件名称完全匹配,并默认导出组件函数。使用一个局部符号作为接口名和函数名,然后导出一次 export default Name.
不强制赋值 as Name 在return语句中。给函数一个显式的返回类型和let component.extend(...) 检查结果的类型。
自定义组件方法应该返回 this 除非有强烈的理由退回其他东西。
import { Component } from "@inthedark/kitsui"
interface ButtonExtensions {
setBusy (busy: boolean): this
setLabel (label: string): this
}
interface Button extends Component<HTMLButtonElement>, ButtonExtensions { }
function Button (this: Component<HTMLButtonElement> | void, label: string): Button {
const button = Component(this ?? "button", Button)
.attribute.set("type", "button")
const labelText = Component("span")
.text.set(label)
.appendTo(button)
return button.extend<ButtonExtensions>(root => ({
setBusy (busy) {
root.attribute.toggle("disabled", busy)
return root
},
setLabel (label) {
labelText.text.set(label)
return root
},
}))
}
export default Button
可组合构造器必须将自己的函数标识传递给初始的 Component(...) 呼叫。 and 将当前组件注入 this,转发参数,并跳过已经应用于该组件的构建器。 is 和 as 检查相同的构建器标识。
const standalone = Button("Save")
const composed = Component("button").and(Button, "Save")
if (composed.is(Button)) {
composed.setBusy(true)
}
特定组件的事件
基本组件事件仅适用于每个组件上可用的事件,例如 Mount 和 Dispose. 特定于功能的组件应该通过 Component.WithEvents<Events> 而不是增加共享组件事件映射。
自定义kitsui组件事件名称应该是 PascalCase,例如 DragStart 或者 WidgetCommit,因此它们与本地DOM事件(如 click 和 pointerdown.
import { Component } from "@inthedark/kitsui"
interface WidgetEvents {
WidgetCommit: CustomEvent<{
readonly value: string
}> & {
readonly source: "user" | "api"
}
}
interface WidgetExtensions {
commit (value: string): this
}
interface Widget extends Component.WithEvents<WidgetEvents>, WidgetExtensions { }
function emitWidgetCommit (widget: Widget, value: string): boolean {
return widget.event.emit.WidgetCommit({ value }, {
tweak: event => Object.defineProperty(event, "source", {
value: "api",
}),
})
}
declare const widget: Widget
widget.event.owned.on.WidgetCommit(event => {
event.detail.value.toUpperCase()
event.source
})
event.emit.* 和 event.dispatch.* 构造映射的是别名吗 CustomEvent,运行可选的 tweak 回调,并返回目标的native dispatchEvent 结果。本地的 CustomEvent 默认值被保留,所以kitsui自定义事件不会冒泡,除非 bubbles: true 显式提供。冒泡不会将特性事件添加到祖先组件类型;在类型化之前,祖先仍然必须选择进入事件映射 event API公开该名称。
同步 Dispose 处理程序可以发出最终的自定义通知,而所有者同时报告这两种通知 disposed 和 disposing. 这是一个狭窄的生命周期窗口:侦听器注册和其他组件突变仍然被禁止,并且在处理完成后再次拒绝自定义事件发射。
状态值和派生值
当立即使用派生值时,使用短形式派生状态。当派生状态被重用、分组、传递或需要明显的所有者时,使用所有者显式派生状态。
保持行为和UI状态一致。被禁用的按钮不应该是唯一强制执行不变量的东西。
const root = Component("section")
const count = State(root, 0)
const atLimit = count.map(root, value => value >= 10)
Component("output")
.text.set(count.map(value => `Count: ${value}`))
.appendTo(root)
Component("button")
.attribute.set("type", "button")
.attribute.bind(atLimit, "disabled")
.text.set(atLimit.map(value => value ? "Limit reached" : "Increment"))
.event.owned.on.click(() => {
count.update(value => Math.min(value + 1, 10))
})
.appendTo(root)
Component("p")
.text.bind(atLimit, "The counter is capped.")
.appendTo(root)
使用 State.Group(owner, states) 当一个派生值需要多个状态的一致快照时。使用 truthy, falsy, or(...) 而不是手工编写重复的布尔或空值映射。
独立所有者范围
使用 Owner() 当一个活的范围没有所有权 Component. 将所有者传递给 State 和其他所有者感知的api,这样整个作用域共享一个生命周期。在范围结束时处置所有者。
const session = Owner()
const status = State(session, "connecting")
const label = status.map(session, value => `Status: ${value}`)
label.subscribe(session, value => {
console.log(value)
})
session.dispose()
当作用域属于UI子树时,使用 Component 作为它的主人。用于路由器、套接字、观察者和其他系统的集成帮助程序可以接受 Owner 和使用 owner.onCleanup(...) 在内部;普通应用程序代码通常只将所有者传递给帮助程序。
每个所有者暴露的都是终身的 signal 用于可取消的平台工作。
机械手绑定
为响应式DOM效果使用操纵符。单个状态可以驱动文本、类、原始属性、类型化ARIA属性和事件,而无需手动DOM记账。
const expandedStyle = Style.Class("panel-expanded", {
borderColor: "#0f766e",
})
const panel = Component("section")
const expanded = State(panel, false)
Component("button")
.attribute.set("type", "button")
.aria.expanded(expanded)
.text.set(expanded.map(value => value ? "Collapse" : "Expand"))
.event.owned.on.click(() => {
expanded.update(value => !value)
})
.appendTo(panel)
Component("div")
.class.bind(expanded, expandedStyle)
.attribute.bind(expanded, "data-expanded")
.aria.hidden(expanded.falsy)
.text.set(expanded.map(value => value ? "Expanded" : "Collapsed"))
.appendTo(panel)
使用 attribute.toggle(name, enabled) 用于在命令式方法中立即切换布尔属性。使用 attribute.bind(state, name) 什么时候属性的存在应该跟随状态。
运动和放置
使用 appendTo, prependTo, insertTo 当现有组件应该相对于另一个组件、节点或标记移动时。使用 appendToWhen, prependToWhen, insertToWhen, appendWhen, prependWhen, insertWhen 用于条件放置。使用 place 当目标位置本身是被动的。
每个组件或原始节点保持一个活跃的kitsui放置流。如果通过另一个kitsui API再次放置它,则新调用将获得控制权,而旧的条件流或选择流将停止移动它。从共享选择中替换一个组件不会阻止它的兄弟组件。
条件放置使隐藏节点在dom外保持活动状态,并在它们的条件为真时恢复它们。除去它们的终身主人,它们就被处理掉了。
const root = Component("section")
const open = State(root, false)
const details = Component("p")
.text.set("More detail")
.appendToWhen(open, root)
const root = Component("section")
const left = Component("div").appendTo(root)
const right = Component("div").appendTo(root)
const side = State<"left" | "right">(root, "left")
const badge = Component("span")
.text.set("moving")
badge.place(root, (Place) => {
const leftPlace = Place().appendTo(left)
const rightPlace = Place().appendTo(right)
return side.map(value => value === "left" ? leftPlace : rightPlace)
})
使用 append, prepend, insert 当主人应该选择哪些孩子在场。国家支持的儿童选择被固定在适当的位置;当状态发生变化时,kitsui替换和处置旧的选择。
const root = Component("section")
const view = State<"summary" | "details">(root, "summary")
Component("button")
.attribute.set("type", "button")
.text.set("Summary")
.event.owned.on.click(() => view.set("summary"))
.appendTo(root)
Component("button")
.attribute.set("type", "button")
.text.set("Details")
.event.owned.on.click(() => view.set("details"))
.appendTo(root)
root.append(view.map(root, value => {
if (value === "summary") {
return Component("p").text.set("Short summary")
}
return Component("p").text.set("Full details")
}))
样式
使用 Style(...) 对于可重用的样式片段和 Style.Class(...) 浏览已注册的CSS类。使用 Style.after(...) 当可重用类之间的顺序很重要时。
const cardBase = Style({
borderRadius: 8,
padding: 12,
})
const cardStyle = Style.Class("card", {
...cardBase,
$accent: "#0a7",
border: "1px solid $accent",
})
const raisedStyle = Style.after(cardStyle).Class("card-raised", {
...cardBase,
boxShadow: "0 2px 12px rgba(0, 0, 0, 0.15)",
})
const card = Component("button")
const raised = State(card, false)
card
.attribute.set("type", "button")
.class.add(cardStyle)
.class.bind(raised, raisedStyle)
.style.set({
$panelAccent: "#0a7",
borderColor: "$panelAccent",
})
.text.set("Hover for elevation")
.event.owned.on.pointerenter(() => raised.set(true))
.event.owned.on.pointerleave(() => raised.set(false))
字符串样式的值支持计算速记。Kitsui编译 [expression] 来 calc(expression) 并在表达式内展开变量简写。当CSS需要文本括号时,例如命名的网格线,将方括号翻倍。
const layoutStyle = Style.Class("layout", {
$gap: "1rem",
gridTemplateColumns: "[[content-start]] [100% - ${sidebarWidth: 18rem} - $gap] [[content-end]]",
width: "[100% - $gap]",
})
它发出文字 [content-start] 和 [content-end] 网格线名称。括号内的表达式变成 calc(...) 值, $gap 加上 ${sidebarWidth: 18rem} 变成CSS变量引用。
分解
使用 component.breakdown(...) 当State驱动一个键列表或区域时,每个组件都应该被键重用。
interface Todo {
id: string
label: string
}
const list = Component("ul")
const todos = State(list, [
{ id: "first", label: "Write the README" },
])
list.breakdown(todos, (list, Part, todos) => {
for (const todo of todos) {
Part(todo.id, () => Component("li"))
.text.set(todo.label)
.appendTo(list)
}
})
为每个逻辑项使用一个稳定的键。 Part 返回相同的组件,而该键仍然存在。它的构建器在该键需要一个新组件时运行,并且必须返回一个尚未放置的新的、活动的、无主的组件。
更新和放置每一个返回的零件在每一个过程中。 Part 永远不会为你放置组件,放置调用的顺序建立了呈现的顺序。如果一个成功的传球漏掉了一个关键,kitsui就会处理掉那个部分。
保持回调同步:不要使用异步回调, await,或延迟 Part 还有安置电话。以后的状态更新是批处理的,可以跳过中间值,因此每次都从提供给回调的值传递。
在回调中使用kitsui放置api。如果您需要检查物理DOM,请在回调返回后执行。即使你把每个部件放在别的地方,故障仍然占有它。
功能指数
Component:append,prepend,insert、条件变量;clear,use,and,is,as,remove,extend.- 分解:
component.breakdown(...)和Component.Breakdown(...)用于从状态值进行稳定的键化呈现。 - 放置扩展:
appendTo,prependTo,insertTo,条件变量,以及place. - 机械手:
ClassManipulator,StyleManipulator,AttributeManipulator,TextManipulator,EventManipulator,AriaManipulator. State:set,update平等选项,订阅,map,debounce,throttle,mapAsync,truthy,falsy,or,Group,extend.Style:片段、类、有序类after、变量、嵌套选择器、媒体查询、伪选择器、动画帮助器和粘性帮助器。- ARIA:用于角色、标签、引用、布尔值、
current,live. - 文档的例子: 计数器, 作文, append-when, bind-manipulators, place-iframe, 状态组, mount-dispose.