React 与 Vue 示例

根据下载状态更新按钮和图标的类名,并在多个组件间共用命名配置。

示例适用于 React + TypeScript 或 Vue 3.3+ 项目。安装 @lai9fox/bem 后,选择对应框架的组件,并添加下方的 button.css。

React:下载按钮

保存为 DownloadButton.tsx。父组件通过 onDownload 处理下载,下载期间传入 loading={true}。类名生成器在组件外创建,渲染时根据 props 生成类名。

import { createBem } from "@lai9fox/bem";
import "./button.css";

const button = createBem({ namespace: "ui-" }).block("button");

type DownloadButtonProps = {
  disabled?: boolean;
  loading?: boolean;
  onDownload: () => void;
};

export function DownloadButton({
  disabled = false,
  loading = false,
  onDownload,
}: DownloadButtonProps) {
  return (
    <button
      type="button"
      disabled={disabled || loading}
      aria-busy={loading}
      onClick={onDownload}
      className={button.classes({
        block: true,
        modifiers: { disabled: disabled || loading, loading },
      })}
    >
      <span
        aria-hidden="true"
        className={button.classes({
          elements: { icon: true },
          elementModifiers: { icon: { loading } },
        })}
      >
        {loading ? "…" : "↓"}
      </span>
      {loading ? "下载中" : "下载"}
    </button>
  );
}

Vue:响应式下载状态

保存为 DownloadButton.vue。父组件通过 :loading="loading" 传入状态,通过 @download="startDownload" 处理下载。computed 在相关 props 变化时更新类名。

<script setup lang="ts">
import { computed } from "vue";
import { createBem } from "@lai9fox/bem";
import "./button.css";

const props = withDefaults(
  defineProps<{
    disabled?: boolean;
    loading?: boolean;
  }>(),
  { disabled: false, loading: false },
);

const emit = defineEmits<{ download: [] }>();
const button = createBem({ namespace: "ui-" }).block("button");
const buttonClass = computed(() =>
  button.classes({
    block: true,
    modifiers: {
      disabled: props.disabled || props.loading,
      loading: props.loading,
    },
  }),
);
const iconClass = computed(() =>
  button.classes({
    elements: { icon: true },
    elementModifiers: { icon: { loading: props.loading } },
  }),
);
</script>

<template>
  <button
    type="button"
    :disabled="props.disabled || props.loading"
    :aria-busy="props.loading"
    :class="buttonClass"
    @click="emit('download')"
  >
    <span :class="iconClass" aria-hidden="true">{{
      props.loading ? "…" : "↓"
    }}</span>
    {{ props.loading ? "下载中" : "下载" }}
  </button>
</template>

配套样式与输出

在组件所在目录创建 button.css:

.ui-button {
  display: inline-flex;
  align-items: center;
  padding: 0.5rem 0.75rem;
  border: 1px solid currentColor;
  background: white;
  color: #14532d;
}

.ui-button--disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.ui-button--loading {
  cursor: wait;
}

.ui-button__icon {
  margin-inline-end: 0.5rem;
}

.ui-button__icon--loading {
  font-weight: 700;
}

下载期间,类名如下:

  • 按钮:ui-button ui-button--disabled ui-button--loading。
  • 图标 span:ui-button__icon ui-button__icon--loading。

disabled 属性禁用按钮,aria-busy 标记忙碌状态,类名用于设置样式。下载状态和请求由父组件管理。条件组合规则见 classes() API。

共用命名配置

在 bem.ts 中创建工厂,供各组件导入并调用 bem.block(name)。下面以导航项为例,为当前项添加修饰类:

// bem.ts
import { createBem } from "@lai9fox/bem";

export const bem = createBem({ namespace: "ui-" });
// navigation.ts
import { bem } from "./bem";

const nav = bem.block("nav");

export function navigationItemClass(current: boolean) {
  return nav.classes({
    elements: { item: true },
    elementModifiers: { item: { current } },
  });
}

navigationItemClass(true);
// 'ui-nav__item ui-nav__item--current'

按钮组件也可以导入这个 bem,调用 bem.block("button")。修改类名前缀或连接符时,需同步更新 CSS 选择器。配置选项见 createBem。