9 个版本

0.2.3 2024 年 8 月 8 日
0.2.2 2024 年 8 月 6 日
0.2.1 2024 年 5 月 15 日
0.2.0 2024 年 3 月 31 日
0.1.1 2023 年 11 月 27 日

#77 in 模板引擎

Download history 107/week @ 2024-05-11 20/week @ 2024-05-18 2/week @ 2024-05-25 1/week @ 2024-06-08 228/week @ 2024-08-03 23/week @ 2024-08-10

每月 251 次下载

GPL-3.0-or-later

65KB
1K SLoC

CEWT

带有(类型守卫)模板的定制元素。发音像“cute”

此项目的范围目前正在变化。该项目最初打算最终成为一个 SSR/SSG 套件的一部分,但我意识到,“这过于复杂了”!我不认为 TypeScript 代码生成会有太多(如果有的话)变化,所以如果您想将其用作项目的一部分,最好只使用 cewt codegen 命令和 --inline-html 选项。

安装

目前我不提供任何预编译的二进制文件,因此现在您可以 cargo install cewt

概述

Cewt 不打算强迫您接受一种全新的抽象(即:撒谎并让用户承担后果)来与 DOM 交互。从 HTML 片段自动生成 TypeScript 代码有 2 个主要优点。

  1. 运行时的计算开销更少
  2. 对其为您所做的工作完全透明。

TypeScript 代码生成

在定义您的元素时,cewt 会递归地在指定的文件夹中搜索 .html 文件。它要查找的 .html 文件是那些在根级别定义了一个或多个 <template> 元素的文件。这些模板可以用来创建 自主定制元素定制内置元素

自主定制元素

  • 扩展 HTMLElement
  • 在文档中显示为(例如)<my-custom-element>
    • 因此可以查询选择为 my-custom-element
  • 利用模板片段,通过克隆内部元素到Shadow-DOM中
    • 允许使用<slot>元素来帮助实现SSR
    • 类似于自己的文档树,这意味着查询选择文档无法触及由模板定义的元素。
    • 类似于自己的CSS上下文,允许您链接CSS文件而不会影响整个文档,尽管文档的CSS规则仍然可能影响您的元素,例如继承字体样式。

自定义内置元素

  • 扩展您希望扩展的任何元素的类
  • 在文档中以(例如)<div is="my-custom-element">的形式显示
    • 因此,可按如下查询选择:div[is="my-custom-element"]
  • 通过将内部元素作为自定义元素的子元素来克隆您的模板片段,但这只有在从服务器检索元素时该元素为空的情况下才有效。
  • 如果您只想有一个与HTML运行相关联的元素,并且不想改变文档的语义,那么这很有用。

注意:在撰写本文时,Safari目前不能做自定义内置元素,在苹果的典型的“不同”风格。Cewt本身不提供任何polyfill。我的个人解决方案是在以下检查失败的情况下,可选地导入@ungap/custom-elements npm包。

function supportsExtendingBuiltinElements() {
    try {
        const newElemName = "test-button-" + Date.now().toString(36);
        class HTMLTestButton extends HTMLButtonElement {};
        customElements.define(newElemName, HTMLTestButton, { extends: "button" });
        const newBtn = document.createElement("button", { is: newElemName });
        return newBtn instanceof HTMLButtonElement && newBtn instanceof HTMLTestButton;
    }catch(ex: any) {
        return false;
    }
}

如果您想在Safari上使用自定义内置元素,则必须这样做(鉴于iPhone上只有这个唯一浏览器,在iOS上,苹果只给您一个选择的错觉)。如果您的项目只使用自主自定义元素,或者您不关心iPhone,那么polyfill不是必需的。如果您确实使用了polyfill,请记住在注册自定义元素之前导入它。

如何使用

使用cewt有两个步骤。

  1. 定义您的模板
  2. 扩展自动生成的代码
  3. 生成HTML
    • 不一定需要。我可能无论如何都会去掉这一步。

模板定义

要使用Cewt定义您的自定义元素,请使用cewt codegen命令。

  • 此命令有一个必需参数
    • <PATH>,一个包含HTML模板的文件夹,这些模板可能位于子文件夹中。
  • 此命令还有两个可选参数
    • --exclude <NAME>,(可以指定多次)指定要排除的文件夹名称,默认为node_modules
    • --inline-html,这将包括您的模板片段的内部HTML作为自动生成代码的一部分。
      • 如果没有指定,默认情况下,自动生成的代码将引用一个由唯一自动生成的ID指定的<template>。尽管这需要下面的“捆绑”步骤。
      • 您可以在以下情况下指定此选项
        • 您无法使用内联模板(无代码的模板)。
        • 您希望自定义元素成为可移植模块的一部分。
        • 在每个生成的完整 *.html 文件中包含您的模板 HTML 片段似乎很愚蠢。

自动生成的 TypeScript 文件的位置取决于您的 HTML 片段文件是否被命名为 template.html 或类似 my-element-thing.html。如果您的 HTML 片段文件被命名为 template.html,它将在 HTML 文件所在的同一文件夹中创建一个 _autogen.ts 文件,否则如果不存在,它将创建一个 _autogen 文件夹,并创建一个与 .html 文件具有相同基本名称的 .ts 文件。例如

  • (...)
    • my-component
      • _autogen.ts (请勿触摸)
      • template.html (包含 1 个或多个模板)
      • index.ts (您的代码在此,文件可以命名为任何名字)
    • my-other-component
      • _autogen.ts (请勿触摸)
      • template.html (包含 1 个或多个模板)
      • index.ts (您的代码在此,文件可以命名为任何名字)
  • (...)
    • _autogen (请勿触摸)
      • my-component.ts
      • my-other-component.ts
    • my-component.html
    • my-component.ts
    • my-other-component.html
    • my-other-component.ts

请注意,您可以在每个 HTML 片段文件中有多个模板定义。所有自动生成的类都将位于同一个结果 .ts 文件中。

基本示例

以下是一个基本 自主自定义元素 定义的示例。

<template cewt-name="cewt-intro">
    <h1>The Custom Element</h1>
    <p>Says: <slot name="intro-text"></slot></p>
</template>

请注意,在生成 TypeScript 代码时,它检查 <slot> 元素的第一子元素以确定哪些元素将填充该插槽。如果没有找到,它假设插槽将由一个 <span> 填充。当然,没有任何保证 slots.introText 是一个 HTMLSpanElement,但 TypeScript 是一种真正的语言,我们只是对自己说谎,让我们的 IDE 的自动完成稍微更了解我们的代码要做的事情,这就是我选择使用“类型守卫”而不是“类型安全”来描述这个工具的原因。

假设文件被命名为 template.html,我们可以创建以下 TypeScript。

import {CewtIntroAutogen} from "./_autogen.ts";

class CewtIntroElement extends CewtIntroAutogen {
    constructor() {
        super();
        // Custom behaviour here if desired.
        // Note that the element might not be in the document at this point.
        // This is called after registration if the element was already on the page.
    }
    connectedCallback() {
        // Called whenever the element has been added to the page.
        // Also called after registration if the element was already on the page.
        // It is recommended to move have logic here as elements might be constructed only to be thrown away.
        // Though also note elementes removed from the page may also be re-added.

        // All properties of `this.slots` are getter functions. If an element doesn't exist, it is created.
        console.log("<cewt-intro> has been added to the page, and it says: " + this.slots.introText.innerText);
    }
    disconnectedCallback() {
        // cleanup logic, etc.
        console.log("<cewt-intro> has been removed from the page");
    }
    adoptedCallback() {
        // Not needed in most cases, see https://stackoverflow.com/questions/50995139/ for details.
    }
}
CewtIntroElement.registerElement(); // Required. Make sure this is called with the child class.

现在,当以下内容在发送给用户的 HTML 文档中(假设您的代码也已导入)

<cewt-intro>
    <span slot="intro-text">This is the first example!</span>
</cewt-intro>

以下内容将打印到控制台

<cewt-intro> has been added to the page, and it says: This is the first example!

要程序化创建此元素,您只需 new CewtIntroElement(),出于某种原因,document.createElement("cewt-intro") 在所有浏览器中都不起作用。

自定义属性示例

如果您想使自定义元素具有自定义属性,您可以定义它们如下

<template cewt-name="example-element" cewt-attributes="my-attribute, my-other-attribute">
</template>

这允许您像这样使用它们

import {ExampleElementAutogen} from "./_autogen.ts";

class ExampleElementElement extends ExampleElementAutogen {
    constructor() {
        super();
    }
    onMyAttributeChanged(oldValue: string | null, newValue: string | null) {
        // called when the function name implies
    }
    onMyOtherAttributeChanged(oldValue: string | null, newValue: string | null) {
        // called when the function name implies
    }
    connectedCallback() {
        // The super-class includes getters/setters for your custom attributes
        console.log("my-attribute is:", this.myAttribute);
        console.log("my-other-attribute is:", this.myOtherAttribute);
    }
}
ExampleElementElement.registerElement(); // Required. Make sure this is called with the child class.

这样可以在文档中引用

<example-element my-attribute="value" my-other-attribute="valueeee"></example-element>

扩展内置元素

尽管 Webkit 团队对此事的看法,但扩展内置元素实际上是有用的,因为您可以使用自定义定义的行为创建 <button> 元素或 <dialog> 元素,这些行为存在于作用域上下文中,即您的扩展类。有关更多信息,请参阅WHATWG 的“自主自定义元素的缺点”

无论如何,这里有一个示例。

<template cewt-name="counter-example" cewt-extends="button">
    You've clicked me <span cewt-ref="count">0</span> time(s)!
</template>
class CounterExampleElement extends CounterExampleAutogen {
    count: number;
	constructor() {
		super();
		this.count = 0;
		this.addEventListener("click", (ev) => {
			this.count += 1;
            // Refs are just references to elements that aren't a part of the slot system
			this.refs.count.innerText = this.count;
		})
	}
}
CounterExampleElement.registerElement();

由于这扩展了 <button>,你可以随心所欲地进行制表符焦点和点击。只需将 <button is="counter-example"></button> 添加到文档中,或者程序化地构建一个 new CounterExampleElement() 即可。

HTML文档生成

如果需要这一步

  • 你有内联模板
  • 在TypeScript代码生成时没有指定 --inline-html

我在争论HTML文档生成是否超出这个工具的范围,所以我不打算写详细的文档,但现在,你可以运行 cewt bundle-single --help 来开始。

依赖关系

~11–20MB
~261K SLoC