コンテンツにスキップ

pt —— パススルー属性の結合

コンポーネント内部には複数の要素があります(ボタンの <button> とその内側の <span>、入力フィールドの <input> と先頭アイコン <i> など)。ユーザーが外部からそのうちの 1 つを微調整したい場合、どうすればよいでしょうか? lh-ui の答えは PT(PassThrough)属性パックです:コンポーネントは pt prop を公開し、内部要素の名前ごとに class / style / attrs をそれぞれパススルーします。mergePt がそれをコンポーネント自身のベースクラス名と結合する役目を担います。

  • コンポーネントライブラリ内部:各内部要素に対して mergePt(props.pt?.icon, 'lh-button__icon') を呼び、そこに付けるべき class / style / attrs を得る。
  • ユーザー側は pt prop を通じてコンポーネント内部に踏み込んで微調整し、:deep() で貫く必要がない。

使うべきでない:テーマレベルのカスタマイズ(themeVars を使う);単なるクラス名の追加(class prop を使う)。

import { mergePt, type LhPtAttrs } from '@lh-ui/share/core/shared/pt'
export interface LhPtAttrs {
class?: string | string[] | Record<string, boolean>
style?: Record<string, string>
attrs?: Record<string, any>
}
関数 シグネチャ 役割
mergePt (pt: LhPtAttrs | undefined, baseClass: string) => { class: string[]; style: Record<string, string>; attrs: Record<string, any> } PT 属性とベースクラス名を結合

baseClass は常に配列の先頭で、ユーザーの class は後ろです:

mergePt({ class: 'my-icon' }, 'lh-button__icon')
// { class: ['lh-button__icon', 'my-icon'], style: {}, attrs: {} }
mergePt({ class: ['a', 'b'] }, 'lh-button__icon')
// class: ['lh-button__icon', 'a', 'b']
mergePt({ class: { active: true, disabled: false } }, 'lh-button__icon')
// class: ['lh-button__icon', 'active'] —— オブジェクトは値が真のキーだけを取る
mergePt({ class: 'a b ' }, 'lh-button__icon')
// class: ['lh-button__icon', 'a', 'b'] —— 文字列は空白で分割し、空文字を除外する

なぜ baseClass を先にするのか:BEM ベースクラス名の特異度がユーザーのクラス名に圧倒されてはならないからです(同じく単一クラス名の場合、後ろに書いたものが先に書いたものを上書きする)。コンポーネントの --primary モディファイアは、依然としてユーザーの my-icon に勝っていなければなりません。

mergePt(undefined, 'lh-button__icon')
// { class: ['lh-button__icon'], style: {}, attrs: {} } —— pt が空のときはすべて空
mergePt({ style: { color: 'red' }, attrs: { 'data-x': '1' } }, 'lh-button__icon')
// { class: ['lh-button__icon'], style: { color: 'red' }, attrs: { 'data-x': '1' } }

注意:style / attrs は参照のパススルーで、コピーは行いません。UI 層は受け取った後そのまま v-bind="merged.attrs" / :style="merged.style" できます;コンポーネント内部で戻り値を mutate しないでください。

コンポーネント内部(ボタンのアイコン要素)

Section titled “コンポーネント内部(ボタンのアイコン要素)”
// button コンポーネント
const iconPt = computed(() => mergePt(props.pt?.icon, 'lh-button__icon'))
<template>
<button class="lh-button">
<i v-if="icon" v-bind="iconPt.attrs" :class="iconPt.class" :style="iconPt.style" />
<span><slot /></span>
</button>
</template>
<lh-button
:pt="{
icon: { class: 'text-lg', style: { marginRight: '4px' } },
$root: { attrs: { 'data-test': 'submit' } },
}"
>
送信
</lh-button>
const iconPt = mergePt(pt?.icon, 'lh-button__icon')
return (
<button className="lh-button">
{icon && <i className={iconPt.class.join(' ')} style={iconPt.style} {...iconPt.attrs} />}
</button>
)
  • 浅い結合で、deepMerge はしない:class は配列レベルで継ぎ接ぎし、style / attrs はパック全体を差し替える。ここで深い結合にセマンティクスはない(class はツリーではなくリストである)。
  • pt が空のときは全空のオブジェクトを返す:UI 層は何も考えず分解でき、空判定が不要。
  • 戻り値の class は配列:Vue の :class は配列を受け取るが、React は .join(' ') が必要。
  • ベースクラス名は省略できない:mergePt(pt, '') は先頭が空文字の配列になってしまう——BEM のベースクラス名はスタイルのアンカーであり、各内部要素に必ずなければならない。
@lh-ui/share/core/pt → mergePt( )
返回值
(填好参数后点运行)

Copyright © 2024-2026 FengMo WenXue