チェックボックス・入力コントロールの属性/プロパティ分離とSSR対応の修正

shoelace-style/webawesome

WebAwesomeのフォーム関連コンポーネント群で、属性(attribute)とプロパティ(property)の分離が不完全だった問題と、サーバーサイドレンダリング(SSR)環境でのグローバルAPI呼び出しによるエラーを一括修正したPRです。

背景

今回の修正は、複数の独立したバグが共通する設計上の根本原因を持っていたことで、一括対応が可能でした。問題は大きく「属性とプロパティの分離」と「SSR非対応API呼び出し」の2カテゴリに分類されます。

属性とプロパティの分離については、HTMLの仕様上、checked 属性はデフォルト値(defaultChecked)を表し、プロパティがユーザーの現在の状態を保持します。しかし <wa-checkbox><wa-switch> では checked プロパティが this.hasAttribute('checked') で初期化されており、属性とプロパティが混同されていました。この設計の結果、ReactやVue 3のフレームワークからプロパティバインディング(:checked)で値を渡しても初回レンダリング時に反映されないという問題が発生していました(#1878#1230)。

SSR非対応API呼び出しについては、<wa-drawer><wa-dialog> のモジュールトップレベルで document.addEventListener を呼び出していたため、document オブジェクトが存在しないサーバー環境でインポートするだけでエラーになっていました(#1860)。同様に HasSlotControllerchildNodesquerySelector などのクライアント専用APIをSSR環境でも呼び出していました(#1861)。

技術的な変更

checked プロパティのget/setアクセサへの置き換え

<wa-checkbox><wa-switch>checked プロパティが、単純なフィールドからget/setアクセサに置き換えられました。これがフォームコントロールの属性/プロパティ分離における中心的な変更です。

変更前(checkbox.ts):

@property({ type: Boolean, attribute: false }) checked: boolean = this.hasAttribute('checked');

変更後(checkbox.ts):

_checked: boolean | null = null;

get checked() {
  if (this.valueHasChanged) {
    return Boolean(this._checked);
  }
  return this._checked ?? this.defaultChecked;
}

@property({ type: Boolean, attribute: false })
set checked(val: boolean) {
  this._checked = Boolean(val);
  this.valueHasChanged = true;
}

_checkednull で初期化し、valueHasChanged フラグで「プロパティが明示的にセットされたか」を追跡する設計です。プロパティが未設定の場合は defaultChecked(=checked 属性の値)にフォールバックし、一度でもプロパティがセットされれば _checked の値を優先します。同様のパターンが <wa-switch> にも適用されています。

また、defaultChecked の変更を監視していた @watch('defaultChecked') デコレータが削除され、代わりに connectedCallback での初期化と @watch(['checked', 'defaultChecked']) での一元的な変更監視に変更されました。

フォームリセット時のシャドウDOM内inputの明示的なリセット

<wa-input><wa-textarea> のフォームリセット処理が修正されました。

変更前(input.ts):

formResetCallback() {
  this.value = this.defaultValue;
  super.formResetCallback();
}

変更後(input.ts):

formResetCallback() {
  this.value = null;

  if (this.input) {
    // @ts-expect-error
    this.input.value = this.value;
  }

  super.formResetCallback();
}

従来の実装では this.value = this.defaultValue とした際、LitElementのリアクティブプロパティはシャドウDOM内のネイティブ <input> 要素のライブ値(.value プロパティ)を更新しないケースがありました。明示的に this.input.value を書き換えることで、リセット後に内部inputの実際の値が確実にクリアされます(#1640)。

さらに updated() メソッドで defaultValue の変更時にも updateValidity() を呼び出すよう修正し、required<wa-input> に対してプロパティで値をセットした際にバリデーション状態が即座に更新されるようになりました(#1205)。

radio-group と slider の defaultValue 変更への対応

<wa-radio-group>updated() フックで defaultValue の変更も syncRadioElements() のトリガーに追加されました。これにより、属性(value attribute)が動的に変更された際に defaultValue 経由でラジオボタンの選択状態が同期されるようになります(#1273)。

<wa-slider> では _value の型が number から number | null に変更され、valueHasChanged フラグで属性由来のデフォルト値とプロパティ由来の値を区別するようになりました。value ゲッターでは clamp() を呼び出す場所を値の返却時に統合し、フォームリセット時に _value = nullvalueHasChanged = false をセットすることで、リセット後は属性値(defaultValue)にフォールバックする動作が実現されています。

drawer と dialog のトップレベルAPI呼び出しを isServer でガード

<wa-drawer><wa-dialog> で、クラス定義の外部(モジュールトップレベル)に置かれていた document.addEventListener 呼び出しが、既存の isServer フラグによる条件分岐の内側に移動されました。

変更前(drawer.ts):

// モジュールトップレベルに配置
document.addEventListener('click', (event: MouseEvent) => {
  // data-drawer="open ..." の処理
});

if (!isServer) {
  // Safari の light dismiss 対応
  document.body.addEventListener('pointerdown', ...);
}

変更後(drawer.ts):

if (!isServer) {
  // data-drawer="open ..." の処理もサーバーでは実行しない
  document.addEventListener('click', (event: MouseEvent) => {
    // ...
  });

  // Safari の light dismiss 対応
  document.body.addEventListener('pointerdown', ...);
}

これでモジュールをインポートするだけでSSRがクラッシュしていた問題が解消されます。

HasSlotController のSSR対応

HasSlotController では3箇所にSSR対応の防御的コードが追加されました。

  • hasDefaultSlot(): this.host.childNodes が存在しない場合は false を返す早期リターンを追加
  • hasNamedSlot(): querySelector をオプショナルチェーン(querySelector?.)で呼び出すように変更
  • hostConnected() / hostDisconnected(): shadowRoot?.addEventListener?.shadowRoot?.removeEventListener?. でオプショナルチェーンを使用

これらの変更により、HasSlotController を使用するすべてのコンポーネント(<wa-drawer><wa-button> など)がSSR環境の render() メソッド内で安全に動作するようになります。

設計判断

valueHasChanged フラグによる属性/プロパティ分離が、複数コンポーネントにわたる一貫したパターンとして採用されました。

HTMLネイティブの <input> 要素では defaultValue 属性と value プロパティが独立して管理され、フォームリセット時はプロパティが defaultValue に戻ります。このPRではカスタム要素でも同等のセマンティクスを実現するため、「プロパティが一度でも明示的にセットされたか」を valueHasChanged で追跡するアプローチを統一的に適用しています。_checked = null という初期値は「まだプロパティがセットされていない」状態を表す番兵値として機能しており、null チェックにより属性由来の defaultChecked へのフォールバックを自然に実現しています。

SSRの修正では、既存の isServer ユーティリティを最大限に活用する方針が一貫しています。HasSlotController でのオプショナルチェーン採用は、isServer による明示的な条件分岐を避けながら、サーバー環境での null / undefined なオブジェクトへのアクセスを安全に無効化する簡潔な手法です。

まとめ

本PRは、HTMLの属性/プロパティ分離のセマンティクスをWebAwesomeのカスタム要素に正確に実装し直すと同時に、SSR環境でのグローバルAPI依存を排除した修正です。valueHasChanged フラグを軸とした設計パターンが複数コンポーネントに統一的に適用されており、ReactやVue 3などのフレームワーク連携でのバインディング問題、フォームリセット後のバリデーション不整合、SSRクラッシュといった複数のバグが共通の根本原因から解消されたことがわかります。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
985251bb

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

「総論→各論→結論」の3部構成が明確です。リード文、背景、技術詳細、設計判断、まとめの各要素が過不足なく含まれており、理想的な構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きのシンタックスハイライト、Issue番号のリンク記法(例: [#1878](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

SSR、属性/プロパティの分離、LitElementのライフサイクルなど、専門知識を持つエンジニアを対象とした適切な技術レベルと表現で書かれています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論の構成になっており、各段落もトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られています。非常に読みやすい構成です。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているコードブロックは、提供されたDiffの内容と正確に一致しています。変更前後のコードが的確に示されており、変更点が理解しやすいです。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「属性/プロパティ分離」「get/setアクセサ」「オプショナルチェーン」などの技術用語が正確かつ適切な文脈で使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

コード変更の理由やその影響に関する説明は、Diffや関連Issueの内容に裏付けられており、技術的に正確かつ論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張は、PRのDescriptionに記載された複数のIssue番号やDiffの内容と整合性が取れています。ハルシネーション(捏造)は見られませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#2105)や複数のIssue番号が正確に記載・リンクされています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルはPRのタイトル「fix checkbox + input issues and some SSR issues」の内容を的確に要約しており、主題にズレはありません。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事の内容はPR情報(Diff、Issue)に限定されており、サポート状況やリリース日程など、PR外の不確かな外部知識は含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現に関する不正確な記述は見られず、事実関係が正しく記述されています。