使用 Tailwind CSS 编写组件变体的不同方法

使用 tailwind css 编写组件变体的不同方法

问题

传统上,当使用 tailwind css 编写组件变体时,我会使用简单的类映射将 prop 值映射到组件插槽:

type ttheme = "default" | "secondary";
interface icomponentslot {
  root: string;
  accent: string;
}

const theme_map: record<ttheme, icomponentslot> = {
  default: {
    root: "bg-red hover:background-pink",
    accent: "text-blue hover:text-green",
  },
  secondary: {
    root: "bg-green hover:background-black",
    accent: "text-pink hover:text-white",
  }
}

<div :class="theme_map['default'].root">
  <div :class="theme_map['default'].accent">/**/</div>
</div>

这种方法的问题是保持所需的类相互一致,确保每个变体都具有所有必需的类,特别是在更复杂的组件中。在我们想要跨不同组件插槽共享样式(例如文本颜色)的组件中,需要我们单独更新每个插槽。

tailwind 的局限性

tailwind 通过扫描代码库和匹配字符串来生成实用程序类,这意味着虽然 tailwind 可以从任意值创建类,但我们无法在不创建安全列表的情况下动态启动它们。所以这行不通:

// .ts
type ttheme = "default" | "secondary";

const colors: record<ttheme, string> = {
  default: "red",
  secondary: "blue",
}

// .html
<div :class="`text-[${colors[default]}]`">

但是,我们可以通过利用 css 变量来模仿所需的行为,这是 tailwind 在其许多类的底层使用的东西。我们可以使用以下语法通过 tailwind 中的类设置变量: [--my-variable-key:--my-variable-value]
那么我们如何更新上面的代码示例以使用动态值?

// .ts
type ttheme = "default" | "secondary";

const colors: record<ttheme, string> = {
  default: "[--text-color:red]",
  secondary: "[--text-color:blue]",
}

// .html
<div
  :class="[
    colors[default],
    'text-[--text-color]'
  ]">

解决最初的问题

现在我们了解了 tailwind 的局限性,我们需要研究解决由类映射方法引起的初始问题的方法。我们可以从简化我们的类映射开始:

type ttheme = "default" | "secondary";
interface icomponentslot {
  root: string;
  accent: string;
}

const theme_map: record<ttheme, string> = {
  default: "[--backgound:red] [--hover__background:pink] [--text:blue] [--hover__text:green]",
  secondary: "[--backgound:green] [--hover__background:black] [--text:pink] [--hover__text:white]",
}

<div class="bg-[--background] hover:bg-[--hover__background]">
  <div class="text-[--text] hover:text-[--hover__text">/**/</div>
</div>

不幸的是,仅此并不能解决我们的问题,我们仍然无法确保我们已经设置了正确显示每个变体所需的所有类。那么我们如何才能更进一步呢?好吧,我们可以开始编写一个接口来强制我们设置指定的值:

interface icomponentthemevariables {
  backgound: string;
  hover__backgound: string;
  text: string;
  hover__text: string;
}

const theme_map: record<ttheme, icomponentthemevariables> = {
  default: {
    backgound: "[--backgound:red]",
    text: "[--hover__background:pink]",
    hover__background: "[--text:blue]",
    hover__text:"[--hover__text:green]",
  },
  secondary: {
    backgound: "[--backgound:green]",
    text: "[--hover__background:black]",
    hover__background: "[--text:pink]",
    hover__text:"[--hover__text:white]",
  },
}

所以这可行,但是,仍然有一个问题,没有什么可以阻止我们混合字符串值。例如,我们可能不小心将关键背景设置为 [--text:blue]。

所以也许我们也应该输入我们的值。我们无法输入整个类,这将是维护的噩梦,那么如果我们输入颜色并编写一个辅助方法来生成 css 变量会怎么样:

type tcolor = "red" | "pink" | "blue" | "green" | "black" | "white";

interface icomponentthemevariables {
  backgound: tcolor;
  hover__backgound: tcolor;
  text: tcolor;
  hover__text: tcolor;
}

// example variablemap method at the end of the article

const theme_map: record<ttheme, string> = {
  default: variablemap({
    backgound: "red",
    text: "pink",
    hover__background: "blue",
    hover__text:"green",
  }),
  secondary: variablemap({
    backgound: "green",
    text: "black",
    hover__background: "pink",
    hover__text:"white",
  }),
}

好的,这太棒了,我们可以确保始终为组件的每个变体设置正确的变量。但是等等,我们刚刚遇到了 tailwind 发现的最初问题,我们不能只生成类,tailwind 不会选择它们。那么我们该如何解决这个问题呢?

js 中的 css 怎么样?

js 中的 css 似乎是显而易见的答案,只需生成一个类,该类创建具有正确变量的自定义类。但有一个障碍,javascript 在客户端上运行,这会导致“flash”,组件最初加载时没有设置变量,然后更新才能正确显示。

js 库中的 css 如何处理这个问题?

像 emotion 这样的库通过插入有关组件的内联样式标签来处理这个问题:

<body>
  <div>
    <style data-emotion-css="21cs4">.css-21cs4 { font-size: 12 }</style>
    <div class="css-21cs4">text</div>
  </div>
</body>

这对我来说不是正确的方法。

那么我们如何解决这个问题呢?

我正在使用 vue,这让我走上了 css 中 v-bind 的道路,这是 vue 中将 javascript 绑定为 css 值的功能。过去我只很少使用这个功能,并且从未深入研究过它的用途。 css 中的 v-bind 只是在相关元素上设置内联样式。

这让我想起了几个月前我从 tailwind css 的创建者 adam wathan 那里看到的一条推文:

那么这对我们有什么帮助呢?好吧,虽然我们无法动态生成 tailwind 类,但我们可以动态生成内联样式并使用 tailwind 类中的这些内联样式。那会是什么样子?

type tcolor = "red" | "pink" | "blue" | "green" | "black" | "white";

interface icomponentthemevariables {
  backgound: tcolor;
  hover__backgound: tcolor;
  text: tcolor;
  hover__text: tcolor;
}

// example variablemap method at the end of the article

const theme_map: record<ttheme, string> = {
  default: variablemap({
    backgound: "red",
    text: "pink",
    hover__background: "blue",
    hover__text: "green",
  }),
  secondary: variablemap({
    backgound: "green",
    text: "black",
    hover__background: "pink",
    hover__text: "white",
  }),
}

<div
  class="bg-[--background] hover:bg-[--hover__background]"
  :style="theme_map['default']"
>
  <div class="text-[--text] hover:text-[--hover__text">/**/</div>
</div>

/*
output:
<div
  class="bg-[--background] hover:bg-[--hover__background]"
  style="--background: red; --text: pink; --hover__background: blue; --hover__text: green;"
>
  <div class="text-[--text] hover:text-[--hover__text">...</div>
</div>
*/

结论

通过结合 typescriptcss 变量和内联样式的功能,我们能够确保在使用 tailwind css 时,我们组件的每个变体都会设置每个选项并具有正确的类型。

这是一种实验性方法,我相信会有一些强烈的意见。我确信这是最好的方法吗?现阶段,我不确定,但我认为它有腿。

如果您觉得这篇文章有趣或有用,请在 bluesky(我在这里最活跃)、medium、dev 和/或 twitter 上关注我。

示例:变量映射

// variableMap example
export const variableMap = <T extends Record<string, string>>(
  map: T
): string => {
  const styles: string[] = [];
  Object.entries(map).forEach(([key, value]) => {
    const wrappedValue = value.startsWith("--") ? `var(${value})` : value;
    const variableClass = `--${key}: ${wrappedValue};`;
    styles.push(variableClass);
  });
  return styles.join(" ");
};

以上就是使用 Tailwind CSS 编写组件变体的不同方法的详细内容,更多请关注硕下网其它相关文章!