From 23f91e209d93216ecc2053ea02eaad11e7f9256c Mon Sep 17 00:00:00 2001 From: rdlabo Date: Sat, 22 Aug 2026 03:02:35 +0900 Subject: [PATCH 1/8] docs: add theme special markup guides --- scripts/project-manifest.ts | 24 +++++++ .../docs/ja/special-markup.md | 72 +++++++++++++++++++ src/ionic-theme-md3/docs/ja/special-markup.md | 36 ++++++++++ .../docs/ja/using-ion-item-group.md | 36 ++++++++++ 4 files changed, 168 insertions(+) create mode 100644 src/ionic-theme-ios26/docs/ja/special-markup.md create mode 100644 src/ionic-theme-md3/docs/ja/special-markup.md create mode 100644 src/ionic-theme-md3/docs/ja/using-ion-item-group.md diff --git a/scripts/project-manifest.ts b/scripts/project-manifest.ts index 5d33cd7..52274c0 100644 --- a/scripts/project-manifest.ts +++ b/scripts/project-manifest.ts @@ -560,6 +560,14 @@ export const projectDefinitions: readonly ProjectDefinition[] = [ 'ガイド', ), page('Features', '機能', 'features', 'features.md', 'Guides', 'ガイド'), + page( + 'Special markup and classes', + '特別なマークアップとクラス', + 'special-markup', + 'special-markup.md', + 'Guides', + 'ガイド', + ), page( 'Experimental Animation', '実験的なアニメーション', @@ -622,6 +630,22 @@ export const projectDefinitions: readonly ProjectDefinition[] = [ ], pages: [ page('Getting Started', 'はじめに', 'readme', 'readme.md', 'Documentation', 'ドキュメント'), + page( + 'Special markup', + '特別なマークアップ', + 'special-markup', + 'special-markup.md', + 'Guides', + 'ガイド', + ), + page( + 'Using ion-item-group', + 'ion-item-groupの使用方法', + 'using-ion-item-group', + 'using-ion-item-group.md', + 'Guides', + 'ガイド', + ), page('API', 'API', 'api', 'api.md', 'Reference', 'リファレンス'), ], }, diff --git a/src/ionic-theme-ios26/docs/ja/special-markup.md b/src/ionic-theme-ios26/docs/ja/special-markup.md new file mode 100644 index 0000000..27b4975 --- /dev/null +++ b/src/ionic-theme-ios26/docs/ja/special-markup.md @@ -0,0 +1,72 @@ +--- +title: 特別なマークアップとクラス +--- + +# 特別なマークアップとクラス + +ほとんどのIonicマークアップは変更せずに使えます。以下はテーマが提供し、`demo/src/app/docs` でも使用している明示的なopt-inの組み合わせです。 + +## Primaryのsubmit button + +solidのPrimary submit buttonは、foregroundとborderの表現に `--ion-color-primary-brightness` を使います。Primary colorに対して十分なcontrastを確保できる値を定義してください。 + +```css +:root { + --ion-color-primary-brightness: #96feff; +} +``` + +```html +Submit +``` + +## 2行のinset list item + +slotを指定しない `ion-label` と `ion-note` を隣接させると、2行のitemとして表示します。iOSスタイルのinset list背景を使う場合はitemを `ion-item-group` で囲み、`ion-list-header` はgroupの外に置きます。 + +```html + + + Connections + + + + Network & internet + Mobile, Wi-Fi, hotspot + + + +``` + +通常の末尾noteとして表示したい場合は、`ion-note` に `slot="end"` を指定します。 + +## 幅いっぱいのsegment + +segment buttonを利用可能な幅に均等配置する場合は `.segment-expand` を追加します。`registerSegmentEffect` を使う場合、このclassはLiquid Glass effectのsizeも変更します。 + +```html + + New + Replied + +``` + +## Classic search bar + +ThemeのdefaultはiOS 26のsearch fieldです。従来のIonic iOS search barを維持するには `.searchbar-classic` を追加します。Toolbar固有のlayout ruleを適用するため、`ion-toolbar` の中に配置してください。 + +```html + + + +``` + +## Themeを無効にする + +個別のIonic componentで標準のiOS styleを維持する場合は `.ios26-disabled` を追加します。 + +```html +Standard Ionic button +``` + +Inset listの背景modelについては [ion-item-groupの使用方法](/docs/using-ion-item-group) を参照してください。 diff --git a/src/ionic-theme-md3/docs/ja/special-markup.md b/src/ionic-theme-md3/docs/ja/special-markup.md new file mode 100644 index 0000000..74e54a0 --- /dev/null +++ b/src/ionic-theme-md3/docs/ja/special-markup.md @@ -0,0 +1,36 @@ +--- +title: 特別なマークアップ +--- + +# 特別なマークアップ + +ほとんどのIonicマークアップは変更せずに使えます。以下は `demo/src/app/docs` でも使用している明示的なopt-inの組み合わせで、同じtemplateを `@rdlabo/ionic-theme-ios26` でも使う場合に役立ちます。 + +## 2行のinset list item + +slotを指定しない `ion-label` と `ion-note` を隣接させると、2行のitemとして表示します。通常の末尾noteとして表示したい場合は、`ion-note` に `slot="end"` を指定します。 + +```html + + + Network & internet + Mobile, Wi-Fi, hotspot + + +``` + +Applicationが `@rdlabo/ionic-theme-ios26/dist/css/md-ion-list-inset.css` をimportしている場合は、list itemを `ion-item-group` で囲み、`ion-list-header` はgroupの外に置きます。 + +```html + + Connections + + + Network & internet + Mobile, Wi-Fi, hotspot + + + +``` + +必要なimportと背景modelは [ion-item-groupの使用方法](/docs/using-ion-item-group) を参照してください。 diff --git a/src/ionic-theme-md3/docs/ja/using-ion-item-group.md b/src/ionic-theme-md3/docs/ja/using-ion-item-group.md new file mode 100644 index 0000000..e3f3d04 --- /dev/null +++ b/src/ionic-theme-md3/docs/ja/using-ion-item-group.md @@ -0,0 +1,36 @@ +--- +title: ion-item-groupの使用方法 +--- + +# MD3テーマでの `ion-item-group` の使用方法 + +MD3テーマは `@rdlabo/ionic-theme-ios26` と同じinset listのmarkupに対応しているため、Ionic modeをまたいで1つのtemplateを共有できます。この共有patternをopt-inするときに `ion-item-group` を追加します。 + +## 必要になる条件 + +次の両方に該当するときだけ必要です。 + +- `ion-list` の `inset` propertyを有効にしている +- iOS 26 packageの任意stylesheet `md-ion-list-inset.css` をimportしている + +この場合はlist itemを `ion-item-group` で囲み、list headerはgroupの外に置きます。 + +## 実装例 + +```diff + + Label ++ + ... + ... ++ + +``` + +## Import + +```css +@import '@rdlabo/ionic-theme-ios26/dist/css/md-ion-list-inset.css'; +``` + +このstylesheetは `ion-list` の背景を透明にし、背景を `ion-item-group` に持たせます。これにより `ion-list-header` を独立した要素として扱いながら、iOS 26とMD3でmarkupを共有できます。 From e2ce7405400cfc31de451c969b90a74a67e13287 Mon Sep 17 00:00:00 2001 From: rdlabo Date: Sat, 22 Aug 2026 10:52:51 +0900 Subject: [PATCH 2/8] docs: synchronize theme guide translations --- .../docs/ja/experimental-animation.md | 12 +-- src/ionic-theme-ios26/docs/ja/features.md | 90 ++++++++++++----- .../docs/ja/special-markup.md | 97 ++++++++++++++++--- .../docs/ja/using-ion-item-group.md | 61 +++++------- src/ionic-theme-md3/docs/ja/special-markup.md | 54 ++++++++--- .../docs/ja/using-ion-item-group.md | 43 ++++---- 6 files changed, 242 insertions(+), 115 deletions(-) diff --git a/src/ionic-theme-ios26/docs/ja/experimental-animation.md b/src/ionic-theme-ios26/docs/ja/experimental-animation.md index 78b56d3..6ca4ef9 100644 --- a/src/ionic-theme-ios26/docs/ja/experimental-animation.md +++ b/src/ionic-theme-ios26/docs/ja/experimental-animation.md @@ -4,13 +4,11 @@ code: [] scrollActiveLine: [] --- -任意のジェスチャーとアニメーション補助です。この機能なしでもテーマは使えます。 +以下のgestureとanimation helperは実験的な任意機能です。これらを使わなくてもテーマは動作します。 -**この機能は実験的です。この機能なしでもライブラリは利用できます。** +## `ion-tab-button` / `ion-segment-button` の Sheet of Glass -### `ion-tab-button` / `ion-segment-button` の Sheet of Glass - -`ion-tab-bar` / `ion-segment` を登録すると、`ion-tab-button` / `ion-segment-button` にアニメーション効果を表示できます。 +`ion-tab-bar` または `ion-segment` elementを登録すると、そのbuttonに移動するselection effectを追加できます。 [![ion-tab-button と ion-segment-button の Sheet of Glass アニメーション](https://i.gyazo.com/fafd726b520827f042c76b6c73abd81c.gif)](https://gyazo.com/fafd726b520827f042c76b6c73abd81c) @@ -36,9 +34,9 @@ const destroy = () => { }; ``` -### TabBarSearchable: `ion-tab-bar` と `ion-fab-button` の Searchable +## TabBarSearchable: `ion-tab-bar` と `ion-fab-button` の Searchable -`ion-tabs` 内の指定マークアップ構成で Searchable を有効にします。 +`ion-tabs` 内で次の構造を使うと、search buttonからsearch toolbarへのanimationを適用できます。 [![ion-fab-button からタブバーへ展開する TabBarSearchable アニメーション](https://i.gyazo.com/06bc63f4a474f9f19f5b1d865f5c2a85.gif)](https://gyazo.com/06bc63f4a474f9f19f5b1d865f5c2a85) diff --git a/src/ionic-theme-ios26/docs/ja/features.md b/src/ionic-theme-ios26/docs/ja/features.md index f95fb79..10e895c 100644 --- a/src/ionic-theme-ios26/docs/ja/features.md +++ b/src/ionic-theme-ios26/docs/ja/features.md @@ -1,42 +1,88 @@ --- -title: '機能' -code: [] -scrollActiveLine: [] +title: 機能 --- -CSS 変数、オプトアウトクラス、Liquid Glass mixin です。リストのマークアップは [ion-item-groupの使用方法](/docs/using-ion-item-group) を見てください。 +# 機能 -### CSS変数 +CSS変数とSass mixinでテーマをカスタマイズしたり、コンポーネント単位で段階的に導入したりできます。マークアップ固有のopt-inは [特別なマークアップとクラス](/docs/special-markup) を参照してください。 -ライブラリのデフォルトスタイルをデザインに合わせてカスタマイズできるよう、いくつかのCSS変数が用意されています。詳細はこのファイルを参照してください: -https://github.com/rdlabo-dev/ionic-theme-ios26/blob/v2.3.2/src/styles/default-variables.scss +## CSS変数 -### `.ios26-disabled` クラス +ライブラリのデフォルトスタイルをデザインに合わせてカスタマイズできるよう、複数のCSS変数を提供しています。詳細は次のファイルを参照してください。 +https://github.com/rdlabo-dev/ionic-theme-ios26/blob/v3.0.0-1/src/styles/default-variables.scss -特定のコンポーネントでiOS26テーマを無効にするには、`.ios26-disabled` クラスを追加します。 +## Liquid Glass mixin -```html - -iOS26 Design +Liquid Glass mixinを使うには、メインパッケージからSCSSファイルをimportします。 - -Standard Ionic Design +```scss +@use '@rdlabo/ionic-theme-ios26/src/styles/utils/api.scss'; + +ion-textarea label.textarea-wrapper { + @include api.glass-background; +} ``` -### Liquid Glass Mixin +## コンポーネント単位のimport -liquid glass mixinを使うには、メインパッケージからSCSSファイルをインポートします。 +段階的に導入する場合は、テーマ全体ではなく個別のコンポーネントをimportできます。 + +```css +@import '@rdlabo/ionic-theme-ios26/dist/css/utils/translucent'; +@import '@rdlabo/ionic-theme-ios26/dist/css/components/ion-action-sheet'; +@import '@rdlabo/ionic-theme-ios26/dist/css/components/ion-alert'; +@import '@rdlabo/ionic-theme-ios26/dist/css/components/ion-button'; +/* Import the remaining components your application uses. */ +``` + +### コンポーネント単位でのDark Mode + +Dark Mode対応のコンポーネントを個別にimportする場合は、Always、System、Class modeでselectorが異なるためSCSSを使います。 + +Always: ```scss -@use '@rdlabo/ionic-theme-ios26/src/styles/utils/api.scss'; +@use '@rdlabo/ionic-theme-ios26/src/styles/utils/theme-dark'; -ion-textarea label.textarea-wrapper { - @include api.glass-background; +:root { + @include theme-dark.default-variables; +} +@include theme-dark.ion-button; +@include theme-dark.ion-fab; +@include theme-dark.ion-tabs; +@include theme-dark.ion-segment; +``` + +System: + +```scss +@use '@rdlabo/ionic-theme-ios26/src/styles/utils/theme-dark'; + +@media (prefers-color-scheme: dark) { + :root { + @include theme-dark.default-variables; + } + @include theme-dark.ion-button; + @include theme-dark.ion-fab; + @include theme-dark.ion-tabs; + @include theme-dark.ion-segment; } ``` -### 追加デザイン +Class: + +```scss +@use '@rdlabo/ionic-theme-ios26/src/styles/utils/theme-dark'; + +.ion-palette-dark { + @include theme-dark.default-variables; + @include theme-dark.ion-button; + @include theme-dark.ion-fab; + @include theme-dark.ion-tabs; + @include theme-dark.ion-segment; +} +``` -iOS26デザインへの忠実度をさらに高めるため、このライブラリが提供する追加デザインを実装できます。詳細は次を参照してください: +## インタラクティブな例 -https://ionic-theme-ios26.netlify.app/main/docs +[デモでrender済みの例を見る](https://ionic-theme-ios26.rdlabo.dev/main/docs)。 diff --git a/src/ionic-theme-ios26/docs/ja/special-markup.md b/src/ionic-theme-ios26/docs/ja/special-markup.md index 27b4975..b1b1d7e 100644 --- a/src/ionic-theme-ios26/docs/ja/special-markup.md +++ b/src/ionic-theme-ios26/docs/ja/special-markup.md @@ -4,7 +4,7 @@ title: 特別なマークアップとクラス # 特別なマークアップとクラス -ほとんどのIonicマークアップは変更せずに使えます。以下はテーマが提供し、`demo/src/app/docs` でも使用している明示的なopt-inの組み合わせです。 +ほとんどのIonicマークアップは変更せずに使えます。以下はテーマが提供する明示的なopt-inです。 ## Primaryのsubmit button @@ -16,15 +16,18 @@ solidのPrimary submit buttonは、foregroundとborderの表現に `--ion-color- } ``` -```html +```html preview Submit +Continue ``` +buttonに `type="submit"` を指定できない場合に同じstyleを適用するには、`.button-submit` を使います。 + ## 2行のinset list item -slotを指定しない `ion-label` と `ion-note` を隣接させると、2行のitemとして表示します。iOSスタイルのinset list背景を使う場合はitemを `ion-item-group` で囲み、`ion-list-header` はgroupの外に置きます。 +slotを指定しない `ion-label` と `ion-note` を隣接させると、2行のitemとして表示します。iOS styleのinset list背景を使う場合はitemを `ion-item-group` で囲み、`ion-list-header` はgroupの外に置きます。 -```html +```html preview Connections @@ -40,24 +43,94 @@ slotを指定しない `ion-label` と `ion-note` を隣接させると、2行 通常の末尾noteとして表示したい場合は、`ion-note` に `slot="end"` を指定します。 +## inset listのsection header + +`.item-group-header` を `ion-item-group` に追加すると、component demo pageの先頭で使われている中央揃えのicon、title、descriptionを表示できます。 + +これは導入用のgroupです。通常のlist itemは、その後に置く別の `ion-item-group` に入れてください。 + +```html preview + + + + + +

Lists

+ Inset-list examples +
+
+
+ + First item + +
+``` + ## 幅いっぱいのsegment segment buttonを利用可能な幅に均等配置する場合は `.segment-expand` を追加します。`registerSegmentEffect` を使う場合、このclassはLiquid Glass effectのsizeも変更します。 -```html +```html preview New Replied ``` -## Classic search bar +## condense header内のclassic search bar + +Themeはdefaultでsearch barにiOS 26の外観を適用します。`collapse="condense"` を指定した `ion-header` のlarge titleの下に表示するsearch fieldには、`.searchbar-classic` を追加します。従来の塗りつぶされたiOSの外観になり、固定headerに残らずlarge titleと一緒にcollapseします。 + +`color="light"` など、colorを指定したtoolbar内に配置してください。classic背景は、そのcolorのcontrast値から生成されます。 + +次の例はIonic標準のcollapse可能なlarge title構造です。previewをscrollするとlarge titleがcollapseし、固定headerが表示されます。 + +```html preview +
+ + + Search + + + + + + Search + + + + + + + + Recent item 1 + Recent item 2 + Recent item 3 + Recent item 4 + Recent item 5 + Recent item 6 + Recent item 7 + Recent item 8 + Recent item 9 + Recent item 10 + + + +
+``` + +`.ion-page` wrapperによって、埋め込みpreviewが完全なrouted pageとして動作します。`ion-router-outlet` を使うapplicationでは通常、このpage containerは自動的に追加されます。Inset listとitemはscrollを実演するための十分なcontentを用意しているだけで、`.searchbar-classic` の必須要素ではありません。 + +## search barを含むtoolbar -ThemeのdefaultはiOS 26のsearch fieldです。従来のIonic iOS search barを維持するには `.searchbar-classic` を追加します。Toolbar固有のlayout ruleを適用するため、`ion-toolbar` の中に配置してください。 +search barとstartまたはend buttonを組み合わせる `ion-toolbar` には、`.toolbar-searchbar` を追加します。このclassはslot付きcontrolを中央揃えにし、search field周辺のspacingを調整します。 -```html - - +```html preview + + + Cancel + + ``` @@ -65,8 +138,8 @@ ThemeのdefaultはiOS 26のsearch fieldです。従来のIonic iOS search barを 個別のIonic componentで標準のiOS styleを維持する場合は `.ios26-disabled` を追加します。 -```html -Standard Ionic button +```html preview +iOS 26 theme Standard Ionic button ``` Inset listの背景modelについては [ion-item-groupの使用方法](/docs/using-ion-item-group) を参照してください。 diff --git a/src/ionic-theme-ios26/docs/ja/using-ion-item-group.md b/src/ionic-theme-ios26/docs/ja/using-ion-item-group.md index aff6da3..120b196 100644 --- a/src/ionic-theme-ios26/docs/ja/using-ion-item-group.md +++ b/src/ionic-theme-ios26/docs/ja/using-ion-item-group.md @@ -2,59 +2,44 @@ title: ion-item-groupの使用方法 --- -# ion-item-groupの使用方法 +# inset listでの `ion-item-group` の使用方法 -このテーマは、Ionic Frameworkアプリケーションを可能な限りiOS 26デザインに近づけることを目指しています。多くの場合、既存のIonicコードをそのまま使えますが、**特定の条件下でのみ** `ion-item-group` の追加が必要です。 +ほとんどのIonicマークアップは変更せずに使えます。`ion-list` で `inset="true"` を使う場合は、itemを `ion-item-group` で囲み、`ion-list-header` はgroupの外に置きます。 -## `ion-item-group` が必要な場合 +以下はframeworkに依存しないWeb Component形式のマークアップです。ReactまたはVueでは、各frameworkに対応するcomponentとpropertyの構文に置き換えてください。 -次の条件を満たす場合にのみ必要です: - -- `ion-list` で `inset` プロパティを有効にしている - -この条件に当てはまる場合のみ、リスト項目を `ion-item-group` で囲む必要があります。 - -## 実装例 - -```diff - - Label -+ - ... - ... -+ - +```html + + Connections + + ... + ... + + ``` -## なぜこの変更が必要か +`inset="true"` を使わないlistでは、このwrapperは不要です。 -### 背景: iOSデザイン再現の課題 +## wrapperが必要な理由 -Ionic Frameworkのデフォルトでは、`ion-list` に背景色があり、`ion-list-header` はリストの一部として扱われます。しかし、この構造では **iOSのネイティブなデザインパターン** を正確に再現できません。 +Ionicは通常、`ion-list` 自体に背景を設定するため、`ion-list-header` もitemと同じsurface内に表示されます。iOS 26のlayoutではheaderとitemのsurfaceを分離します。 -![ion-list の inset 背景比較。ion-item-group が必要な理由を示す図](https://raw.githubusercontent.com/rdlabo-dev/ionic-theme-ios26/v2.3.2/screenshots/why-ion-list-inset.png) +![ion-item-groupが必要な理由を示すinset listの背景比較](https://raw.githubusercontent.com/rdlabo-dev/ionic-theme-ios26/v3.0.0-1/screenshots/why-ion-list-inset.png) -### 解決策: 背景色の分離 +そのため、このテーマは次のようにstyleを適用します。 -iOSデザインを忠実に再現するため、このテーマは次の変更を行います: +- inset `ion-list` の背景を透明にする +- itemのsurfaceを `ion-item-group` に適用する +- `ion-list-header` をsurfaceの外に置く -- `ion-list` の背景色を透明にする -- 背景色を `ion-item-group` に委ねる +## Material Designとのマークアップ共有 -この変更により、`ion-list-header` を独立した要素として扱え、ネイティブなiOSの見た目を実現できます。 +`@rdlabo/ionic-theme-md3` も同じgroup構造に対応しているため、両方のIonic modeで1つのtemplateを共有できます。 -## Material Designで同じデザインを使う - -同じデザインパターンをMaterial Designテーマでも使いたい場合は、次のCSSをインポートします: +このpackageを `@rdlabo/ionic-theme-md3` なしで使うapplicationでは、Material modeにも同じgroup layoutを適用するため、任意のstylesheetをimportします。 ```css @import '@rdlabo/ionic-theme-ios26/dist/css/md-ion-list-inset.css'; ``` -これにより、Material Designテーマにも同じ `ion-item-group` パターンが適用されます。 - -## まとめ - -- **ほとんどの場合**: 既存のIonicコードをそのまま使えます -- **特定条件のみ**: `ion-list` で `inset` を使う場合にのみ `ion-item-group` が必要です -- **目的**: iOS 26のネイティブなデザインパターンを正確に再現するため +2行itemとsection header groupについては [特別なマークアップとクラス](/docs/special-markup) を参照してください。 diff --git a/src/ionic-theme-md3/docs/ja/special-markup.md b/src/ionic-theme-md3/docs/ja/special-markup.md index 74e54a0..055d1d5 100644 --- a/src/ionic-theme-md3/docs/ja/special-markup.md +++ b/src/ionic-theme-md3/docs/ja/special-markup.md @@ -4,33 +4,61 @@ title: 特別なマークアップ # 特別なマークアップ -ほとんどのIonicマークアップは変更せずに使えます。以下は `demo/src/app/docs` でも使用している明示的なopt-inの組み合わせで、同じtemplateを `@rdlabo/ionic-theme-ios26` でも使う場合に役立ちます。 +ほとんどのIonicマークアップは変更せずに使えます。以下は明示的なopt-inで、同じtemplateを `@rdlabo/ionic-theme-ios26` でも使う場合に役立ちます。 ## 2行のinset list item slotを指定しない `ion-label` と `ion-note` を隣接させると、2行のitemとして表示します。通常の末尾noteとして表示したい場合は、`ion-note` に `slot="end"` を指定します。 -```html +```html preview - - Network & internet - Mobile, Wi-Fi, hotspot - + + + Network & internet + Mobile, Wi-Fi, hotspot + + ``` -Applicationが `@rdlabo/ionic-theme-ios26/dist/css/md-ion-list-inset.css` をimportしている場合は、list itemを `ion-item-group` で囲み、`ion-list-header` はgroupの外に置きます。 +## square button + +buttonのcornerをよりsquareにする場合は `.button-square` を追加します。text buttonとicon-only buttonの両方で使えます。 + +```html preview +Continue + + + +``` + +## inset listのsection header -```html +`.item-group-header` を `ion-item-group` に追加すると、component demo pageの先頭で使われている中央揃えのicon、title、descriptionを表示できます。 + +これは導入用のgroupです。通常のlist itemは、その後に置く別の `ion-item-group` に入れてください。 + +```html preview - Connections - + - Network & internet - Mobile, Wi-Fi, hotspot + + +

Lists

+ Inset-list examples +
+ + First item +
``` -必要なimportと背景modelは [ion-item-groupの使用方法](/docs/using-ion-item-group) を参照してください。 +## Themeを無効にする + +個別のIonic componentで標準のMaterial styleを維持する場合は `.md3-disabled` を追加します。 + +```html preview +MD3 theme Standard Ionic +``` diff --git a/src/ionic-theme-md3/docs/ja/using-ion-item-group.md b/src/ionic-theme-md3/docs/ja/using-ion-item-group.md index e3f3d04..305946d 100644 --- a/src/ionic-theme-md3/docs/ja/using-ion-item-group.md +++ b/src/ionic-theme-md3/docs/ja/using-ion-item-group.md @@ -2,35 +2,32 @@ title: ion-item-groupの使用方法 --- -# MD3テーマでの `ion-item-group` の使用方法 +# inset listでの `ion-item-group` の使用方法 -MD3テーマは `@rdlabo/ionic-theme-ios26` と同じinset listのmarkupに対応しているため、Ionic modeをまたいで1つのtemplateを共有できます。この共有patternをopt-inするときに `ion-item-group` を追加します。 +MD3テーマは `@rdlabo/ionic-theme-ios26` と同じinset list構造を使うため、Ionic modeをまたいで1つのtemplateを利用できます。`ion-list` で `inset="true"` を使う場合は、itemを `ion-item-group` で囲み、`ion-list-header` はgroupの外に置きます。 -## 必要になる条件 +以下はframeworkに依存しないWeb Component形式のマークアップです。ReactまたはVueでは、各frameworkに対応するcomponentとpropertyの構文に置き換えてください。 -次の両方に該当するときだけ必要です。 - -- `ion-list` の `inset` propertyを有効にしている -- iOS 26 packageの任意stylesheet `md-ion-list-inset.css` をimportしている +```html + + Connections + + ... + ... + + +``` -この場合はlist itemを `ion-item-group` で囲み、list headerはgroupの外に置きます。 +`inset="true"` を使わないlistでは、このwrapperは不要です。 -## 実装例 +## wrapperが必要な理由 -```diff - - Label -+ - ... - ... -+ - -``` +共有構造では、`ion-list-header` をitemのsurfaceから分離します。これにより、platform固有のtemplateを用意せずに、iOS 26のlayoutとMD3のstyleを同じマークアップへ適用できます。 -## Import +そのため、このテーマは次のようにstyleを適用します。 -```css -@import '@rdlabo/ionic-theme-ios26/dist/css/md-ion-list-inset.css'; -``` +- inset `ion-list` の背景を透明にする +- itemのsurfaceを `ion-item-group` に適用する +- `ion-list-header` をsurfaceの外に置く -このstylesheetは `ion-list` の背景を透明にし、背景を `ion-item-group` に持たせます。これにより `ion-list-header` を独立した要素として扱いながら、iOS 26とMD3でmarkupを共有できます。 +2行itemとsection header groupについては [特別なマークアップ](/docs/special-markup) を参照してください。 From a5be90379743f1dbd00ed682432ed212dcbbd5bc Mon Sep 17 00:00:00 2001 From: rdlabo Date: Sat, 22 Aug 2026 11:27:58 +0900 Subject: [PATCH 3/8] docs: add localized dual-theme setup --- src/ionic-theme-ios26/docs/ja/readme.md | 64 +++++++++++++++++++++++-- src/ionic-theme-md3/docs/ja/readme.md | 58 +++++++++++++++++++++- 2 files changed, 117 insertions(+), 5 deletions(-) diff --git a/src/ionic-theme-ios26/docs/ja/readme.md b/src/ionic-theme-ios26/docs/ja/readme.md index b802563..af83349 100644 --- a/src/ionic-theme-ios26/docs/ja/readme.md +++ b/src/ionic-theme-ios26/docs/ja/readme.md @@ -43,14 +43,14 @@ Note: **@ionic/core@ < 8.8.1 を使う場合は**、@rdlabo/ionic-theme-ios26@2. * This file is to eliminate the impact of class name changes for iOS26. * For example, `ion-buttons ion-button[fill=default]` is not normally implemented, but may be required for iOS26. * This file is to eliminate such effects. - * Note: This is not include `@rdlabo/ionic-theme-md3` + * Note: This stylesheet is not included in `@rdlabo/ionic-theme-md3`. */ @import '@rdlabo/ionic-theme-ios26/dist/css/md-remove-ios-class-effect.css'; /** * If you will use the design of ion-item-group with ion-list on Android as well, import it. - * More info: https://github.com/rdlabo-dev/ionic-theme-ios26/blob/v2.3.2/USING_ION_ITEM_GROUP.md - * Note: This is include `@rdlabo/ionic-theme-md3` + * More info: ./docs/using-ion-item-group.md + * Note: This stylesheet is included in `@rdlabo/ionic-theme-md3`. * @import '@rdlabo/ionic-theme-ios26/dist/css/md-ion-list-inset.css'; */ @@ -63,7 +63,63 @@ Note: **@ionic/core@ < 8.8.1 を使う場合は**、@rdlabo/ionic-theme-ios26@2. */ ``` -次に、iOS 26向けのアニメーションを設定します。Ionicの設定オプションに次を追加してください。 +### オプション: iOS 26テーマとMD3テーマを併用する + +同じアプリケーションでIonicの両モードをスタイルするには、MD3テーマをインストールします。 + +両テーマの現行リリースには、`@ionic/core` 8.8.1以降が必要です。 + +```bash +npm install @rdlabo/ionic-theme-md3 +``` + +グローバルスタイルシートでSassを使っている場合は、次の順序でテーマを初期化します。 + +```scss +@use '@rdlabo/ionic-theme-ios26/src/styles/default-variables.scss' as ios26-vars; +@use '@rdlabo/ionic-theme-ios26/src/styles/ionic-theme-ios26.scss'; +@use '@rdlabo/ionic-theme-ios26/src/styles/ionic-theme-ios26-dark-class.scss'; +@use '@rdlabo/ionic-theme-ios26/src/styles/md-remove-ios-class-effect.scss'; +@use '@rdlabo/ionic-theme-md3/dist/css/default-variables.css' as md3-vars; +@use '@rdlabo/ionic-theme-md3/dist/css/ionic-theme-md3.css'; +``` + +この例ではIonicのclassベースのダークモードを使います。グローバルスタイルシートでは、Angularの `@ionic/angular/css/palettes/dark.class.css` など、Ionic側の対応するダークパレットも読み込んでください。`dark-system` または `dark-always` を使う場合は、IonicのパレットとiOS 26テーマの両方で同じvariantを選びます。詳しくはIonicの[ダークモードのドキュメント](https://ionicframework.com/docs/theming/dark-mode)を参照してください。`ios26-vars` と `md3-vars` を明示することで、2つの変数モジュールが同じデフォルトnamespaceを使うことを防ぎます。 + +両テーマをインストールした場合は、両方のtransition実装を設定します。 + +```ts +import { isPlatform } from '@ionic/core'; // or @ionic/angular/standalone, @ionic/react, @ionic/vue +import { iosTransitionAnimation, popoverEnterAnimation, popoverLeaveAnimation } from '@rdlabo/ionic-theme-ios26'; +import { mdTransitionAnimation } from '@rdlabo/ionic-theme-md3'; + +// Angular +provideIonicAngular({ + ... + navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation, + popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined, + popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined, +}); + +// React +setupIonicReact({ + ... + navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation, + popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined, + popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined, +}); + +// Vue +createApp(App) + .use(IonicVue, { + ... + navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation, + popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined, + popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined, + }); +``` + +iOS 26テーマだけをインストールした場合は、次のようにアニメーションを設定します。 ```ts import { isPlatform } from '@ionic/core'; // or @ionic/angular/standalone, @ionic/react, @ionic/vue diff --git a/src/ionic-theme-md3/docs/ja/readme.md b/src/ionic-theme-md3/docs/ja/readme.md index b03fcbe..e2238be 100644 --- a/src/ionic-theme-md3/docs/ja/readme.md +++ b/src/ionic-theme-md3/docs/ja/readme.md @@ -50,7 +50,63 @@ Note: **@ionic/core@ < 8.8.0 を使う場合は**、@rdlabo/ionic-theme-md3@1.0. @import '@rdlabo/ionic-theme-md3/dist/css/ionic-theme-md3.css'; ``` -次に、MD3向けのアニメーションを設定します。Ionicの設定オプションに次を追加してください。 +### オプション: MD3テーマとiOS 26テーマを併用する + +同じアプリケーションでIonicの両モードをスタイルするには、iOS 26テーマをインストールします。 + +両テーマの現行リリースには、`@ionic/core` 8.8.1以降が必要です。アプリケーションが8.8.0以前の場合は、この設定を使う前にIonicをアップグレードしてください。 + +```bash +npm install @rdlabo/ionic-theme-ios26 +``` + +グローバルスタイルシートでSassを使っている場合は、次の順序でテーマを初期化します。 + +```scss +@use '@rdlabo/ionic-theme-ios26/src/styles/default-variables.scss' as ios26-vars; +@use '@rdlabo/ionic-theme-ios26/src/styles/ionic-theme-ios26.scss'; +@use '@rdlabo/ionic-theme-ios26/src/styles/ionic-theme-ios26-dark-class.scss'; +@use '@rdlabo/ionic-theme-ios26/src/styles/md-remove-ios-class-effect.scss'; +@use '@rdlabo/ionic-theme-md3/dist/css/default-variables.css' as md3-vars; +@use '@rdlabo/ionic-theme-md3/dist/css/ionic-theme-md3.css'; +``` + +この例ではIonicのclassベースのダークモードを使います。グローバルスタイルシートでは、Angularの `@ionic/angular/css/palettes/dark.class.css` など、Ionic側の対応するダークパレットも読み込んでください。`dark-system` または `dark-always` を使う場合は、IonicのパレットとiOS 26テーマの両方で同じvariantを選びます。詳しくはIonicの[ダークモードのドキュメント](https://ionicframework.com/docs/theming/dark-mode)を参照してください。`ios26-vars` と `md3-vars` を明示することで、2つの変数モジュールが同じデフォルトnamespaceを使うことを防ぎます。 + +両テーマをインストールした場合は、両方のtransition実装を設定します。 + +```ts +import { isPlatform } from '@ionic/core'; // or @ionic/angular/standalone, @ionic/react, @ionic/vue +import { iosTransitionAnimation, popoverEnterAnimation, popoverLeaveAnimation } from '@rdlabo/ionic-theme-ios26'; +import { mdTransitionAnimation } from '@rdlabo/ionic-theme-md3'; + +// Angular +provideIonicAngular({ + ... + navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation, + popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined, + popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined, +}); + +// React +setupIonicReact({ + ... + navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation, + popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined, + popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined, +}); + +// Vue +createApp(App) + .use(IonicVue, { + ... + navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation, + popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined, + popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined, + }); +``` + +MD3テーマだけをインストールした場合は、次のようにアニメーションを設定します。 ```ts import { isPlatform } from '@ionic/core'; // or @ionic/angular/standalone, @ionic/react, @ionic/vue From 7f6fb2ebce2037f9fc8912dc36827ab6042351de Mon Sep 17 00:00:00 2001 From: rdlabo Date: Sat, 22 Aug 2026 14:39:06 +0900 Subject: [PATCH 4/8] docs: document Ionic 9 major upgrades --- package-lock.json | 110 +-- package.json | 9 +- public/sitemap.xml | 108 ++- scripts/build-output.test.ts | 4 +- scripts/package-repository.test.ts | 24 + scripts/package-repository.ts | 47 +- scripts/project-manifest.ts | 6 +- scripts/site-contract.test.ts | 46 +- src/app/docs/plugin-index.spec.ts | 8 +- .../generated/project-catalog.generated.ts | 142 +++- .../eslint-plugin-rules.en.generated.ts | 776 ++++++++++++++++-- .../eslint-plugin-rules.ja.generated.ts | 716 ++++++++++++++-- ...onic-angular-collect-icons.en.generated.ts | 63 +- ...onic-angular-collect-icons.ja.generated.ts | 70 +- .../ionic-theme-ios26.en.generated.ts | 171 ++-- .../ionic-theme-ios26.ja.generated.ts | 169 ++-- .../projects/ionic-theme-md3.en.generated.ts | 86 +- .../projects/ionic-theme-md3.ja.generated.ts | 88 +- .../projects/stripe-terminal.en.generated.ts | 4 +- .../generated/projects/stripe.en.generated.ts | 4 +- src/eslint-plugin-rules/docs/api.md | 4 +- src/eslint-plugin-rules/docs/ja/api.md | 4 +- .../docs/ja/configuration.md | 9 +- .../docs/ja/getting-started.md | 9 +- src/eslint-plugin-rules/docs/ja/migration.md | 66 ++ src/eslint-plugin-rules/docs/ja/rules.md | 7 +- .../rules/component-property-use-readonly.md | 97 +-- .../docs/ja/rules/deny-constructor-di.md | 52 +- .../docs/ja/rules/deny-element.md | 71 +- .../ja/rules/deny-import-from-ionic-module.md | 35 - .../docs/ja/rules/deny-overlay-create.md | 122 +-- .../ja/rules/deny-soft-private-modifier.md | 22 +- .../ja/rules/implements-ionic-lifecycle.md | 71 +- .../docs/ja/rules/ionic-attr-type-check.md | 94 +-- .../no-component-method-except-lifecycle.md | 70 +- .../ja/rules/no-component-writable-signal.md | 82 +- .../docs/ja/rules/no-reactive-forms.md | 75 +- .../docs/ja/rules/no-template-driven-forms.md | 98 ++- .../docs/ja/rules/prefer-disable-handler.md | 157 ++-- .../docs/ja/rules/prefer-ionic-standalone.md | 47 ++ .../docs/ja/rules/prefer-modal-launcher.md | 136 +-- .../docs/ja/rules/require-ion-item-group.md | 87 ++ .../docs/ja/rules/require-viewmodel.md | 210 ++--- .../docs/ja/rules/restrict-try-block.md | 197 ++++- .../ja/rules/signal-use-as-signal-template.md | 160 ++-- .../docs/ja/rules/signal-use-as-signal.md | 139 +++- src/ionic-angular-collect-icons/docs/api.md | 2 +- .../docs/ja/api.md | 2 +- .../docs/ja/migration.md | 113 +++ .../docs/ja/readme.md | 27 +- src/ionic-theme-ios26/docs/api.md | 2 +- src/ionic-theme-ios26/docs/features.md | 2 +- src/ionic-theme-ios26/docs/ja/api.md | 2 +- src/ionic-theme-ios26/docs/ja/features.md | 4 +- src/ionic-theme-ios26/docs/ja/ios-18.md | 5 +- src/ionic-theme-ios26/docs/ja/migration.md | 103 ++- src/ionic-theme-ios26/docs/ja/readme.md | 15 +- .../docs/ja/special-markup.md | 2 - .../docs/ja/using-ion-item-group.md | 4 +- src/ionic-theme-ios26/docs/readme.md | 6 +- .../docs/using-ion-item-group.md | 2 +- src/ionic-theme-md3/docs/api.md | 2 +- src/ionic-theme-md3/docs/ja/api.md | 2 +- src/ionic-theme-md3/docs/ja/migration.md | 22 + src/ionic-theme-md3/docs/ja/readme.md | 12 +- src/ionic-theme-md3/docs/ja/special-markup.md | 2 - .../docs/ja/using-ion-item-group.md | 2 - src/ionic-theme-md3/docs/readme.md | 4 +- 68 files changed, 3877 insertions(+), 1232 deletions(-) create mode 100644 src/eslint-plugin-rules/docs/ja/migration.md delete mode 100644 src/eslint-plugin-rules/docs/ja/rules/deny-import-from-ionic-module.md create mode 100644 src/eslint-plugin-rules/docs/ja/rules/prefer-ionic-standalone.md create mode 100644 src/eslint-plugin-rules/docs/ja/rules/require-ion-item-group.md create mode 100644 src/ionic-angular-collect-icons/docs/ja/migration.md create mode 100644 src/ionic-theme-md3/docs/ja/migration.md diff --git a/package-lock.json b/package-lock.json index c34a10d..148f38d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "zenn-markdown-html": "^0.5.2" }, "devDependencies": { + "@angular-eslint/template-parser": "22.1.0", "@angular/build": "^22.1.3", "@angular/cli": "^22.1.3", "@angular/compiler-cli": "^22.1.0", @@ -41,13 +42,13 @@ "@rdlabo/capacitor-docgen": "0.4.1", "@rdlabo/capacitor-printer": "8.0.1", "@rdlabo/capacitor-screenshot-event": "8.0.0", - "@rdlabo/eslint-plugin-rules": "21.3.0", - "@rdlabo/ionic-angular-collect-icons": "2.1.0", + "@rdlabo/eslint-plugin-rules": "22.0.0", + "@rdlabo/ionic-angular-collect-icons": "3.0.0", "@rdlabo/ionic-angular-kit": "21.6.2", "@rdlabo/ionic-angular-photo-editor": "21.6.2", "@rdlabo/ionic-angular-scroll-header": "21.6.2", - "@rdlabo/ionic-theme-ios26": "2.3.2", - "@rdlabo/ionic-theme-md3": "1.1.0", + "@rdlabo/ionic-theme-ios26": "3.0.0", + "@rdlabo/ionic-theme-md3": "2.0.0", "@rdlabo/ngx-cdk-scroll-strategies": "21.6.2", "@rdlabo/workers-hono-kit": "0.10.6", "@tailwindcss/postcss": "^4.1.12", @@ -186,9 +187,9 @@ } }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "21.4.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.4.0.tgz", - "integrity": "sha512-/3H4BPbQ1BHJkkrUsfusZtmHc+qiFWBBZ9UDPWah4xZMjflexOK9U4GYeH7nMjcuyqFnIlMMeJJNwNLGt/hmdg==", + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-22.1.0.tgz", + "integrity": "sha512-iOtOQ2jtrtko1rIQo6i+g3ezxGL0lyYv80j4GccFTK1JGh4K+AqYkmaBvfUfNtqoE/7VcKsOoyxaFt6iIpKhaQ==", "dev": true, "license": "MIT" }, @@ -229,20 +230,6 @@ "typescript": "*" } }, - "node_modules/@angular-eslint/eslint-plugin-template/node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-22.1.0.tgz", - "integrity": "sha512-iOtOQ2jtrtko1rIQo6i+g3ezxGL0lyYv80j4GccFTK1JGh4K+AqYkmaBvfUfNtqoE/7VcKsOoyxaFt6iIpKhaQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@angular-eslint/eslint-plugin/node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-22.1.0.tgz", - "integrity": "sha512-iOtOQ2jtrtko1rIQo6i+g3ezxGL0lyYv80j4GccFTK1JGh4K+AqYkmaBvfUfNtqoE/7VcKsOoyxaFt6iIpKhaQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@angular-eslint/schematics": { "version": "22.1.0", "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-22.1.0.tgz", @@ -263,17 +250,17 @@ } }, "node_modules/@angular-eslint/template-parser": { - "version": "21.4.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.4.0.tgz", - "integrity": "sha512-BaUSLSyS+43fzDoJkTMkGqNdCXq3fGnUZsfXTmrlZPJf5AYFbgAlAPGZXDJyoNWw43fux+DafdlrlKcYUSgSIw==", + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-22.1.0.tgz", + "integrity": "sha512-gcufZLI/Rl2fOWtBk2MgMRkH1t+OrbJGxIsfT0w3pVjKm0zwi7njM/dtPbVjCHzsGhx7szQMvY0i0UwTBVYkSw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.4.0", + "@angular-eslint/bundled-angular-compiler": "22.1.0", "eslint-scope": "9.1.2" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "eslint": "^9.0.0 || ^10.0.0", "typescript": "*" } }, @@ -292,13 +279,6 @@ "typescript": "*" } }, - "node_modules/@angular-eslint/utils/node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-22.1.0.tgz", - "integrity": "sha512-iOtOQ2jtrtko1rIQo6i+g3ezxGL0lyYv80j4GccFTK1JGh4K+AqYkmaBvfUfNtqoE/7VcKsOoyxaFt6iIpKhaQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@angular/build": { "version": "22.1.3", "resolved": "https://registry.npmjs.org/@angular/build/-/build-22.1.3.tgz", @@ -4663,9 +4643,9 @@ } }, "node_modules/@rdlabo/eslint-plugin-rules": { - "version": "21.3.0", - "resolved": "https://registry.npmjs.org/@rdlabo/eslint-plugin-rules/-/eslint-plugin-rules-21.3.0.tgz", - "integrity": "sha512-EjCDLpsbbd1NXmel4h4qKw/Z+l1qS/6BB//zIlF/9ia0ugHApcdB93J7uGrouDpE0qdZ4iiU6KyKX99Tqe4whw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/@rdlabo/eslint-plugin-rules/-/eslint-plugin-rules-22.0.0.tgz", + "integrity": "sha512-xqtgbzcP6HKRsyCmpINvsRZod+ZlF/vj1JgKZDFuKlrxZxgsm7yrRiDqUFiizrNt6CgG8wyW1g6dlvznIqlw3g==", "dev": true, "license": "MIT", "dependencies": { @@ -4675,8 +4655,9 @@ "node": ">=20.0.0" }, "peerDependencies": { - "@angular-eslint/template-parser": ">=21.0.0 <22.0.0", - "@ionic/core": ">=8.0.0 <9.0.0", + "@angular-eslint/template-parser": ">=21.0.0 <23.0.0", + "@ionic/angular": ">=9.0.0 <10.0.0", + "@ionic/core": ">=9.0.0 <10.0.0", "@typescript-eslint/utils": ">=8.33.0 <9.0.0", "eslint": ">=9.0.0" }, @@ -4684,18 +4665,20 @@ "@angular-eslint/template-parser": { "optional": true }, + "@ionic/angular": { + "optional": true + }, "@ionic/core": { "optional": true } } }, "node_modules/@rdlabo/ionic-angular-collect-icons": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@rdlabo/ionic-angular-collect-icons/-/ionic-angular-collect-icons-2.1.0.tgz", - "integrity": "sha512-l/Wxj5whtgXwl7EqOgiNUyDia9F8qSZaFFRGyy7OhAB3mJiDXYcuzPzp1AqdQmtsnSgXOL9zVMl0ZCo/OxKp9A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@rdlabo/ionic-angular-collect-icons/-/ionic-angular-collect-icons-3.0.0.tgz", + "integrity": "sha512-fp9WzYS2Ez28U4fH6lvgpav+cpsAZ6bqM3CKSHSne28x1vVBU3sB2hYCcYIdWxFHT+s5GuBf0iYT+4pm1soCKQ==", "dev": true, "dependencies": { - "@angular-eslint/template-parser": "^21.0.0", "@clack/core": "^0.5.0", "@clack/prompts": "^0.11.0", "@ionic/utils-terminal": "^2.3.5", @@ -4708,7 +4691,10 @@ "ionic-angular-collect-icons": "dist/index.js" }, "engines": { - "node": ">=20" + "node": ">=22" + }, + "peerDependencies": { + "@angular-eslint/template-parser": "^21.0.0 || ^22.0.0" } }, "node_modules/@rdlabo/ionic-angular-kit": { @@ -4814,23 +4800,23 @@ } }, "node_modules/@rdlabo/ionic-theme-ios26": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@rdlabo/ionic-theme-ios26/-/ionic-theme-ios26-2.3.2.tgz", - "integrity": "sha512-jakkFfQD6a1I3M28HhrZHM8PY9OaN1G3SWoIX60jH8DsZh0vJI2wdcadVqG+wTVKe7Zh52wvpWYW46RmV2DX7w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@rdlabo/ionic-theme-ios26/-/ionic-theme-ios26-3.0.0.tgz", + "integrity": "sha512-uZdPpAxuSpRqCKWpEgbqMp5MaCkPyJG/X3X/tozRjG1FIYL4S1rtnK0uSLZCeihze4WB/ssi3ZKWlTp8ljplUA==", "dev": true, "license": "MIT", "peerDependencies": { - "@ionic/core": ">= 8.8.1" + "@ionic/core": ">=8.8.1 <10" } }, "node_modules/@rdlabo/ionic-theme-md3": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rdlabo/ionic-theme-md3/-/ionic-theme-md3-1.1.0.tgz", - "integrity": "sha512-RjaWHmf+CMpFuifOtwIbcnwlpt9pgySr4qteZzv9m4ZvZ06vHT+pGqVBU1OYWhtUZMYGtntMP1a+B3GJPcxDuA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@rdlabo/ionic-theme-md3/-/ionic-theme-md3-2.0.0.tgz", + "integrity": "sha512-YcqJgnXGQ0cCy45FJQAhODQscU/u/ttn1S+EEZ7FDovoNzhZ+F4c89Thh8pd0mnfXdedNol2xkMmix5GhbVZpw==", "dev": true, "license": "MIT", "peerDependencies": { - "@ionic/core": ">= 8.8.0" + "@ionic/core": ">=8.8.0 <10" } }, "node_modules/@rdlabo/ngx-cdk-scroll-strategies": { @@ -6958,28 +6944,6 @@ "typescript-eslint": "^8.0.0" } }, - "node_modules/angular-eslint/node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-22.1.0.tgz", - "integrity": "sha512-iOtOQ2jtrtko1rIQo6i+g3ezxGL0lyYv80j4GccFTK1JGh4K+AqYkmaBvfUfNtqoE/7VcKsOoyxaFt6iIpKhaQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/angular-eslint/node_modules/@angular-eslint/template-parser": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-22.1.0.tgz", - "integrity": "sha512-gcufZLI/Rl2fOWtBk2MgMRkH1t+OrbJGxIsfT0w3pVjKm0zwi7njM/dtPbVjCHzsGhx7szQMvY0i0UwTBVYkSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-eslint/bundled-angular-compiler": "22.1.0", - "eslint-scope": "9.1.2" - }, - "peerDependencies": { - "eslint": "^9.0.0 || ^10.0.0", - "typescript": "*" - } - }, "node_modules/ansi-escapes": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", diff --git a/package.json b/package.json index c2e8d8c..e3e6df9 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "zenn-markdown-html": "^0.5.2" }, "devDependencies": { + "@angular-eslint/template-parser": "22.1.0", "@angular/build": "^22.1.3", "@angular/cli": "^22.1.3", "@angular/compiler-cli": "^22.1.0", @@ -58,13 +59,13 @@ "@rdlabo/capacitor-docgen": "0.4.1", "@rdlabo/capacitor-printer": "8.0.1", "@rdlabo/capacitor-screenshot-event": "8.0.0", - "@rdlabo/eslint-plugin-rules": "21.3.0", - "@rdlabo/ionic-angular-collect-icons": "2.1.0", + "@rdlabo/eslint-plugin-rules": "22.0.0", + "@rdlabo/ionic-angular-collect-icons": "3.0.0", "@rdlabo/ionic-angular-kit": "21.6.2", "@rdlabo/ionic-angular-photo-editor": "21.6.2", "@rdlabo/ionic-angular-scroll-header": "21.6.2", - "@rdlabo/ionic-theme-ios26": "2.3.2", - "@rdlabo/ionic-theme-md3": "1.1.0", + "@rdlabo/ionic-theme-ios26": "3.0.0", + "@rdlabo/ionic-theme-md3": "2.0.0", "@rdlabo/ngx-cdk-scroll-strategies": "21.6.2", "@rdlabo/workers-hono-kit": "0.10.6", "@tailwindcss/postcss": "^4.1.12", diff --git a/public/sitemap.xml b/public/sitemap.xml index 89e6d17..1ba9ca9 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -396,6 +396,18 @@ + + https://docs.rdlabo.dev/projects/ionic-theme-ios26/docs/special-markup + + + + + + https://docs.rdlabo.dev/ja/projects/ionic-theme-ios26/docs/special-markup + + + + https://docs.rdlabo.dev/projects/ionic-theme-ios26/docs/experimental-animation @@ -468,6 +480,42 @@ + + https://docs.rdlabo.dev/projects/ionic-theme-md3/docs/special-markup + + + + + + https://docs.rdlabo.dev/ja/projects/ionic-theme-md3/docs/special-markup + + + + + + https://docs.rdlabo.dev/projects/ionic-theme-md3/docs/using-ion-item-group + + + + + + https://docs.rdlabo.dev/ja/projects/ionic-theme-md3/docs/using-ion-item-group + + + + + + https://docs.rdlabo.dev/projects/ionic-theme-md3/docs/migration + + + + + + https://docs.rdlabo.dev/ja/projects/ionic-theme-md3/docs/migration + + + + https://docs.rdlabo.dev/projects/ionic-theme-md3/docs/api @@ -552,6 +600,18 @@ + + https://docs.rdlabo.dev/projects/ionic-angular-collect-icons/docs/migration + + + + + + https://docs.rdlabo.dev/ja/projects/ionic-angular-collect-icons/docs/migration + + + + https://docs.rdlabo.dev/projects/ionic-angular-collect-icons/docs/api @@ -684,6 +744,18 @@ + + https://docs.rdlabo.dev/projects/eslint-plugin-rules/docs/migration + + + + + + https://docs.rdlabo.dev/ja/projects/eslint-plugin-rules/docs/migration + + + + https://docs.rdlabo.dev/projects/eslint-plugin-rules/docs/rules @@ -744,18 +816,6 @@ - - https://docs.rdlabo.dev/projects/eslint-plugin-rules/docs/rules/deny-import-from-ionic-module - - - - - - https://docs.rdlabo.dev/ja/projects/eslint-plugin-rules/docs/rules/deny-import-from-ionic-module - - - - https://docs.rdlabo.dev/projects/eslint-plugin-rules/docs/rules/deny-overlay-create @@ -864,6 +924,18 @@ + + https://docs.rdlabo.dev/projects/eslint-plugin-rules/docs/rules/prefer-ionic-standalone + + + + + + https://docs.rdlabo.dev/ja/projects/eslint-plugin-rules/docs/rules/prefer-ionic-standalone + + + + https://docs.rdlabo.dev/projects/eslint-plugin-rules/docs/rules/prefer-modal-launcher @@ -876,6 +948,18 @@ + + https://docs.rdlabo.dev/projects/eslint-plugin-rules/docs/rules/require-ion-item-group + + + + + + https://docs.rdlabo.dev/ja/projects/eslint-plugin-rules/docs/rules/require-ion-item-group + + + + https://docs.rdlabo.dev/projects/eslint-plugin-rules/docs/rules/require-viewmodel diff --git a/scripts/build-output.test.ts b/scripts/build-output.test.ts index ff64362..dd50522 100644 --- a/scripts/build-output.test.ts +++ b/scripts/build-output.test.ts @@ -125,12 +125,12 @@ test('builds bounded English and Japanese search indexes with the component UI', assert.ok(files.some((file) => /^pagefind\.ja_.+\.pf_meta$/.test(file))); assert.equal( files.filter((file) => /^fragment\/en_.+\.pf_fragment$/.test(file)).length, - 142, + 149, 'English search index must contain only canonical pages', ); assert.equal( files.filter((file) => /^fragment\/ja_.+\.pf_fragment$/.test(file)).length, - 142, + 149, 'Japanese search index must contain only canonical pages', ); const sizes = await Promise.all( diff --git a/scripts/package-repository.test.ts b/scripts/package-repository.test.ts index 50dcabe..8b9a204 100644 --- a/scripts/package-repository.test.ts +++ b/scripts/package-repository.test.ts @@ -3,6 +3,7 @@ import test from 'node:test'; import { DOCS_PORTAL_REPOSITORY_URL, DOCS_PORTAL_REF, + pinPackageSourceLinks, parseRepositoryUrl, repositoryRawUrl, repositorySourceLabel, @@ -19,6 +20,29 @@ test('parses GitHub repository URLs', () => { }); }); +test('pins only source links that belong to the package repository', async () => { + const project = { + repositoryUrl: 'https://github.com/rdlabo-dev/capacitor-docgen', + packageName: '@rdlabo/capacitor-docgen', + }; + const markdown = [ + '[fork](https://github.com/rdlabo-dev/capacitor-docgen/tree/main/src) [upstream](https://github.com/ionic-team/capacitor-docgen/tree/v0.3.1/src)', + '[blob](https://github.com/rdlabo-dev/capacitor-docgen/blob/next/docs/api.md)', + '![raw](https://raw.githubusercontent.com/rdlabo-dev/capacitor-docgen/main/image.png)', + '[pinned](https://github.com/rdlabo-dev/capacitor-docgen/blob/v0.4.1/README.md)', + ].join('\n'); + + assert.equal( + await pinPackageSourceLinks(project, markdown), + [ + '[fork](https://github.com/rdlabo-dev/capacitor-docgen/tree/v0.4.1/src) [upstream](https://github.com/ionic-team/capacitor-docgen/tree/v0.3.1/src)', + '[blob](https://github.com/rdlabo-dev/capacitor-docgen/blob/v0.4.1/docs/api.md)', + '![raw](https://raw.githubusercontent.com/rdlabo-dev/capacitor-docgen/v0.4.1/image.png)', + '[pinned](https://github.com/rdlabo-dev/capacitor-docgen/blob/v0.4.1/README.md)', + ].join('\n'), + ); +}); + test('builds raw and source labels for repository docs', () => { const repositoryUrl = 'https://github.com/capacitor-community/admob'; assert.equal( diff --git a/scripts/package-repository.ts b/scripts/package-repository.ts index 365b8b6..987dd21 100644 --- a/scripts/package-repository.ts +++ b/scripts/package-repository.ts @@ -8,7 +8,7 @@ export interface RepositoryCoordinates { } export const DOCS_PORTAL_REPOSITORY_URL = 'https://github.com/rdlabo-dev/docs'; -export const DOCS_PORTAL_REF = 'main'; +export const DOCS_PORTAL_REF = process.env['RDLABO_DOCS_REF'] ?? 'main'; const portalDocsRoot = join(process.cwd(), 'src'); const pinnedVersionCache = new Map(); @@ -25,6 +25,37 @@ async function pinnedVersionFor(packageName: string): Promise { + const version = await pinnedVersionFor(project.packageName); + if (!version) return content; + + const { owner, repo } = parseRepositoryUrl(project.repositoryUrl); + const escapedOwner = owner.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const escapedRepo = repo.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return content + .replace( + /\(\.\.\/\.\.\/(src|tests)\//g, + `(https://github.com/${owner}/${repo}/blob/v${version}/$1/`, + ) + .replace( + new RegExp( + `(https://github\\.com/${escapedOwner}/${escapedRepo}/(?:blob|tree)/)[A-Za-z0-9._-]+/`, + 'g', + ), + `$1v${version}/`, + ) + .replace( + new RegExp( + `(https://raw\\.githubusercontent\\.com/${escapedOwner}/${escapedRepo}/)[A-Za-z0-9._-]+/`, + 'g', + ), + `$1v${version}/`, + ); +} + async function portalEnglishTrackedLocally( sourceDirectory: string, file: string, @@ -171,7 +202,10 @@ export async function fetchEnglishProjectMarkdown( cache, ); if (fromPackage) { - return fromPackage; + return { + ...fromPackage, + content: await pinPackageSourceLinks(project, fromPackage.content), + }; } if (shouldFallbackToRepositoryReadme) { @@ -189,17 +223,20 @@ export async function fetchEnglishProjectMarkdown( ? rewritePackageDocLinks(fromReadme.content, new Map()) : fromReadme.content; - // ionic-theme-ios26 README expects a pinned blob URL inside a CSS code fence comment. + // Keep the README guide link pinned to the installed documentation release. if (file === 'readme.md' && project.packageName === '@rdlabo/ionic-theme-ios26') { const version = await pinnedVersionFor(project.packageName); if (version) { rewrittenContent = rewrittenContent.replace( /More info:\s+\.\.?\/docs\/using-ion-item-group\.md/g, - `More info: https://github.com/rdlabo-dev/ionic-theme-ios26/blob/v${version}/USING_ION_ITEM_GROUP.md`, + `More info: https://github.com/rdlabo-dev/ionic-theme-ios26/blob/v${version}/docs/using-ion-item-group.md`, ); } } - return { ...fromReadme, content: rewrittenContent }; + return { + ...fromReadme, + content: await pinPackageSourceLinks(project, rewrittenContent), + }; } } diff --git a/scripts/project-manifest.ts b/scripts/project-manifest.ts index 52274c0..62d4510 100644 --- a/scripts/project-manifest.ts +++ b/scripts/project-manifest.ts @@ -112,7 +112,6 @@ const eslintRuleNames = [ 'component-property-use-readonly', 'deny-constructor-di', 'deny-element', - 'deny-import-from-ionic-module', 'deny-overlay-create', 'deny-soft-private-modifier', 'implements-ionic-lifecycle', @@ -122,7 +121,9 @@ const eslintRuleNames = [ 'no-reactive-forms', 'no-template-driven-forms', 'prefer-disable-handler', + 'prefer-ionic-standalone', 'prefer-modal-launcher', + 'require-ion-item-group', 'require-viewmodel', 'restrict-try-block', 'signal-use-as-signal-template', @@ -646,6 +647,7 @@ export const projectDefinitions: readonly ProjectDefinition[] = [ 'Guides', 'ガイド', ), + page('Migration', '移行', 'migration', 'migration.md', 'Guides', 'ガイド'), page('API', 'API', 'api', 'api.md', 'Reference', 'リファレンス'), ], }, @@ -702,6 +704,7 @@ export const projectDefinitions: readonly ProjectDefinition[] = [ page('Usage', '使い方', 'usage', 'usage.md', 'Guides', 'ガイド'), page('CLI Options', 'CLI オプション', 'options', 'options.md', 'Guides', 'ガイド'), page('FAQ', 'FAQ', 'faq', 'faq.md', 'Guides', 'ガイド'), + page('Migration', '移行', 'migration', 'migration.md', 'Guides', 'ガイド'), page('CLI API', 'CLI API', 'api', 'api.md', 'Reference', 'リファレンス'), ], }, @@ -853,6 +856,7 @@ export const projectDefinitions: readonly ProjectDefinition[] = [ 'ガイド', ), page('Configuration', '設定', 'configuration', 'configuration.md', 'Guide', 'ガイド'), + page('Migration', '移行', 'migration', 'migration.md', 'Guide', 'ガイド'), page('Rules', 'ルール一覧', 'rules', 'rules.md', 'Reference', 'リファレンス'), page('API', 'API', 'api', 'api.md', 'Reference', 'リファレンス'), ...eslintRulePages, diff --git a/scripts/site-contract.test.ts b/scripts/site-contract.test.ts index a4fbab3..4f38710 100644 --- a/scripts/site-contract.test.ts +++ b/scripts/site-contract.test.ts @@ -226,7 +226,7 @@ test('imports every installed ESLint rule README with matching EN/JA code fences .sort(); const installedRuleNames = await installedEslintRuleNames(); - assert.equal(manifestRuleNames.length, 18); + assert.equal(manifestRuleNames.length, 19); assert.deepEqual(manifestRuleNames, installedRuleNames); const docsRoot = new URL('../src/eslint-plugin-rules/docs/', import.meta.url); @@ -287,14 +287,6 @@ test('imports every installed ESLint rule README with matching EN/JA code fences assert.match(englishRulesIndex, new RegExp(`rules/${ruleName}(?:\\.md)?`)); assert.match(japaneseRulesIndex, new RegExp(localRoute.replaceAll('/', '\\/'))); } - - // v21.3.0 tagged the deny-constructor-di test file with a historical typo. - const denyConstructorDiDocs = await Promise.all([ - englishGuideSource(eslintProject, 'rules/deny-constructor-di.md'), - readFile(new URL('ja/rules/deny-constructor-di.md', docsRoot), 'utf8'), - ]); - assert.match(denyConstructorDiDocs[1], /\/blob\/v21\.3\.0\/tests\/rules\/deny-costructor-di\.ts/); - assert.doesNotMatch(denyConstructorDiDocs[1], /\/tests\/rules\/deny-constructor-di\.ts/); }); test('lists every ionic-angular-library package and imports localized READMEs', async () => { @@ -412,8 +404,8 @@ test('lists every ionic-angular-library package and imports localized READMEs', test('lists ionic theme packages and pins localized README imports', async () => { const expectedProjects = new Map([ - ['ionic-theme-ios26', { packageName: '@rdlabo/ionic-theme-ios26', version: '2.3.2' }], - ['ionic-theme-md3', { packageName: '@rdlabo/ionic-theme-md3', version: '1.1.0' }], + ['ionic-theme-ios26', { packageName: '@rdlabo/ionic-theme-ios26', version: '3.0.0' }], + ['ionic-theme-md3', { packageName: '@rdlabo/ionic-theme-md3', version: '2.0.0' }], ]); const packageJson = JSON.parse( await readFile(new URL('../package.json', import.meta.url), 'utf8'), @@ -504,7 +496,7 @@ test('lists ionic theme packages and pins localized README imports', async () => assert.match(iosReadme, /\]\(\/docs\/using-ion-item-group\)/); assert.match( iosReadme, - /https:\/\/github\.com\/rdlabo-dev\/ionic-theme-ios26\/blob\/v2\.3\.2\/USING_ION_ITEM_GROUP\.md/, + /https:\/\/github\.com\/rdlabo-dev\/ionic-theme-ios26\/blob\/v3\.0\.0\/docs\/using-ion-item-group\.md/, ); assert.equal(yamlTitle(usingDocJa), 'ion-item-groupの使用方法'); @@ -515,29 +507,11 @@ test('lists ionic theme packages and pins localized README imports', async () => } else { assert.equal(usingPage.title.en, 'Using ion-item-group'); } - assert.match(usingDocJa, /^# ion-item-groupの使用方法\s*$/m); - assert.match(usingDoc, /when the following condition is met/); - assert.doesNotMatch(usingDoc, /when \*\*both\*\* of the following conditions are met/); - - const selectiveImportPattern = - /@rdlabo\/ionic-theme-ios26\/dist\/css\/(?:utils|components)\/([A-Za-z0-9/_-]+)(?!\.css)/g; - for (const [locale, markdown] of [ - ['EN', iosMigration], - ['JA', iosMigrationJa], - ] as const) { - const imports = [...markdown.matchAll(selectiveImportPattern)].map((match) => match[0]); - assert.ok( - imports.length > 0, - `${locale} iOS migration guide must show selective utils/components imports`, - ); - for (const importPath of imports) { - const relativeCss = `${importPath.slice('@rdlabo/ionic-theme-ios26/'.length)}.css`; - await access( - new URL(`../node_modules/@rdlabo/ionic-theme-ios26/${relativeCss}`, import.meta.url), - constants.F_OK, - ); - } - assert.doesNotMatch(markdown, /dist\/css\/components\/ion-breadcrumbs(?!\.css)/); + assert.doesNotMatch(usingDocJa, /^# /m); + assert.match(usingDoc, /wrap its items in `ion-item-group`/); + for (const markdown of [iosMigration, iosMigrationJa]) { + assert.match(markdown, /\.header-item-group/); + assert.match(markdown, /\.item-group-header/); } }); @@ -552,7 +526,7 @@ test('imports the remaining rdlabo utility READMEs from exact public releases', ['capacitor-brotherprint', ['@rdlabo/capacitor-brotherprint', '8.1.1', 'capacitor-plugins']], [ 'ionic-angular-collect-icons', - ['@rdlabo/ionic-angular-collect-icons', '2.1.0', 'frontend-tools'], + ['@rdlabo/ionic-angular-collect-icons', '3.0.0', 'frontend-tools'], ], ] as const); const packageJson = JSON.parse( diff --git a/src/app/docs/plugin-index.spec.ts b/src/app/docs/plugin-index.spec.ts index c359e59..753e5d4 100644 --- a/src/app/docs/plugin-index.spec.ts +++ b/src/app/docs/plugin-index.spec.ts @@ -144,7 +144,7 @@ describe('PluginIndexComponent', () => { 'method login(...)', ); const eslint = await loadProject('eslint-plugin-rules', 'ja'); - expect(eslint?.version).toBe('21.3.0'); + expect(eslint?.version).toBe('22.0.0'); expect(eslint?.pages.find((page) => page.slug === 'rules')?.html).toContain( 'signal-use-as-signal-template', ); @@ -154,7 +154,7 @@ describe('PluginIndexComponent', () => { ); expect(restrictTryBlock?.html).toMatch(/オプション|Options/); expect(restrictTryBlock?.html).toContain('allowPromise'); - expect(restrictTryBlock?.html).toContain('incorrect'); + expect(restrictTryBlock?.html).toMatch(/誤り|Incorrect/i); const hono = await loadProject('workers-hono-kit', 'ja'); expect(hono?.version).toBe('0.10.6'); expect(hono?.pages.find((page) => page.slug === 'data-layer')?.html).toContain( @@ -175,7 +175,7 @@ describe('PluginIndexComponent', () => { expect(codeScannerApi?.html).toContain('method present(...)'); expect(codeScannerApi?.html).toMatch(/interface<\/code>[\s\S]*?ScannerOption/); const iosTheme = await loadProject('ionic-theme-ios26', 'ja'); - expect(iosTheme?.version).toBe('2.3.2'); + expect(iosTheme?.version).toBe('3.0.0'); expect(iosTheme?.pages.find((page) => page.slug === 'readme')?.html).toContain( 'iosTransitionAnimation', ); @@ -183,7 +183,7 @@ describe('PluginIndexComponent', () => { 'md-ion-list-inset.css', ); const md3Theme = await loadProject('ionic-theme-md3', 'ja'); - expect(md3Theme?.version).toBe('1.1.0'); + expect(md3Theme?.version).toBe('2.0.0'); expect(md3Theme?.pages.find((page) => page.slug === 'readme')?.html).toContain( 'mdTransitionAnimation', ); diff --git a/src/app/generated/project-catalog.generated.ts b/src/app/generated/project-catalog.generated.ts index 3a9195b..5f1edc3 100644 --- a/src/app/generated/project-catalog.generated.ts +++ b/src/app/generated/project-catalog.generated.ts @@ -420,7 +420,7 @@ export const PROJECTS_EN = [ "repositoryUrl": "https://github.com/rdlabo-dev/ionic-theme-ios26", "category": "frontend-tools", "icon": "theme", - "version": "2.3.2", + "version": "3.0.0", "description": "iOS 26 design styling for Ionic applications.", "headline": "Bring iOS 26 design to Ionic apps", "overview": "Apply iOS 26 CSS, transitions, and Liquid Glass interactions to Ionic components, with dark mode and selective migration support.", @@ -462,6 +462,13 @@ export const PROJECTS_EN = [ "section": "Guides", "path": "/projects/ionic-theme-ios26/docs/features" }, + { + "title": "Special markup and classes", + "navTitle": "Special markup and classes", + "slug": "special-markup", + "section": "Guides", + "path": "/projects/ionic-theme-ios26/docs/special-markup" + }, { "title": "Experimental Animation", "navTitle": "Experimental Animation", @@ -501,7 +508,7 @@ export const PROJECTS_EN = [ "repositoryUrl": "https://github.com/rdlabo-dev/ionic-theme-md3", "category": "frontend-tools", "icon": "theme", - "version": "1.1.0", + "version": "2.0.0", "description": "Material Design 3 styling for Ionic applications.", "headline": "Bring Material Design 3 to Ionic apps", "overview": "Apply Material Design 3 styling to Ionic while keeping markup compatible with the iOS 26 theme and shared transition animations.", @@ -529,6 +536,27 @@ export const PROJECTS_EN = [ "section": "Documentation", "path": "/projects/ionic-theme-md3/docs/readme" }, + { + "title": "Special markup", + "navTitle": "Special markup", + "slug": "special-markup", + "section": "Guides", + "path": "/projects/ionic-theme-md3/docs/special-markup" + }, + { + "title": "Using ion-item-group", + "navTitle": "Using ion-item-group", + "slug": "using-ion-item-group", + "section": "Guides", + "path": "/projects/ionic-theme-md3/docs/using-ion-item-group" + }, + { + "title": "Migration", + "navTitle": "Migration", + "slug": "migration", + "section": "Guides", + "path": "/projects/ionic-theme-md3/docs/migration" + }, { "title": "API", "navTitle": "API", @@ -547,7 +575,7 @@ export const PROJECTS_EN = [ "repositoryUrl": "https://github.com/rdlabo-dev/ionic-angular-collect-icons", "category": "frontend-tools", "icon": "app", - "version": "2.1.0", + "version": "3.0.0", "description": "Automate ionIcons collection and export for Ionic Angular projects.", "headline": "Collect used ionIcons before production builds", "overview": "Group unique ionIcons in a project and generate an export file—register all icons during development, then collect icons used in templates before production builds.", @@ -603,6 +631,13 @@ export const PROJECTS_EN = [ "section": "Guides", "path": "/projects/ionic-angular-collect-icons/docs/faq" }, + { + "title": "Migration", + "navTitle": "Migration", + "slug": "migration", + "section": "Guides", + "path": "/projects/ionic-angular-collect-icons/docs/migration" + }, { "title": "CLI API", "navTitle": "CLI API", @@ -699,7 +734,7 @@ export const PROJECTS_EN = [ "repositoryUrl": "https://github.com/rdlabo-dev/eslint-plugin-rules", "category": "developer-tools", "icon": "lint", - "version": "21.3.0", + "version": "22.0.0", "description": "Opinionated Angular, Ionic, and TypeScript rules for maintainable applications.", "headline": "Keep Angular and Ionic architecture consistent", "overview": "Adopt a flat-config preset or select individual rules for Signals, component boundaries, Ionic overlays, forms, and safe asynchronous code.", @@ -738,6 +773,13 @@ export const PROJECTS_EN = [ "section": "Guide", "path": "/projects/eslint-plugin-rules/docs/configuration" }, + { + "title": "Migration", + "navTitle": "Migration", + "slug": "migration", + "section": "Guide", + "path": "/projects/eslint-plugin-rules/docs/migration" + }, { "title": "Rules", "navTitle": "Rules", @@ -773,13 +815,6 @@ export const PROJECTS_EN = [ "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/deny-element" }, - { - "title": "deny-import-from-ionic-module", - "navTitle": "deny-import-from-ionic-module", - "slug": "rules/deny-import-from-ionic-module", - "section": "Rules", - "path": "/projects/eslint-plugin-rules/docs/rules/deny-import-from-ionic-module" - }, { "title": "deny-overlay-create", "navTitle": "deny-overlay-create", @@ -843,6 +878,13 @@ export const PROJECTS_EN = [ "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/prefer-disable-handler" }, + { + "title": "prefer-ionic-standalone", + "navTitle": "prefer-ionic-standalone", + "slug": "rules/prefer-ionic-standalone", + "section": "Rules", + "path": "/projects/eslint-plugin-rules/docs/rules/prefer-ionic-standalone" + }, { "title": "prefer-modal-launcher", "navTitle": "prefer-modal-launcher", @@ -850,6 +892,13 @@ export const PROJECTS_EN = [ "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/prefer-modal-launcher" }, + { + "title": "require-ion-item-group", + "navTitle": "require-ion-item-group", + "slug": "rules/require-ion-item-group", + "section": "Rules", + "path": "/projects/eslint-plugin-rules/docs/rules/require-ion-item-group" + }, { "title": "require-viewmodel", "navTitle": "require-viewmodel", @@ -1970,7 +2019,7 @@ export const PROJECTS_JA = [ "repositoryUrl": "https://github.com/rdlabo-dev/ionic-theme-ios26", "category": "frontend-tools", "icon": "theme", - "version": "2.3.2", + "version": "3.0.0", "description": "Ionicアプリ向けのiOS 26デザインスタイル。", "headline": "IonicアプリにiOS 26デザインを取り入れる", "overview": "IonicコンポーネントへiOS 26のCSS、トランジション、Liquid Glassインタラクションを適用し、ダークモードと段階的な移行にも対応します。", @@ -2012,6 +2061,13 @@ export const PROJECTS_JA = [ "section": "ガイド", "path": "/projects/ionic-theme-ios26/docs/features" }, + { + "title": "特別なマークアップとクラス", + "navTitle": "特別なマークアップとクラス", + "slug": "special-markup", + "section": "ガイド", + "path": "/projects/ionic-theme-ios26/docs/special-markup" + }, { "title": "実験的なアニメーション", "navTitle": "実験的なアニメーション", @@ -2051,7 +2107,7 @@ export const PROJECTS_JA = [ "repositoryUrl": "https://github.com/rdlabo-dev/ionic-theme-md3", "category": "frontend-tools", "icon": "theme", - "version": "1.1.0", + "version": "2.0.0", "description": "Ionicアプリ向けのMaterial Design 3スタイル。", "headline": "IonicアプリにMaterial Design 3を取り入れる", "overview": "iOS 26テーマと共通のマークアップ互換性を保ちつつ、IonicへMaterial Design 3スタイルとトランジションアニメーションを適用します。", @@ -2079,6 +2135,27 @@ export const PROJECTS_JA = [ "section": "ドキュメント", "path": "/projects/ionic-theme-md3/docs/readme" }, + { + "title": "特別なマークアップ", + "navTitle": "特別なマークアップ", + "slug": "special-markup", + "section": "ガイド", + "path": "/projects/ionic-theme-md3/docs/special-markup" + }, + { + "title": "ion-item-groupの使用方法", + "navTitle": "ion-item-groupの使用方法", + "slug": "using-ion-item-group", + "section": "ガイド", + "path": "/projects/ionic-theme-md3/docs/using-ion-item-group" + }, + { + "title": "移行", + "navTitle": "移行", + "slug": "migration", + "section": "ガイド", + "path": "/projects/ionic-theme-md3/docs/migration" + }, { "title": "API", "navTitle": "API", @@ -2097,7 +2174,7 @@ export const PROJECTS_JA = [ "repositoryUrl": "https://github.com/rdlabo-dev/ionic-angular-collect-icons", "category": "frontend-tools", "icon": "app", - "version": "2.1.0", + "version": "3.0.0", "description": "Ionic Angularプロジェクト向けのionIcons収集・エクスポート自動化。", "headline": "本番ビルド前に使用中のionIconsを収集する", "overview": "プロジェクト内のionIconsを一意にまとめエクスポート用ファイルを生成します。開発時は全アイコンを登録し、本番ビルド前にテンプレートで使われているアイコンを収集します。", @@ -2153,6 +2230,13 @@ export const PROJECTS_JA = [ "section": "ガイド", "path": "/projects/ionic-angular-collect-icons/docs/faq" }, + { + "title": "移行", + "navTitle": "移行", + "slug": "migration", + "section": "ガイド", + "path": "/projects/ionic-angular-collect-icons/docs/migration" + }, { "title": "CLI API", "navTitle": "CLI API", @@ -2249,7 +2333,7 @@ export const PROJECTS_JA = [ "repositoryUrl": "https://github.com/rdlabo-dev/eslint-plugin-rules", "category": "developer-tools", "icon": "lint", - "version": "21.3.0", + "version": "22.0.0", "description": "保守しやすいアプリケーションのためのAngular・Ionic・TypeScript向けESLintルール集。", "headline": "Angular・Ionicの設計規約を一貫させる", "overview": "Flat Configプリセットまたは個別ルールを使い、Signal、Component境界、Ionic Overlay、フォーム、非同期コードの規約を自動検査します。", @@ -2288,6 +2372,13 @@ export const PROJECTS_JA = [ "section": "ガイド", "path": "/projects/eslint-plugin-rules/docs/configuration" }, + { + "title": "移行", + "navTitle": "移行", + "slug": "migration", + "section": "ガイド", + "path": "/projects/eslint-plugin-rules/docs/migration" + }, { "title": "ルール一覧", "navTitle": "ルール一覧", @@ -2323,13 +2414,6 @@ export const PROJECTS_JA = [ "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/deny-element" }, - { - "title": "deny-import-from-ionic-module", - "navTitle": "deny-import-from-ionic-module", - "slug": "rules/deny-import-from-ionic-module", - "section": "ルール", - "path": "/projects/eslint-plugin-rules/docs/rules/deny-import-from-ionic-module" - }, { "title": "deny-overlay-create", "navTitle": "deny-overlay-create", @@ -2393,6 +2477,13 @@ export const PROJECTS_JA = [ "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/prefer-disable-handler" }, + { + "title": "prefer-ionic-standalone", + "navTitle": "prefer-ionic-standalone", + "slug": "rules/prefer-ionic-standalone", + "section": "ルール", + "path": "/projects/eslint-plugin-rules/docs/rules/prefer-ionic-standalone" + }, { "title": "prefer-modal-launcher", "navTitle": "prefer-modal-launcher", @@ -2400,6 +2491,13 @@ export const PROJECTS_JA = [ "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/prefer-modal-launcher" }, + { + "title": "require-ion-item-group", + "navTitle": "require-ion-item-group", + "slug": "rules/require-ion-item-group", + "section": "ルール", + "path": "/projects/eslint-plugin-rules/docs/rules/require-ion-item-group" + }, { "title": "require-viewmodel", "navTitle": "require-viewmodel", diff --git a/src/app/generated/projects/eslint-plugin-rules.en.generated.ts b/src/app/generated/projects/eslint-plugin-rules.en.generated.ts index 6410f79..8380801 100644 --- a/src/app/generated/projects/eslint-plugin-rules.en.generated.ts +++ b/src/app/generated/projects/eslint-plugin-rules.en.generated.ts @@ -8,7 +8,7 @@ export const PROJECT = { "repositoryUrl": "https://github.com/rdlabo-dev/eslint-plugin-rules", "category": "developer-tools", "icon": "lint", - "version": "21.3.0", + "version": "22.0.0", "description": "Opinionated Angular, Ionic, and TypeScript rules for maintainable applications.", "headline": "Keep Angular and Ionic architecture consistent", "overview": "Adopt a flat-config preset or select individual rules for Signals, component boundaries, Ionic overlays, forms, and safe asynchronous code.", @@ -40,7 +40,7 @@ export const PROJECT = { "file": "getting-started.md", "section": "Guide", "path": "/projects/eslint-plugin-rules/docs/getting-started", - "html": "

Install the plugin as a development dependency:

\n
npm install --save-dev @rdlabo/eslint-plugin-rules\n

The package root provides Angular and Ionic rules. Install @angular-eslint/template-parser and @ionic/core when you enable those rules. Framework-independent TypeScript projects can use the /typescript entry point without loading Angular or Ionic.

\n

Requirements

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
PackageSupported version
Node.js20 or later
ESLint9 or later
@typescript-eslint/utils8.33 or later, before 9
@angular-eslint/template-parser21.x when template rules are used
@ionic/core8.x when Ionic rules are used
\n

Choose an entry point

\n
    \n
  • Use @rdlabo/eslint-plugin-rules for Angular and Ionic applications.
  • \n
  • Use @rdlabo/eslint-plugin-rules/typescript for backend and other framework-independent TypeScript projects.
  • \n
\n

The recommended preset is designed for ESLint Flat Config. Add it at the top level so its TypeScript and HTML file selectors remain intact.

\n

Next step

\n

Continue to Configuration to enable the recommended preset or individual rules.

\n

Documentation

\n\n", + "html": "

Install the plugin as a development dependency:

\n
npm install --save-dev @rdlabo/eslint-plugin-rules\n

The package root provides Angular and Ionic rules. Install @angular-eslint/template-parser, @ionic/angular, and @ionic/core when you enable those rules. Framework-independent TypeScript projects can use the /typescript entry point without loading Angular or Ionic.

\n

Requirements

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
PackageSupported version
Node.js20 or later
ESLint9 or later
@typescript-eslint/utils8.33 or later, before 9
@angular-eslint/template-parser21.x or 22.x
@ionic/angular9.x when Ionic rules are used
@ionic/core9.x when Ionic rules are used
\n

Choose an entry point

\n
    \n
  • Use @rdlabo/eslint-plugin-rules for Angular and Ionic applications.
  • \n
  • Use @rdlabo/eslint-plugin-rules/typescript for backend and other framework-independent TypeScript projects.
  • \n
\n

The recommended preset is designed for ESLint Flat Config. Add it at the top level so its TypeScript and HTML file selectors remain intact.

\n

For Ionic templates, the preset also requires ion-item elements inside ion-list to use ion-item-group, ion-reorder-group, ion-radio-group, or ion-accordion within ion-accordion-group, matching the iOS 26 and Material Design 3 list structure.

\n

Next step

\n

Continue to Configuration to enable the recommended preset or individual rules.

\n

Documentation

\n\n", "headings": [ { "id": "requirements", @@ -74,7 +74,7 @@ export const PROJECT = { "file": "configuration.md", "section": "Guide", "path": "/projects/eslint-plugin-rules/docs/configuration", - "html": "

Angular and Ionic

\n

Register the plugin, spread its recommended configs at the top level, then add the standard Angular and TypeScript configs for your project.

\n
const eslint = require('@eslint/js');\nconst tseslint = require('typescript-eslint');\nconst angular = require('angular-eslint');\nconst rdlabo = require('@rdlabo/eslint-plugin-rules');\n\nmodule.exports = tseslint.config(\n  {\n    plugins: { '@rdlabo/rules': rdlabo },\n  },\n  ...rdlabo.configs.recommended,\n  {\n    files: ['**/*.ts'],\n    languageOptions: {\n      parserOptions: { projectService: true, tsconfigRootDir: __dirname },\n    },\n    extends: [eslint.configs.recommended, ...tseslint.configs.recommended, ...tseslint.configs.stylistic, ...angular.configs.tsRecommended],\n    processor: angular.processInlineTemplates,\n  },\n  {\n    files: ['**/*.html'],\n    extends: [...angular.configs.templateRecommended, ...angular.configs.templateAccessibility],\n  },\n);\n

Do not place rdlabo.configs.recommended inside a scoped extends. The typescript-eslint config helper would replace the preset's internal files selectors and could run TypeScript-only rules against templates.

\n

Framework-independent TypeScript

\n
import tseslint from 'typescript-eslint';\nimport rdlabo from '@rdlabo/eslint-plugin-rules/typescript';\n\nexport default tseslint.config({\n  files: ['**/*.ts'],\n  plugins: { '@rdlabo/rules': rdlabo },\n  rules: {\n    '@rdlabo/rules/deny-soft-private-modifier': 'error',\n    '@rdlabo/rules/restrict-try-block': [\n      'error',\n      {\n        allowPromise: false,\n        allowPromiseResolve: true,\n        allowRxjs: false,\n        allowInSignal: false,\n        maxLines: 3,\n      },\n    ],\n  },\n});\n

Typed linting is required for the full Promise and RxJS checks in restrict-try-block.

\n

Recommended preset

\n

The preset enables the common Signal, component boundary, lifecycle, overlay, readonly, and try-block rules for TypeScript. Its HTML config enables Ionic attribute checking, denied overlay elements, and double-action prevention.

\n

deny-constructor-di is deprecated and is not in the preset. Prefer Angular's inject() migration.

\n", + "html": "

Angular and Ionic

\n

Plugin 22 supports Angular and Angular ESLint 21–22 with Ionic Framework 9. When upgrading from plugin 21, review the migration guide before enabling the updated recommended preset.

\n

Register the plugin, spread its recommended configs at the top level, then add the standard Angular and TypeScript configs for your project.

\n
const eslint = require('@eslint/js');\nconst tseslint = require('typescript-eslint');\nconst angular = require('angular-eslint');\nconst rdlabo = require('@rdlabo/eslint-plugin-rules');\n\nmodule.exports = tseslint.config(\n  {\n    plugins: { '@rdlabo/rules': rdlabo },\n  },\n  ...rdlabo.configs.recommended,\n  {\n    files: ['**/*.ts'],\n    languageOptions: {\n      parserOptions: { projectService: true, tsconfigRootDir: __dirname },\n    },\n    extends: [eslint.configs.recommended, ...tseslint.configs.recommended, ...tseslint.configs.stylistic, ...angular.configs.tsRecommended],\n    processor: angular.processInlineTemplates,\n  },\n  {\n    files: ['**/*.html'],\n    extends: [...angular.configs.templateRecommended, ...angular.configs.templateAccessibility],\n  },\n);\n

Do not place rdlabo.configs.recommended inside a scoped extends. The typescript-eslint config helper would replace the preset's internal files selectors and could run TypeScript-only rules against templates.

\n

Framework-independent TypeScript

\n
import tseslint from 'typescript-eslint';\nimport rdlabo from '@rdlabo/eslint-plugin-rules/typescript';\n\nexport default tseslint.config({\n  files: ['**/*.ts'],\n  plugins: { '@rdlabo/rules': rdlabo },\n  rules: {\n    '@rdlabo/rules/deny-soft-private-modifier': 'error',\n    '@rdlabo/rules/restrict-try-block': [\n      'error',\n      {\n        allowPromise: false,\n        allowPromiseResolve: true,\n        allowRxjs: false,\n        allowInSignal: false,\n        maxLines: 3,\n      },\n    ],\n  },\n});\n

Typed linting is required for the full Promise and RxJS checks in restrict-try-block.

\n

Recommended preset

\n

The preset enables the common Signal, component boundary, lifecycle, overlay, readonly, and try-block rules for TypeScript. Its HTML config enables Ionic attribute checking, denied overlay elements, and double-action prevention.

\n

The TypeScript preset includes prefer-ionic-standalone, which requires Ionic 9 root imports and rejects IonicModule and NgModule-based lazy imports.

\n

deny-constructor-di is deprecated and is not in the preset. Prefer Angular's inject() migration.

\n", "headings": [ { "id": "angular-and-ionic", @@ -96,6 +96,45 @@ export const PROJECT = { "scrollMap": [], "editUrl": "https://github.com/rdlabo-dev/eslint-plugin-rules/edit/main/docs/configuration.md" }, + { + "title": "Migration", + "navTitle": "Migration", + "slug": "migration", + "file": "migration.md", + "section": "Guide", + "path": "/projects/eslint-plugin-rules/docs/migration", + "html": "

21.x to 22.x

\n

Version 22 targets Angular 21 and 22 with Ionic Framework 9. Ionic 8 applications must remain on version 21 of this plugin.

\n

Dependencies

\n

Commit your application changes first, then run Ionic's official @ionic/migrate tool from the application root:

\n
npx @ionic/migrate --dry-run\nnpx @ionic/migrate\n

The migrator detects the installed Ionic major, updates @ionic/angular and @ionic/core together, applies safe v8-to-v9 changes, and prints a checklist for changes that require manual decisions. Review and test its diff before continuing. Version 22 of this plugin supports Angular and Angular ESLint 21 through 22.

\n

Ionic Angular imports

\n

Replace the removed deny-import-from-ionic-module rule with prefer-ionic-standalone:

\n
- '@rdlabo/rules/deny-import-from-ionic-module': 'error'\n+ '@rdlabo/rules/prefer-ionic-standalone': 'error'\n

For Angular applications, the official migrator moves existing NgModule imports from @ionic/angular to @ionic/angular/lazy and standalone imports from @ionic/angular/standalone to the package root. This preserves the application's current architecture during the framework upgrade.

\n

For example, the migrator performs this safe standalone import rewrite automatically:

\n
- import { IonButton } from '@ionic/angular/standalone';\n+ import { IonButton } from '@ionic/angular';\n

This plugin supports only Ionic 9 standalone applications. The official migrator reports IonicModule without an autofix because converting an NgModule application requires architectural decisions. After running it, complete the Angular standalone migration and import Ionic components from the package root. Do not mechanically replace @ionic/angular/lazy paths: first convert each NgModule consumer to standalone, then replace IonicModule with the specific Ionic components it uses.

\n

The new rule rejects the NgModule-based @ionic/angular/lazy entry point and IonicModule. Migrate to standalone bootstrap with provideIonicAngular() and import standalone Ionic components directly:

\n
- platformBrowserDynamic().bootstrapModule(AppModule);\n+ bootstrapApplication(AppComponent, {\n+   providers: [provideIonicAngular(config)],\n+ });\n

Import provideIonicAngular from @ionic/angular. Complete the Angular NgModule-to-standalone migration before removing IonicModule; it cannot be replaced safely inside an NgModule with a one-line autofix.

\n

Recommended preset list structure

\n

Version 22 also enables require-ion-item-group in the recommended preset.
\nExisting Ionic templates may therefore report new errors when an ion-item
\ninside ion-list is not wrapped by ion-item-group, ion-reorder-group,
\nion-radio-group, or ion-accordion within ion-accordion-group.

\n

The rule applies safe automatic fixes only when it can determine the intended
\ngroup boundary. Reusable or ambiguous templates are reported without being
\nmodified. Wrapper components are checked through their own templates, so a
\ncustom element that renders a valid grouped list is not treated as a bare
\nion-item in its caller. See
\nrequire-ion-item-group for the supported
\nstructures and fix constraints.

\n

Boolean autocorrect

\n

Ionic 9 changes autocorrect on ion-input and ion-searchbar from 'on' | 'off' to boolean. The ionic-attr-type-check rule now fixes the old string form:

\n
- <ion-input autocorrect=\"off\"></ion-input>\n+ <ion-input [autocorrect]=\"false\"></ion-input>\n

The official Ionic migrator handles this v8-to-v9 change automatically. The rule remains useful for detecting old or newly introduced string values after migration and reads Ionic 9 component types, so it also follows other property type and accepted-value changes exposed by those definitions. Run ESLint with --fix, review the resulting template changes, and then run the Angular build and tests before committing.

\n", + "headings": [ + { + "id": "21.x-to-22.x", + "text": "21.x to 22.x", + "level": 2 + }, + { + "id": "dependencies", + "text": "Dependencies", + "level": 3 + }, + { + "id": "ionic-angular-imports", + "text": "Ionic Angular imports", + "level": 3 + }, + { + "id": "recommended-preset-list-structure", + "text": "Recommended preset list structure", + "level": 3 + }, + { + "id": "boolean-autocorrect", + "text": "Boolean autocorrect", + "level": 3 + } + ], + "codes": [], + "scrollMap": [], + "editUrl": "https://github.com/rdlabo-dev/eslint-plugin-rules/edit/main/docs/migration.md" + }, { "title": "Rules", "navTitle": "Rules", @@ -103,7 +142,7 @@ export const PROJECT = { "file": "rules.md", "section": "Reference", "path": "/projects/eslint-plugin-rules/docs/rules", - "html": "

The package exposes 18 rules. Rules marked “recommended” are enabled by rdlabo.configs.recommended; the remaining rules are opt-in.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
RulePurposeFixPreset
component-property-use-readonlyRequire readonly on immutable Angular component properties.YesYes
deny-constructor-diDisallow constructor dependency injection. Deprecated in favor of inject().NoNo
deny-elementReject configured HTML elements, such as inline Ionic overlays.NoYes
deny-import-from-ionic-moduleReplace accidental @ionic/angular imports with standalone imports.YesYes
deny-overlay-createDisallow direct .create() calls on modal and popover controllers.NoYes
deny-soft-private-modifierReplace TypeScript private with hard-private # fields.YesYes
implements-ionic-lifecycleRequire the matching interface for Angular and Ionic lifecycle methods.YesYes
ionic-attr-type-checkRequire property binding for non-string Ionic attributes.YesYes
no-component-method-except-lifecycleKeep arbitrary methods out of Angular components.NoYes
no-component-writable-signalKeep writable component state in a ViewModel, with a Signal Forms model exception.NoNo
no-reactive-formsDisallow Reactive Forms in favor of Angular Signal Forms.NoNo
no-template-driven-formsDisallow template-driven forms except configured interoperability elements.NoNo
prefer-disable-handlerWrap configured event handlers to prevent duplicate async actions.NoYes
prefer-modal-launcherRestrict presentModal calls to launch* functions.NoYes
require-viewmodelEnforce component ownership and the ViewModelStore boundary.NoYes
restrict-try-blockKeep try blocks small and exclude Promise, RxJS, and Signal contexts by policy.NoYes
signal-use-as-signal-templateRequire () when reading Angular Signals in templates.NoYes
signal-use-as-signalRequire correct Signal reads and writes in TypeScript.YesYes
\n

Rule documentation

\n

Each rule page in this documentation contains options and correct/incorrect examples. The table above reflects v21.3.0.

\n

Typed rules

\n

Enable parserOptions.projectService for rules that inspect TypeScript types. Without typed linting, restrict-try-block still performs syntax-based checks but skips type-dependent Promise and RxJS detection.

\n", + "html": "

The package exposes 19 rules. Rules marked “recommended” are enabled by rdlabo.configs.recommended; the remaining rules are opt-in.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
RulePurposeFixPreset
component-property-use-readonlyRequire readonly on immutable Angular component properties.YesYes
deny-constructor-diDisallow constructor dependency injection. Deprecated in favor of inject().NoNo
deny-elementReject configured HTML elements, such as inline Ionic overlays.NoYes
deny-overlay-createDisallow direct .create() calls on modal and popover controllers.NoYes
deny-soft-private-modifierReplace TypeScript private with hard-private # fields.YesYes
implements-ionic-lifecycleRequire the matching interface for Angular and Ionic lifecycle methods.YesYes
ionic-attr-type-checkRequire property binding for non-string Ionic attributes.YesYes
no-component-method-except-lifecycleKeep arbitrary methods out of Angular components.NoYes
no-component-writable-signalKeep writable component state in a ViewModel, with a Signal Forms model exception.NoNo
no-reactive-formsDisallow Reactive Forms in favor of Angular Signal Forms.NoNo
no-template-driven-formsDisallow template-driven forms except configured interoperability elements.NoNo
prefer-disable-handlerWrap configured event handlers to prevent duplicate async actions.NoYes
prefer-ionic-standalonePrefer Ionic 9 standalone imports and disallow IonicModule.YesYes
prefer-modal-launcherRestrict presentModal calls to launch* functions.NoYes
require-ion-item-groupRequire grouped Ionic list items for iOS 26 and Material Design 3.YesYes
require-viewmodelEnforce component ownership and the ViewModelStore boundary.NoYes
restrict-try-blockKeep try blocks small and exclude Promise, RxJS, and Signal contexts by policy.NoYes
signal-use-as-signal-templateRequire () when reading Angular Signals in templates.NoYes
signal-use-as-signalRequire correct Signal reads and writes in TypeScript.YesYes
\n

Rule documentation

\n

Each rule page in this documentation contains options and correct/incorrect examples.

\n

Typed rules

\n

Enable parserOptions.projectService for rules that inspect TypeScript types. Without typed linting, restrict-try-block still performs syntax-based checks but skips type-dependent Promise and RxJS detection.

\n", "headings": [ { "id": "rule-documentation", @@ -161,18 +200,13 @@ export const PROJECT = { "file": "rules/component-property-use-readonly.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/component-property-use-readonly", - "html": "
\n

Warns when a property should be readonly

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
  • ✒️ The --fix option on the command line can automatically fix some of the problems reported by this rule.
  • \n
\n
\n

This rule enforces the use of the readonly modifier for properties in Angular components that should be immutable. This helps make component state management safer by preventing unexpected changes.

\n

Rule Details

\n

❌ Incorrect: Properties without the readonly modifier

\n
@Component({\n  selector: 'app-example',\n  template: '<div>example</div>',\n})\nexport class ExampleComponent {\n  x = 1;\n  public y = 2;\n  private z = 3;\n  protected w = 4;\n  #secret = 42;\n  static a = 1;\n  ['foo'] = 1;\n  @Input() i = 8;\n  h: number;\n}\n

✅ Correct: Properties with the readonly modifier

\n
@Component({\n  selector: 'app-example',\n  template: '<div>example</div>',\n})\nexport class ExampleComponent {\n  readonly x = 1;\n  public readonly y = 2;\n  private readonly z = 3;\n  protected readonly w = 4;\n  readonly #secret = 42;\n  static readonly a = 1;\n  readonly ['foo'] = 1;\n  @Input() readonly i = 8;\n  readonly h: number;\n}\n

Rule Settings

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/component-property-use-readonly\": [\n      \"error\",\n      {\n        \"ignorePrivateProperties\": true\n      }\n    ]\n  }\n}\n

Options

\n
const options: {\n  ignorePrivateProperties?: boolean; // Whether to ignore private properties (default: false)\n};\n

ignorePrivateProperties

\n

When set to true, this option ignores both hard private properties (using the private modifier) and soft private properties (using the # prefix). This is useful because private properties are typically not accessed from outside the component, making the readonly modifier less critical.

\n

❌ Incorrect: Private properties without ignorePrivateProperties: true

\n
@Component({\n  selector: 'app-example',\n  template: '<div>example</div>',\n})\nexport class ExampleComponent {\n  private privateProp = 1; // error\n  #secretProp = 2; // error\n}\n

✅ Correct: Private properties with ignorePrivateProperties: true

\n
// .eslintrc.json\n{\n  \"rules\": {\n    \"@rdlabo/rules/component-property-use-readonly\": [\n      \"error\",\n      {\n        \"ignorePrivateProperties\": true\n      }\n    ]\n  }\n}\n\n// Component code\n@Component({\n  selector: 'app-example',\n  template: '<div>example</div>',\n})\nexport class ExampleComponent {\n  private privateProp = 1; // no error\n  #secretProp = 2; // no error\n  public publicProp = 3; // still requires readonly\n}\n

Implementation

\n\n", + "html": "
\n

Warns when a property should be readonly

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
  • ✒️ The --fix option on the command line can automatically fix some of the problems reported by this rule.
  • \n
\n
\n

This rule requires non-function properties declared by Angular components to use the readonly modifier. It reports initialized, uninitialized, static, computed, decorated, soft-private, and hard-private properties, and can add readonly automatically.

\n

Rule Details

\n

Only classes decorated with @Component() are checked. Methods, getters, setters, arrow-function properties, function-expression properties, properties that are already readonly, and properties of other classes are ignored.

\n

Options

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/component-property-use-readonly\": [\n      \"error\",\n      {\n        \"ignorePrivateProperties\": true\n      }\n    ]\n  }\n}\n

ignorePrivateProperties

\n
    \n
  • Type: boolean
  • \n
  • Default: false
  • \n
\n

When true, properties declared with the TypeScript private modifier and ECMAScript # private properties are ignored. Public, protected, and static properties are still checked.

\n

Examples

\n

Incorrect

\n
@Component({\n  selector: 'app-example',\n  template: '<div>example</div>',\n})\nexport class ExampleComponent {\n  x = 1;\n  public y = 2;\n  private z = 3;\n  protected w = 4;\n  #secret = 42;\n  static a = 1;\n  ['foo'] = 1;\n  @Input() i = 8;\n  h: number;\n}\n

Correct

\n
@Component({\n  selector: 'app-example',\n  template: '<div>example</div>',\n})\nexport class ExampleComponent {\n  readonly x = 1;\n  public readonly y = 2;\n  private readonly z = 3;\n  protected readonly w = 4;\n  readonly #secret = 42;\n  static readonly a = 1;\n  readonly ['foo'] = 1;\n  @Input() readonly i = 8;\n  readonly h: number;\n}\n

With ignorePrivateProperties: true, private properties may remain writable:

\n
@Component({\n  selector: 'app-example',\n  template: '<div>example</div>',\n})\nexport class ExampleComponent {\n  private privateProp = 1; // no error\n  #secretProp = 2; // no error\n  public readonly publicProp = 3;\n}\n

When to enable

\n

Enable this rule when component properties should expose stable references and writable state is managed through Signals or a ViewModel.

\n

Implementation

\n\n", "headings": [ { "id": "rule-details", "text": "Rule Details", "level": 2 }, - { - "id": "rule-settings", - "text": "Rule Settings", - "level": 2 - }, { "id": "options", "text": "Options", @@ -183,6 +217,26 @@ export const PROJECT = { "text": "ignorePrivateProperties", "level": 3 }, + { + "id": "examples", + "text": "Examples", + "level": 2 + }, + { + "id": "incorrect", + "text": "Incorrect", + "level": 3 + }, + { + "id": "correct", + "text": "Correct", + "level": 3 + }, + { + "id": "when-to-enable", + "text": "When to enable", + "level": 2 + }, { "id": "implementation", "text": "Implementation", @@ -200,18 +254,38 @@ export const PROJECT = { "file": "rules/deny-constructor-di.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/deny-constructor-di", - "html": "
\n

This plugin disallows Dependency Injection within the constructor.

\n
\n

This rule encourages modern Angular dependency injection practices by using the inject function.

\n

Rule Details

\n

❌ Incorrect: Using constructor-based dependency injection

\n
@Component({\n  selector: 'app-confirm',\n  templateUrl: './confirm.page.html',\n  styleUrls: ['./confirm.page.scss'],\n})\nexport class SigninPage {\n  constructor(public platform: Platform) {}\n}\n

✅ Correct: Using the inject function for dependency injection

\n
@Component({\n  selector: 'app-confirm',\n  templateUrl: './confirm.page.html',\n  styleUrls: ['./confirm.page.scss'],\n})\nexport class SigninPage {\n  public platform = inject(Platform);\n\n  constructor() {}\n}\n

Options

\n

No Options.

\n

Implementation

\n\n", + "html": "
\n

This plugin disallows Dependency Injection within the constructor.

\n
\n

This rule reports constructor parameter properties that are used for dependency injection, such as constructor(private readonly auth: AuthService). Angular's inject() function is the modern way to request dependencies in standalone components and services. It avoids constructor boilerplate and makes DI explicit.

\n

Rule Details

\n

The rule checks the constructor of classes and reports any parameter that is a TSParameterProperty (a parameter with a modifier like public, private, or readonly). These are the parameters that become class fields and are used for DI.

\n
    \n
  • Plain constructor parameters without modifiers are allowed.
  • \n
  • The rule does not auto-fix; you must manually replace constructor DI with inject().
  • \n
\n

Examples

\n

Incorrect

\n
@Component({\n  selector: 'app-signin',\n  templateUrl: './signin.page.html',\n})\nexport class SigninPage {\n  constructor(\n    private store: Store<IApp>,\n    public readonly navCtrl: NavController,\n  ) {}\n}\n

Correct

\n
import { inject } from '@angular/core';\n\n@Component({\n  selector: 'app-signin',\n  templateUrl: './signin.page.html',\n})\nexport class SigninPage {\n  private readonly store = inject(Store<IApp>);\n  private readonly navCtrl = inject(NavController);\n}\n
// Non-DI constructor parameters are allowed\nexport class LogManager {\n  constructor(logDomain: string) {\n    this.logDomain = logDomain;\n  }\n}\n

Options

\n

This rule has no options.

\n

When to enable

\n

Enable this opt-in rule when a project requires Angular dependencies to be obtained with inject() instead of constructor parameter properties. Plain constructor parameters remain allowed because the rule only reports TSParameterProperty nodes.

\n

Implementation

\n\n", "headings": [ { "id": "rule-details", "text": "Rule Details", "level": 2 }, + { + "id": "examples", + "text": "Examples", + "level": 2 + }, + { + "id": "incorrect", + "text": "Incorrect", + "level": 3 + }, + { + "id": "correct", + "text": "Correct", + "level": 3 + }, { "id": "options", "text": "Options", "level": 2 }, + { + "id": "when-to-enable", + "text": "When to enable", + "level": 2 + }, { "id": "implementation", "text": "Implementation", @@ -229,50 +303,46 @@ export const PROJECT = { "file": "rules/deny-element.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/deny-element", - "html": "
\n

This plugin disallows the use of certain HTML tags.

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
\n
\n

This rule is particularly useful for Ionic applications where you want to enforce the use of Controller-based modals and action sheets instead of inline components.

\n

Rule Details

\n

❌ Incorrect: Using disallowed elements in templates

\n
<ion-modal></ion-modal>\n<!-- error -->\n

✅ Correct: Configure the rule in .eslintrc.json to specify which elements to disallow

\n

Rule Settings

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/deny-element\": [\n      \"error\",\n      {\n        \"elements\": [\"ion-modal\"]\n      }\n    ]\n  }\n}\n

Options

\n
const options: {\n  elements: string[]; // Array of element names to disallow\n};\n

Implementation

\n\n", + "html": "
\n

This plugin disallows the use of certain HTML tags.

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
\n
\n

This rule prevents specific elements from being used in Angular templates. It is commonly used to ban inline overlay components such as <ion-modal>, <ion-popover>, <ion-toast>, <ion-alert>, <ion-loading>, <ion-picker>, and <ion-action-sheet>, which should be presented through launcher methods or dedicated services instead of being declared in the template.

\n

Rule Details

\n

The rule runs on .html template files and reports any element whose tag name is in the configured elements list. It traverses the template AST, including Angular control flow syntax such as @if, @for, @else, and nested then / else branches.

\n
    \n
  • .spec.html files are ignored so that tests are not affected.
  • \n
  • Without an explicit option, the rule uses its default Ionic overlay element list. When an option object is supplied, its schema requires an elements array.
  • \n
\n

Options

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/deny-element\": [\n      \"error\",\n      {\n        \"elements\": [\"ion-modal\", \"ion-popover\", \"ion-toast\", \"ion-alert\", \"ion-loading\", \"ion-picker\", \"ion-action-sheet\"]\n      }\n    ]\n  }\n}\n

elements

\n
    \n
  • Type: string[]
  • \n
  • Default: ion-modal, ion-popover, ion-toast, ion-alert, ion-loading, ion-picker, ion-action-sheet
  • \n
\n

Array of element tag names to disallow. The rule compares these names to the Element node type in the Angular template AST, so it checks both the element itself and its presence inside control flow branches.

\n

Examples

\n

Incorrect

\n
<ion-modal></ion-modal>\n\n<div>\n  <ion-toast></ion-toast>\n  <ion-alert></ion-alert>\n</div>\n
@if (showModal) {\n<ion-modal>Modal content</ion-modal>\n}\n

Correct

\n
<ion-button (click)=\"presentModal()\">Open</ion-button>\n
@for (item of items; track item.id) {\n<ion-card>\n  <ion-card-header>{{ item.name }}</ion-card-header>\n</ion-card>\n}\n

When to enable

\n

Enable this rule in projects that use the launcher pattern for overlays. It pairs with @rdlabo/rules/prefer-modal-launcher and @rdlabo/rules/prefer-disable-handler to keep modal and overlay logic out of the template.

\n

See also

\n\n

Implementation

\n\n", "headings": [ { "id": "rule-details", "text": "Rule Details", "level": 2 }, - { - "id": "rule-settings", - "text": "Rule Settings", - "level": 2 - }, { "id": "options", "text": "Options", "level": 2 }, { - "id": "implementation", - "text": "Implementation", + "id": "elements", + "text": "elements", + "level": 3 + }, + { + "id": "examples", + "text": "Examples", "level": 2 - } - ], - "codes": [], - "scrollMap": [], - "editUrl": "https://github.com/rdlabo-dev/eslint-plugin-rules/edit/main/docs/rules/deny-element.md" - }, - { - "title": "deny-import-from-ionic-module", - "navTitle": "deny-import-from-ionic-module", - "slug": "rules/deny-import-from-ionic-module", - "file": "rules/deny-import-from-ionic-module.md", - "section": "Rules", - "path": "/projects/eslint-plugin-rules/docs/rules/deny-import-from-ionic-module", - "html": "
\n

This plugin prevents accidental imports from @ionic/angular instead of @ionic/angular/standalone.

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
  • ✒️ The --fix option on the command line can automatically fix some of the problems reported by this rule.
  • \n
\n
\n

Mixing imports from @ionic/angular and @ionic/angular/standalone can cause runtime issues. While the build process may succeed, the application might fail when running in the browser. This rule helps prevent such issues by enforcing consistent import paths.

\n

Rule Details

\n

❌ Incorrect: Importing from @ionic/angular

\n
import { ModalController } from '@ionic/angular';\n

✅ Correct: Importing from @ionic/angular/standalone

\n
import { ModalController } from '@ionic/angular/standalone';\n

Options

\n

No Options.

\n

Implementation

\n\n", - "headings": [ + }, { - "id": "rule-details", - "text": "Rule Details", + "id": "incorrect", + "text": "Incorrect", + "level": 3 + }, + { + "id": "correct", + "text": "Correct", + "level": 3 + }, + { + "id": "when-to-enable", + "text": "When to enable", "level": 2 }, { - "id": "options", - "text": "Options", + "id": "see-also", + "text": "See also", "level": 2 }, { @@ -283,7 +353,7 @@ export const PROJECT = { ], "codes": [], "scrollMap": [], - "editUrl": "https://github.com/rdlabo-dev/eslint-plugin-rules/edit/main/docs/rules/deny-import-from-ionic-module.md" + "editUrl": "https://github.com/rdlabo-dev/eslint-plugin-rules/edit/main/docs/rules/deny-element.md" }, { "title": "deny-overlay-create", @@ -292,7 +362,7 @@ export const PROJECT = { "file": "rules/deny-overlay-create.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/deny-overlay-create", - "html": "
\n

Disallow .create() on ModalController / PopoverController; open overlays via launchers instead.

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
\n
\n

In Ionic apps, modals and popovers should be opened through a shared helper (presentModal) and an exported launch* function — not by calling ModalController.create() / PopoverController.create() directly.

\n

Use together with:

\n
    \n
  • @rdlabo/rules/deny-element — bans inline <ion-modal> / <ion-popover> in templates
  • \n
  • @rdlabo/rules/prefer-modal-launcher — requires presentModal to live inside launch*
  • \n
\n

LoadingController, AlertController, ToastController, and ActionSheetController stay allowed by default. dismiss() on ModalController is also allowed.

\n

Rule Details

\n

❌ Incorrect: create a modal / popover via the controller

\n
import { inject } from '@angular/core';\nimport { ModalController } from '@ionic/angular/standalone';\n\nexport class ExamplePage {\n  readonly #modalCtrl = inject(ModalController);\n\n  async open() {\n    await this.#modalCtrl.create({ component: OtherPage }); // error\n  }\n}\n

The same applies to:

\n
    \n
  • this.modalCtrl.create(...)
  • \n
  • inject(ModalController).create(...)
  • \n
  • parameters typed as ModalController / PopoverController
  • \n
\n

✅ Correct: open via launcher; keep ModalController only for dismiss

\n
export const launchOtherPage = (helper: HelperService, props: OtherProps) => {\n  return helper.presentModal(OtherPage, props, { watchKeyboard: false });\n};\n\nexport class ExamplePage {\n  readonly #modalCtrl = inject(ModalController);\n  readonly helper = inject(HelperService);\n\n  async open() {\n    await launchOtherPage(this.helper, {});\n  }\n\n  dismiss() {\n    this.#modalCtrl.dismiss();\n  }\n}\n

✅ Correct: Loading / Alert / Toast / ActionSheet create

\n
readonly #loadingCtrl = inject(LoadingController);\nawait this.#loadingCtrl.create({ message: '...' });\n

Options

\n
{\n  // Controllers whose `.create()` is denied.\n  // default: ['ModalController', 'PopoverController']\n  deny?: string[];\n}\n
'@rdlabo/rules/deny-overlay-create': [\n  'error',\n  { deny: ['ModalController', 'PopoverController'] },\n],\n

To also ban alert creation:

\n
{\n  deny: ['ModalController', 'PopoverController', 'AlertController'];\n}\n

Implementation

\n\n", + "html": "
\n

Disallow .create() on ModalController / PopoverController; open overlays via launchers instead.

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
\n
\n

This rule prevents direct creation of Ionic overlays through controller .create() calls. In the rdlabo architecture, overlays should be opened through launcher functions and a shared presentModal / presentPopover helper. This keeps overlay logic centralized and the call site decoupled from the controller API.

\n

Rule Details

\n

The rule detects .create() calls where the receiver is a ModalController or PopoverController (or other configured controllers). It resolves the controller through several patterns:

\n
    \n
  • this.modalCtrl.create()
  • \n
  • modalCtrl.create() (where modalCtrl is inject(ModalController))
  • \n
  • inject(ModalController).create()
  • \n
  • Constructor parameter constructor(private modalCtrl: ModalController)
  • \n
  • Class property typed as ModalController
  • \n
\n

Other overlay controllers such as LoadingController, AlertController, ToastController, and ActionSheetController are not denied by default, because they may be intentionally used directly.

\n

Options

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/deny-overlay-create\": [\n      \"error\",\n      {\n        \"deny\": [\"ModalController\", \"PopoverController\"]\n      }\n    ]\n  }\n}\n

deny

\n
    \n
  • Type: string[]
  • \n
  • Default: [\"ModalController\", \"PopoverController\"]
  • \n
\n

Controller class names whose .create() calls should be disallowed. Use an empty array to disable the rule.

\n

Examples

\n

Incorrect

\n
export class ExamplePage {\n  readonly #modalCtrl = inject(ModalController);\n\n  async open() {\n    await this.#modalCtrl.create({ component: OtherPage });\n  }\n}\n
export async function open(modalCtrl: ModalController) {\n  await modalCtrl.create({ component: OtherPage });\n}\n
export class ExamplePage {\n  constructor(private modalCtrl: ModalController) {}\n\n  async open() {\n    await this.modalCtrl.create({ component: OtherPage });\n  }\n}\n

Correct

\n
export const launchOtherPage = (overlay: Helper, props: Props) => {\n  return overlay.presentModal(OtherPage, props);\n};\n
export class ExamplePage {\n  readonly #loadingCtrl = inject(LoadingController);\n\n  async showLoading() {\n    await this.#loadingCtrl.create({ message: '...' });\n  }\n}\n
export class ExamplePage {\n  readonly #modalCtrl = inject(ModalController);\n\n  dismiss(data?: unknown) {\n    this.#modalCtrl.dismiss(data);\n  }\n}\n

When to enable

\n

Enable this rule in Ionic projects that follow the launcher pattern and use a shared overlay helper. It pairs with @rdlabo/rules/prefer-modal-launcher and @rdlabo/rules/deny-element.

\n

See also

\n\n

Implementation

\n\n", "headings": [ { "id": "rule-details", @@ -304,6 +374,36 @@ export const PROJECT = { "text": "Options", "level": 2 }, + { + "id": "deny", + "text": "deny", + "level": 3 + }, + { + "id": "examples", + "text": "Examples", + "level": 2 + }, + { + "id": "incorrect", + "text": "Incorrect", + "level": 3 + }, + { + "id": "correct", + "text": "Correct", + "level": 3 + }, + { + "id": "when-to-enable", + "text": "When to enable", + "level": 2 + }, + { + "id": "see-also", + "text": "See also", + "level": 2 + }, { "id": "implementation", "text": "Implementation", @@ -321,13 +421,43 @@ export const PROJECT = { "file": "rules/deny-soft-private-modifier.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/deny-soft-private-modifier", - "html": "
\n

This plugin disallows the use of soft private modifier.

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
  • ✒️ The --fix option on the command line can automatically fix some of the problems reported by this rule.
  • \n
\n
\n

Rule Details

\n

❌ Incorrect: Using the private modifier for class fields

\n
@Component({})\nexport class SigninPage {\n  private platform = inject(Platform);\n}\n

✅ Correct: Using the hard private field syntax (#)

\n
@Component({})\nexport class SigninPage {\n  #platform = inject(Platform);\n}\n

Implementation

\n\n", + "html": "
\n

This plugin disallows the use of soft private modifier.

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
  • ✒️ The --fix option on the command line can automatically fix some of the problems reported by this rule.
  • \n
\n
\n

TypeScript's private modifier is only enforced at compile time. It can still be accessed at runtime through bracket notation or by casting to any. JavaScript hard-private fields (#) are runtime-enforced and cannot be bypassed from outside the class. This rule replaces private properties and methods with # fields and updates this.x references to this.#x.

\n

Rule Details

\n

This rule checks classes for the following patterns:

\n
    \n
  • A private property definition (private field = ...)
  • \n
  • A private method definition (private method() { ... })
  • \n
  • A this.field reference where field was declared as private
  • \n
\n

It does not report constructors, because private constructor() has a different meaning (preventing external instantiation). A private readonly property is reported; the fix removes private, adds #, and preserves readonly.

\n

The rule auto-fixes by:

\n
    \n
  1. Removing the private keyword.
  2. \n
  3. Inserting # before the property or method name.
  4. \n
  5. Updating all this.field or this.method() references in the class to this.#field or this.#method().
  6. \n
\n

Examples

\n

Incorrect

\n
class TokenStore {\n  private token = '';\n\n  private refresh() {\n    this.token = 'new-token';\n  }\n}\n

Correct

\n
class TokenStore {\n  #token = '';\n\n  #refresh() {\n    this.#token = 'new-token';\n  }\n}\n

Options

\n

This rule has no options.

\n

When to enable

\n

Enable this rule when a project wants runtime-enforced encapsulation for class internals. It is safe to run with --fix on existing code, but it changes public API surface: any code that was relying on compile-time private access at runtime will break.

\n

See also

\n\n

Implementation

\n\n", "headings": [ { "id": "rule-details", "text": "Rule Details", "level": 2 }, + { + "id": "examples", + "text": "Examples", + "level": 2 + }, + { + "id": "incorrect", + "text": "Incorrect", + "level": 3 + }, + { + "id": "correct", + "text": "Correct", + "level": 3 + }, + { + "id": "options", + "text": "Options", + "level": 2 + }, + { + "id": "when-to-enable", + "text": "When to enable", + "level": 2 + }, + { + "id": "see-also", + "text": "See also", + "level": 2 + }, { "id": "implementation", "text": "Implementation", @@ -345,18 +475,38 @@ export const PROJECT = { "file": "rules/implements-ionic-lifecycle.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/implements-ionic-lifecycle", - "html": "
\n

This plugin recommend to implements Ionic Lifecycle.

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
  • ✒️ The --fix option on the command line can automatically fix some of the problems reported by this rule.
  • \n
\n
\n

Rule Details

\n

❌ Incorrect: Using Ionic Lifecycle methods without implementing the interface

\n
@Component({\n  selector: 'app-confirm',\n  templateUrl: './confirm.page.html',\n  styleUrls: ['./confirm.page.scss'],\n})\nexport class SigninPage {\n  ionViewWillEnter() {}\n}\n

✅ Correct: Using Ionic Lifecycle methods with proper interface implementation

\n
@Component({\n  selector: 'app-confirm',\n  templateUrl: './confirm.page.html',\n  styleUrls: ['./confirm.page.scss'],\n})\nexport class SigninPage implements ionViewWillEnter {\n  ionViewWillEnter() {}\n}\n

Options

\n

No Options.

\n

Implementation

\n\n", + "html": "
\n

This plugin recommend to implements Ionic Lifecycle.

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
  • ✒️ The --fix option on the command line can automatically fix some of the problems reported by this rule.
  • \n
\n
\n

Ionic provides framework-level lifecycle methods such as ionViewWillEnter and ionViewDidLeave. When a Component declares these methods, it should also implement the corresponding interface (ViewWillEnter, ViewDidEnter, ViewWillLeave, ViewDidLeave) so TypeScript can type-check the contract. This rule enforces that pairing and can auto-fix the implements clause.

\n

Rule Details

\n

This rule checks @Component decorated classes. It looks for method definitions named after Ionic lifecycle methods:

\n
    \n
  • ionViewWillEnter -> ViewWillEnter
  • \n
  • ionViewDidEnter -> ViewDidEnter
  • \n
  • ionViewWillLeave -> ViewWillLeave
  • \n
  • ionViewDidLeave -> ViewDidLeave
  • \n
\n

If a method is present and its matching interface is missing, the rule reports it. When fixing a missing interface, the rule replaces the entire implements clause with the Ionic lifecycle interfaces that correspond to the used methods. This can remove unrelated interfaces such as OnInit, so review the fix and restore any non-Ionic interfaces that the class still requires. If every required interface is already present, extra lifecycle interfaces are not reported or removed.

\n
    \n
  • The rule does not check non-Component classes.
  • \n
  • If the class body is empty but it implements lifecycle interfaces, the rule removes the stale implements clause.
  • \n
  • The rule only reports once per fixable group to avoid overlapping fixes.
  • \n
\n

Examples

\n

Incorrect

\n
@Component({\n  selector: 'app-scanner',\n  standalone: true,\n})\nexport class ScannerPage {\n  ionViewWillEnter() {}\n  ionViewWillLeave() {}\n}\n
@Component({\n  selector: 'app-scanner',\n  standalone: true,\n})\nexport class ScannerPage implements ViewDidEnter, ViewDidLeave {\n  ionViewWillEnter() {}\n  ionViewWillLeave() {}\n}\n

Correct

\n
import { ViewWillEnter, ViewWillLeave } from '@ionic/angular';\n\n@Component({\n  selector: 'app-scanner',\n  standalone: true,\n})\nexport class ScannerPage implements ViewWillEnter, ViewWillLeave {\n  ionViewWillEnter() {}\n  ionViewWillLeave() {}\n}\n
@Component({\n  selector: 'app-scanner',\n  standalone: true,\n})\nexport class ScannerPage implements ViewDidEnter, ViewDidLeave {\n  ionViewDidEnter() {}\n  ionViewDidLeave() {}\n}\n

Options

\n

This rule has no options.

\n

When to enable

\n

Enable this rule in any Ionic Angular project. It helps keep the implements clause accurate when lifecycle methods are added, renamed, or removed, and it works well with --fix.

\n

Implementation

\n\n", "headings": [ { "id": "rule-details", "text": "Rule Details", "level": 2 }, + { + "id": "examples", + "text": "Examples", + "level": 2 + }, + { + "id": "incorrect", + "text": "Incorrect", + "level": 3 + }, + { + "id": "correct", + "text": "Correct", + "level": 3 + }, { "id": "options", "text": "Options", "level": 2 }, + { + "id": "when-to-enable", + "text": "When to enable", + "level": 2 + }, { "id": "implementation", "text": "Implementation", @@ -374,7 +524,7 @@ export const PROJECT = { "file": "rules/ionic-attr-type-check.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/ionic-attr-type-check", - "html": "
\n

Disallows string values for non-string attributes in Ionic components and suggests proper property binding. Supports boolean, number, and object type attributes.

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
  • ✒️ The --fix option on the command line can automatically fix some of the problems reported by this rule.
  • \n
\n
\n

This rule prevents TypeScript build errors by detecting when string values are assigned to non-string attributes (boolean, number, object, complex) in Ionic component templates and suggests proper property binding.

\n

Rule Details

\n

This rule detects when string values are assigned to non-string attributes (boolean, number, object, complex) in Ionic component templates.

\n

❌ Incorrect: Using string values for non-string attributes

\n
<ion-item button=\"true\"></ion-item>\n<ion-list inset=\"true\"></ion-list>\n<ion-progress-bar value=\"50\"></ion-progress-bar>\n<input disabled=\"false\"></input>\n<button readonly=\"1\"></button>\n

✅ Correct: Using property binding

\n
<ion-item [button]=\"true\"></ion-item>\n<ion-list [inset]=\"true\"></ion-list>\n<ion-progress-bar [value]=\"50\"></ion-progress-bar>\n<input [disabled]=\"false\"></input>\n<button [readonly]=\"true\"></button>\n

Options

\n

No Options.

\n

Supported Attribute Types

\n

This rule automatically identifies non-string attributes from Ionic component type definitions and detects attributes such as:

\n

Ionic Component Attribute Examples

\n
    \n
  • ion-item: button, disabled, detail
  • \n
  • ion-list: inset, lines
  • \n
  • ion-button: disabled, expand, fill, strong
  • \n
  • ion-checkbox: checked, disabled, indeterminate
  • \n
  • ion-toggle: checked, disabled
  • \n
  • ion-radio: checked, disabled
  • \n
  • ion-input: disabled, readonly, required
  • \n
  • ion-textarea: disabled, readonly, required
  • \n
  • ion-select: disabled, multiple, required
  • \n
  • ion-datetime: disabled, readonly
  • \n
  • ion-range: disabled, pin, snaps
  • \n
  • ion-segment: disabled
  • \n
  • ion-slides: pager, scrollbar
  • \n
  • ion-tab: selected
  • \n
  • ion-menu: disabled, swipeGesture
  • \n
  • ion-modal: animated, backdropDismiss, showBackdrop
  • \n
  • ion-popover: animated, backdropDismiss, showBackdrop
  • \n
  • ion-alert: animated, backdropDismiss
  • \n
  • ion-loading: animated, backdropDismiss
  • \n
  • ion-toast: animated
  • \n
  • ion-action-sheet: animated, backdropDismiss
  • \n
\n

Error Message

\n

This rule displays the following message:

\n
boolean attribute 'button' should not have a string value 'true'. Use property binding [button]=\"true\" instead.\nnumber attribute 'value' should not have a string value '50'. Use property binding [value]=\"50\" instead.\n

Implementation

\n\n", + "html": "
\n

Require property bindings for supported non-string Ionic attributes and validate string-literal attributes.

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
  • ✒️ The --fix option on the command line can automatically fix some of the problems reported by this rule.
  • \n
\n
\n

Ionic component attributes can be boolean, number, object, or string. Passing a string such as button=\"true\" to a boolean property is a common mistake and can cause unexpected behavior. This rule reads the Ionic type definitions from @ionic/core and reports mismatches.

\n

Rule Details

\n

The rule runs on Angular templates. For each Ionic element, it looks at the @ionic/core type definitions and classifies each attribute as one of:

\n
    \n
  • string — string literals are allowed
  • \n
  • string literal — only a specific set of values is allowed
  • \n
  • boolean — use [attr]=\"true\" or [attr]=\"false\"
  • \n
  • number — use [attr]=\"50\"
  • \n
  • object — use [attr]=\"...\"
  • \n
  • skip / unknown — not checked
  • \n
\n

For boolean attributes, the rule recognizes the string values true, false, 1, 0, yes, no, on, and off; other strings are not reported by the boolean check. Supported boolean, number, and object mismatches are auto-fixed to property bindings:

\n
    \n
  • button=\"true\" -> [button]=\"true\"
  • \n
  • value=\"50\" -> [value]=\"50\"
  • \n
  • autocorrect=\"off\" -> [autocorrect]=\"false\" on Ionic 9
  • \n
\n

When a string value is invalid for a string-literal attribute, the rule reports the accepted values.

\n

Examples

\n

Incorrect

\n
<ion-item button=\"true\"></ion-item>\n
<ion-progress-bar value=\"50\"></ion-progress-bar>\n
<ion-modal isOpen=\"true\" backdropDismiss=\"false\"></ion-modal>\n

Correct

\n
<ion-item [button]=\"true\"></ion-item>\n
<ion-progress-bar [value]=\"50\"></ion-progress-bar>\n
<ion-modal [isOpen]=\"true\" [backdropDismiss]=\"false\"></ion-modal>\n
<!-- string-typed attributes are still allowed -->\n<ion-item lines=\"full\"></ion-item>\n<ion-button color=\"primary\">Click me</ion-button>\n

Options

\n

This rule has no options.

\n

When to enable

\n

Enable this rule in any Ionic Angular project. It is especially useful when migrating from older Ionic syntax or when onboarding developers who are used to plain HTML attributes.

\n

Requirements

\n

The rule requires @ionic/core to be installed in the same project so it can read node_modules/@ionic/core/dist/types/components.d.ts. If the package is not present, the rule returns an empty result and does not report.

\n

Implementation

\n\n", "headings": [ { "id": "rule-details", @@ -382,23 +532,33 @@ export const PROJECT = { "level": 2 }, { - "id": "options", - "text": "Options", + "id": "examples", + "text": "Examples", "level": 2 }, { - "id": "supported-attribute-types", - "text": "Supported Attribute Types", - "level": 2 + "id": "incorrect", + "text": "Incorrect", + "level": 3 }, { - "id": "ionic-component-attribute-examples", - "text": "Ionic Component Attribute Examples", + "id": "correct", + "text": "Correct", "level": 3 }, { - "id": "error-message", - "text": "Error Message", + "id": "options", + "text": "Options", + "level": 2 + }, + { + "id": "when-to-enable", + "text": "When to enable", + "level": 2 + }, + { + "id": "requirements", + "text": "Requirements", "level": 2 }, { @@ -418,7 +578,7 @@ export const PROJECT = { "file": "rules/no-component-method-except-lifecycle.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/no-component-method-except-lifecycle", - "html": "
\n

Disallow non-lifecycle methods on @Component. Allowed lifecycle methods are derived from implements (properties are allowed).

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
\n
\n

Keep @Component classes thin. Behavior belongs on ViewModel (or modal launch* helpers).

\n

Allowed methods are only those that match lifecycle interfaces listed in implements. A lifecycle method without the corresponding interface is also an error.

\n

Properties are out of scope, including arrow-function fields such as readonly open = () => .... Getters / setters and constructor are allowed.

\n

@Directive / @Injectable / plain classes are not checked. Pair with @rdlabo/rules/require-viewmodel and @rdlabo/rules/implements-ionic-lifecycle.

\n
\n

Existing apps often have many Component methods. Prefer starting with \"warn\", then move logic to ViewModel before flipping to \"error\".

\n
\n

Rule Details

\n

✅ Correct: methods match implements

\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage implements ViewWillEnter, ViewWillLeave, OnDestroy {\n  readonly vm = new ViewModel(this);\n  readonly open = () => launchOtherPage(this.helper, {});\n\n  constructor() {}\n\n  ionViewWillEnter() {\n    this.vm.reload$.next();\n  }\n\n  ionViewWillLeave() {}\n\n  ngOnDestroy() {}\n}\n

❌ Incorrect: lifecycle method without implements

\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage {\n  ionViewWillEnter() {} // error — needs implements ViewWillEnter\n  ngOnDestroy() {} // error — needs implements OnDestroy\n}\n

❌ Incorrect: implemented interface does not cover the method

\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage implements ViewWillEnter {\n  ionViewWillEnter() {}\n  ionViewWillLeave() {} // error — needs implements ViewWillLeave\n}\n

❌ Incorrect: arbitrary methods on Component

\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage implements ViewWillEnter {\n  ionViewWillEnter() {}\n\n  open() {\n    // error\n    launchOtherPage(this.helper, {});\n  }\n}\n

Interface → method mapping

\n

Angular

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
implementsmethod
OnChangesngOnChanges
OnInitngOnInit
DoCheckngDoCheck
AfterContentInitngAfterContentInit
AfterContentCheckedngAfterContentChecked
AfterViewInitngAfterViewInit
AfterViewCheckedngAfterViewChecked
OnDestroyngOnDestroy
\n

Ionic

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
implementsmethod
ViewWillEnterionViewWillEnter
ViewDidEnterionViewDidEnter
ViewWillLeaveionViewWillLeave
ViewDidLeaveionViewDidLeave
ViewWillUnloadionViewWillUnload
\n

Also allowed (not reported): constructor, get / set accessors, and all properties.

\n

Options

\n
{\n  // Extra method names to allow (e.g. trackBy helpers during migration).\n  additionalAllowedMethods?: string[];\n}\n
'@rdlabo/rules/no-component-method-except-lifecycle': [\n  'warn',\n  { additionalAllowedMethods: ['trackById'] },\n],\n

Implementation

\n\n", + "html": "
\n

Disallow non-lifecycle methods on @Component. Allowed lifecycle methods are derived from implements (properties are allowed).

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
\n
\n

This rule enforces thin Components. A Component should contain lifecycle hooks, delegated event handlers, and read-only view properties. Arbitrary business logic should live in a ViewModel, accessed through the Component's vm property.

\n

Rule Details

\n

The rule checks methods inside @Component decorated classes:

\n
    \n
  • constructor, getters, and setters are ignored.
  • \n
  • Methods whose name matches a lifecycle interface declared in implements are allowed (for example, ngOnInit when OnInit is implemented, or ionViewWillEnter when ViewWillEnter is implemented).
  • \n
  • Methods listed in additionalAllowedMethods are allowed.
  • \n
  • All other method definitions are reported.
  • \n
\n

The rule also reports lifecycle methods that are used without the matching interface being implemented. For example, an ionViewWillEnter method without implements ViewWillEnter is reported.

\n

Supported lifecycle interfaces

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
InterfaceMethod
OnChangesngOnChanges
OnInitngOnInit
DoCheckngDoCheck
AfterContentInitngAfterContentInit
AfterContentCheckedngAfterContentChecked
AfterViewInitngAfterViewInit
AfterViewCheckedngAfterViewChecked
OnDestroyngOnDestroy
ViewWillEnterionViewWillEnter
ViewDidEnterionViewDidEnter
ViewWillLeaveionViewWillLeave
ViewDidLeaveionViewDidLeave
ViewWillUnloadionViewWillUnload
\n

Examples

\n

Incorrect

\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage {\n  open() {\n    launchOtherPage(this.helper, {});\n  }\n\n  reload() {\n    this.vm.reload$.next();\n  }\n}\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage {\n  ionViewWillEnter() {} // missing implements ViewWillEnter\n}\n

Correct

\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage implements ViewWillEnter, ViewWillLeave, OnDestroy {\n  readonly vm = new ViewModel(this);\n  readonly open = () => launchOtherPage(this.helper, {});\n\n  ionViewWillEnter() {\n    this.vm.reload$.next();\n  }\n\n  ionViewWillLeave() {}\n  ngOnDestroy() {}\n}\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage implements ViewWillEnter {\n  ionViewWillEnter() {}\n\n  trackById(_index: number, item: { id: number }) {\n    return item.id;\n  }\n\n  customHook() {}\n}\n
{\n  \"rules\": {\n    \"@rdlabo/rules/no-component-method-except-lifecycle\": [\n      \"error\",\n      {\n        \"additionalAllowedMethods\": [\"trackById\", \"customHook\"]\n      }\n    ]\n  }\n}\n

Options

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/no-component-method-except-lifecycle\": [\n      \"error\",\n      {\n        \"additionalAllowedMethods\": []\n      }\n    ]\n  }\n}\n

additionalAllowedMethods

\n
    \n
  • Type: string[]
  • \n
  • Default: []
  • \n
\n

Method names that are allowed in addition to lifecycle methods. Use this for helper methods such as trackById that are part of the Component template contract.

\n

When to enable

\n

Enable this rule when a project wants Components to stay thin and push logic to ViewModels. It pairs with @rdlabo/rules/require-viewmodel.

\n

See also

\n\n

Implementation

\n\n", "headings": [ { "id": "rule-details", @@ -426,18 +586,23 @@ export const PROJECT = { "level": 2 }, { - "id": "interface-%E2%86%92-method-mapping", - "text": "Interface → method mapping", + "id": "supported-lifecycle-interfaces", + "text": "Supported lifecycle interfaces", "level": 2 }, { - "id": "angular", - "text": "Angular", + "id": "examples", + "text": "Examples", + "level": 2 + }, + { + "id": "incorrect", + "text": "Incorrect", "level": 3 }, { - "id": "ionic", - "text": "Ionic", + "id": "correct", + "text": "Correct", "level": 3 }, { @@ -445,6 +610,21 @@ export const PROJECT = { "text": "Options", "level": 2 }, + { + "id": "additionalallowedmethods", + "text": "additionalAllowedMethods", + "level": 3 + }, + { + "id": "when-to-enable", + "text": "When to enable", + "level": 2 + }, + { + "id": "see-also", + "text": "See also", + "level": 2 + }, { "id": "implementation", "text": "Implementation", @@ -462,8 +642,43 @@ export const PROJECT = { "file": "rules/no-component-writable-signal.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/no-component-writable-signal", - "html": "
\n

Keep writable component state in ViewModel, except models passed to Angular Signal Forms form().

\n
\n

Keeps writable Component state in ViewModel. Component-local signal() and linkedSignal() are rejected unless the property is a model passed directly to a Signal Forms field initializer such as profileForm = form(this.model).

\n
readonly model = signal({ name: '' });\nreadonly profileForm = form(this.model); // valid\n\nreadonly isLoading = signal(false); // invalid: move to ViewModel\n

computed() and effect() remain Component responsibilities and are not restricted by this rule. Non-Component classes are ignored.

\n

Named aliases and namespace imports from @angular/core and @angular/forms/signals are supported. Same-named local helpers are ignored because the rule verifies import provenance.

\n

Implementation

\n\n", + "html": "
\n

Keep writable component state in ViewModel, except models passed to Angular Signal Forms form().

\n
\n

This rule enforces a clear boundary between Angular Components and ViewModels. Components should expose read-only derived state to templates; writable state should live in a ViewModel so that changes are centralized and testable. The only writable Signal allowed on a Component is one passed directly to Signal Forms form() as its model.

\n

Rule Details

\n

This rule inspects @Component decorated classes and reports class properties initialized with signal() or linkedSignal() from @angular/core, unless the same property is passed as the first argument to form() from @angular/forms/signals.

\n
    \n
  • computed() and effect() remain Component responsibilities and are not reported.
  • \n
  • Non-Component classes are ignored.
  • \n
  • Aliased and namespace imports from @angular/core and @angular/forms/signals are recognized.
  • \n
  • Same-named local helpers are ignored because the rule verifies import provenance.
  • \n
\n

The Signal Forms exception only recognizes a Component property initializer such as readonly pageForm = form(this.model). Passing the Signal to form() inside a method does not create an exception, so the writable Signal property is still reported.

\n

Examples

\n

Incorrect

\n
import { Component, signal } from '@angular/core';\n\n@Component({ template: '' })\nclass Page {\n  readonly isLoading = signal(false); // reported: move to ViewModel\n}\n
import { Component, signal } from '@angular/core';\nimport { form } from '@angular/forms/signals';\n\n@Component({ template: '' })\nclass Page {\n  readonly model = signal({ name: '' });\n  readonly loading = signal(false); // reported\n  readonly pageForm = form(this.model);\n}\n

Correct

\n
import { Component, computed } from '@angular/core';\nimport { form } from '@angular/forms/signals';\nimport { PageViewModel } from './page.viewmodel';\n\n@Component({ template: '' })\nclass Page {\n  private readonly vm = new PageViewModel(this);\n  readonly isLoading = this.vm.isLoading; // read-only view of ViewModel state\n  readonly model = this.vm.model;\n  readonly pageForm = form(this.model);\n  readonly title = computed(() => this.model().name);\n}\n
import { Component, signal as writable } from '@angular/core';\nimport { form as signalForm } from '@angular/forms/signals';\n\n@Component({ template: '' })\nclass Page {\n  readonly data = writable({ name: '' });\n  readonly pageForm = signalForm(this.data); // data is the Signal Forms model\n}\n

Options

\n

This rule has no options.

\n

When to enable

\n

Enable this rule when a project uses the ViewModel pattern with @rdlabo/rules/require-viewmodel. It ensures that Component properties are read-only views into shared state, which prevents Components from mutating state directly.

\n

See also

\n\n

Implementation

\n\n", "headings": [ + { + "id": "rule-details", + "text": "Rule Details", + "level": 2 + }, + { + "id": "examples", + "text": "Examples", + "level": 2 + }, + { + "id": "incorrect", + "text": "Incorrect", + "level": 3 + }, + { + "id": "correct", + "text": "Correct", + "level": 3 + }, + { + "id": "options", + "text": "Options", + "level": 2 + }, + { + "id": "when-to-enable", + "text": "When to enable", + "level": 2 + }, + { + "id": "see-also", + "text": "See also", + "level": 2 + }, { "id": "implementation", "text": "Implementation", @@ -481,8 +696,43 @@ export const PROJECT = { "file": "rules/no-reactive-forms.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/no-reactive-forms", - "html": "
\n

Disallow Angular Reactive Forms in favor of Signal Forms.

\n
\n

Disallows Angular Reactive Forms APIs and template bindings in favor of @angular/forms/signals.

\n

The rule rejects ReactiveFormsModule, typed and untyped controls/groups/arrays/builders, reactive directives, Validators, and related imports from @angular/forms. Namespace/default imports are rejected because they can bypass named-API checks. In templates it rejects formControl, formControlName, formGroup, formGroupName, and formArrayName.

\n

FormsModule and ngModel are intentionally outside this rule; use no-template-driven-forms for those bindings.

\n

Implementation

\n\n", + "html": "
\n

Disallow Angular Reactive Forms in favor of Signal Forms.

\n
\n

This rule helps migrate from Angular Reactive Forms to @angular/forms/signals. Reactive Forms require mutable FormControl / FormGroup state that is often shared between components and services, which makes it harder to track where state changes originate. Signal Forms keep form state in Signals, so the dependency graph is explicit and reactive by default.

\n

Use this rule when you want to prevent new Reactive Forms code from being introduced while a project is adopting Signal Forms.

\n

Rule Details

\n

This rule reports three patterns:

\n
    \n
  1. \n

    Named imports of Reactive Forms APIs from @angular/forms
    \nAny import of the following names is reported:

    \n

    AbstractControl, FormArray, FormArrayName, FormBuilder, FormControl, FormControlDirective, FormControlName, FormGroup, FormGroupDirective, FormGroupName, FormRecord, NonNullableFormBuilder, ReactiveFormsModule, UntypedFormArray, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators.

    \n
  2. \n
  3. \n

    Namespace or default imports from @angular/forms
    \nimport * as forms from '@angular/forms' and import forms from '@angular/forms' are reported because they can bypass the named-API checks.

    \n
  4. \n
  5. \n

    Reactive Forms template bindings
    \nThe following bindings are reported in Angular templates:
    \nformControl, formControlName, formGroup, formGroupName, formArrayName.

    \n
  6. \n
\n

FormsModule and ngModel are intentionally outside the scope of this rule. Use @rdlabo/rules/no-template-driven-forms to restrict those.

\n

Examples

\n

Incorrect

\n
// TypeScript: importing Reactive Forms APIs\nimport { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';\n\nimport * as forms from '@angular/forms';\nconst control = new forms.FormControl('');\n
<!-- Template: Reactive Forms bindings -->\n<form [formGroup]=\"userForm\">\n  <input formControlName=\"name\" />\n</form>\n

Correct

\n
import { signal } from '@angular/core';\nimport { form, required } from '@angular/forms/signals';\n\nconst userModel = signal({ name: '' });\nconst userForm = form(userModel, (path) => {\n  required(path.name);\n});\n
<!-- Template: Signal Forms field binding -->\n<input [formField]=\"userForm.name\" />\n

Options

\n

This rule has no options.

\n

When to enable

\n

Enable this rule in Angular projects that have adopted Signal Forms, or in projects that are migrating away from Reactive Forms. It is safe to enable alongside @rdlabo/rules/no-template-driven-forms to cover both form styles.

\n

See also

\n\n

Implementation

\n\n", "headings": [ + { + "id": "rule-details", + "text": "Rule Details", + "level": 2 + }, + { + "id": "examples", + "text": "Examples", + "level": 2 + }, + { + "id": "incorrect", + "text": "Incorrect", + "level": 3 + }, + { + "id": "correct", + "text": "Correct", + "level": 3 + }, + { + "id": "options", + "text": "Options", + "level": 2 + }, + { + "id": "when-to-enable", + "text": "When to enable", + "level": 2 + }, + { + "id": "see-also", + "text": "See also", + "level": 2 + }, { "id": "implementation", "text": "Implementation", @@ -500,8 +750,48 @@ export const PROJECT = { "file": "rules/no-template-driven-forms.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/no-template-driven-forms", - "html": "
\n

Disallow template-driven forms except ngModel bindings on explicitly allowed elements.

\n
\n

Disallows ngModel unless its element name is explicitly allowed for an Ionic View binding that is not suitable for Signal Forms. ngForm and ngModelGroup are always rejected.

\n
'@rdlabo/rules/no-template-driven-forms': [\n  'error',\n  {\n    allowedElements: [\n      'ion-searchbar',\n      'ion-segment',\n      'ion-radio-group',\n      'ion-select',\n      'ion-range',\n      'ion-toggle',\n      'ion-checkbox',\n      'ion-input-otp',\n    ],\n  },\n];\n

An allowed element is an interoperability exception, not a recommendation to use template-driven forms. Submission forms should use Signal Forms even when they contain an allowed element.

\n

Implementation

\n\n", + "html": "
\n

Disallow template-driven forms except ngModel bindings on explicitly allowed elements.

\n
\n

This rule restricts template-driven forms in Angular templates. ngForm and ngModelGroup are always rejected because they carry mutable form state inside the template. ngModel is also rejected unless it is placed on an element that has been explicitly allowlisted for an Ionic View binding that is not suitable for Signal Forms.

\n

An allowed element is an interoperability exception, not a recommendation to use template-driven forms. Submission forms should use Signal Forms even when they contain an allowed element.

\n

Rule Details

\n

The rule runs against Angular templates and checks three patterns:

\n
    \n
  1. \n

    ngModel on an element that is not in allowedElements
    \nReports ngModel, [(ngModel)], and [ngModel] on elements whose tag name is not in the allowlist. A standalone (ngModelChange) output is not inspected.

    \n
  2. \n
  3. \n

    ngModelGroup attribute
    \nReports any ngModelGroup attribute on any element.

    \n
  4. \n
  5. \n

    ngForm reference or directive
    \nReports <form #form=\"ngForm\"> and <div ngForm>.

    \n
  6. \n
\n

The rule is not a type-aware rule; it operates purely on parsed template AST.

\n

Examples

\n

Incorrect

\n
<!-- ngModel on an ordinary input -->\n<input [(ngModel)]=\"name\" />\n\n<!-- ngForm reference -->\n<form #form=\"ngForm\"></form>\n\n<!-- ngModelGroup directive -->\n<div ngModelGroup=\"address\"></div>\n

Correct

\n
<!-- Signal Forms field binding -->\n<input [formField]=\"userForm.name\" />\n\n<!-- ngModel allowed on ion-searchbar for a View binding -->\n<ion-searchbar [(ngModel)]=\"query\"></ion-searchbar>\n

Options

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/no-template-driven-forms\": [\n      \"error\",\n      {\n        \"allowedElements\": [\"ion-searchbar\", \"ion-segment\", \"ion-radio-group\", \"ion-select\", \"ion-range\", \"ion-toggle\", \"ion-checkbox\", \"ion-input-otp\"]\n      }\n    ]\n  }\n}\n

allowedElements

\n
    \n
  • Type: string[]
  • \n
  • Default: []
  • \n
\n

Element tag names that are permitted to use ngModel. This is intended for Ionic components that expose a value through ngModel as a view convenience, such as ion-searchbar or ion-toggle. Even when an element is allowed, ngModelGroup and ngForm are still reported.

\n

When to enable

\n

Enable this rule when a project is migrating to Angular Signal Forms but still needs limited ngModel bindings for specific Ionic View components. Disable it only when a project is fully committed to Reactive Forms and does not plan to adopt Signal Forms.

\n

See also

\n\n

Implementation

\n\n", "headings": [ + { + "id": "rule-details", + "text": "Rule Details", + "level": 2 + }, + { + "id": "examples", + "text": "Examples", + "level": 2 + }, + { + "id": "incorrect", + "text": "Incorrect", + "level": 3 + }, + { + "id": "correct", + "text": "Correct", + "level": 3 + }, + { + "id": "options", + "text": "Options", + "level": 2 + }, + { + "id": "allowedelements", + "text": "allowedElements", + "level": 3 + }, + { + "id": "when-to-enable", + "text": "When to enable", + "level": 2 + }, + { + "id": "see-also", + "text": "See also", + "level": 2 + }, { "id": "implementation", "text": "Implementation", @@ -519,7 +809,7 @@ export const PROJECT = { "file": "rules/prefer-disable-handler.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/prefer-disable-handler", - "html": "
\n

Require a wrapper method (default: disableHandler($event, work)) on configured element/event bindings to prevent double taps while async work runs

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
\n
\n

disableHandler (from @rdlabo/ionic-angular-kit) disables the triggering control while an async operation runs and restores it when the Promise settles. Using it on sync work is harmless: the control is briefly disabled and re-enabled, and if no disableable target is found it is a no-op.

\n

Element names, event names, and the wrapper method name are all configurable so the same rule can cover fleet defaults and repo-specific bindings (e.g. (ionComplete)).

\n

Enabling this rule in recommended will flag existing templates that call async work without the wrapper — expect a migration pass when bumping the plugin.

\n

Rule Details

\n

❌ Incorrect (defaults)

\n
<ion-button (click)=\"vm.save()\">Save</ion-button>\n<form (submit)=\"vm.save()\"></form>\n

Also incorrect — wrapper must receive $event and a second work argument:

\n
<ion-button (click)=\"vm.disableHandler($event)\">Save</ion-button> <ion-button (click)=\"vm.disableHandler(vm.save())\">Save</ion-button>\n

✅ Correct (defaults)

\n
<ion-button (click)=\"vm.disableHandler($event, vm.save())\">Save</ion-button>\n<form (submit)=\"vm.disableHandler($event, vm.save())\"></form>\n

Allowed without the wrapper (event-only, defaults):

\n
<ion-button (click)=\"$event.stopPropagation()\"></ion-button>\n
<ion-button (click)=\"$event.preventDefault()\"></ion-button>\n

Out of scope by default (not listed in targets.elements):

\n
<ion-chip (click)=\"vm.toggle()\"></ion-chip>\n
<ion-item [button]=\"true\" (click)=\"vm.open()\"></ion-item>\n

Options

\n
{\n  method?: string; // default: 'disableHandler'\n  eventParam?: string; // default: '$event'\n  targets?: Array<{\n    events: string[]; // e.g. ['click'], ['submit'], ['ionComplete']\n    elements?: string[]; // omit / [] = any element for those events\n  }>;\n  allowEventMethods?: string[]; // default: ['stopPropagation', 'preventDefault']\n}\n

targets fully replaces the default list (it is not merged). To keep click/submit and add more bindings, re-list the defaults plus your extras.

\n

Defaults

\n
{\n  method: 'disableHandler',\n  eventParam: '$event',\n  targets: [\n    { events: ['click'], elements: ['ion-button', 'button'] },\n    { events: ['submit'] }, // any element\n  ],\n  allowEventMethods: ['stopPropagation', 'preventDefault'],\n}\n

Examples

\n

Require a custom wrapper name:

\n
'@rdlabo/rules/prefer-disable-handler': ['error', { method: 'guardClick' }]\n

Also enforce (ionComplete) on ion-input (re-list default targets):

\n
'@rdlabo/rules/prefer-disable-handler': [\n  'error',\n  {\n    targets: [\n      { events: ['click'], elements: ['ion-button', 'button'] },\n      { events: ['submit'] },\n      { events: ['ionComplete'], elements: ['ion-input'] },\n    ],\n  },\n]\n

Implementation

\n\n", + "html": "
\n

Require a wrapper method (default: disableHandler($event, work)) on configured element/event bindings to prevent double taps while async work runs

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
\n
\n

When a user taps a button that triggers async work, the control should be disabled until the work settles. Otherwise, a second tap can fire the action again. This rule enforces the wrapper-call syntax for configured (event) bindings. The wrapper implementation is responsible for disabling the UI and handling the work value correctly.

\n

Rule Details

\n

The rule runs on Angular templates. For each BoundEvent that matches a configured target, the handler expression must be a call to a wrapper method with at least two arguments:

\n
    \n
  1. The event parameter (default $event).
  2. \n
  3. A work expression passed to the wrapper.
  4. \n
\n

For example, (click)=\"vm.disableHandler($event, vm.save())\" is valid. (click)=\"vm.save()\" is reported. The rule does not inspect the second argument's type or verify that it returns a Promise.

\n

The rule also allows bare event method calls such as $event.stopPropagation() and $event.preventDefault() (configurable with allowEventMethods).

\n

By default, the rule targets:

\n
    \n
  • click on <ion-button> and <button>
  • \n
  • submit on any element
  • \n
\n

It ignores .spec.html files.

\n

Options

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/prefer-disable-handler\": [\n      \"error\",\n      {\n        \"method\": \"disableHandler\",\n        \"eventParam\": \"$event\",\n        \"targets\": [{ \"events\": [\"click\"], \"elements\": [\"ion-button\", \"button\"] }, { \"events\": [\"submit\"] }],\n        \"allowEventMethods\": [\"stopPropagation\", \"preventDefault\"]\n      }\n    ]\n  }\n}\n

method

\n
    \n
  • Type: string
  • \n
  • Default: \"disableHandler\"
  • \n
\n

The wrapper method name expected in the handler expression.

\n

eventParam

\n
    \n
  • Type: string
  • \n
  • Default: \"$event\"
  • \n
\n

The first argument that must be passed to the wrapper method.

\n

targets

\n
    \n
  • Type: Target[]
  • \n
  • Default: [{ events: ['click'], elements: ['ion-button', 'button'] }, { events: ['submit'] }]
  • \n
\n

Each target specifies which events and elements require the wrapper. elements is optional; when omitted, the rule applies to any element for those events.

\n

allowEventMethods

\n
    \n
  • Type: string[]
  • \n
  • Default: [\"stopPropagation\", \"preventDefault\"]
  • \n
\n

Event methods that are allowed without the wrapper. For example, (click)=\"$event.stopPropagation()\" is valid.

\n

Examples

\n

Incorrect

\n
<ion-button (click)=\"vm.save()\">Save</ion-button>\n
<form (submit)=\"vm.save()\"></form>\n
<ion-button (click)=\"vm.disableHandler(vm.save())\">missing $event</ion-button>\n

Correct

\n
<ion-button (click)=\"vm.disableHandler($event, vm.save())\">Save</ion-button>\n
<form (submit)=\"vm.disableHandler($event, vm.save())\">\n  <ion-button type=\"submit\">Save</ion-button>\n</form>\n
<ion-button (click)=\"$event.stopPropagation()\"></ion-button>\n

Custom configuration

\n
<ion-input (ionComplete)=\"vm.disableHandler($event, vm.join())\"></ion-input>\n
{\n  \"rules\": {\n    \"@rdlabo/rules/prefer-disable-handler\": [\n      \"error\",\n      {\n        \"targets\": [{ \"events\": [\"ionComplete\"], \"elements\": [\"ion-input\"] }]\n      }\n    ]\n  }\n}\n

When to enable

\n

Enable this rule in Ionic/Angular projects where user actions trigger async operations such as API calls, navigation, or modal presentation. It pairs with @rdlabo/rules/prefer-modal-launcher and @rdlabo/rules/deny-element to keep overlay logic centralized.

\n

See also

\n\n

Implementation

\n\n", "headings": [ { "id": "rule-details", @@ -532,15 +822,55 @@ export const PROJECT = { "level": 2 }, { - "id": "defaults", - "text": "Defaults", + "id": "method", + "text": "method", + "level": 3 + }, + { + "id": "eventparam", + "text": "eventParam", + "level": 3 + }, + { + "id": "targets", + "text": "targets", + "level": 3 + }, + { + "id": "alloweventmethods", + "text": "allowEventMethods", "level": 3 }, { "id": "examples", "text": "Examples", + "level": 2 + }, + { + "id": "incorrect", + "text": "Incorrect", + "level": 3 + }, + { + "id": "correct", + "text": "Correct", + "level": 3 + }, + { + "id": "custom-configuration", + "text": "Custom configuration", "level": 3 }, + { + "id": "when-to-enable", + "text": "When to enable", + "level": 2 + }, + { + "id": "see-also", + "text": "See also", + "level": 2 + }, { "id": "implementation", "text": "Implementation", @@ -551,6 +881,55 @@ export const PROJECT = { "scrollMap": [], "editUrl": "https://github.com/rdlabo-dev/eslint-plugin-rules/edit/main/docs/rules/prefer-disable-handler.md" }, + { + "title": "prefer-ionic-standalone", + "navTitle": "prefer-ionic-standalone", + "slug": "rules/prefer-ionic-standalone", + "file": "rules/prefer-ionic-standalone.md", + "section": "Rules", + "path": "/projects/eslint-plugin-rules/docs/rules/prefer-ionic-standalone", + "html": "
\n

Prefer the Ionic 9 standalone API and disallow IonicModule and obsolete or NgModule-based entry points.

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
  • ✒️ The --fix option on the command line can automatically fix some of the problems reported by this rule.
  • \n
\n
\n

Ionic 9 exports standalone Angular components from @ionic/angular. This rule keeps applications on that API surface by rejecting the obsolete @ionic/angular/standalone entry point, the NgModule-based @ionic/angular/lazy entry point, and IonicModule itself.

\n

Rule Details

\n

The rule checks imports, named re-exports, export-all declarations, and IonicModule access through a namespace import. Namespace access is resolved by scope, so a shadowed local variable with the same name is not reported.

\n

Examples

\n

Incorrect

\n
import { IonButton } from '@ionic/angular/standalone';\nimport { IonInput } from '@ionic/angular/lazy';\nimport { IonicModule } from '@ionic/angular';\n

Correct

\n
import { IonButton, IonInput, ModalController, provideIonicAngular } from '@ionic/angular';\n

Named imports and named re-exports from /standalone and /lazy are auto-fixed to @ionic/angular, preserving the original quote style. Side-effect imports, namespace imports, and export * declarations are reported without a fix because changing their entry point may change runtime behavior. IonicModule is also reported without a fix because replacing IonicModule.forRoot() and NgModule metadata requires application-level changes.

\n

Options

\n

This rule has no options. Configure its severity as warn or error in ESLint configuration.

\n

When to enable

\n

Enable this rule in Ionic 9 Angular applications after adopting standalone bootstrap. NgModule applications should complete the standalone migration before enabling it because @ionic/angular/lazy and IonicModule are always rejected.

\n

Implementation

\n\n", + "headings": [ + { + "id": "rule-details", + "text": "Rule Details", + "level": 2 + }, + { + "id": "examples", + "text": "Examples", + "level": 2 + }, + { + "id": "incorrect", + "text": "Incorrect", + "level": 3 + }, + { + "id": "correct", + "text": "Correct", + "level": 3 + }, + { + "id": "options", + "text": "Options", + "level": 2 + }, + { + "id": "when-to-enable", + "text": "When to enable", + "level": 2 + }, + { + "id": "implementation", + "text": "Implementation", + "level": 2 + } + ], + "codes": [], + "scrollMap": [], + "editUrl": "https://github.com/rdlabo-dev/eslint-plugin-rules/edit/main/docs/rules/prefer-ionic-standalone.md" + }, { "title": "prefer-modal-launcher", "navTitle": "prefer-modal-launcher", @@ -558,7 +937,7 @@ export const PROJECT = { "file": "rules/prefer-modal-launcher.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/prefer-modal-launcher", - "html": "
\n

Require presentModal calls to live inside a launch* launcher function.

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
\n
\n

Call sites must use await launchXxxPage(helper, props) instead of inlining helper.presentModal(...).

\n

Each modal page should export:

\n
    \n
  • XxxProps / XxxDismiss types
  • \n
  • launchXxxPage(helper, props) that wraps presentModal
  • \n
\n

Pair with @rdlabo/rules/deny-overlay-create and @rdlabo/rules/deny-element.

\n

Rule Details

\n

❌ Incorrect: inline presentModal at the call site

\n
export class ExamplePage {\n  readonly helper = inject(HelperService);\n\n  async open() {\n    await this.helper.presentModal(OtherPage, {}); // error\n  }\n}\n

Also incorrect:

\n
export async function openModal(overlay: Helper) {\n  await overlay.presentModal(ExamplePage, {}); // error — name does not match /^launch/\n}\n\nconst show = () => overlay.presentModal(ExamplePage, {}); // error\n

✅ Correct: presentModal only inside a launcher

\n
export interface OtherProps {\n  id: number;\n}\nexport type OtherDismiss = { saved: boolean } | undefined;\n\nexport const launchOtherPage = (helper: HelperService, props: OtherProps): Promise<OtherDismiss> => {\n  return helper.presentModal(OtherPage, props, { watchKeyboard: false });\n};\n\nexport class ExamplePage {\n  readonly helper = inject(HelperService);\n\n  async open() {\n    const data = await launchOtherPage(this.helper, { id: 1 });\n    if (data?.saved) {\n      // ...\n    }\n  }\n}\n

Nested calls inside a launcher are fine:

\n
export const launchExamplePage = (overlay: Helper, props: Props) => {\n  const run = () => overlay.presentModal(ExamplePage, props);\n  return run();\n};\n

Options

\n
{\n  // Method names treated as overlay presenters.\n  // default: ['presentModal']\n  presentMethodNames?: string[];\n\n  // RegExp source for allowed enclosing function / method names.\n  // default: '^launch'\n  launcherNamePattern?: string;\n}\n
'@rdlabo/rules/prefer-modal-launcher': [\n  'error',\n  {\n    presentMethodNames: ['presentModal'],\n    launcherNamePattern: '^launch',\n  },\n],\n

If your project uses open* launchers:

\n
{\n  launcherNamePattern: '^(launch|open)';\n}\n

Implementation

\n\n", + "html": "
\n

Require presentModal calls to live inside a launch* launcher function.

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
\n
\n

Modals and sheets should be presented through a dedicated launcher function exported from the target page. This keeps call sites decoupled from modal construction details and makes the modal API consistent across the application. This rule ensures that presentModal (or other configured present methods) are only called inside functions whose name matches a launcher pattern.

\n

Rule Details

\n

The rule checks CallExpression nodes for calls such as presentModal, helper.presentModal(...), or overlay.presentSheet(...). If the call is not inside a launcher function, it is reported.

\n

A launcher function is one whose name matches the configured regular expression (default ^launch). The rule looks at:

\n
    \n
  • function launchXxx(...)
  • \n
  • const launchXxx = (...)
  • \n
  • class Foo { launchXxx = (...) }
  • \n
  • class Foo { launchXxx() {} }
    \nNested functions are also considered; for example, a run arrow inside launchExamplePage is allowed.
  • \n
\n

Options

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/prefer-modal-launcher\": [\n      \"error\",\n      {\n        \"presentMethodNames\": [\"presentModal\"],\n        \"launcherNamePattern\": \"^launch\"\n      }\n    ]\n  }\n}\n

presentMethodNames

\n
    \n
  • Type: string[]
  • \n
  • Default: [\"presentModal\"]
  • \n
\n

The present method names to restrict.

\n

launcherNamePattern

\n
    \n
  • Type: string
  • \n
  • Default: \"^launch\"
  • \n
\n

A regular expression string. Present method calls must be inside a function whose name matches this pattern.

\n

Examples

\n

Incorrect

\n
export class ExamplePage {\n  readonly helper = inject(HelperService);\n\n  async open() {\n    await this.helper.presentModal(OtherPage, {}); // not in a launcher\n  }\n}\n
export class ExamplePage {\n  readonly launchOtherPage = this.helper.presentModal(OtherPage, {}); // not a function\n}\n
export async function openModal(overlay: Helper) {\n  await overlay.presentModal(ExamplePage, {}); // name does not match ^launch\n}\n

Correct

\n
export const launchExamplePage = (overlay: Helper, props: Props) => {\n  return overlay.presentModal(ExamplePage, props);\n};\n
export function launchExamplePage(overlay: Helper, props: Props) {\n  return overlay.presentModal(ExamplePage, props);\n}\n
export const launchExamplePage = (overlay: Helper, props: Props) => {\n  const run = () => overlay.presentModal(ExamplePage, props);\n  return run();\n};\n

Custom configuration

\n
export const openSheet = (overlay: Helper) => {\n  return overlay.presentSheet(SheetPage, {});\n};\n
{\n  \"rules\": {\n    \"@rdlabo/rules/prefer-modal-launcher\": [\n      \"error\",\n      {\n        \"presentMethodNames\": [\"presentSheet\"],\n        \"launcherNamePattern\": \"^(launch|open)\"\n      }\n    ]\n  }\n}\n

When to enable

\n

Enable this rule in Ionic/Angular projects that use a launcher pattern for modals, sheets, and other overlays. It pairs with @rdlabo/rules/deny-element and @rdlabo/rules/prefer-disable-handler.

\n

See also

\n\n

Implementation

\n\n", "headings": [ { "id": "rule-details", @@ -570,6 +949,46 @@ export const PROJECT = { "text": "Options", "level": 2 }, + { + "id": "presentmethodnames", + "text": "presentMethodNames", + "level": 3 + }, + { + "id": "launchernamepattern", + "text": "launcherNamePattern", + "level": 3 + }, + { + "id": "examples", + "text": "Examples", + "level": 2 + }, + { + "id": "incorrect", + "text": "Incorrect", + "level": 3 + }, + { + "id": "correct", + "text": "Correct", + "level": 3 + }, + { + "id": "custom-configuration", + "text": "Custom configuration", + "level": 3 + }, + { + "id": "when-to-enable", + "text": "When to enable", + "level": 2 + }, + { + "id": "see-also", + "text": "See also", + "level": 2 + }, { "id": "implementation", "text": "Implementation", @@ -580,6 +999,60 @@ export const PROJECT = { "scrollMap": [], "editUrl": "https://github.com/rdlabo-dev/eslint-plugin-rules/edit/main/docs/rules/prefer-modal-launcher.md" }, + { + "title": "require-ion-item-group", + "navTitle": "require-ion-item-group", + "slug": "rules/require-ion-item-group", + "file": "rules/require-ion-item-group.md", + "section": "Rules", + "path": "/projects/eslint-plugin-rules/docs/rules/require-ion-item-group", + "html": "
\n

Require ion-item elements in ion-list to be wrapped by a supported Ionic item group.

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
  • ✒️ The --fix option on the command line can automatically fix some of the problems reported by this rule.
  • \n
\n
\n

Ionic's iOS 26 and Material Design 3 list styling expects list items to be organized through the group component that matches their behavior. This rule prevents a bare ion-item from being rendered directly under ion-list.

\n

Rule Details

\n

An ion-item within ion-list must use exactly one of these structures:

\n
    \n
  • ion-list > ion-item-group > ion-item
  • \n
  • ion-list > ion-reorder-group > ion-item
  • \n
  • ion-list > ion-accordion-group > ion-accordion > ion-item
  • \n
  • ion-list > ion-radio-group > ion-item
  • \n
\n

Angular control-flow blocks such as @if, @for, @empty, @switch, and @defer are transparent for this structural check because they do not render an element. ng-container and ng-template are also transparent. Rendered HTML or Angular elements are not transparent: inserting a div between the list, group, or item is reported.

\n

The rule only checks ion-item elements contained by ion-list. An ion-item outside a list is not reported, and .spec.html files are ignored.

\n

Examples

\n

Incorrect

\n
<ion-list>\n  <ion-item>Direct item</ion-item>\n</ion-list>\n

\n
<ion-list>\n  @for (item of items; track item.id) {\n    <ion-item>{{ item.name }}</ion-item>\n  }\n</ion-list>\n

Correct

\n

\n
<ion-list>\n  <ion-item-group>\n    @for (item of items; track item.id) {\n      <ion-item>{{ item.name }}</ion-item>\n    }\n  </ion-item-group>\n</ion-list>\n
<ion-list>\n  <ion-radio-group>\n    <ion-item>First choice</ion-item>\n    <ion-item>Second choice</ion-item>\n  </ion-radio-group>\n</ion-list>\n

Options

\n

This rule has no options.

\n

Automatic fixes

\n

When a list contains only ungrouped ion-item elements, including through transparent Angular control-flow blocks or ng-container, the rule can wrap the entire list contents in one ion-item-group.

\n

The automatic fix is available when the same template already uses ion-item-group, which indicates that the standalone IonItemGroup component is available to the template. Otherwise, the rule offers an editor suggestion that also reminds you to add IonItemGroup to the component imports if needed.

\n

No fix or suggestion is offered when the list mixes grouped and ungrouped content, contains other rendered content, contains a reusable ng-template definition, contains a nested list, has an intervening rendered element, or uses an invalid accordion structure. In these cases, the intended group boundary cannot be determined safely.

\n

When to enable

\n

Enable this rule in Ionic Angular applications that target the iOS 26 and Material Design 3 list designs. It is included in the recommended preset and has no effect when a template does not contain an ion-item within ion-list.

\n

Implementation

\n\n", + "headings": [ + { + "id": "rule-details", + "text": "Rule Details", + "level": 2 + }, + { + "id": "examples", + "text": "Examples", + "level": 2 + }, + { + "id": "incorrect", + "text": "Incorrect", + "level": 3 + }, + { + "id": "correct", + "text": "Correct", + "level": 3 + }, + { + "id": "options", + "text": "Options", + "level": 2 + }, + { + "id": "automatic-fixes", + "text": "Automatic fixes", + "level": 2 + }, + { + "id": "when-to-enable", + "text": "When to enable", + "level": 2 + }, + { + "id": "implementation", + "text": "Implementation", + "level": 2 + } + ], + "codes": [], + "scrollMap": [], + "editUrl": "https://github.com/rdlabo-dev/eslint-plugin-rules/edit/main/docs/rules/require-ion-item-group.md" + }, { "title": "require-viewmodel", "navTitle": "require-viewmodel", @@ -587,18 +1060,73 @@ export const PROJECT = { "file": "rules/require-viewmodel.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/require-viewmodel", - "html": "
\n

Enforce Component new ViewModel(this), ViewModelStore<ComponentType, Keys> inheritance, and keep View APIs off ViewModel.

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
\n
\n

Enforces the Ionic Angular Component / ViewModel split in one rule. The co-located class name is ViewModel by default.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
CheckRequirement
Component ownershipEvery @Component has a field initialized with new ViewModel(this)
ConstructionFirst argument must be this
Host boundaryViewModel extends ViewModelStore<ComponentType, Keys>; Keys optionally exposes explicit non-Signal Component properties
Constructor / hostViewModel inherits both by default; an optional constructor must forward host with super(host)
View APIsviewChild / viewChildren / contentChild / contentChildren / effect / computed / render lifecycle APIs must not appear inside ViewModel
\n

Non-@Component classes are ignored for ownership. Pair with @rdlabo/rules/no-component-method-except-lifecycle.

\n

Rule Details

\n

✅ Signal and output host only

\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage {\n  readonly vm = new ViewModel(this);\n}\n\nclass ViewModel extends ViewModelStore<ExamplePage> {\n  save(): void {\n    this.host.saved.emit();\n  }\n}\n

ViewModelStore owns the constructor and retains the Component object. Its public host uses the ViewModelHost<T, K> type from @rdlabo/ionic-angular-kit, so values are read when ViewModel methods run instead of being copied during class-field initialization. The base constructor delegates host narrowing and the one-time render hook to the kit's mountViewModel() helper.

\n

✅ Explicit non-Signal dependencies

\n
class ViewModel extends ViewModelStore<EntryPage, 'entryForm' | 'inventoryModel'> {\n  save(): void {\n    this.host.entryForm.save();\n  }\n}\n

The second type argument is optional. TypeScript checks that its keys belong to the Component. Hard-private Component fields cannot be exposed with Pick; use a public readonly boundary property when the ViewModel needs one.

\n

✅ Shared ViewModel bases

\n
class ViewModel extends MainViewModel<FoodsPage> {}\nclass ViewModel extends ListViewModel<WineListPage> {}\nclass ViewModel extends ModelSearch<SearchPage, SearchCondition> {}\n

An intermediate base whose name ends in ViewModel, plus the established ModelSearch base, is accepted when its first type argument matches the owning Component. The intermediate base itself must inherit from ViewModelStore.

\n

A generic ViewModel may use a default Component host. The default is used for the ownership check:

\n
class ViewModel<THost = MainPage> extends ViewModelStore<THost> {}\n

Hard-private ViewModel ownership is also fine:

\n
readonly #vm = new ViewModel(this);\n

❌ Component without ViewModel or without this

\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage {\n  readonly title = 'x'; // error: missingViewModel\n}\n\nreadonly vm = new ViewModel();\nreadonly vm = new ViewModel(other);\n

❌ Old per-ViewModel host pattern

\n
class ViewModel extends StoreModel {\n  readonly host: ReactiveHost<ExamplePage>; // error\n\n  constructor(host: ExamplePage) {\n    super();\n    this.host = host;\n  }\n}\n

Use extends ViewModelStore<ExamplePage> and remove the host member. Do not cache host.someProperty in a ViewModel constructor: Component class fields declared after vm have not initialized yet.

\n

A constructor is usually unnecessary. Prefer onMount() for setup that must wait until the Component has initialized. If an existing immediate constructor side effect must be preserved, its constructor must forward the same typed host:

\n
class ViewModel extends ViewModelStore<ExamplePage, 'inventoryModel'> {\n  protected override onMount(): void {\n    this.host.inventoryModel.initialize();\n  }\n}\n

An immediate constructor remains valid for compatibility:

\n
class ViewModel extends ViewModelStore<ExamplePage> {\n  constructor(host: ExamplePage) {\n    super(host);\n    registerCleanup();\n  }\n}\n

❌ View APIs on ViewModel

\n
class ViewModel extends ViewModelStore<ExamplePage> {\n  readonly el = viewChild('host'); // error\n  readonly label = computed(() => 'x'); // error\n}\n

viewChild.required(...) is also denied. afterNextRender / afterEveryRender / afterRenderEffect belong to the kit mountViewModel() helper or the Component, not an individual ViewModel.

\n

Options

\n
{\n  // Class name treated as the ViewModel. default: 'ViewModel'\n  viewModelClassName?: string;\n\n  // Required base-class name. default: 'ViewModelStore'\n  viewModelStoreClassName?: string;\n\n  // Call expressions banned inside ViewModel.\n  // default also denies afterNextRender / afterEveryRender / afterRenderEffect\n  bannedApis?: string[];\n}\n
'@rdlabo/rules/require-viewmodel': 'error';\n

Custom names:

\n
[\n  'error',\n  {\n    viewModelClassName: 'PageState',\n    viewModelStoreClassName: 'HostedStore',\n  },\n];\n

Implementation

\n\n", + "html": "
\n

Enforce Component new ViewModel(this), ViewModelStore<ComponentType, Keys> inheritance, and keep View APIs off ViewModel.

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
\n
\n

This rule enforces the ViewModel architecture pattern. An Angular Component must own a ViewModel initialized with new ViewModel(this). The rule requires at least one matching property; it does not reject additional ViewModel instances. The ViewModel must extend ViewModelStore<ComponentType> and should not redeclare host or contain View-specific APIs such as viewChild, effect, computed, or afterNextRender.

\n

Rule Details

\n

The rule performs three checks:

\n

1. Component must own a ViewModel

\n

A @Component class must contain a property initialized with new ViewModel(this). The first argument of the constructor call must be this.

\n

2. ViewModel must extend ViewModelStore<ComponentType>

\n

The class named ViewModel (or the configured viewModelClassName) must extend ViewModelStore<...> or a base whose name ends with ViewModel or is ModelSearch. The first generic argument must be the host Component type. Intermediate generic defaults are resolved.

\n
    \n
  • If ViewModelStore<ExamplePage, 'model' | 'form'> is used, the second and later type arguments are allowed.
  • \n
  • More than two type arguments when extending ViewModelStore directly is reported.
  • \n
  • The host type must match the Component that owns the ViewModel.
  • \n
\n

3. ViewModel must not contain View APIs

\n

The ViewModel class must not call the following APIs:

\n

viewChild, viewChildren, contentChild, contentChildren, effect, computed, afterNextRender, afterEveryRender, afterRenderEffect.

\n

This list can be customized with the bannedApis option. The rule recognizes direct calls such as viewChild() and the .required() variant such as viewChild.required(). It does not resolve namespace-prefixed calls.

\n

Examples

\n

Incorrect

\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage {\n  readonly title = 'x'; // no ViewModel\n}\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage {\n  readonly vm = new ViewModel(); // missing `this`\n}\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage {\n  readonly vm = new ViewModel(this);\n}\n\nclass ViewModel extends StoreModel {} // wrong base class\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage {\n  readonly vm = new ViewModel(this);\n}\n\nclass ViewModel extends ViewModelStore<ExamplePage> {\n  readonly el = viewChild('host'); // View API in ViewModel\n}\n

Correct

\n
import { Component, computed, effect, viewChild } from '@angular/core';\n\n@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage {\n  readonly vm = new ViewModel(this);\n  readonly title = computed(() => this.vm.label());\n  readonly el = viewChild('host');\n\n  constructor() {\n    effect(() => this.vm.label());\n  }\n}\n\nclass ViewModel extends ViewModelStore<ExamplePage> {\n  readonly label = signal('hello');\n}\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage {\n  readonly vm = new ViewModel(this);\n}\n\nclass ViewModel extends ViewModelStore<ExamplePage, 'inventoryModel'> {\n  readonly inventoryModel = signal<Inventory | null>(null);\n}\n
@Component({ selector: 'app-example', template: '' })\nexport class FoodsPage {\n  readonly vm = new ViewModel(this);\n}\n\nclass ViewModel extends MainViewModel<FoodsPage> {}\n

Options

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/require-viewmodel\": [\n      \"error\",\n      {\n        \"viewModelClassName\": \"ViewModel\",\n        \"viewModelStoreClassName\": \"ViewModelStore\",\n        \"bannedApis\": [\n          \"viewChild\",\n          \"viewChildren\",\n          \"contentChild\",\n          \"contentChildren\",\n          \"effect\",\n          \"computed\",\n          \"afterNextRender\",\n          \"afterEveryRender\",\n          \"afterRenderEffect\"\n        ]\n      }\n    ]\n  }\n}\n

viewModelClassName

\n
    \n
  • Type: string
  • \n
  • Default: \"ViewModel\"
  • \n
\n

The class name the rule looks for in the Component. Use this when the project uses a different naming convention, such as PageState.

\n

viewModelStoreClassName

\n
    \n
  • Type: string
  • \n
  • Default: \"ViewModelStore\"
  • \n
\n

The base class name the ViewModel must extend, or an intermediate base whose name ends with ViewModel.

\n

bannedApis

\n
    \n
  • Type: string[]
  • \n
  • Default: the list above
  • \n
\n

APIs that are not allowed inside the ViewModel. The rule detects direct calls and .required(...) usage; namespace-prefixed calls are not resolved.

\n

When to enable

\n

Enable this rule when a project adopts the ViewModel pattern with @rdlabo/ionic-angular-kit or a similar architecture. It pairs with @rdlabo/rules/no-component-writable-signal to keep Component state read-only and ViewModel state writable.

\n

See also

\n\n

Implementation

\n\n", "headings": [ { "id": "rule-details", "text": "Rule Details", "level": 2 }, + { + "id": "1.-component-must-own-a-viewmodel", + "text": "1. Component must own a ViewModel", + "level": 3 + }, + { + "id": "2.-viewmodel-must-extend-viewmodelstore%3Ccomponenttype%3E", + "text": "2. ViewModel must extend ViewModelStore", + "level": 3 + }, + { + "id": "3.-viewmodel-must-not-contain-view-apis", + "text": "3. ViewModel must not contain View APIs", + "level": 3 + }, + { + "id": "examples", + "text": "Examples", + "level": 2 + }, + { + "id": "incorrect", + "text": "Incorrect", + "level": 3 + }, + { + "id": "correct", + "text": "Correct", + "level": 3 + }, { "id": "options", "text": "Options", "level": 2 }, + { + "id": "viewmodelclassname", + "text": "viewModelClassName", + "level": 3 + }, + { + "id": "viewmodelstoreclassname", + "text": "viewModelStoreClassName", + "level": 3 + }, + { + "id": "bannedapis", + "text": "bannedApis", + "level": 3 + }, + { + "id": "when-to-enable", + "text": "When to enable", + "level": 2 + }, + { + "id": "see-also", + "text": "See also", + "level": 2 + }, { "id": "implementation", "text": "Implementation", @@ -616,7 +1144,7 @@ export const PROJECT = { "file": "rules/restrict-try-block.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/restrict-try-block", - "html": "
\n

Restrict Promise, RxJS, Angular Signal contexts, Promise.resolve() escape hatches, and physical code lines inside try blocks.

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
\n
\n

Restricts asynchronous/reactive processing and physical code lines inside try blocks.

\n

Rule Details

\n

This rule keeps try as a small boundary for synchronous exceptions. By default it reports:

\n
    \n
  • await and expressions whose TypeScript type is Promise-like
  • \n
  • Promise.resolve() calls anywhere, including chains used to convert synchronous exceptions into Promise rejections
  • \n
  • expressions whose type or base type is declared by the rxjs package, including Observable and Subject variants
  • \n
  • try statements inside Angular computed() and effect() callbacks
  • \n
  • try bodies containing more than three physical code lines
  • \n
\n

For the try-specific checks, only the try body is inspected. Its catch and finally clauses are not. Nested functions, classes, and nested try statements are separate execution boundaries and are not attributed to the outer try. The Promise.resolve() check applies throughout the file.

\n

Promise rejections should normally be handled by a Promise error boundary such as .catch(). Do not manufacture that boundary with Promise.resolve() to move synchronous failures into the rejection channel:

\n
// incorrect\nPromise.resolve()\n  .then(() => fallibleSynchronousWork())\n  .catch(handleError);\n

Keep a synchronous try boundary small and place it in the layer responsible for handling that failure. Return a value or an existing Promise directly instead of normalizing it with Promise.resolve(value).

\n

RxJS errors should be handled through the Observable error channel, such as catchError() or an explicit subscriber error handler.

\n

Promise-like and RxJS type detection uses TypeScript type information when available. Without typed linting, those type-dependent checks are skipped instead of stopping ESLint; syntax-based await, Promise.resolve(), Angular Signal context, and maxLines checks still run. Configure typed linting for full enforcement, for example:

\n
languageOptions: {\n  parserOptions: {\n    projectService: true,\n    tsconfigRootDir: __dirname,\n  },\n},\n

Options

\n
{\n  allowPromise: false,\n  allowPromiseResolve: false,\n  allowRxjs: false,\n  allowInSignal: false,\n  maxLines: 3,\n}\n
    \n
  • allowPromise: Allow Promise-like processing and await inside try.
  • \n
  • allowPromiseResolve: Disable the dedicated file-wide Promise.resolve() check. Inside a try body, allowPromise: true is also required because the call is independently Promise-like processing.
  • \n
  • allowRxjs: Allow values and operations backed by types declared by rxjs. This includes Observable, Subject, and their subclasses.
  • \n
  • allowInSignal: Allow try inside inline Angular computed() and effect() callbacks. Aliased and namespace imports from @angular/core are recognized. Nested function and class bodies are separate execution boundaries.
  • \n
  • maxLines: Maximum physical code lines in the try body, or false to disable the size check.
  • \n
\n

allowPromise: false and allowRxjs: false are fully enforced when typed linting is configured. Without type information, only syntax-based checks such as await remain available for those categories.

\n

The Promise.resolve() check recognizes the unshadowed global Promise and explicit globalThis.Promise, including static bracket notation. It intentionally does not follow aliases. A locally declared or imported value named Promise, or a locally shadowed globalThis, is not treated as the built-in API.

\n

For maxLines, the outer braces, comments, and blank lines are excluded. A unique physical line containing any other token counts once. Internal braces and multiline tokens count, so formatting can affect the result intentionally: the rule keeps the boundary visually small as well as logically narrow.

\n

Implementation

\n\n", + "html": "
\n

Restrict Promise, RxJS, Angular Signal contexts, Promise.resolve() escape hatches, and physical code lines inside try blocks.

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
\n
\n

try/catch should protect a small, synchronous operation that can actually throw. Putting async work, long blocks, or reactive callbacks inside try obscures error boundaries and can swallow or misroute errors. This rule enforces those constraints.

\n

Rule Details

\n

The rule checks every try block and reports the following by default:

\n
    \n
  • await or other Promise/thenable usage inside try
  • \n
  • Promise.resolve() anywhere (even outside a try) as an escape hatch
  • \n
  • RxJS types or operations inside try
  • \n
  • A try block inside a computed() or effect() callback
  • \n
  • A try block longer than 3 physical code lines
  • \n
\n

For checks scoped to a try, only the try body is inspected. catch and finally clauses are excluded. Nested functions, classes, and nested try statements are separate execution boundaries and are not attributed to the outer block. The Promise.resolve() check applies throughout the file.

\n

Promise-like and RxJS detection uses TypeScript type information when available. Without typed linting, those checks are skipped instead of stopping ESLint; syntax-based await, Promise.resolve(), Angular Signal context, and line-count checks still run. Configure parserOptions.projectService for full enforcement.

\n

Options

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/restrict-try-block\": [\n      \"error\",\n      {\n        \"allowPromise\": false,\n        \"allowPromiseResolve\": false,\n        \"allowRxjs\": false,\n        \"allowInSignal\": false,\n        \"maxLines\": 3\n      }\n    ]\n  }\n}\n

allowPromise

\n
    \n
  • Type: boolean
  • \n
  • Default: false
  • \n
\n

Allow Promise/thenable usage inside try.

\n

allowPromiseResolve

\n
    \n
  • Type: boolean
  • \n
  • Default: false
  • \n
\n

Disable the file-wide Promise.resolve() check. Inside a try body, allowPromise: true is also required because the call is independently Promise-like processing.

\n

allowRxjs

\n
    \n
  • Type: boolean
  • \n
  • Default: false
  • \n
\n

Allow RxJS usage inside try.

\n

allowInSignal

\n
    \n
  • Type: boolean
  • \n
  • Default: false
  • \n
\n

Allow try blocks inside computed() or effect() callbacks.

\n

maxLines

\n
    \n
  • Type: number | false
  • \n
  • Default: 3
  • \n
\n

Maximum physical code lines inside a try block. Set to false to disable the size check. Outer braces, comments, and blank lines are excluded; a unique line containing any other token counts once.

\n

Examples

\n

Incorrect

\n
async function run() {\n  try {\n    await work();\n  } catch {}\n}\n
try {\n  Promise.resolve(1).catch(() => 0);\n} catch {}\n
import { of } from 'rxjs';\n\ntry {\n  of(1).pipe().subscribe();\n} catch {}\n
import { computed } from '@angular/core';\n\nconst value = computed(() => {\n  try {\n    return JSON.parse('1');\n  } catch {\n    return 0;\n  }\n});\n
try {\n  first();\n  second();\n  third();\n  fourth();\n} catch {}\n

Correct

\n
function parse(source: string) {\n  try {\n    return JSON.parse(source);\n  } catch {\n    return null;\n  }\n}\n
async function run() {\n  try {\n    doWork();\n  } catch {\n    await recover();\n  } finally {\n    cleanup();\n  }\n}\n
import { of } from 'rxjs';\nimport { catchError } from 'rxjs/operators';\n\nof(1)\n  .pipe(catchError(() => of(0)))\n  .subscribe();\n

Relaxing a check

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/restrict-try-block\": [\n      \"error\",\n      {\n        \"allowPromise\": true,\n        \"allowPromiseResolve\": true,\n        \"allowRxjs\": true,\n        \"allowInSignal\": true,\n        \"maxLines\": false\n      }\n    ]\n  }\n}\n

When to enable

\n

Enable this rule in any project where you want try/catch to be a small, explicit error boundary. It is especially useful in Angular Signal code and when migrating away from Promise/RxJS-heavy error handling.

\n

The Promise.resolve() check recognizes the unshadowed global Promise and explicit globalThis.Promise, including static bracket notation. It intentionally does not follow aliases. A locally declared or imported Promise, or a shadowed globalThis, is not treated as the built-in API.

\n

Implementation

\n\n", "headings": [ { "id": "rule-details", @@ -628,6 +1156,56 @@ export const PROJECT = { "text": "Options", "level": 2 }, + { + "id": "allowpromise", + "text": "allowPromise", + "level": 3 + }, + { + "id": "allowpromiseresolve", + "text": "allowPromiseResolve", + "level": 3 + }, + { + "id": "allowrxjs", + "text": "allowRxjs", + "level": 3 + }, + { + "id": "allowinsignal", + "text": "allowInSignal", + "level": 3 + }, + { + "id": "maxlines", + "text": "maxLines", + "level": 3 + }, + { + "id": "examples", + "text": "Examples", + "level": 2 + }, + { + "id": "incorrect", + "text": "Incorrect", + "level": 3 + }, + { + "id": "correct", + "text": "Correct", + "level": 3 + }, + { + "id": "relaxing-a-check", + "text": "Relaxing a check", + "level": 3 + }, + { + "id": "when-to-enable", + "text": "When to enable", + "level": 2 + }, { "id": "implementation", "text": "Implementation", @@ -645,21 +1223,46 @@ export const PROJECT = { "file": "rules/signal-use-as-signal-template.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/signal-use-as-signal-template", - "html": "
\n

Require () when accessing Angular Signals in templates

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
\n
\n

This rule ensures that Signals are properly accessed in templates by requiring the use of the function call syntax (). This is necessary because Signals in Angular are functions that need to be called to access their current value.

\n

Rule Details

\n

❌ Incorrect: Using Signals without function call syntax

\n
@Component({\n  template: `\n    <div>{{ count }}</div>\n    <div>{{ count.signal }}</div>\n    <div>{{ count + 1 }}</div>\n    @if (count) {\n      <div>{{ count }}</div>\n    }\n    @switch (count) {\n      @case (0) {\n        <div>Zero</div>\n      }\n    }\n    @defer (on viewport) {\n      <div>{{ count }}</div>\n    }\n  `,\n})\nexport class TestComponent {\n  count = signal(0);\n}\n

✅ Correct: Using Signals with function call syntax

\n
@Component({\n  template: `\n    <div>{{ count() }}</div>\n    <div>{{ count() + 1 }}</div>\n    <div>{{ count() > 0 ? 'Positive' : 'Zero' }}</div>\n    @if (count()) {\n      <div>{{ count() | async }}</div>\n    }\n    @switch (count()) {\n      @case (0) {\n        <div>Zero</div>\n      }\n      @case (1) {\n        <div>One</div>\n      }\n      @default {\n        <div>Other</div>\n      }\n    }\n    @defer (on viewport) {\n      <div>{{ count() }}</div>\n    }\n  `,\n})\nexport class TestComponent {\n  count = signal(0);\n}\n

✅ Correct: Passing a Signal reference as an input binding

\n

BoundAttribute で Signal 名だけを渡す場合は、Signal 参照の props 渡しとして許可されます。

\n
@Component({\n  template: `<child [inventorySignal]=\"inventorySignal\"></child>`,\n})\nexport class TestComponent {\n  inventorySignal = signal(0);\n}\n

値として演算する場合は () が必要です。

\n
// ❌\n<child [disabled]=\"count > 0\"></child>\n// ✅\n<child [disabled]=\"count() > 0\"></child>\n

Options

\n

No Options.

\n

Unsupport Pattern

\n

This rule does not support nested Signals patterns. For example:

\n
@Component({\n  template: `\n    <div>{{ nestedSignal().child() }}</div>\n    // Correct usage\n    <div>{{ nestedSignal().child }}</div>\n    // Incorrect: missing function call\n  `,\n})\nexport class TestComponent {\n  nestedSignal = signal({\n    child: signal<number>(0),\n  });\n}\n

The rule cannot detect when nested signals are not properly accessed with function calls.

\n

Implementation

\n\n", + "html": "
\n

Require () when accessing Angular Signals in templates

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
\n
\n

Angular Signals are functions. In a template, a Signal must be called with () to read its current value. Forgetting the parentheses is a common mistake when migrating from RxJS BehaviorSubject or from model() inputs. This rule detects Signal identifiers in Angular templates and reports bare reads such as {{ count }} or [hidden]=\"count\".

\n

Rule Details

\n

The rule parses the Angular template of each @Component. It collects Signal identifiers from:

\n
    \n
  • Class properties initialized by a call whose callee name is signal, model, computed, linkedSignal, input, or toSignal.
  • \n
  • Nested Signal properties inside object literals (for example count = { first: signal(0) }).
  • \n
\n

Detection is name-based and does not resolve import provenance. Aliased factory imports are not recognized, while an unrelated local function with one of these names may be treated as a Signal factory. toSignal is commonly imported from @angular/core/rxjs-interop; the rule recognizes it by name rather than module.

\n

It then reports any place in the template where the Signal is read without (). This includes:

\n
    \n
  • Interpolation {{ count }}
  • \n
  • Property bindings [hidden]=\"count\"
  • \n
  • Event bindings `(click)=\"count > 0 ? ...\"
  • \n
  • Control flow expressions @if (count), @switch (count), @for (...; track count)
  • \n
  • Optional chaining count?.signal
  • \n
  • Pipe usage count | async
  • \n
\n

The rule supports both template and templateUrl components.

\n

Examples

\n

Incorrect

\n
<div>{{ count }}</div>\n
<child [hidden]=\"count > 0\"></child>\n
@if (count) {\n<div>Positive</div>\n}\n
<ion-input [formField]=\"count.first\"></ion-input>\n

Correct

\n
<div>{{ count() }}</div>\n
<child [hidden]=\"count() > 0\"></child>\n
@if (count()) {\n<div>Positive</div>\n}\n
<ion-input [formField]=\"count.first()\"></ion-input>\n

Passing a Signal reference to a child

\n

If a child component expects a Signal object (not its value), you can pass the reference without ():

\n
<child [inventorySignal]=\"inventorySignal\"></child>\n

The rule recognizes this case and does not report a bare Signal passed as a bound attribute.

\n

Options

\n

This rule has no options.

\n

When to enable

\n

Enable this rule in any Angular project that uses Signals. It is especially useful during migration from Observable-based code or when model() and input() are introduced, because those APIs return Signal-like objects that must be called in the template.

\n

See also

\n\n

Implementation

\n\n", "headings": [ { "id": "rule-details", "text": "Rule Details", "level": 2 }, + { + "id": "examples", + "text": "Examples", + "level": 2 + }, + { + "id": "incorrect", + "text": "Incorrect", + "level": 3 + }, + { + "id": "correct", + "text": "Correct", + "level": 3 + }, + { + "id": "passing-a-signal-reference-to-a-child", + "text": "Passing a Signal reference to a child", + "level": 3 + }, { "id": "options", "text": "Options", "level": 2 }, { - "id": "unsupport-pattern", - "text": "Unsupport Pattern", + "id": "when-to-enable", + "text": "When to enable", + "level": 2 + }, + { + "id": "see-also", + "text": "See also", "level": 2 }, { @@ -679,21 +1282,46 @@ export const PROJECT = { "file": "rules/signal-use-as-signal.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/signal-use-as-signal", - "html": "
\n

This plugin check to valid signal use as signal.

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
  • ✒️ The --fix option on the command line can automatically fix some of the problems reported by this rule.
  • \n
\n
\n

This rule prevents Signals from being used incorrectly as regular properties.

\n

Rule Details

\n

❌ Incorrect: Using a Signal as a regular property

\n
@Component()\nexport class SigninPage {\n  readonly #id = signal<number>(undefined);\n\n  useMethod() {\n    if (this.#id) {\n      // error\n      this.#id() = 1; // error\n    }\n  }\n}\n

✅ Correct: Using a Signal properly

\n
@Component()\nexport class SigninPage {\n  readonly #id = signal<number>(undefined);\n\n  useMethod() {\n    if (this.#id()) {\n      this.#id.set(1); // error\n    }\n  }\n}\n

✅ Correct: Passing a Signal reference as props

\n

Signal を値として読むのではなく、Signal 自体を渡す場合は () は不要です。

\n
@Component()\nexport class SigninPage {\n  readonly food = signal<number>(0);\n\n  openPreview() {\n    // componentProps / modal launcher などへ参照渡し\n    launchModal({ food: this.food });\n    const food = this.food;\n    return this.food;\n  }\n}\n

Options

\n

No Options.

\n

Unsupport Pattern

\n

This rule does not support nested Signals patterns. For example:

\n
@Component({...})\nexport class TestComponent {\n  nestedSignal = signal({\n    child: signal<number>(0)\n  });\n\n  ngOnInit() {\n    if (this.nestedSignal().child) {  // Incorrect: missing function call\n      ...\n    }\n  }\n}\n

The rule cannot detect when nested signals are not properly accessed with function calls.

\n

Implementation

\n\n", + "html": "
\n

This plugin check to valid signal use as signal.

\n
    \n
  • ⭐️ This rule is included in plugin:@rdlabo/rules/recommended preset.
  • \n
  • ✒️ The --fix option on the command line can automatically fix some of the problems reported by this rule.
  • \n
\n
\n

Angular Signals are getter functions. Reading them requires (), and writing them must go through .set() or .update(). This rule catches code that uses a Signal variable as if it were a plain value, and it can auto-fix many common mistakes.

\n

Rule Details

\n

The rule tracks class properties initialized with Signal factories (signal, model, input, linkedSignal, toSignal, asReadonly) and reports misuse such as:

\n
    \n
  • this.count instead of this.count() in an expression context
  • \n
  • this.count() = value instead of this.count.set(value)
  • \n
  • this.user().name = 'Jane' instead of this.user.update(user => ({ ...user, name: 'Jane' }))
  • \n
  • this.items().push(x) instead of this.items.update(items => { items.push(x); return items; })
  • \n
  • this.#user = value (direct assignment to a Signal property) instead of this.#user.set(value)
  • \n
\n

The rule distinguishes between contexts where a Signal reference is expected and contexts where its value is expected. For example, passing a Signal object as a prop is allowed:

\n
const props = { food: this.food };\nlaunchModal({ food: this.food });\n

Examples

\n

Incorrect

\n
export class SigninPage {\n  readonly #id = signal<number | undefined>(undefined);\n\n  constructor() {\n    this.#id = 1;\n  }\n\n  useMethod() {\n    if (this.#id) {\n      this.#id().hoge = 1;\n    }\n  }\n}\n
export class SigninPage {\n  readonly #user = signal<{ name: string }>({ name: 'John' });\n\n  updateUser() {\n    this.#user().name = 'Jane';\n  }\n}\n
export class SigninPage {\n  readonly #numbers = signal<number[]>([1, 2, 3]);\n\n  updateNumbers() {\n    this.#numbers().push(4);\n  }\n}\n
export class SigninPage {\n  readonly #value = signal<number>(0);\n\n  updateValue() {\n    this.#value() = 42;\n  }\n}\n

Correct

\n
export class SigninPage {\n  readonly #user = signal<{ name: string }>({ name: 'John' });\n\n  updateUser() {\n    this.#user.update((user) => ({ ...user, name: 'Jane' }));\n  }\n}\n
export class SigninPage {\n  readonly #numbers = signal<number[]>([1, 2, 3]);\n\n  updateNumbers() {\n    this.#numbers.update((numbers) => {\n      numbers.push(4);\n      return numbers;\n    });\n  }\n}\n
export class SigninPage {\n  readonly #value = signal<number>(0);\n\n  updateValue() {\n    this.#value.set(42);\n  }\n}\n
export class SigninPage {\n  readonly food = signal<number>(0);\n\n  openPreview() {\n    const props = { food: this.food };\n    launchModal({ food: this.food });\n  }\n}\n

Auto-fix

\n

The rule provides auto-fix for the patterns above:

\n
    \n
  • this.count = value -> this.count.set(value)
  • \n
  • this.count() = value -> this.count.set(value)
  • \n
  • this.count().x = value -> this.count.update(value => ({ ...value, x: value }))
  • \n
  • this.count().push(x) -> this.count.update(value => { value.push(x); return value; })
  • \n
\n

Options

\n

This rule has no options.

\n

When to enable

\n

Enable this rule in any Angular project that uses Signals. It is complementary to @rdlabo/rules/signal-use-as-signal-template, which checks Signal usage in templates.

\n

See also

\n\n

Implementation

\n\n", "headings": [ { "id": "rule-details", "text": "Rule Details", "level": 2 }, + { + "id": "examples", + "text": "Examples", + "level": 2 + }, + { + "id": "incorrect", + "text": "Incorrect", + "level": 3 + }, + { + "id": "correct", + "text": "Correct", + "level": 3 + }, + { + "id": "auto-fix", + "text": "Auto-fix", + "level": 2 + }, { "id": "options", "text": "Options", "level": 2 }, { - "id": "unsupport-pattern", - "text": "Unsupport Pattern", + "id": "when-to-enable", + "text": "When to enable", + "level": 2 + }, + { + "id": "see-also", + "text": "See also", "level": 2 }, { diff --git a/src/app/generated/projects/eslint-plugin-rules.ja.generated.ts b/src/app/generated/projects/eslint-plugin-rules.ja.generated.ts index 06b9032..9aa8a5e 100644 --- a/src/app/generated/projects/eslint-plugin-rules.ja.generated.ts +++ b/src/app/generated/projects/eslint-plugin-rules.ja.generated.ts @@ -8,7 +8,7 @@ export const PROJECT = { "repositoryUrl": "https://github.com/rdlabo-dev/eslint-plugin-rules", "category": "developer-tools", "icon": "lint", - "version": "21.3.0", + "version": "22.0.0", "description": "保守しやすいアプリケーションのためのAngular・Ionic・TypeScript向けESLintルール集。", "headline": "Angular・Ionicの設計規約を一貫させる", "overview": "Flat Configプリセットまたは個別ルールを使い、Signal、Component境界、Ionic Overlay、フォーム、非同期コードの規約を自動検査します。", @@ -40,7 +40,7 @@ export const PROJECT = { "file": "getting-started.md", "section": "ガイド", "path": "/projects/eslint-plugin-rules/docs/getting-started", - "html": "

開発依存関係としてプラグインをインストールします。

\n
npm install --save-dev @rdlabo/eslint-plugin-rules\n

パッケージルートはAngular・Ionic向けルールを公開します。これらを使う場合は @angular-eslint/template-parser@ionic/core もインストールしてください。Angular・Ionicに依存しないTypeScriptプロジェクトでは、両者を読み込まない /typescript エントリポイントを利用できます。

\n

要件

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
パッケージ対応バージョン
Node.js20以降
ESLint9以降
@typescript-eslint/utils8.33以上9未満
@angular-eslint/template-parserテンプレートルール利用時は21.x
@ionic/coreIonicルール利用時は8.x
\n

エントリポイントを選ぶ

\n
    \n
  • Angular・Ionicアプリでは @rdlabo/eslint-plugin-rules を使います。
  • \n
  • バックエンドなどの汎用TypeScriptでは @rdlabo/eslint-plugin-rules/typescript を使います。
  • \n
\n

推奨プリセットはESLint Flat Config向けです。TypeScriptとHTMLの対象指定を維持するため、設定のトップレベルへ追加します。

\n

次は設定で推奨プリセットまたは個別ルールを有効にします。

\n", + "html": "

開発依存関係としてプラグインをインストールします。

\n
npm install --save-dev @rdlabo/eslint-plugin-rules\n

パッケージルートはAngular・Ionic向けルールを公開します。これらを使う場合は @angular-eslint/template-parser@ionic/angular@ionic/core もインストールしてください。Angular・Ionicに依存しないTypeScriptプロジェクトでは、これらを読み込まない /typescript エントリポイントを利用できます。

\n

要件

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
パッケージ対応バージョン
Node.js20以降
ESLint9以降
@typescript-eslint/utils8.33以上9未満
@angular-eslint/template-parser21.xまたは22.x
@ionic/angularIonicルール利用時は9.x
@ionic/coreIonicルール利用時は9.x
\n

エントリポイントを選ぶ

\n
    \n
  • Angular・Ionicアプリでは @rdlabo/eslint-plugin-rules を使います。
  • \n
  • バックエンドなどの汎用TypeScriptでは @rdlabo/eslint-plugin-rules/typescript を使います。
  • \n
\n

推奨プリセットはESLint Flat Config向けです。TypeScriptとHTMLの対象指定を維持するため、設定のトップレベルへ追加します。

\n

Ionic templateでは、推奨プリセットが ion-list 内の ion-item に対し、ion-item-groupion-reorder-groupion-radio-group、または ion-accordion-group 内の ion-accordion を使うことも要求します。これはiOS 26とMaterial Design 3のlist構造に対応するためです。

\n

次は設定で推奨プリセットまたは個別ルールを有効にします。

\n", "headings": [ { "id": "%E8%A6%81%E4%BB%B6", @@ -64,7 +64,7 @@ export const PROJECT = { "file": "configuration.md", "section": "ガイド", "path": "/projects/eslint-plugin-rules/docs/configuration", - "html": "

Angular・Ionic

\n

プラグインを登録し、推奨設定をトップレベルで展開してから、プロジェクトで使うAngular・TypeScript設定を追加します。

\n
const eslint = require('@eslint/js');\nconst tseslint = require('typescript-eslint');\nconst angular = require('angular-eslint');\nconst rdlabo = require('@rdlabo/eslint-plugin-rules');\n\nmodule.exports = tseslint.config(\n  {\n    plugins: { '@rdlabo/rules': rdlabo },\n  },\n  ...rdlabo.configs.recommended,\n  {\n    files: ['**/*.ts'],\n    languageOptions: {\n      parserOptions: { projectService: true, tsconfigRootDir: __dirname },\n    },\n    extends: [\n      eslint.configs.recommended,\n      ...tseslint.configs.recommended,\n      ...tseslint.configs.stylistic,\n      ...angular.configs.tsRecommended,\n    ],\n    processor: angular.processInlineTemplates,\n  },\n  {\n    files: ['**/*.html'],\n    extends: [...angular.configs.templateRecommended, ...angular.configs.templateAccessibility],\n  },\n);\n

rdlabo.configs.recommended をスコープ付き extends の内側へ置かないでください。typescript-eslint のconfig helperがプリセット内部の files を置き換え、TypeScript専用ルールがテンプレートへ適用される可能性があります。

\n

汎用TypeScript

\n
import tseslint from 'typescript-eslint';\nimport rdlabo from '@rdlabo/eslint-plugin-rules/typescript';\n\nexport default tseslint.config({\n  files: ['**/*.ts'],\n  plugins: { '@rdlabo/rules': rdlabo },\n  rules: {\n    '@rdlabo/rules/deny-soft-private-modifier': 'error',\n    '@rdlabo/rules/restrict-try-block': [\n      'error',\n      {\n        allowPromise: false,\n        allowPromiseResolve: true,\n        allowRxjs: false,\n        allowInSignal: false,\n        maxLines: 3,\n      },\n    ],\n  },\n});\n

restrict-try-block のPromise・RxJS検査をすべて有効にするにはtyped lintingが必要です。

\n

推奨プリセット

\n

TypeScriptではSignal、Component境界、ライフサイクル、Overlay、readonly、tryブロックの共通ルールを有効にします。HTMLではIonic属性型、禁止Overlay要素、非同期操作の二重実行防止を有効にします。

\n

非推奨の deny-constructor-di はプリセットに含まれません。Angularの inject() migrationを利用してください。

\n", + "html": "

Angular・Ionic

\n

プラグインを登録し、推奨設定をトップレベルで展開してから、プロジェクトで使うAngular・TypeScript設定を追加します。

\n
const eslint = require('@eslint/js');\nconst tseslint = require('typescript-eslint');\nconst angular = require('angular-eslint');\nconst rdlabo = require('@rdlabo/eslint-plugin-rules');\n\nmodule.exports = tseslint.config(\n  {\n    plugins: { '@rdlabo/rules': rdlabo },\n  },\n  ...rdlabo.configs.recommended,\n  {\n    files: ['**/*.ts'],\n    languageOptions: {\n      parserOptions: { projectService: true, tsconfigRootDir: __dirname },\n    },\n    extends: [eslint.configs.recommended, ...tseslint.configs.recommended, ...tseslint.configs.stylistic, ...angular.configs.tsRecommended],\n    processor: angular.processInlineTemplates,\n  },\n  {\n    files: ['**/*.html'],\n    extends: [...angular.configs.templateRecommended, ...angular.configs.templateAccessibility],\n  },\n);\n

rdlabo.configs.recommended をスコープ付き extends の内側へ置かないでください。typescript-eslint のconfig helperがプリセット内部の files を置き換え、TypeScript専用ルールがテンプレートへ適用される可能性があります。

\n

汎用TypeScript

\n
import tseslint from 'typescript-eslint';\nimport rdlabo from '@rdlabo/eslint-plugin-rules/typescript';\n\nexport default tseslint.config({\n  files: ['**/*.ts'],\n  plugins: { '@rdlabo/rules': rdlabo },\n  rules: {\n    '@rdlabo/rules/deny-soft-private-modifier': 'error',\n    '@rdlabo/rules/restrict-try-block': [\n      'error',\n      {\n        allowPromise: false,\n        allowPromiseResolve: true,\n        allowRxjs: false,\n        allowInSignal: false,\n        maxLines: 3,\n      },\n    ],\n  },\n});\n

restrict-try-block のPromise・RxJS検査をすべて有効にするにはtyped lintingが必要です。

\n

推奨プリセット

\n

TypeScriptではSignal、Component境界、ライフサイクル、Overlay、Ionic 9 standalone import、readonly、tryブロックの共通ルールを有効にします。HTMLではIonic属性型、禁止Overlay要素、非同期操作の二重実行防止、iOS 26・MD3向けのgroup化されたlist構造を有効にします。

\n

非推奨の deny-constructor-di はプリセットに含まれません。Angularの inject() migrationを利用してください。

\n", "headings": [ { "id": "angular%E3%83%BBionic", @@ -86,6 +86,45 @@ export const PROJECT = { "scrollMap": [], "editUrl": "https://github.com/rdlabo-dev/docs/edit/main/src/eslint-plugin-rules/docs/ja/configuration.md" }, + { + "title": "移行", + "navTitle": "移行", + "slug": "migration", + "file": "migration.md", + "section": "ガイド", + "path": "/projects/eslint-plugin-rules/docs/migration", + "html": "

21.xから22.xへ

\n

version 22はAngular 21・22とIonic Framework 9を対象にします。Ionic 8のアプリケーションでは、このpluginのversion 21を使い続けてください。

\n

依存関係

\n

まずアプリケーションの変更をcommitし、アプリケーションrootでIonic公式の @ionic/migrate を実行します。

\n
npx @ionic/migrate --dry-run\nnpx @ionic/migrate\n

migratorはインストール済みのIonic major versionを検出し、@ionic/angular@ionic/core を同時に更新します。安全なv8からv9への変更を適用し、手動判断が必要な項目をchecklistとして表示します。続行前に差分を確認してtestしてください。このpluginのversion 22はAngularおよびAngular ESLint 21から22に対応します。

\n

Ionic Angular import

\n

削除された deny-import-from-ionic-module ruleを prefer-ionic-standalone に置き換えます。

\n
- '@rdlabo/rules/deny-import-from-ionic-module': 'error'\n+ '@rdlabo/rules/prefer-ionic-standalone': 'error'\n

Angularアプリケーションでは、公式migratorが既存のNgModule importを @ionic/angular から @ionic/angular/lazy へ、standalone importを @ionic/angular/standalone からpackage rootへ移します。これによりframework更新中も現在のarchitectureが維持されます。

\n

たとえば、migratorは次の安全なstandalone importの書き換えを自動で行います。

\n
- import { IonButton } from '@ionic/angular/standalone';\n+ import { IonButton } from '@ionic/angular';\n

このpluginはIonic 9のstandaloneアプリケーションだけをサポートします。NgModuleアプリケーションの変換にはarchitecture上の判断が必要なため、公式migratorは IonicModule を自動修正せず報告します。実行後にAngular standalone migrationを完了し、Ionic componentをpackage rootからimportしてください。@ionic/angular/lazy pathを機械的に置換してはいけません。まず各NgModule consumerをstandaloneへ変換し、その後 IonicModule を実際に使う個別のIonic componentへ置き換えます。

\n

新しいruleはNgModule向けの @ionic/angular/lazy entry pointと IonicModule を禁止します。provideIonicAngular() を使ったstandalone bootstrapへ移行し、standalone Ionic componentを直接importします。

\n
- platformBrowserDynamic().bootstrapModule(AppModule);\n+ bootstrapApplication(AppComponent, {\n+   providers: [provideIonicAngular(config)],\n+ });\n

provideIonicAngular@ionic/angular からimportします。IonicModule を削除する前にAngular NgModuleからstandaloneへの移行を完了してください。NgModule内で安全に1行置換することはできません。

\n

recommended presetのlist構造

\n

version 22ではrecommended presetに require-ion-item-group も追加されます。そのため、ion-list 内の ion-itemion-item-groupion-reorder-groupion-radio-group、または ion-accordion-group 内の ion-accordion で囲まれていない既存のIonic templateでは、新しいerrorが報告される場合があります。

\n

意図したgroup境界を判断できる場合だけ安全な自動修正を適用します。再利用可能または曖昧なtemplateは変更せずに報告します。wrapper componentはそれ自身のtemplateを通して検査されるため、正しいgroup化済みlistをrenderするcustom elementが、caller側でgroup化されていない ion-item として扱われることはありません。対応構造と修正条件は require-ion-item-group を参照してください。

\n

booleanのautocorrect

\n

Ionic 9では ion-inpution-searchbarautocorrect'on' | 'off' からbooleanへ変わります。ionic-attr-type-check ruleは古いstring形式を修正します。

\n
- <ion-input autocorrect=\"off\"></ion-input>\n+ <ion-input [autocorrect]=\"false\"></ion-input>\n

Ionic公式migratorもこのv8からv9への変更を自動処理します。このruleは移行後に残った値や新たに追加された古いstring値の検出にも利用でき、Ionic 9のcomponent type定義から、ほかのproperty typeや許容値の変更も追従します。ESLintを --fix 付きで実行し、templateの変更を確認してからAngular buildとtestを実行してください。

\n", + "headings": [ + { + "id": "21.x%E3%81%8B%E3%82%8922.x%E3%81%B8", + "text": "21.xから22.xへ", + "level": 2 + }, + { + "id": "%E4%BE%9D%E5%AD%98%E9%96%A2%E4%BF%82", + "text": "依存関係", + "level": 3 + }, + { + "id": "ionic-angular-import", + "text": "Ionic Angular import", + "level": 3 + }, + { + "id": "recommended-preset%E3%81%AElist%E6%A7%8B%E9%80%A0", + "text": "recommended presetのlist構造", + "level": 3 + }, + { + "id": "boolean%E3%81%AEautocorrect", + "text": "booleanのautocorrect", + "level": 3 + } + ], + "codes": [], + "scrollMap": [], + "editUrl": "https://github.com/rdlabo-dev/docs/edit/main/src/eslint-plugin-rules/docs/ja/migration.md" + }, { "title": "ルール一覧", "navTitle": "ルール一覧", @@ -93,7 +132,7 @@ export const PROJECT = { "file": "rules.md", "section": "リファレンス", "path": "/projects/eslint-plugin-rules/docs/rules", - "html": "

パッケージは18個のルールを公開します。「推奨」がYesのルールは rdlabo.configs.recommended で有効になり、それ以外は個別に設定します。

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
ルール目的Fix推奨
component-property-use-readonlyAngular Componentの不変プロパティに readonly を要求する。YesYes
deny-constructor-diconstructor DIを禁止する。inject()を推奨する非推奨ルール。NoNo
deny-elementインラインIonic Overlayなど、設定したHTML要素を禁止する。NoYes
deny-import-from-ionic-module@ionic/angularからの誤importをstandalone importへ置換する。YesYes
deny-overlay-createModal・Popover Controllerの直接 .create() を禁止する。NoYes
deny-soft-private-modifierTypeScriptの private をハードプライベート # へ置換する。YesYes
implements-ionic-lifecycleAngular・Ionic lifecycle methodに対応するinterfaceを要求する。YesYes
ionic-attr-type-check文字列以外のIonic属性へproperty bindingを要求する。YesYes
no-component-method-except-lifecyclelifecycle以外の任意methodをComponentへ置かない。NoYes
no-component-writable-signalSignal Forms modelの例外を除き、書き込み可能状態をViewModelへ置く。NoNo
no-reactive-formsReactive Formsを禁止し、Angular Signal Formsへ移行する。NoNo
no-template-driven-forms許可した相互運用要素を除きtemplate-driven formsを禁止する。NoNo
prefer-disable-handler非同期操作の二重実行を防ぐwrapperを要求する。NoYes
prefer-modal-launcherpresentModal 呼び出しを launch* 関数に限定する。NoYes
require-viewmodelComponent所有と ViewModelStore 境界を検査する。NoYes
restrict-try-blocktry を短く保ち、Promise・RxJS・Signal contextをポリシーで制限する。NoYes
signal-use-as-signal-templateテンプレートでSignalを読むときに () を要求する。NoYes
signal-use-as-signalTypeScriptで正しいSignalの読み書きを要求する。YesYes
\n

ルール別ドキュメント

\n

各ルールページに詳細・オプション・正誤例があります。上の表はv21.3.0を反映しています。

\n

型情報を使うルール

\n

TypeScript型を調べるルールでは parserOptions.projectService を有効にします。typed lintingがない場合も restrict-try-block の構文検査は動作しますが、Promise・RxJSの型依存検査はスキップされます。

\n", + "html": "

パッケージは19個のルールを公開します。「推奨」がYesのルールは rdlabo.configs.recommended で有効になり、それ以外は個別に設定します。

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
ルール目的Fix推奨
component-property-use-readonlyAngular Componentの不変プロパティに readonly を要求する。YesYes
deny-constructor-diconstructor DIを禁止する。inject()を推奨する非推奨ルール。NoNo
deny-elementインラインIonic Overlayなど、設定したHTML要素を禁止する。NoYes
deny-overlay-createModal・Popover Controllerの直接 .create() を禁止する。NoYes
deny-soft-private-modifierTypeScriptの private をハードプライベート # へ置換する。YesYes
implements-ionic-lifecycleAngular・Ionic lifecycle methodに対応するinterfaceを要求する。YesYes
ionic-attr-type-check文字列以外のIonic属性へproperty bindingを要求する。YesYes
no-component-method-except-lifecyclelifecycle以外の任意methodをComponentへ置かない。NoYes
no-component-writable-signalSignal Forms modelの例外を除き、書き込み可能状態をViewModelへ置く。NoNo
no-reactive-formsReactive Formsを禁止し、Angular Signal Formsへ移行する。NoNo
no-template-driven-forms許可した相互運用要素を除きtemplate-driven formsを禁止する。NoNo
prefer-disable-handler非同期操作の二重実行を防ぐwrapperを要求する。NoYes
prefer-ionic-standaloneIonic 9 standalone importを優先し、IonicModule を禁止する。YesYes
prefer-modal-launcherpresentModal 呼び出しを launch* 関数に限定する。NoYes
require-ion-item-groupiOS 26・MD3向けにIonic list itemのgroup化を要求する。YesYes
require-viewmodelComponent所有と ViewModelStore 境界を検査する。NoYes
restrict-try-blocktry を短く保ち、Promise・RxJS・Signal contextをポリシーで制限する。NoYes
signal-use-as-signal-templateテンプレートでSignalを読むときに () を要求する。NoYes
signal-use-as-signalTypeScriptで正しいSignalの読み書きを要求する。YesYes
\n

ルール別ドキュメント

\n

各ルールページに詳細・オプション・正誤例があります。

\n

型情報を使うルール

\n

TypeScript型を調べるルールでは parserOptions.projectService を有効にします。typed lintingがない場合も restrict-try-block の構文検査は動作しますが、Promise・RxJSの型依存検査はスキップされます。

\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E5%88%A5%E3%83%89%E3%82%AD%E3%83%A5%E3%83%A1%E3%83%B3%E3%83%88", @@ -117,7 +156,7 @@ export const PROJECT = { "file": "api.md", "section": "リファレンス", "path": "/projects/eslint-plugin-rules/docs/api", - "html": "

@rdlabo/eslint-plugin-rules v21.3.0 のpublic plugin surfaceです。詳細なoptionと例は各Ruleページにあります。

\n

Module

\n

module @rdlabo/eslint-plugin-rules

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
ExportDescription
rulesRule名をkeyにした全Rule実装です。
configs共有可能なPlugin設定です。
\n\n

Rule

\n

rule Rule set

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
GroupRules
Component境界component-property-use-readonly, deny-constructor-di, deny-soft-private-modifier, no-component-method-except-lifecycle, no-component-writable-signal, require-viewmodel
Ionic APIdeny-element, deny-import-from-ionic-module, deny-overlay-create, implements-ionic-lifecycle, ionic-attr-type-check, prefer-disable-handler, prefer-modal-launcher
Form・Signalno-reactive-forms, no-template-driven-forms, signal-use-as-signal, signal-use-as-signal-template
Control flowrestrict-try-block
\n\n
", + "html": "

@rdlabo/eslint-plugin-rules v22.0.0 のpublic plugin surfaceです。詳細なoptionと例は各Ruleページにあります。

\n

Module

\n

module @rdlabo/eslint-plugin-rules

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
ExportDescription
rulesRule名をkeyにした全Rule実装です。
configs共有可能なPlugin設定です。
\n\n

Rule

\n

rule Rule set

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
GroupRules
Component境界component-property-use-readonly, deny-constructor-di, deny-soft-private-modifier, no-component-method-except-lifecycle, no-component-writable-signal, require-viewmodel
Ionic APIdeny-element, deny-overlay-create, implements-ionic-lifecycle, ionic-attr-type-check, prefer-disable-handler, prefer-ionic-standalone, prefer-modal-launcher, require-ion-item-group
Form・Signalno-reactive-forms, no-template-driven-forms, signal-use-as-signal, signal-use-as-signal-template
Control flowrestrict-try-block
\n\n
", "headings": [ { "id": "module", @@ -151,18 +190,13 @@ export const PROJECT = { "file": "rules/component-property-use-readonly.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/component-property-use-readonly", - "html": "\n
\n

プロパティをreadonlyにすべきときに警告する

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
  • ✒️ コマンドライン--fix オプションで、このルールが報告する問題の一部を自動修正できます。
  • \n
\n
\n

このルールは、不変であるべきAngularコンポーネントのプロパティに readonly 修飾子の使用を強制します。予期しない変更を防ぎ、コンポーネントの状態管理をより安全にします。

\n

ルール詳細

\n

❌ 誤り: readonly 修飾子のないプロパティ

\n
@Component({\n  selector: 'app-example',\n  template: '<div>example</div>',\n})\nexport class ExampleComponent {\n  x = 1;\n  public y = 2;\n  private z = 3;\n  protected w = 4;\n  #secret = 42;\n  static a = 1;\n  ['foo'] = 1;\n  @Input() i = 8;\n  h: number;\n}\n

✅ 正しい: readonly 修飾子付きのプロパティ

\n
@Component({\n  selector: 'app-example',\n  template: '<div>example</div>',\n})\nexport class ExampleComponent {\n  readonly x = 1;\n  public readonly y = 2;\n  private readonly z = 3;\n  protected readonly w = 4;\n  readonly #secret = 42;\n  static readonly a = 1;\n  readonly ['foo'] = 1;\n  @Input() readonly i = 8;\n  readonly h: number;\n}\n

ルール設定

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/component-property-use-readonly\": [\n      \"error\",\n      {\n        \"ignorePrivateProperties\": true\n      }\n    ]\n  }\n}\n

オプション

\n
const options: {\n  ignorePrivateProperties?: boolean; // Whether to ignore private properties (default: false)\n};\n

ignorePrivateProperties

\n

true にすると、ソフトプライベートプロパティ(private 修飾子)とハードプライベートプロパティ(# 接頭辞)の両方を無視します。プライベートプロパティは通常コンポーネント外からアクセスされないため、readonly 修飾子の重要度が下がる場合に有用です。

\n

❌ 誤り: ignorePrivateProperties: true なしのプライベートプロパティ

\n
@Component({\n  selector: 'app-example',\n  template: '<div>example</div>',\n})\nexport class ExampleComponent {\n  private privateProp = 1; // error\n  #secretProp = 2; // error\n}\n

✅ 正しい: ignorePrivateProperties: true ありのプライベートプロパティ

\n
// .eslintrc.json\n{\n  \"rules\": {\n    \"@rdlabo/rules/component-property-use-readonly\": [\n      \"error\",\n      {\n        \"ignorePrivateProperties\": true\n      }\n    ]\n  }\n}\n\n// Component code\n@Component({\n  selector: 'app-example',\n  template: '<div>example</div>',\n})\nexport class ExampleComponent {\n  private privateProp = 1; // no error\n  #secretProp = 2; // no error\n  public publicProp = 3; // still requires readonly\n}\n

実装

\n\n", + "html": "\n
\n

プロパティをreadonlyにすべきときに警告する

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
  • ✒️ コマンドライン--fix オプションで、このルールが報告する問題の一部を自動修正できます。
  • \n
\n
\n

このルールは、Angular Componentで宣言された関数以外のプロパティに readonly 修飾子を要求します。初期化済み・未初期化・static・computed・decorator付き・soft private・hard privateの各プロパティを報告し、readonly を自動的に追加できます。

\n

ルール詳細

\n

@Component() で装飾されたクラスだけを検査します。method、getter、setter、arrow functionプロパティ、function expressionプロパティ、すでに readonly のプロパティ、および他のクラスのプロパティは無視します。

\n

オプション

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/component-property-use-readonly\": [\n      \"error\",\n      {\n        \"ignorePrivateProperties\": true\n      }\n    ]\n  }\n}\n

ignorePrivateProperties

\n
    \n
  • 型: boolean
  • \n
  • デフォルト: false
  • \n
\n

true の場合、TypeScriptの private 修飾子を指定したプロパティとECMAScriptの # privateプロパティを無視します。public、protected、staticプロパティは引き続き検査します。

\n

\n

誤り

\n
@Component({\n  selector: 'app-example',\n  template: '<div>example</div>',\n})\nexport class ExampleComponent {\n  x = 1;\n  public y = 2;\n  private z = 3;\n  protected w = 4;\n  #secret = 42;\n  static a = 1;\n  ['foo'] = 1;\n  @Input() i = 8;\n  h: number;\n}\n

正しい

\n
@Component({\n  selector: 'app-example',\n  template: '<div>example</div>',\n})\nexport class ExampleComponent {\n  readonly x = 1;\n  public readonly y = 2;\n  private readonly z = 3;\n  protected readonly w = 4;\n  readonly #secret = 42;\n  static readonly a = 1;\n  readonly ['foo'] = 1;\n  @Input() readonly i = 8;\n  readonly h: number;\n}\n

ignorePrivateProperties: true の場合、privateプロパティは書き込み可能なままでも構いません。

\n
@Component({\n  selector: 'app-example',\n  template: '<div>example</div>',\n})\nexport class ExampleComponent {\n  private privateProp = 1; // no error\n  #secretProp = 2; // no error\n  public readonly publicProp = 3;\n}\n

有効にする場面

\n

Componentプロパティに安定した参照を公開させ、書き込み可能な状態をSignalsまたはViewModelで管理する場合に、このルールを有効にします。

\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", "text": "ルール詳細", "level": 2 }, - { - "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A8%AD%E5%AE%9A", - "text": "ルール設定", - "level": 2 - }, { "id": "%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3", "text": "オプション", @@ -173,6 +207,26 @@ export const PROJECT = { "text": "ignorePrivateProperties", "level": 3 }, + { + "id": "%E4%BE%8B", + "text": "例", + "level": 2 + }, + { + "id": "%E8%AA%A4%E3%82%8A", + "text": "誤り", + "level": 3 + }, + { + "id": "%E6%AD%A3%E3%81%97%E3%81%84", + "text": "正しい", + "level": 3 + }, + { + "id": "%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B%E5%A0%B4%E9%9D%A2", + "text": "有効にする場面", + "level": 2 + }, { "id": "%E5%AE%9F%E8%A3%85", "text": "実装", @@ -190,18 +244,38 @@ export const PROJECT = { "file": "rules/deny-constructor-di.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/deny-constructor-di", - "html": "\n
\n

このプラグインはconstructor内のDependency Injectionを禁止します。

\n
\n

このルールは inject 関数を使う、現代的なAngularの依存性注入の作法を促します。

\n

ルール詳細

\n

❌ 誤り: constructorベースの依存性注入を使う

\n
@Component({\n  selector: 'app-confirm',\n  templateUrl: './confirm.page.html',\n  styleUrls: ['./confirm.page.scss'],\n})\nexport class SigninPage {\n  constructor(public platform: Platform) {}\n}\n

✅ 正しい: 依存性注入に inject 関数を使う

\n
@Component({\n  selector: 'app-confirm',\n  templateUrl: './confirm.page.html',\n  styleUrls: ['./confirm.page.scss'],\n})\nexport class SigninPage {\n  public platform = inject(Platform);\n\n  constructor() {}\n}\n

オプション

\n

オプションなし。

\n

実装

\n\n", + "html": "\n
\n

このプラグインはconstructor内のDependency Injectionを禁止します。

\n
\n

このルールは、constructor(private readonly auth: AuthService) のようにDependency Injectionに使われるconstructor parameter propertyを報告します。Angularの inject() 関数は、standalone Componentやserviceで依存関係を取得する現代的な方法です。constructorの定型コードをなくし、DIを明示的にします。

\n

ルール詳細

\n

クラスのconstructorを検査し、TSParameterPropertypublicprivatereadonly などの修飾子を持つparameter)を報告します。これらはクラスフィールドになり、DIに使われるparameterです。

\n
    \n
  • 修飾子のない通常のconstructor parameterは許可されます。
  • \n
  • このルールは自動修正しません。constructor DIを手動で inject() に置き換える必要があります。
  • \n
\n

\n

誤り

\n
@Component({\n  selector: 'app-signin',\n  templateUrl: './signin.page.html',\n})\nexport class SigninPage {\n  constructor(\n    private store: Store<IApp>,\n    public readonly navCtrl: NavController,\n  ) {}\n}\n

正しい

\n
import { inject } from '@angular/core';\n\n@Component({\n  selector: 'app-signin',\n  templateUrl: './signin.page.html',\n})\nexport class SigninPage {\n  private readonly store = inject(Store<IApp>);\n  private readonly navCtrl = inject(NavController);\n}\n
// Non-DI constructor parameters are allowed\nexport class LogManager {\n  constructor(logDomain: string) {\n    this.logDomain = logDomain;\n  }\n}\n

オプション

\n

このルールにオプションはありません。

\n

有効にする場面

\n

constructor parameter propertyではなく inject() でAngularの依存関係を取得することをプロジェクトで要求する場合に、このopt-inルールを有効にします。このルールは TSParameterProperty nodeだけを報告するため、通常のconstructor parameterは引き続き許可されます。

\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", "text": "ルール詳細", "level": 2 }, + { + "id": "%E4%BE%8B", + "text": "例", + "level": 2 + }, + { + "id": "%E8%AA%A4%E3%82%8A", + "text": "誤り", + "level": 3 + }, + { + "id": "%E6%AD%A3%E3%81%97%E3%81%84", + "text": "正しい", + "level": 3 + }, { "id": "%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3", "text": "オプション", "level": 2 }, + { + "id": "%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B%E5%A0%B4%E9%9D%A2", + "text": "有効にする場面", + "level": 2 + }, { "id": "%E5%AE%9F%E8%A3%85", "text": "実装", @@ -219,50 +293,46 @@ export const PROJECT = { "file": "rules/deny-element.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/deny-element", - "html": "\n
\n

このプラグインは特定のHTMLタグの使用を禁止します。

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
\n
\n

このルールは、インラインコンポーネントではなくControllerベースのmodalやaction sheetの利用を強制したいIonicアプリで特に有用です。

\n

ルール詳細

\n

❌ 誤り: テンプレートで禁止された要素を使う

\n
<ion-modal></ion-modal>\n<!-- error -->\n

✅ 正しい: .eslintrc.json で禁止する要素を指定するようルールを設定する

\n

ルール設定

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/deny-element\": [\n      \"error\",\n      {\n        \"elements\": [\"ion-modal\"]\n      }\n    ]\n  }\n}\n

オプション

\n
const options: {\n  elements: string[]; // Array of element names to disallow\n};\n

実装

\n\n", + "html": "\n
\n

このプラグインは特定のHTMLタグの使用を禁止します。

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
\n
\n

このルールは、Angular templateで特定のelementが使われることを防ぎます。一般的には、templateで宣言する代わりにlauncher methodや専用serviceを通じて表示すべき <ion-modal><ion-popover><ion-toast><ion-alert><ion-loading><ion-picker><ion-action-sheet> などのinline overlay componentを禁止するために使います。

\n

ルール詳細

\n

このルールは .html template fileで実行され、tag nameが設定済みの elements listに含まれるelementを報告します。template ASTを走査し、@if@for@else と、ネストした then / else branchなどのAngular control flow構文にも対応します。

\n
    \n
  • testに影響しないよう .spec.html fileは無視します。
  • \n
  • 明示的なオプションがなければ、デフォルトのIonic overlay element listを使います。option objectを指定する場合、そのschemaでは elements arrayが必須です。
  • \n
\n

オプション

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/deny-element\": [\n      \"error\",\n      {\n        \"elements\": [\"ion-modal\", \"ion-popover\", \"ion-toast\", \"ion-alert\", \"ion-loading\", \"ion-picker\", \"ion-action-sheet\"]\n      }\n    ]\n  }\n}\n

elements

\n
    \n
  • 型: string[]
  • \n
  • デフォルト: ion-modal, ion-popover, ion-toast, ion-alert, ion-loading, ion-picker, ion-action-sheet
  • \n
\n

禁止するelement tag nameの配列です。このルールはこれらの名前をAngular template ASTの Element node typeと比較するため、element自体とcontrol flow branch内の存在の両方を検査します。

\n

\n

誤り

\n
<ion-modal></ion-modal>\n\n<div>\n  <ion-toast></ion-toast>\n  <ion-alert></ion-alert>\n</div>\n
@if (showModal) {\n<ion-modal>Modal content</ion-modal>\n}\n

正しい

\n
<ion-button (click)=\"presentModal()\">Open</ion-button>\n
@for (item of items; track item.id) {\n<ion-card>\n  <ion-card-header>{{ item.name }}</ion-card-header>\n</ion-card>\n}\n

有効にする場面

\n

overlayにlauncher patternを使うプロジェクトで、このルールを有効にします。@rdlabo/rules/prefer-modal-launcherおよび@rdlabo/rules/prefer-disable-handlerと組み合わせることで、modalとoverlayのlogicをtemplateから分離できます。

\n

関連項目

\n\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", "text": "ルール詳細", "level": 2 }, - { - "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A8%AD%E5%AE%9A", - "text": "ルール設定", - "level": 2 - }, { "id": "%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3", "text": "オプション", "level": 2 }, { - "id": "%E5%AE%9F%E8%A3%85", - "text": "実装", + "id": "elements", + "text": "elements", + "level": 3 + }, + { + "id": "%E4%BE%8B", + "text": "例", "level": 2 - } - ], - "codes": [], - "scrollMap": [], - "editUrl": "https://github.com/rdlabo-dev/docs/edit/main/src/eslint-plugin-rules/docs/ja/rules/deny-element.md" - }, - { - "title": "deny-import-from-ionic-module", - "navTitle": "deny-import-from-ionic-module", - "slug": "rules/deny-import-from-ionic-module", - "file": "rules/deny-import-from-ionic-module.md", - "section": "ルール", - "path": "/projects/eslint-plugin-rules/docs/rules/deny-import-from-ionic-module", - "html": "\n
\n

このプラグインは、@ionic/angular/standalone ではなく @ionic/angular からの誤ったimportを防ぎます。

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
  • ✒️ コマンドライン--fix オプションで、このルールが報告する問題の一部を自動修正できます。
  • \n
\n
\n

@ionic/angular@ionic/angular/standalone のimportを混在させるとランタイム問題が起き得ます。ビルドは成功しても、ブラウザ実行時に失敗することがあります。このルールは一貫したimportパスを強制することで、そうした問題を防ぎます。

\n

ルール詳細

\n

❌ 誤り: @ionic/angular からimportする

\n
import { ModalController } from '@ionic/angular';\n

✅ 正しい: @ionic/angular/standalone からimportする

\n
import { ModalController } from '@ionic/angular/standalone';\n

オプション

\n

オプションなし。

\n

実装

\n\n", - "headings": [ + }, { - "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", - "text": "ルール詳細", + "id": "%E8%AA%A4%E3%82%8A", + "text": "誤り", + "level": 3 + }, + { + "id": "%E6%AD%A3%E3%81%97%E3%81%84", + "text": "正しい", + "level": 3 + }, + { + "id": "%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B%E5%A0%B4%E9%9D%A2", + "text": "有効にする場面", "level": 2 }, { - "id": "%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3", - "text": "オプション", + "id": "%E9%96%A2%E9%80%A3%E9%A0%85%E7%9B%AE", + "text": "関連項目", "level": 2 }, { @@ -273,7 +343,7 @@ export const PROJECT = { ], "codes": [], "scrollMap": [], - "editUrl": "https://github.com/rdlabo-dev/docs/edit/main/src/eslint-plugin-rules/docs/ja/rules/deny-import-from-ionic-module.md" + "editUrl": "https://github.com/rdlabo-dev/docs/edit/main/src/eslint-plugin-rules/docs/ja/rules/deny-element.md" }, { "title": "deny-overlay-create", @@ -282,7 +352,7 @@ export const PROJECT = { "file": "rules/deny-overlay-create.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/deny-overlay-create", - "html": "\n
\n

ModalController / PopoverController の .create() を禁止し、launcher経由でoverlayを開く。

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
\n
\n

Ionicアプリでは、modalとpopoverは共有ヘルパー(presentModal)とエクスポートした launch* 関数経由で開くべきであり、ModalController.create() / PopoverController.create() を直接呼んではいけません。

\n

次のルールと併用します。

\n
    \n
  • @rdlabo/rules/deny-element — テンプレート内のインライン <ion-modal> / <ion-popover> を禁止する
  • \n
  • @rdlabo/rules/prefer-modal-launcherpresentModallaunch* 内に置くことを要求する
  • \n
\n

LoadingControllerAlertControllerToastControllerActionSheetController はデフォルトで許可されたままです。ModalControllerdismiss() も許可されます。

\n

ルール詳細

\n

❌ 誤り: controller経由でmodal / popoverをcreateする

\n
import { inject } from '@angular/core';\nimport { ModalController } from '@ionic/angular/standalone';\n\nexport class ExamplePage {\n  readonly #modalCtrl = inject(ModalController);\n\n  async open() {\n    await this.#modalCtrl.create({ component: OtherPage }); // error\n  }\n}\n

次も同様です。

\n
    \n
  • this.modalCtrl.create(...)
  • \n
  • inject(ModalController).create(...)
  • \n
  • ModalController / PopoverController 型のパラメータ
  • \n
\n

✅ 正しい: launcher経由で開き、ModalController はdismiss用にだけ持つ

\n
export const launchOtherPage = (helper: HelperService, props: OtherProps) => {\n  return helper.presentModal(OtherPage, props, { watchKeyboard: false });\n};\n\nexport class ExamplePage {\n  readonly #modalCtrl = inject(ModalController);\n  readonly helper = inject(HelperService);\n\n  async open() {\n    await launchOtherPage(this.helper, {});\n  }\n\n  dismiss() {\n    this.#modalCtrl.dismiss();\n  }\n}\n

✅ 正しい: Loading / Alert / Toast / ActionSheet のcreate

\n
readonly #loadingCtrl = inject(LoadingController);\nawait this.#loadingCtrl.create({ message: '...' });\n

オプション

\n
{\n  // Controllers whose `.create()` is denied.\n  // default: ['ModalController', 'PopoverController']\n  deny?: string[];\n}\n
'@rdlabo/rules/deny-overlay-create': [\n  'error',\n  { deny: ['ModalController', 'PopoverController'] },\n],\n

alertのcreateも禁止するには次のようにします。

\n
{\n  deny: ['ModalController', 'PopoverController', 'AlertController'];\n}\n

実装

\n\n", + "html": "\n
\n

ModalController / PopoverControllerの .create() を禁止し、launcher経由でoverlayを開く。

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
\n
\n

このルールは、controllerの .create() 呼び出しによるIonic overlayの直接生成を防ぎます。rdlabo architectureでは、overlayはlauncher functionと共有の presentModal / presentPopover helperを通じて開きます。これによりoverlay logicを一元化し、呼び出し側をcontroller APIから分離できます。

\n

ルール詳細

\n

receiverが ModalControllerPopoverController(または設定した他のcontroller)である .create() 呼び出しを検出します。次のような複数のpatternからcontrollerを解決します。

\n
    \n
  • this.modalCtrl.create()
  • \n
  • modalCtrl.create()modalCtrlinject(ModalController) の場合)
  • \n
  • inject(ModalController).create()
  • \n
  • constructor parameter constructor(private modalCtrl: ModalController)
  • \n
  • ModalController 型のclass property
  • \n
\n

LoadingControllerAlertControllerToastControllerActionSheetController など、その他のoverlay controllerは直接使うことが意図されている場合があるため、デフォルトでは禁止しません。

\n

オプション

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/deny-overlay-create\": [\n      \"error\",\n      {\n        \"deny\": [\"ModalController\", \"PopoverController\"]\n      }\n    ]\n  }\n}\n

deny

\n
    \n
  • 型: string[]
  • \n
  • デフォルト: [\"ModalController\", \"PopoverController\"]
  • \n
\n

.create() 呼び出しを禁止するcontroller class nameです。空の配列を指定するとルールを無効にできます。

\n

\n

誤り

\n
export class ExamplePage {\n  readonly #modalCtrl = inject(ModalController);\n\n  async open() {\n    await this.#modalCtrl.create({ component: OtherPage });\n  }\n}\n
export async function open(modalCtrl: ModalController) {\n  await modalCtrl.create({ component: OtherPage });\n}\n
export class ExamplePage {\n  constructor(private modalCtrl: ModalController) {}\n\n  async open() {\n    await this.modalCtrl.create({ component: OtherPage });\n  }\n}\n

正しい

\n
export const launchOtherPage = (overlay: Helper, props: Props) => {\n  return overlay.presentModal(OtherPage, props);\n};\n
export class ExamplePage {\n  readonly #loadingCtrl = inject(LoadingController);\n\n  async showLoading() {\n    await this.#loadingCtrl.create({ message: '...' });\n  }\n}\n
export class ExamplePage {\n  readonly #modalCtrl = inject(ModalController);\n\n  dismiss(data?: unknown) {\n    this.#modalCtrl.dismiss(data);\n  }\n}\n

有効にする場面

\n

launcher patternと共有overlay helperを使うIonicプロジェクトで、このルールを有効にします。@rdlabo/rules/prefer-modal-launcherおよび@rdlabo/rules/deny-elementと組み合わせて使います。

\n

関連項目

\n\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", @@ -294,6 +364,36 @@ export const PROJECT = { "text": "オプション", "level": 2 }, + { + "id": "deny", + "text": "deny", + "level": 3 + }, + { + "id": "%E4%BE%8B", + "text": "例", + "level": 2 + }, + { + "id": "%E8%AA%A4%E3%82%8A", + "text": "誤り", + "level": 3 + }, + { + "id": "%E6%AD%A3%E3%81%97%E3%81%84", + "text": "正しい", + "level": 3 + }, + { + "id": "%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B%E5%A0%B4%E9%9D%A2", + "text": "有効にする場面", + "level": 2 + }, + { + "id": "%E9%96%A2%E9%80%A3%E9%A0%85%E7%9B%AE", + "text": "関連項目", + "level": 2 + }, { "id": "%E5%AE%9F%E8%A3%85", "text": "実装", @@ -311,7 +411,7 @@ export const PROJECT = { "file": "rules/deny-soft-private-modifier.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/deny-soft-private-modifier", - "html": "\n
\n

このプラグインはソフトプライベート修飾子の使用を禁止します。

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
  • ✒️ コマンドライン--fix オプションで、このルールが報告する問題の一部を自動修正できます。
  • \n
\n
\n

ルール詳細

\n

❌ 誤り: クラスフィールドに private 修飾子を使う

\n
@Component({})\nexport class SigninPage {\n  private platform = inject(Platform);\n}\n

✅ 正しい: ハードプライベートフィールド構文(#)を使う

\n
@Component({})\nexport class SigninPage {\n  #platform = inject(Platform);\n}\n

実装

\n\n", + "html": "\n
\n

このプラグインはソフトプライベート修飾子の使用を禁止します。

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
  • ✒️ コマンドライン--fix オプションで、このルールが報告する問題の一部を自動修正できます。
  • \n
\n
\n

ルール詳細

\n

❌ 誤り: クラスフィールドに private 修飾子を使う

\n
class TokenStore {\n  private token = '';\n\n  private refresh() {\n    this.token = 'new-token';\n  }\n}\n

✅ 正しい: ハードプライベートフィールド構文(#)を使う

\n
class TokenStore {\n  #token = '';\n\n  #refresh() {\n    this.#token = 'new-token';\n  }\n}\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", @@ -335,18 +435,38 @@ export const PROJECT = { "file": "rules/implements-ionic-lifecycle.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/implements-ionic-lifecycle", - "html": "\n
\n

このプラグインはIonic Lifecycleのimplementsを推奨します。

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
  • ✒️ コマンドライン--fix オプションで、このルールが報告する問題の一部を自動修正できます。
  • \n
\n
\n

ルール詳細

\n

❌ 誤り: interfaceをimplementsせずにIonic Lifecycle methodを使う

\n
@Component({\n  selector: 'app-confirm',\n  templateUrl: './confirm.page.html',\n  styleUrls: ['./confirm.page.scss'],\n})\nexport class SigninPage {\n  ionViewWillEnter() {}\n}\n

✅ 正しい: 適切なinterfaceのimplementsとともにIonic Lifecycle methodを使う

\n
@Component({\n  selector: 'app-confirm',\n  templateUrl: './confirm.page.html',\n  styleUrls: ['./confirm.page.scss'],\n})\nexport class SigninPage implements ionViewWillEnter {\n  ionViewWillEnter() {}\n}\n

オプション

\n

オプションなし。

\n

実装

\n\n", + "html": "\n
\n

このプラグインはIonic Lifecycleのimplementsを推奨します。

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
  • ✒️ コマンドライン--fix オプションで、このルールが報告する問題の一部を自動修正できます。
  • \n
\n
\n

Ionicは ionViewWillEnterionViewDidLeave などのframework-level lifecycle methodを提供します。Componentでこれらのmethodを宣言する場合、TypeScriptがcontractを型検査できるよう、対応するinterface(ViewWillEnterViewDidEnterViewWillLeaveViewDidLeave)もimplementsする必要があります。このルールはその組み合わせを強制し、implements clauseを自動修正できます。

\n

ルール詳細

\n

@Component で装飾されたクラスを検査し、次のIonic lifecycle method名を持つmethod definitionを探します。

\n
    \n
  • ionViewWillEnter -> ViewWillEnter
  • \n
  • ionViewDidEnter -> ViewDidEnter
  • \n
  • ionViewWillLeave -> ViewWillLeave
  • \n
  • ionViewDidLeave -> ViewDidLeave
  • \n
\n

methodが存在し、対応するinterfaceがなければ報告します。欠けているinterfaceを修正する際は、implements clause全体を、使用中のmethodに対応するIonic lifecycle interfaceで置き換えます。この処理で OnInit など無関係なinterfaceが削除される場合があるため、修正内容を確認し、クラスに引き続き必要なIonic以外のinterfaceを戻してください。必要なinterfaceがすべて存在する場合、余分なlifecycle interfaceは報告も削除もされません。

\n
    \n
  • Component以外のクラスは検査しません。
  • \n
  • class bodyが空でもlifecycle interfaceをimplementsしている場合、古い implements clauseを削除します。
  • \n
  • fixの重複を避けるため、修正可能なgroupごとに一度だけ報告します。
  • \n
\n

\n

誤り

\n
@Component({\n  selector: 'app-scanner',\n  standalone: true,\n})\nexport class ScannerPage {\n  ionViewWillEnter() {}\n  ionViewWillLeave() {}\n}\n
@Component({\n  selector: 'app-scanner',\n  standalone: true,\n})\nexport class ScannerPage implements ViewDidEnter, ViewDidLeave {\n  ionViewWillEnter() {}\n  ionViewWillLeave() {}\n}\n

正しい

\n
import { ViewWillEnter, ViewWillLeave } from '@ionic/angular';\n\n@Component({\n  selector: 'app-scanner',\n  standalone: true,\n})\nexport class ScannerPage implements ViewWillEnter, ViewWillLeave {\n  ionViewWillEnter() {}\n  ionViewWillLeave() {}\n}\n
@Component({\n  selector: 'app-scanner',\n  standalone: true,\n})\nexport class ScannerPage implements ViewDidEnter, ViewDidLeave {\n  ionViewDidEnter() {}\n  ionViewDidLeave() {}\n}\n

オプション

\n

このルールにオプションはありません。

\n

有効にする場面

\n

すべてのIonic Angularプロジェクトで、このルールを有効にします。lifecycle methodを追加・改名・削除したときに implements clauseを正確に保つのに役立ち、--fix と組み合わせて利用できます。

\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", "text": "ルール詳細", "level": 2 }, + { + "id": "%E4%BE%8B", + "text": "例", + "level": 2 + }, + { + "id": "%E8%AA%A4%E3%82%8A", + "text": "誤り", + "level": 3 + }, + { + "id": "%E6%AD%A3%E3%81%97%E3%81%84", + "text": "正しい", + "level": 3 + }, { "id": "%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3", "text": "オプション", "level": 2 }, + { + "id": "%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B%E5%A0%B4%E9%9D%A2", + "text": "有効にする場面", + "level": 2 + }, { "id": "%E5%AE%9F%E8%A3%85", "text": "実装", @@ -364,7 +484,7 @@ export const PROJECT = { "file": "rules/ionic-attr-type-check.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/ionic-attr-type-check", - "html": "\n
\n

Ionicコンポーネントの非string属性への文字列値を禁止し、適切なproperty bindingを提案する。boolean・number・object型属性に対応する。

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
  • ✒️ コマンドライン--fix オプションで、このルールが報告する問題の一部を自動修正できます。
  • \n
\n
\n

このルールは、Ionicコンポーネントのテンプレートで非string属性(boolean、number、object、complex)に文字列値が割り当てられている箇所を検出し、適切なproperty bindingを提案することで、TypeScriptのビルドエラーを防ぎます。

\n

ルール詳細

\n

このルールは、Ionicコンポーネントのテンプレートで非string属性(boolean、number、object、complex)に文字列値が割り当てられている箇所を検出します。

\n

❌ 誤り: 非string属性に文字列値を使う

\n
<ion-item button=\"true\"></ion-item>\n<ion-list inset=\"true\"></ion-list>\n<ion-progress-bar value=\"50\"></ion-progress-bar>\n<input disabled=\"false\"></input>\n<button readonly=\"1\"></button>\n

✅ 正しい: property bindingを使う

\n
<ion-item [button]=\"true\"></ion-item>\n<ion-list [inset]=\"true\"></ion-list>\n<ion-progress-bar [value]=\"50\"></ion-progress-bar>\n<input [disabled]=\"false\"></input>\n<button [readonly]=\"true\"></button>\n

オプション

\n

オプションなし。

\n

サポートする属性型

\n

このルールはIonicコンポーネントの型定義から非string属性を自動で特定し、次のような属性を検出します。

\n

Ionicコンポーネント属性の例

\n
    \n
  • ion-item: button, disabled, detail
  • \n
  • ion-list: inset, lines
  • \n
  • ion-button: disabled, expand, fill, strong
  • \n
  • ion-checkbox: checked, disabled, indeterminate
  • \n
  • ion-toggle: checked, disabled
  • \n
  • ion-radio: checked, disabled
  • \n
  • ion-input: disabled, readonly, required
  • \n
  • ion-textarea: disabled, readonly, required
  • \n
  • ion-select: disabled, multiple, required
  • \n
  • ion-datetime: disabled, readonly
  • \n
  • ion-range: disabled, pin, snaps
  • \n
  • ion-segment: disabled
  • \n
  • ion-slides: pager, scrollbar
  • \n
  • ion-tab: selected
  • \n
  • ion-menu: disabled, swipeGesture
  • \n
  • ion-modal: animated, backdropDismiss, showBackdrop
  • \n
  • ion-popover: animated, backdropDismiss, showBackdrop
  • \n
  • ion-alert: animated, backdropDismiss
  • \n
  • ion-loading: animated, backdropDismiss
  • \n
  • ion-toast: animated
  • \n
  • ion-action-sheet: animated, backdropDismiss
  • \n
\n

エラーメッセージ

\n

このルールは次のメッセージを表示します。

\n
boolean attribute 'button' should not have a string value 'true'. Use property binding [button]=\"true\" instead.\nnumber attribute 'value' should not have a string value '50'. Use property binding [value]=\"50\" instead.\n

実装

\n\n", + "html": "\n
\n

対応するstring以外のIonic属性にproperty bindingを要求し、string literal属性を検証する。

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
  • ✒️ コマンドライン--fix オプションで、このルールが報告する問題の一部を自動修正できます。
  • \n
\n
\n

Ionic componentの属性はboolean、number、object、stringのいずれかです。boolean propertyに button=\"true\" のようなstringを渡すのはよくある誤りで、予期しない動作を招く場合があります。このルールは @ionic/core の型定義を読み取り、不一致を報告します。

\n

ルール詳細

\n

Angular templateで実行されます。各Ionic elementについて @ionic/core の型定義を参照し、属性を次のいずれかに分類します。

\n
    \n
  • string — string literalを許可
  • \n
  • string literal — 特定の値だけを許可
  • \n
  • boolean[attr]=\"true\" または [attr]=\"false\" を使用
  • \n
  • number[attr]=\"50\" を使用
  • \n
  • object[attr]=\"...\" を使用
  • \n
  • skip / unknown — 検査しない
  • \n
\n

boolean属性では、string値 truefalse10yesnoonoff を認識します。それ以外のstringはboolean検査では報告しません。対応するboolean、number、objectの不一致はproperty bindingへ自動修正されます。

\n
    \n
  • button=\"true\" -> [button]=\"true\"
  • \n
  • value=\"50\" -> [value]=\"50\"
  • \n
  • Ionic 9の autocorrect=\"off\" -> [autocorrect]=\"false\"
  • \n
\n

string literal属性に無効なstring値が指定されている場合、許容される値を報告します。

\n

\n

誤り

\n
<ion-item button=\"true\"></ion-item>\n
<ion-progress-bar value=\"50\"></ion-progress-bar>\n
<ion-modal isOpen=\"true\" backdropDismiss=\"false\"></ion-modal>\n

正しい

\n
<ion-item [button]=\"true\"></ion-item>\n
<ion-progress-bar [value]=\"50\"></ion-progress-bar>\n
<ion-modal [isOpen]=\"true\" [backdropDismiss]=\"false\"></ion-modal>\n
<!-- string-typed attributes are still allowed -->\n<ion-item lines=\"full\"></ion-item>\n<ion-button color=\"primary\">Click me</ion-button>\n

オプション

\n

このルールにオプションはありません。

\n

有効にする場面

\n

すべてのIonic Angularプロジェクトで、このルールを有効にします。古いIonic構文から移行するときや、通常のHTML属性に慣れたdeveloperが参加するときに特に役立ちます。

\n

要件

\n

node_modules/@ionic/core/dist/types/components.d.ts を読み取れるよう、同じプロジェクトに @ionic/core がインストールされている必要があります。packageが存在しない場合、ルールは空の結果を返し、何も報告しません。

\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", @@ -372,23 +492,33 @@ export const PROJECT = { "level": 2 }, { - "id": "%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3", - "text": "オプション", + "id": "%E4%BE%8B", + "text": "例", "level": 2 }, { - "id": "%E3%82%B5%E3%83%9D%E3%83%BC%E3%83%88%E3%81%99%E3%82%8B%E5%B1%9E%E6%80%A7%E5%9E%8B", - "text": "サポートする属性型", - "level": 2 + "id": "%E8%AA%A4%E3%82%8A", + "text": "誤り", + "level": 3 }, { - "id": "ionic%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E5%B1%9E%E6%80%A7%E3%81%AE%E4%BE%8B", - "text": "Ionicコンポーネント属性の例", + "id": "%E6%AD%A3%E3%81%97%E3%81%84", + "text": "正しい", "level": 3 }, { - "id": "%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8", - "text": "エラーメッセージ", + "id": "%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3", + "text": "オプション", + "level": 2 + }, + { + "id": "%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B%E5%A0%B4%E9%9D%A2", + "text": "有効にする場面", + "level": 2 + }, + { + "id": "%E8%A6%81%E4%BB%B6", + "text": "要件", "level": 2 }, { @@ -408,7 +538,7 @@ export const PROJECT = { "file": "rules/no-component-method-except-lifecycle.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/no-component-method-except-lifecycle", - "html": "\n
\n

@Component 上のlifecycle以外のmethodを禁止する。許可するlifecycle methodは implements から導出する(プロパティは許可)。

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
\n
\n

@Component クラスは薄く保ちます。振る舞いは ViewModel(またはmodalの launch* ヘルパー)に置きます。

\n

許可されるmethodは、implements に列挙したlifecycle interfaceと一致するものだけです。対応するinterfaceなしのlifecycle methodもエラーです。

\n

プロパティは対象外です。readonly open = () => ... のようなarrow-functionフィールドも含みます。getter / setter と constructor は許可されます。

\n

@Directive / @Injectable / 通常のクラスは検査しません。@rdlabo/rules/require-viewmodel および @rdlabo/rules/implements-ionic-lifecycle と併用します。

\n
\n

既存アプリにはComponent methodが多いことがよくあります。まず \"warn\" から始め、ViewModelへロジックを移してから \"error\" に切り替えるのが望ましいです。

\n
\n

ルール詳細

\n

✅ 正しい: methodが implements と一致する

\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage implements ViewWillEnter, ViewWillLeave, OnDestroy {\n  readonly vm = new ViewModel(this);\n  readonly open = () => launchOtherPage(this.helper, {});\n\n  constructor() {}\n\n  ionViewWillEnter() {\n    this.vm.reload$.next();\n  }\n\n  ionViewWillLeave() {}\n\n  ngOnDestroy() {}\n}\n

❌ 誤り: implements なしのlifecycle method

\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage {\n  ionViewWillEnter() {} // error — needs implements ViewWillEnter\n  ngOnDestroy() {} // error — needs implements OnDestroy\n}\n

❌ 誤り: implementsしたinterfaceがmethodをカバーしていない

\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage implements ViewWillEnter {\n  ionViewWillEnter() {}\n  ionViewWillLeave() {} // error — needs implements ViewWillLeave\n}\n

❌ 誤り: Component上の任意method

\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage implements ViewWillEnter {\n  ionViewWillEnter() {}\n\n  open() {\n    // error\n    launchOtherPage(this.helper, {});\n  }\n}\n

Interface → method の対応

\n

Angular

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
implementsメソッド
OnChangesngOnChanges
OnInitngOnInit
DoCheckngDoCheck
AfterContentInitngAfterContentInit
AfterContentCheckedngAfterContentChecked
AfterViewInitngAfterViewInit
AfterViewCheckedngAfterViewChecked
OnDestroyngOnDestroy
\n

Ionic

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
implementsメソッド
ViewWillEnterionViewWillEnter
ViewDidEnterionViewDidEnter
ViewWillLeaveionViewWillLeave
ViewDidLeaveionViewDidLeave
ViewWillUnloadionViewWillUnload
\n

許可(報告しない): constructorget / set アクセサ、すべてのプロパティ。

\n

オプション

\n
{\n  // Extra method names to allow (e.g. trackBy helpers during migration).\n  additionalAllowedMethods?: string[];\n}\n
'@rdlabo/rules/no-component-method-except-lifecycle': [\n  'warn',\n  { additionalAllowedMethods: ['trackById'] },\n],\n

実装

\n\n", + "html": "\n
\n

@Component 上のlifecycle以外のmethodを禁止する。許可するlifecycle methodは implements から導出する(プロパティは許可)。

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
\n
\n

@Component クラスは薄く保ちます。振る舞いは ViewModel(またはmodalの launch* ヘルパー)に置きます。

\n

許可されるmethodは、implements に列挙したlifecycle interfaceと一致するものだけです。対応するinterfaceなしのlifecycle methodもエラーです。

\n

プロパティは対象外です。readonly open = () => ... のようなarrow-functionフィールドも含みます。getter / setter と constructor は許可されます。

\n

@Directive / @Injectable / 通常のクラスは検査しません。@rdlabo/rules/require-viewmodel および @rdlabo/rules/implements-ionic-lifecycle と併用します。

\n
\n

既存アプリにはComponent methodが多いことがよくあります。まず \"warn\" から始め、ViewModelへロジックを移してから \"error\" に切り替えるのが望ましいです。

\n
\n

ルール詳細

\n

✅ 正しい: methodが implements と一致する

\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage {\n  open() {\n    launchOtherPage(this.helper, {});\n  }\n\n  reload() {\n    this.vm.reload$.next();\n  }\n}\n

❌ 誤り: implements なしのlifecycle method

\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage {\n  ionViewWillEnter() {} // missing implements ViewWillEnter\n}\n

❌ 誤り: implementsしたinterfaceがmethodをカバーしていない

\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage implements ViewWillEnter, ViewWillLeave, OnDestroy {\n  readonly vm = new ViewModel(this);\n  readonly open = () => launchOtherPage(this.helper, {});\n\n  ionViewWillEnter() {\n    this.vm.reload$.next();\n  }\n\n  ionViewWillLeave() {}\n  ngOnDestroy() {}\n}\n

❌ 誤り: Component上の任意method

\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage implements ViewWillEnter {\n  ionViewWillEnter() {}\n\n  trackById(_index: number, item: { id: number }) {\n    return item.id;\n  }\n\n  customHook() {}\n}\n

Interface → method の対応

\n

Angular

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
implementsメソッド
OnChangesngOnChanges
OnInitngOnInit
DoCheckngDoCheck
AfterContentInitngAfterContentInit
AfterContentCheckedngAfterContentChecked
AfterViewInitngAfterViewInit
AfterViewCheckedngAfterViewChecked
OnDestroyngOnDestroy
\n

Ionic

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
implementsメソッド
ViewWillEnterionViewWillEnter
ViewDidEnterionViewDidEnter
ViewWillLeaveionViewWillLeave
ViewDidLeaveionViewDidLeave
ViewWillUnloadionViewWillUnload
\n

許可(報告しない): constructorget / set アクセサ、すべてのプロパティ。

\n

オプション

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/no-component-method-except-lifecycle\": [\n      \"error\",\n      {\n        \"additionalAllowedMethods\": [\"trackById\", \"customHook\"]\n      }\n    ]\n  }\n}\n
{\n  \"rules\": {\n    \"@rdlabo/rules/no-component-method-except-lifecycle\": [\n      \"error\",\n      {\n        \"additionalAllowedMethods\": []\n      }\n    ]\n  }\n}\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", @@ -452,8 +582,43 @@ export const PROJECT = { "file": "rules/no-component-writable-signal.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/no-component-writable-signal", - "html": "\n
\n

書き込み可能なComponent状態はViewModelに置く。ただしAngular Signal Formsの form() に渡すmodelは例外とする。

\n
\n

書き込み可能なComponent状態をViewModelに置きます。Componentローカルの signal()linkedSignal() は、profileForm = form(this.model) のようにSignal Formsのフィールド初期化へ直接渡すmodelプロパティでない限り拒否されます。

\n
readonly model = signal({ name: '' });\nreadonly profileForm = form(this.model); // valid\n\nreadonly isLoading = signal(false); // invalid: move to ViewModel\n

computed()effect() は引き続きComponentの責務であり、このルールでは制限しません。Component以外のクラスは無視します。

\n

@angular/core および @angular/forms/signals からの名前付きエイリアスと名前空間importに対応します。同名のローカルヘルパーは、ルールがimport元を検証するため無視されます。

\n

実装

\n\n", + "html": "\n
\n

書き込み可能なComponent状態はViewModelに置く。ただしAngular Signal Formsの form() に渡すmodelは例外とする。

\n
\n

このルールは、Angular ComponentとViewModelの間に明確な境界を強制します。Componentはtemplateに読み取り専用の派生状態を公開し、書き込み可能な状態はViewModelに置くことで、変更を一元化しtest可能にします。Componentで許可される唯一の書き込み可能なSignalは、Signal Formsの form() にmodelとして直接渡されるものです。

\n

ルール詳細

\n

@Component で装飾されたクラスを検査し、@angular/forms/signalsform() の第1引数に同じpropertyが渡されている場合を除き、@angular/coresignal() または linkedSignal() で初期化されたclass propertyを報告します。

\n
    \n
  • computed()effect() は引き続きComponentの責務であり、報告しません。
  • \n
  • Component以外のクラスは無視します。
  • \n
  • @angular/core@angular/forms/signals のalias importとnamespace importを認識します。
  • \n
  • import元を検証するため、同名のlocal helperは無視します。
  • \n
\n

Signal Formsの例外は、readonly pageForm = form(this.model) のようなComponent property initializerだけを認識します。method内でSignalを form() に渡しても例外にはならないため、書き込み可能なSignal propertyは引き続き報告されます。

\n

\n

誤り

\n
import { Component, signal } from '@angular/core';\n\n@Component({ template: '' })\nclass Page {\n  readonly isLoading = signal(false); // reported: move to ViewModel\n}\n
import { Component, signal } from '@angular/core';\nimport { form } from '@angular/forms/signals';\n\n@Component({ template: '' })\nclass Page {\n  readonly model = signal({ name: '' });\n  readonly loading = signal(false); // reported\n  readonly pageForm = form(this.model);\n}\n

正しい

\n
import { Component, computed } from '@angular/core';\nimport { form } from '@angular/forms/signals';\nimport { PageViewModel } from './page.viewmodel';\n\n@Component({ template: '' })\nclass Page {\n  private readonly vm = new PageViewModel(this);\n  readonly isLoading = this.vm.isLoading; // read-only view of ViewModel state\n  readonly model = this.vm.model;\n  readonly pageForm = form(this.model);\n  readonly title = computed(() => this.model().name);\n}\n
import { Component, signal as writable } from '@angular/core';\nimport { form as signalForm } from '@angular/forms/signals';\n\n@Component({ template: '' })\nclass Page {\n  readonly data = writable({ name: '' });\n  readonly pageForm = signalForm(this.data); // data is the Signal Forms model\n}\n

オプション

\n

このルールにオプションはありません。

\n

有効にする場面

\n

@rdlabo/rules/require-viewmodel とともにViewModel patternを使うプロジェクトで、このルールを有効にします。Component propertyを共有状態への読み取り専用viewにすることで、Componentによる状態の直接変更を防ぎます。

\n

関連項目

\n\n

実装

\n\n", "headings": [ + { + "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", + "text": "ルール詳細", + "level": 2 + }, + { + "id": "%E4%BE%8B", + "text": "例", + "level": 2 + }, + { + "id": "%E8%AA%A4%E3%82%8A", + "text": "誤り", + "level": 3 + }, + { + "id": "%E6%AD%A3%E3%81%97%E3%81%84", + "text": "正しい", + "level": 3 + }, + { + "id": "%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3", + "text": "オプション", + "level": 2 + }, + { + "id": "%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B%E5%A0%B4%E9%9D%A2", + "text": "有効にする場面", + "level": 2 + }, + { + "id": "%E9%96%A2%E9%80%A3%E9%A0%85%E7%9B%AE", + "text": "関連項目", + "level": 2 + }, { "id": "%E5%AE%9F%E8%A3%85", "text": "実装", @@ -471,8 +636,43 @@ export const PROJECT = { "file": "rules/no-reactive-forms.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/no-reactive-forms", - "html": "\n
\n

Angular Reactive Formsを禁止し、Signal Formsを推奨する。

\n
\n

@angular/forms/signals を優先するため、Angular Reactive FormsのAPIとテンプレートバインディングを禁止します。

\n

このルールは ReactiveFormsModule、型付き・型なしのcontrol/group/array/builder、reactive directive、Validators、および @angular/forms からの関連importを拒否します。名前付きAPI検査を迂回できるため、名前空間importとdefault importも拒否します。テンプレートでは formControlformControlNameformGroupformGroupNameformArrayName を拒否します。

\n

FormsModulengModel は意図的にこのルールの対象外です。それらのバインディングには no-template-driven-forms を使います。

\n

実装

\n\n", + "html": "\n
\n

Angular Reactive Formsを禁止し、Signal Formsを推奨する。

\n
\n

このルールは、Angular Reactive Formsから @angular/forms/signals への移行を支援します。Reactive Formsでは、Componentとservice間で共有されることの多い書き込み可能な FormControl / FormGroup 状態が必要なため、状態変更の発生元を追いにくくなります。Signal Formsではform状態をSignalsに保持するため、依存graphが明示的になり、デフォルトでreactiveになります。

\n

プロジェクトがSignal Formsを採用する間に、新しいReactive Forms codeが追加されるのを防ぎたい場合に使います。

\n

ルール詳細

\n

このルールは3つのpatternを報告します。

\n
    \n
  1. \n

    @angular/forms からのReactive Forms APIのnamed import
    \n次の名前のimportをすべて報告します。

    \n

    AbstractControl, FormArray, FormArrayName, FormBuilder, FormControl, FormControlDirective, FormControlName, FormGroup, FormGroupDirective, FormGroupName, FormRecord, NonNullableFormBuilder, ReactiveFormsModule, UntypedFormArray, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators.

    \n
  2. \n
  3. \n

    @angular/forms からのnamespace importまたはdefault import
    \nnamed APIの検査を迂回できるため、import * as forms from '@angular/forms'import forms from '@angular/forms' を報告します。

    \n
  4. \n
  5. \n

    Reactive Formsのtemplate binding
    \nAngular templateで次のbindingを報告します。
    \nformControl, formControlName, formGroup, formGroupName, formArrayName.

    \n
  6. \n
\n

FormsModulengModel は意図的にこのルールの対象外です。これらを制限するには@rdlabo/rules/no-template-driven-formsを使います。

\n

\n

誤り

\n
// TypeScript: importing Reactive Forms APIs\nimport { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';\n\nimport * as forms from '@angular/forms';\nconst control = new forms.FormControl('');\n
<!-- Template: Reactive Forms bindings -->\n<form [formGroup]=\"userForm\">\n  <input formControlName=\"name\" />\n</form>\n

正しい

\n
import { signal } from '@angular/core';\nimport { form, required } from '@angular/forms/signals';\n\nconst userModel = signal({ name: '' });\nconst userForm = form(userModel, (path) => {\n  required(path.name);\n});\n
<!-- Template: Signal Forms field binding -->\n<input [formField]=\"userForm.name\" />\n

オプション

\n

このルールにオプションはありません。

\n

有効にする場面

\n

Signal Formsを採用済み、またはReactive Formsから移行中のAngularプロジェクトで、このルールを有効にします。両方のform styleを対象にするため、@rdlabo/rules/no-template-driven-forms と同時に安全に有効化できます。

\n

関連項目

\n\n

実装

\n\n", "headings": [ + { + "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", + "text": "ルール詳細", + "level": 2 + }, + { + "id": "%E4%BE%8B", + "text": "例", + "level": 2 + }, + { + "id": "%E8%AA%A4%E3%82%8A", + "text": "誤り", + "level": 3 + }, + { + "id": "%E6%AD%A3%E3%81%97%E3%81%84", + "text": "正しい", + "level": 3 + }, + { + "id": "%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3", + "text": "オプション", + "level": 2 + }, + { + "id": "%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B%E5%A0%B4%E9%9D%A2", + "text": "有効にする場面", + "level": 2 + }, + { + "id": "%E9%96%A2%E9%80%A3%E9%A0%85%E7%9B%AE", + "text": "関連項目", + "level": 2 + }, { "id": "%E5%AE%9F%E8%A3%85", "text": "実装", @@ -490,8 +690,48 @@ export const PROJECT = { "file": "rules/no-template-driven-forms.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/no-template-driven-forms", - "html": "\n
\n

明示的に許可した要素上の ngModel バインディングを除き、template-driven formsを禁止する。

\n
\n

Signal Formsに適さないIonic Viewバインディングとして、要素名が明示的に許可されていない限り ngModel を禁止します。ngFormngModelGroup は常に拒否されます。

\n
'@rdlabo/rules/no-template-driven-forms': [\n  'error',\n  {\n    allowedElements: [\n      'ion-searchbar',\n      'ion-segment',\n      'ion-radio-group',\n      'ion-select',\n      'ion-range',\n      'ion-toggle',\n      'ion-checkbox',\n      'ion-input-otp',\n    ],\n  },\n];\n

許可要素は相互運用のための例外であり、template-driven formsの利用を推奨するものではありません。送信フォームは、許可要素を含んでいてもSignal Formsを使うべきです。

\n

実装

\n\n", + "html": "\n
\n

明示的に許可された要素の ngModel バインディングを除き、template-driven formsを禁止する。

\n
\n

このルールはAngularテンプレート内のtemplate-driven formsを制限します。ngFormngModelGroup はテンプレート内に可変フォーム状態を保持するため、常に拒否されます。ngModel も、Signal Formsに適さないIonic Viewバインディング向けに明示的に許可された要素でない限り拒否されます。

\n

許可要素は相互運用のための例外であり、template-driven formsの利用を推奨するものではありません。送信フォームでは、許可要素を含む場合でもSignal Formsを使用してください。

\n

ルール詳細

\n

このルールはAngularテンプレートに対して次の3パターンを検査します。

\n
    \n
  1. \n

    allowedElements に含まれない要素上の ngModel
    \n許可リストにないタグの ngModel[(ngModel)][ngModel] を報告します。単独の (ngModelChange) outputは検査しません。

    \n
  2. \n
  3. \n

    ngModelGroup 属性
    \nすべての要素上の ngModelGroup 属性を報告します。

    \n
  4. \n
  5. \n

    ngForm referenceまたはdirective
    \n<form #form=\"ngForm\"><div ngForm> を報告します。

    \n
  6. \n
\n

型情報は使用せず、parse済みのtemplate ASTだけを検査します。

\n

\n

誤り

\n
<!-- ngModel on an ordinary input -->\n<input [(ngModel)]=\"name\" />\n\n<!-- ngForm reference -->\n<form #form=\"ngForm\"></form>\n\n<!-- ngModelGroup directive -->\n<div ngModelGroup=\"address\"></div>\n

正しい

\n
<!-- Signal Forms field binding -->\n<input [formField]=\"userForm.name\" />\n\n<!-- ngModel allowed on ion-searchbar for a View binding -->\n<ion-searchbar [(ngModel)]=\"query\"></ion-searchbar>\n

オプション

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/no-template-driven-forms\": [\n      \"error\",\n      {\n        \"allowedElements\": [\"ion-searchbar\", \"ion-segment\", \"ion-radio-group\", \"ion-select\", \"ion-range\", \"ion-toggle\", \"ion-checkbox\", \"ion-input-otp\"]\n      }\n    ]\n  }\n}\n

allowedElements

\n
    \n
  • 型: string[]
  • \n
  • デフォルト: []
  • \n
\n

ngModel の使用を許可する要素のタグ名です。ion-searchbarion-toggle のように、View上の便宜として ngModel で値を公開するIonicコンポーネントを想定しています。要素が許可されていても、ngModelGroupngForm は報告されます。

\n

有効にする場合

\n

Angular Signal Formsへ移行しながら、特定のIonic Viewコンポーネントに限定して ngModel バインディングが必要なプロジェクトで有効にしてください。Reactive Formsを全面的に採用し、Signal Formsを導入する予定がない場合にのみ無効にします。

\n

関連項目

\n\n

実装

\n\n", "headings": [ + { + "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", + "text": "ルール詳細", + "level": 2 + }, + { + "id": "%E4%BE%8B", + "text": "例", + "level": 2 + }, + { + "id": "%E8%AA%A4%E3%82%8A", + "text": "誤り", + "level": 3 + }, + { + "id": "%E6%AD%A3%E3%81%97%E3%81%84", + "text": "正しい", + "level": 3 + }, + { + "id": "%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3", + "text": "オプション", + "level": 2 + }, + { + "id": "allowedelements", + "text": "allowedElements", + "level": 3 + }, + { + "id": "%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88", + "text": "有効にする場合", + "level": 2 + }, + { + "id": "%E9%96%A2%E9%80%A3%E9%A0%85%E7%9B%AE", + "text": "関連項目", + "level": 2 + }, { "id": "%E5%AE%9F%E8%A3%85", "text": "実装", @@ -509,7 +749,7 @@ export const PROJECT = { "file": "rules/prefer-disable-handler.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/prefer-disable-handler", - "html": "\n
\n

設定した要素/イベントバインディングで、非同期処理中の二重タップを防ぐwrapper method(デフォルト: disableHandler($event, work))を要求する

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
\n
\n

disableHandler@rdlabo/ionic-angular-kit 由来)は、非同期操作の実行中にきっかけとなったコントロールを無効化し、Promiseがsettledしたときに復元します。同期処理に使っても害はありません。コントロールは一瞬無効化されて戻され、無効化可能な対象が見つからない場合は何もしません。

\n

要素名・イベント名・wrapper method名はすべて設定可能なので、フリート共通のデフォルトと、リポジトリ固有のバインディング(例: (ionComplete))の両方を同じルールでカバーできます。

\n

recommended でこのルールを有効にすると、wrapperなしで非同期処理を呼ぶ既存テンプレートが指摘されます。プラグインを上げるときは移行パスを見込んでください。

\n

ルール詳細

\n

❌ 誤り(デフォルト)

\n
<ion-button (click)=\"vm.save()\">Save</ion-button>\n<form (submit)=\"vm.save()\"></form>\n

次も誤りです。wrapperは $event と第2引数の work を受け取る必要があります。

\n
<ion-button (click)=\"vm.disableHandler($event)\">Save</ion-button> <ion-button (click)=\"vm.disableHandler(vm.save())\">Save</ion-button>\n

✅ 正しい(デフォルト)

\n
<ion-button (click)=\"vm.disableHandler($event, vm.save())\">Save</ion-button>\n<form (submit)=\"vm.disableHandler($event, vm.save())\"></form>\n

wrapperなしで許可(イベントのみ、デフォルト):

\n
<ion-button (click)=\"$event.stopPropagation()\"></ion-button>\n
<ion-button (click)=\"$event.preventDefault()\"></ion-button>\n

デフォルトでは対象外(targets.elements に未掲載):

\n
<ion-chip (click)=\"vm.toggle()\"></ion-chip>\n
<ion-item [button]=\"true\" (click)=\"vm.open()\"></ion-item>\n

オプション

\n
{\n  method?: string; // default: 'disableHandler'\n  eventParam?: string; // default: '$event'\n  targets?: Array<{\n    events: string[]; // e.g. ['click'], ['submit'], ['ionComplete']\n    elements?: string[]; // omit / [] = any element for those events\n  }>;\n  allowEventMethods?: string[]; // default: ['stopPropagation', 'preventDefault']\n}\n

targets はデフォルト一覧を完全に置き換えます(マージしません)。click/submitを維持しつつ追加する場合は、デフォルトを再掲したうえで追加分を足します。

\n

デフォルト

\n
{\n  method: 'disableHandler',\n  eventParam: '$event',\n  targets: [\n    { events: ['click'], elements: ['ion-button', 'button'] },\n    { events: ['submit'] }, // any element\n  ],\n  allowEventMethods: ['stopPropagation', 'preventDefault'],\n}\n

\n

カスタムwrapper名を要求する:

\n
'@rdlabo/rules/prefer-disable-handler': ['error', { method: 'guardClick' }]\n

ion-input(ionComplete) も強制する(デフォルトのtargetsを再掲する):

\n
'@rdlabo/rules/prefer-disable-handler': [\n  'error',\n  {\n    targets: [\n      { events: ['click'], elements: ['ion-button', 'button'] },\n      { events: ['submit'] },\n      { events: ['ionComplete'], elements: ['ion-input'] },\n    ],\n  },\n]\n

実装

\n\n", + "html": "\n
\n

非同期処理中の二重タップを防ぐため、設定した要素とイベントのバインディングにwrapper method(デフォルト: disableHandler($event, work))を要求する

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
\n
\n

非同期処理を開始するbuttonをユーザーがtapしたら、処理がsettleするまでcontrolを無効にする必要があります。そうしなければ、2回目のtapで同じactionが再実行される可能性があります。このルールは、設定した (event) bindingにwrapper呼び出し構文を強制します。UIの無効化とwork値の適切な処理はwrapper実装の責務です。

\n

ルール詳細

\n

Angularテンプレートを検査します。設定対象に一致する各 BoundEvent のhandler expressionは、2つ以上の引数を持つwrapper method呼び出しでなければなりません。

\n
    \n
  1. event parameter(デフォルトは $event)。
  2. \n
  3. wrapperへ渡すwork expression。
  4. \n
\n

たとえば (click)=\"vm.disableHandler($event, vm.save())\" は有効です。(click)=\"vm.save()\" は報告されます。第2引数の型やPromiseを返すかどうかは検査しません。

\n

$event.stopPropagation()$event.preventDefault() のようなevent methodの単独呼び出しも許可します(allowEventMethods で設定可能)。

\n

デフォルトの対象は次のとおりです。

\n
    \n
  • <ion-button><button>click
  • \n
  • すべての要素の submit
  • \n
\n

.spec.html ファイルは無視します。

\n

オプション

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/prefer-disable-handler\": [\n      \"error\",\n      {\n        \"method\": \"disableHandler\",\n        \"eventParam\": \"$event\",\n        \"targets\": [{ \"events\": [\"click\"], \"elements\": [\"ion-button\", \"button\"] }, { \"events\": [\"submit\"] }],\n        \"allowEventMethods\": [\"stopPropagation\", \"preventDefault\"]\n      }\n    ]\n  }\n}\n

method

\n
    \n
  • 型: string
  • \n
  • デフォルト: \"disableHandler\"
  • \n
\n

handler expressionに要求するwrapper method名です。

\n

eventParam

\n
    \n
  • 型: string
  • \n
  • デフォルト: \"$event\"
  • \n
\n

wrapper methodの第1引数として渡す必要がある値です。

\n

targets

\n
    \n
  • 型: Target[]
  • \n
  • デフォルト: [{ events: ['click'], elements: ['ion-button', 'button'] }, { events: ['submit'] }]
  • \n
\n

各targetはwrapperを要求するeventと要素を指定します。elements は任意で、省略するとそのeventを持つすべての要素に適用されます。

\n

allowEventMethods

\n
    \n
  • 型: string[]
  • \n
  • デフォルト: [\"stopPropagation\", \"preventDefault\"]
  • \n
\n

wrapperなしで許可するevent methodです。たとえば (click)=\"$event.stopPropagation()\" は有効です。

\n

\n

誤り

\n
<ion-button (click)=\"vm.save()\">Save</ion-button>\n
<form (submit)=\"vm.save()\"></form>\n
<ion-button (click)=\"vm.disableHandler(vm.save())\">missing $event</ion-button>\n

正しい

\n
<ion-button (click)=\"vm.disableHandler($event, vm.save())\">Save</ion-button>\n
<form (submit)=\"vm.disableHandler($event, vm.save())\">\n  <ion-button type=\"submit\">Save</ion-button>\n</form>\n
<ion-button (click)=\"$event.stopPropagation()\"></ion-button>\n

カスタム設定

\n
<ion-input (ionComplete)=\"vm.disableHandler($event, vm.join())\"></ion-input>\n
{\n  \"rules\": {\n    \"@rdlabo/rules/prefer-disable-handler\": [\n      \"error\",\n      {\n        \"targets\": [{ \"events\": [\"ionComplete\"], \"elements\": [\"ion-input\"] }]\n      }\n    ]\n  }\n}\n

有効にする場合

\n

API呼び出し、navigation、modal表示などの非同期処理をユーザー操作から開始するIonic/Angularプロジェクトで有効にしてください。@rdlabo/rules/prefer-modal-launcher および @rdlabo/rules/deny-element と組み合わせることで、overlay logicを一元化できます。

\n

関連項目

\n\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", @@ -522,15 +762,55 @@ export const PROJECT = { "level": 2 }, { - "id": "%E3%83%87%E3%83%95%E3%82%A9%E3%83%AB%E3%83%88", - "text": "デフォルト", + "id": "method", + "text": "method", + "level": 3 + }, + { + "id": "eventparam", + "text": "eventParam", + "level": 3 + }, + { + "id": "targets", + "text": "targets", + "level": 3 + }, + { + "id": "alloweventmethods", + "text": "allowEventMethods", "level": 3 }, { "id": "%E4%BE%8B", "text": "例", + "level": 2 + }, + { + "id": "%E8%AA%A4%E3%82%8A", + "text": "誤り", "level": 3 }, + { + "id": "%E6%AD%A3%E3%81%97%E3%81%84", + "text": "正しい", + "level": 3 + }, + { + "id": "%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E8%A8%AD%E5%AE%9A", + "text": "カスタム設定", + "level": 3 + }, + { + "id": "%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88", + "text": "有効にする場合", + "level": 2 + }, + { + "id": "%E9%96%A2%E9%80%A3%E9%A0%85%E7%9B%AE", + "text": "関連項目", + "level": 2 + }, { "id": "%E5%AE%9F%E8%A3%85", "text": "実装", @@ -541,6 +821,55 @@ export const PROJECT = { "scrollMap": [], "editUrl": "https://github.com/rdlabo-dev/docs/edit/main/src/eslint-plugin-rules/docs/ja/rules/prefer-disable-handler.md" }, + { + "title": "prefer-ionic-standalone", + "navTitle": "prefer-ionic-standalone", + "slug": "rules/prefer-ionic-standalone", + "file": "rules/prefer-ionic-standalone.md", + "section": "ルール", + "path": "/projects/eslint-plugin-rules/docs/rules/prefer-ionic-standalone", + "html": "\n
\n

Ionic 9のstandalone APIを優先し、IonicModuleおよび廃止済み・NgModuleベースのentry pointを禁止します。

\n
    \n
  • ⭐️ このruleは plugin:@rdlabo/rules/recommended presetに含まれます。
  • \n
  • ✒️ command line--fix optionで、報告された問題の一部を自動修正できます。
  • \n
\n
\n

Ionic 9はstandalone Angular componentを @ionic/angular からexportします。このruleは、廃止された @ionic/angular/standalone entry point、NgModuleベースの @ionic/angular/lazy entry point、IonicModule 自体を禁止し、アプリケーションをstandalone API surfaceに保ちます。

\n

Rule Details

\n

import、named re-export、export-all declaration、namespace import経由の IonicModule accessを検査します。namespace accessはscopeから解決するため、同名のlocal variableでshadowされている場合は報告しません。

\n

Examples

\n

Incorrect

\n
import { IonButton } from '@ionic/angular/standalone';\nimport { IonInput } from '@ionic/angular/lazy';\nimport { IonicModule } from '@ionic/angular';\n

Correct

\n
import { IonButton, IonInput, ModalController, provideIonicAngular } from '@ionic/angular';\n

/standalone/lazy からのnamed import・named re-exportは、元のquote styleを維持して @ionic/angular へ自動修正されます。side-effect import、namespace import、export * declarationはentry pointの変更がruntime behaviorを変える可能性があるため、修正せず報告します。IonicModule.forRoot() とNgModule metadataの置き換えにはアプリケーション単位の変更が必要なため、IonicModule も修正せず報告します。

\n

Options

\n

このruleにoptionはありません。severityはESLint設定で warn または error に指定します。

\n

When to enable

\n

standalone bootstrapを採用したIonic 9 Angularアプリケーションで有効にしてください。@ionic/angular/lazyIonicModule は常に禁止されるため、NgModuleアプリケーションは有効化前にstandalone migrationを完了してください。

\n

Implementation

\n\n", + "headings": [ + { + "id": "rule-details", + "text": "Rule Details", + "level": 2 + }, + { + "id": "examples", + "text": "Examples", + "level": 2 + }, + { + "id": "incorrect", + "text": "Incorrect", + "level": 3 + }, + { + "id": "correct", + "text": "Correct", + "level": 3 + }, + { + "id": "options", + "text": "Options", + "level": 2 + }, + { + "id": "when-to-enable", + "text": "When to enable", + "level": 2 + }, + { + "id": "implementation", + "text": "Implementation", + "level": 2 + } + ], + "codes": [], + "scrollMap": [], + "editUrl": "https://github.com/rdlabo-dev/docs/edit/main/src/eslint-plugin-rules/docs/ja/rules/prefer-ionic-standalone.md" + }, { "title": "prefer-modal-launcher", "navTitle": "prefer-modal-launcher", @@ -548,7 +877,7 @@ export const PROJECT = { "file": "rules/prefer-modal-launcher.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/prefer-modal-launcher", - "html": "\n
\n

presentModal 呼び出しを launch* launcher関数内に置くことを要求する。

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
\n
\n

呼び出し側は helper.presentModal(...) をインラインで書かず、await launchXxxPage(helper, props) を使う必要があります。

\n

各modalページは次をエクスポートするべきです。

\n
    \n
  • XxxProps / XxxDismiss
  • \n
  • presentModal を包む launchXxxPage(helper, props)
  • \n
\n

@rdlabo/rules/deny-overlay-create および @rdlabo/rules/deny-element と併用します。

\n

ルール詳細

\n

❌ 誤り: 呼び出し側で presentModal をインライン実行する

\n
export class ExamplePage {\n  readonly helper = inject(HelperService);\n\n  async open() {\n    await this.helper.presentModal(OtherPage, {}); // error\n  }\n}\n

次も誤りです。

\n
export async function openModal(overlay: Helper) {\n  await overlay.presentModal(ExamplePage, {}); // error — name does not match /^launch/\n}\n\nconst show = () => overlay.presentModal(ExamplePage, {}); // error\n

✅ 正しい: presentModal はlauncher内だけに置く

\n
export interface OtherProps {\n  id: number;\n}\nexport type OtherDismiss = { saved: boolean } | undefined;\n\nexport const launchOtherPage = (helper: HelperService, props: OtherProps): Promise<OtherDismiss> => {\n  return helper.presentModal(OtherPage, props, { watchKeyboard: false });\n};\n\nexport class ExamplePage {\n  readonly helper = inject(HelperService);\n\n  async open() {\n    const data = await launchOtherPage(this.helper, { id: 1 });\n    if (data?.saved) {\n      // ...\n    }\n  }\n}\n

launcher内のネストした呼び出しは問題ありません。

\n
export const launchExamplePage = (overlay: Helper, props: Props) => {\n  const run = () => overlay.presentModal(ExamplePage, props);\n  return run();\n};\n

オプション

\n
{\n  // Method names treated as overlay presenters.\n  // default: ['presentModal']\n  presentMethodNames?: string[];\n\n  // RegExp source for allowed enclosing function / method names.\n  // default: '^launch'\n  launcherNamePattern?: string;\n}\n
'@rdlabo/rules/prefer-modal-launcher': [\n  'error',\n  {\n    presentMethodNames: ['presentModal'],\n    launcherNamePattern: '^launch',\n  },\n],\n

プロジェクトが open* launcherを使う場合は次のようにします。

\n
{\n  launcherNamePattern: '^(launch|open)';\n}\n

実装

\n\n", + "html": "\n
\n

presentModal 呼び出しを launch* launcher関数内に置くことを要求する。

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
\n
\n

modalとsheetは、対象pageからexportされた専用launcher関数を介して表示してください。これにより、呼び出し側をmodal構築の詳細から分離し、application全体でmodal APIを統一できます。このルールは、presentModal(または設定した他のpresent method)がlauncher patternに一致する名前の関数内でのみ呼び出されることを保証します。

\n

ルール詳細

\n

presentModalhelper.presentModal(...)overlay.presentSheet(...) などの呼び出しについて CallExpression nodeを検査します。launcher関数内にない呼び出しは報告されます。

\n

launcher関数とは、設定した正規表現(デフォルトは ^launch)に名前が一致する関数です。次の形式を検査します。

\n
    \n
  • function launchXxx(...)
  • \n
  • const launchXxx = (...)
  • \n
  • class Foo { launchXxx = (...) }
  • \n
  • class Foo { launchXxx() {} }
    \nネストした関数も考慮されます。たとえば launchExamplePage 内の run arrowは許可されます。
  • \n
\n

オプション

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/prefer-modal-launcher\": [\n      \"error\",\n      {\n        \"presentMethodNames\": [\"presentModal\"],\n        \"launcherNamePattern\": \"^launch\"\n      }\n    ]\n  }\n}\n

presentMethodNames

\n
    \n
  • 型: string[]
  • \n
  • デフォルト: [\"presentModal\"]
  • \n
\n

制限対象とするpresent method名です。

\n

launcherNamePattern

\n
    \n
  • 型: string
  • \n
  • デフォルト: \"^launch\"
  • \n
\n

正規表現を表す文字列です。present method呼び出しは、このpatternに名前が一致する関数内になければなりません。

\n

\n

誤り

\n
export class ExamplePage {\n  readonly helper = inject(HelperService);\n\n  async open() {\n    await this.helper.presentModal(OtherPage, {}); // not in a launcher\n  }\n}\n
export class ExamplePage {\n  readonly launchOtherPage = this.helper.presentModal(OtherPage, {}); // not a function\n}\n
export async function openModal(overlay: Helper) {\n  await overlay.presentModal(ExamplePage, {}); // name does not match ^launch\n}\n

正しい

\n
export const launchExamplePage = (overlay: Helper, props: Props) => {\n  return overlay.presentModal(ExamplePage, props);\n};\n
export function launchExamplePage(overlay: Helper, props: Props) {\n  return overlay.presentModal(ExamplePage, props);\n}\n
export const launchExamplePage = (overlay: Helper, props: Props) => {\n  const run = () => overlay.presentModal(ExamplePage, props);\n  return run();\n};\n

カスタム設定

\n
export const openSheet = (overlay: Helper) => {\n  return overlay.presentSheet(SheetPage, {});\n};\n
{\n  \"rules\": {\n    \"@rdlabo/rules/prefer-modal-launcher\": [\n      \"error\",\n      {\n        \"presentMethodNames\": [\"presentSheet\"],\n        \"launcherNamePattern\": \"^(launch|open)\"\n      }\n    ]\n  }\n}\n

有効にする場合

\n

modal、sheet、その他のoverlayにlauncher patternを採用するIonic/Angularプロジェクトで有効にしてください。@rdlabo/rules/deny-element および @rdlabo/rules/prefer-disable-handler と組み合わせて使用します。

\n

関連項目

\n\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", @@ -560,6 +889,46 @@ export const PROJECT = { "text": "オプション", "level": 2 }, + { + "id": "presentmethodnames", + "text": "presentMethodNames", + "level": 3 + }, + { + "id": "launchernamepattern", + "text": "launcherNamePattern", + "level": 3 + }, + { + "id": "%E4%BE%8B", + "text": "例", + "level": 2 + }, + { + "id": "%E8%AA%A4%E3%82%8A", + "text": "誤り", + "level": 3 + }, + { + "id": "%E6%AD%A3%E3%81%97%E3%81%84", + "text": "正しい", + "level": 3 + }, + { + "id": "%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E8%A8%AD%E5%AE%9A", + "text": "カスタム設定", + "level": 3 + }, + { + "id": "%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88", + "text": "有効にする場合", + "level": 2 + }, + { + "id": "%E9%96%A2%E9%80%A3%E9%A0%85%E7%9B%AE", + "text": "関連項目", + "level": 2 + }, { "id": "%E5%AE%9F%E8%A3%85", "text": "実装", @@ -570,6 +939,60 @@ export const PROJECT = { "scrollMap": [], "editUrl": "https://github.com/rdlabo-dev/docs/edit/main/src/eslint-plugin-rules/docs/ja/rules/prefer-modal-launcher.md" }, + { + "title": "require-ion-item-group", + "navTitle": "require-ion-item-group", + "slug": "rules/require-ion-item-group", + "file": "rules/require-ion-item-group.md", + "section": "ルール", + "path": "/projects/eslint-plugin-rules/docs/rules/require-ion-item-group", + "html": "\n
\n

ion-list 内の ion-item を、対応するIonic item groupで囲むことを要求します。

\n
    \n
  • ⭐️ このruleは plugin:@rdlabo/rules/recommended presetに含まれます。
  • \n
  • ✒️ command line--fix optionで、報告された問題の一部を自動修正できます。
  • \n
\n
\n

IonicのiOS 26とMaterial Design 3のlist styleでは、itemをその挙動に対応するgroup componentで構成する必要があります。このruleは、groupで囲まれていない ion-itemion-list 直下にrenderされることを防ぎます。

\n

Rule Details

\n

ion-list 内の ion-item は、次のいずれかの構造を正確に使う必要があります。

\n
    \n
  • ion-list > ion-item-group > ion-item
  • \n
  • ion-list > ion-reorder-group > ion-item
  • \n
  • ion-list > ion-accordion-group > ion-accordion > ion-item
  • \n
  • ion-list > ion-radio-group > ion-item
  • \n
\n

@if@for@empty@switch@defer などのAngular control-flow blockはelementをrenderしないため、この構造検査ではtransparentとして扱います。ng-containerng-template もtransparentです。renderされるHTMLまたはAngular elementはtransparentではありません。list、group、itemの間に div を挿入すると報告されます。

\n

このruleは ion-list に含まれる ion-item だけを検査します。list外の ion-item は報告せず、.spec.html fileは無視します。

\n

Examples

\n

Incorrect

\n
<ion-list>\n  <ion-item>Direct item</ion-item>\n</ion-list>\n

\n
<ion-list>\n  @for (item of items; track item.id) {\n    <ion-item>{{ item.name }}</ion-item>\n  }\n</ion-list>\n

Correct

\n

\n
<ion-list>\n  <ion-item-group>\n    @for (item of items; track item.id) {\n      <ion-item>{{ item.name }}</ion-item>\n    }\n  </ion-item-group>\n</ion-list>\n
<ion-list>\n  <ion-radio-group>\n    <ion-item>First choice</ion-item>\n    <ion-item>Second choice</ion-item>\n  </ion-radio-group>\n</ion-list>\n

Options

\n

このruleにoptionはありません。

\n

Automatic fixes

\n

listにgroup化されていない ion-item だけが含まれる場合、transparentなAngular control-flow blockや ng-container を経由していても、listの内容全体を1つの ion-item-group で囲めます。

\n

同じtemplateですでに ion-item-group が使われており、standalone IonItemGroup componentを利用できると判断できる場合は、自動修正を利用できます。それ以外では、必要に応じてcomponent importsへ IonItemGroup を追加するよう促すeditor suggestionを提供します。

\n

group化済み・未group化の内容が混在する場合、ほかのrendered content、再利用可能な ng-template 定義、nested list、間に入るrendered element、不正なaccordion構造がある場合は、修正もsuggestionも提供しません。これらのケースでは意図したgroup境界を安全に判断できません。

\n

When to enable

\n

iOS 26とMaterial Design 3のlist designを対象とするIonic Angularアプリケーションで有効にしてください。recommended presetに含まれ、template内の ion-listion-item がなければ影響しません。

\n

Implementation

\n\n", + "headings": [ + { + "id": "rule-details", + "text": "Rule Details", + "level": 2 + }, + { + "id": "examples", + "text": "Examples", + "level": 2 + }, + { + "id": "incorrect", + "text": "Incorrect", + "level": 3 + }, + { + "id": "correct", + "text": "Correct", + "level": 3 + }, + { + "id": "options", + "text": "Options", + "level": 2 + }, + { + "id": "automatic-fixes", + "text": "Automatic fixes", + "level": 2 + }, + { + "id": "when-to-enable", + "text": "When to enable", + "level": 2 + }, + { + "id": "implementation", + "text": "Implementation", + "level": 2 + } + ], + "codes": [], + "scrollMap": [], + "editUrl": "https://github.com/rdlabo-dev/docs/edit/main/src/eslint-plugin-rules/docs/ja/rules/require-ion-item-group.md" + }, { "title": "require-viewmodel", "navTitle": "require-viewmodel", @@ -577,18 +1000,73 @@ export const PROJECT = { "file": "rules/require-viewmodel.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/require-viewmodel", - "html": "\n
\n

Componentの new ViewModel(this)ViewModelStore<ComponentType, Keys> 継承を強制し、View APIをViewModelから排除する。

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
\n
\n

Ionic AngularのComponent / ViewModel分割を1つのルールで強制します。同居するクラス名のデフォルトは ViewModel です。

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
検査要件
Component所有すべての @Componentnew ViewModel(this) で初期化されたフィールドを持つ
構築第1引数は this でなければならない
ホスト境界ViewModelは ViewModelStore<ComponentType, Keys> を継承する。Keys は任意で、明示的な非SignalのComponentプロパティを公開する
Constructor / hostViewModelはデフォルトで両者を継承する。任意のconstructorは super(host)host を転送しなければならない
View APIviewChild / viewChildren / contentChild / contentChildren / effect / computed / 描画lifecycle APIは ViewModel 内に現れてはならない
\n

所有検査では @Component 以外のクラスは無視します。@rdlabo/rules/no-component-method-except-lifecycle と併用します。

\n

ルール詳細

\n

✅ Signalとoutputホストのみ

\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage {\n  readonly vm = new ViewModel(this);\n}\n\nclass ViewModel extends ViewModelStore<ExamplePage> {\n  save(): void {\n    this.host.saved.emit();\n  }\n}\n

ViewModelStore がconstructorを所有し、Componentオブジェクトを保持します。公開の host@rdlabo/ionic-angular-kitViewModelHost<T, K> 型を使うため、クラスフィールド初期化時にコピーされるのではなく、ViewModel method実行時に値が読まれます。基底constructorはホストの絞り込みと一度きりの描画フックを、kitの mountViewModel() ヘルパーへ委譲します。

\n

✅ 明示的な非Signal依存

\n
class ViewModel extends ViewModelStore<EntryPage, 'entryForm' | 'inventoryModel'> {\n  save(): void {\n    this.host.entryForm.save();\n  }\n}\n

第2型引数は任意です。TypeScriptはそのキーがComponentに属することを検査します。ハードプライベートなComponentフィールドは Pick で公開できないため、ViewModelが必要な場合は公開の readonly 境界プロパティを使います。

\n

✅ 共有ViewModel基底

\n
class ViewModel extends MainViewModel<FoodsPage> {}\nclass ViewModel extends ListViewModel<WineListPage> {}\nclass ViewModel extends ModelSearch<SearchPage, SearchCondition> {}\n

名前が ViewModel で終わる中間基底と、確立された ModelSearch 基底は、第1型引数が所有Componentと一致するときに受け入れられます。中間基底自体は ViewModelStore を継承している必要があります。

\n

ジェネリックなViewModelはデフォルトのComponentホストを使えます。所有検査にはそのデフォルトが使われます。

\n
class ViewModel<THost = MainPage> extends ViewModelStore<THost> {}\n

ハードプライベートなViewModel所有も問題ありません。

\n
readonly #vm = new ViewModel(this);\n

❌ ViewModelなし、または this なしのComponent

\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage {\n  readonly title = 'x'; // error: missingViewModel\n}\n\nreadonly vm = new ViewModel();\nreadonly vm = new ViewModel(other);\n

❌ 旧来のViewModelごとのhostパターン

\n
class ViewModel extends StoreModel {\n  readonly host: ReactiveHost<ExamplePage>; // error\n\n  constructor(host: ExamplePage) {\n    super();\n    this.host = host;\n  }\n}\n

extends ViewModelStore<ExamplePage> を使い、hostメンバーを削除します。ViewModelのconstructorで host.someProperty をキャッシュしないでください。vm より後に宣言されたComponentクラスフィールドは、まだ初期化されていません。

\n

通常、constructorは不要です。Componentの初期化後に待つ必要があるセットアップには onMount() を優先します。既存の即時constructor副作用を残す必要がある場合は、同じ型のhostを転送する必要があります。

\n
class ViewModel extends ViewModelStore<ExamplePage, 'inventoryModel'> {\n  protected override onMount(): void {\n    this.host.inventoryModel.initialize();\n  }\n}\n

互換のために即時constructorも有効です。

\n
class ViewModel extends ViewModelStore<ExamplePage> {\n  constructor(host: ExamplePage) {\n    super(host);\n    registerCleanup();\n  }\n}\n

❌ ViewModel上のView API

\n
class ViewModel extends ViewModelStore<ExamplePage> {\n  readonly el = viewChild('host'); // error\n  readonly label = computed(() => 'x'); // error\n}\n

viewChild.required(...) も禁止されます。afterNextRender / afterEveryRender / afterRenderEffect は個別のViewModelではなく、kitの mountViewModel() ヘルパーまたはComponentに属します。

\n

オプション

\n
{\n  // Class name treated as the ViewModel. default: 'ViewModel'\n  viewModelClassName?: string;\n\n  // Required base-class name. default: 'ViewModelStore'\n  viewModelStoreClassName?: string;\n\n  // Call expressions banned inside ViewModel.\n  // default also denies afterNextRender / afterEveryRender / afterRenderEffect\n  bannedApis?: string[];\n}\n
'@rdlabo/rules/require-viewmodel': 'error';\n

カスタム名:

\n
[\n  'error',\n  {\n    viewModelClassName: 'PageState',\n    viewModelStoreClassName: 'HostedStore',\n  },\n];\n

実装

\n\n", + "html": "\n
\n

Componentの new ViewModel(this)ViewModelStore<ComponentType, Keys> 継承を強制し、View APIをViewModelから排除する。

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
\n
\n

ViewModel architecture patternを強制します。Angular Componentは new ViewModel(this) で初期化したViewModelを所有しなければなりません。少なくとも1つの一致するpropertyを要求しますが、追加のViewModel instanceは拒否しません。ViewModelは ViewModelStore<ComponentType> を継承し、host を再宣言したり、viewChildeffectcomputedafterNextRender などのView固有APIを含めたりしないでください。

\n

ルール詳細

\n

次の3つを検査します。

\n

1. ComponentはViewModelを所有する

\n

@Component classには new ViewModel(this) で初期化したpropertyが必要です。constructor呼び出しの第1引数は this でなければなりません。

\n

2. ViewModelは ViewModelStore<ComponentType> を継承する

\n

ViewModel(または設定した viewModelClassName)というclassは、ViewModelStore<...>、名前が ViewModel で終わるbase、または ModelSearch を継承しなければなりません。最初のgeneric引数はhost Component型でなければなりません。中間classのgeneric defaultも解決します。

\n
    \n
  • ViewModelStore<ExamplePage, 'model' | 'form'> を使う場合、第2引数以降の型引数が許可されます。
  • \n
  • ViewModelStore を直接継承するときに型引数が2つを超えると報告されます。
  • \n
  • host型はViewModelを所有するComponentと一致する必要があります。
  • \n
\n

3. ViewModelにView APIを含めない

\n

ViewModel classでは次のAPIを呼び出せません。

\n

viewChild, viewChildren, contentChild, contentChildren, effect, computed, afterNextRender, afterEveryRender, afterRenderEffect.

\n

この一覧は bannedApis optionで変更できます。viewChild() のような直接呼び出しと、viewChild.required() のような .required() variantを認識します。namespace prefix付き呼び出しは解決しません。

\n

\n

誤り

\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage {\n  readonly title = 'x'; // no ViewModel\n}\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage {\n  readonly vm = new ViewModel(); // missing `this`\n}\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage {\n  readonly vm = new ViewModel(this);\n}\n\nclass ViewModel extends StoreModel {} // wrong base class\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage {\n  readonly vm = new ViewModel(this);\n}\n\nclass ViewModel extends ViewModelStore<ExamplePage> {\n  readonly el = viewChild('host'); // View API in ViewModel\n}\n

正しい

\n
import { Component, computed, effect, viewChild } from '@angular/core';\n\n@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage {\n  readonly vm = new ViewModel(this);\n  readonly title = computed(() => this.vm.label());\n  readonly el = viewChild('host');\n\n  constructor() {\n    effect(() => this.vm.label());\n  }\n}\n\nclass ViewModel extends ViewModelStore<ExamplePage> {\n  readonly label = signal('hello');\n}\n
@Component({ selector: 'app-example', template: '' })\nexport class ExamplePage {\n  readonly vm = new ViewModel(this);\n}\n\nclass ViewModel extends ViewModelStore<ExamplePage, 'inventoryModel'> {\n  readonly inventoryModel = signal<Inventory | null>(null);\n}\n
@Component({ selector: 'app-example', template: '' })\nexport class FoodsPage {\n  readonly vm = new ViewModel(this);\n}\n\nclass ViewModel extends MainViewModel<FoodsPage> {}\n

オプション

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/require-viewmodel\": [\n      \"error\",\n      {\n        \"viewModelClassName\": \"ViewModel\",\n        \"viewModelStoreClassName\": \"ViewModelStore\",\n        \"bannedApis\": [\n          \"viewChild\",\n          \"viewChildren\",\n          \"contentChild\",\n          \"contentChildren\",\n          \"effect\",\n          \"computed\",\n          \"afterNextRender\",\n          \"afterEveryRender\",\n          \"afterRenderEffect\"\n        ]\n      }\n    ]\n  }\n}\n

viewModelClassName

\n
    \n
  • 型: string
  • \n
  • デフォルト: \"ViewModel\"
  • \n
\n

Component内で検索するclass名です。PageState など別の命名規則を使うプロジェクトで指定します。

\n

viewModelStoreClassName

\n
    \n
  • 型: string
  • \n
  • デフォルト: \"ViewModelStore\"
  • \n
\n

ViewModelが継承すべきbase class名、または名前が ViewModel で終わる中間base class名です。

\n

bannedApis

\n
    \n
  • 型: string[]
  • \n
  • デフォルト: 上記の一覧
  • \n
\n

ViewModel内で許可しないAPIです。直接呼び出しと .required(...) の使用を検出します。namespace prefix付き呼び出しは解決しません。

\n

有効にする場合

\n

@rdlabo/ionic-angular-kit または同様のarchitectureでViewModel patternを採用するプロジェクトで有効にしてください。@rdlabo/rules/no-component-writable-signal と組み合わせると、Component stateをread-only、ViewModel stateをwritableに保てます。

\n

関連項目

\n\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", "text": "ルール詳細", "level": 2 }, + { + "id": "1.-component%E3%81%AFviewmodel%E3%82%92%E6%89%80%E6%9C%89%E3%81%99%E3%82%8B", + "text": "1. ComponentはViewModelを所有する", + "level": 3 + }, + { + "id": "2.-viewmodel%E3%81%AF-viewmodelstore%3Ccomponenttype%3E-%E3%82%92%E7%B6%99%E6%89%BF%E3%81%99%E3%82%8B", + "text": "2. ViewModelは ViewModelStore を継承する", + "level": 3 + }, + { + "id": "3.-viewmodel%E3%81%ABview-api%E3%82%92%E5%90%AB%E3%82%81%E3%81%AA%E3%81%84", + "text": "3. ViewModelにView APIを含めない", + "level": 3 + }, + { + "id": "%E4%BE%8B", + "text": "例", + "level": 2 + }, + { + "id": "%E8%AA%A4%E3%82%8A", + "text": "誤り", + "level": 3 + }, + { + "id": "%E6%AD%A3%E3%81%97%E3%81%84", + "text": "正しい", + "level": 3 + }, { "id": "%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3", "text": "オプション", "level": 2 }, + { + "id": "viewmodelclassname", + "text": "viewModelClassName", + "level": 3 + }, + { + "id": "viewmodelstoreclassname", + "text": "viewModelStoreClassName", + "level": 3 + }, + { + "id": "bannedapis", + "text": "bannedApis", + "level": 3 + }, + { + "id": "%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88", + "text": "有効にする場合", + "level": 2 + }, + { + "id": "%E9%96%A2%E9%80%A3%E9%A0%85%E7%9B%AE", + "text": "関連項目", + "level": 2 + }, { "id": "%E5%AE%9F%E8%A3%85", "text": "実装", @@ -606,7 +1084,7 @@ export const PROJECT = { "file": "rules/restrict-try-block.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/restrict-try-block", - "html": "\n
\n

tryブロック内のPromise・RxJS・Angular Signalコンテキスト、Promise.resolve() による逃げ道、物理行数を制限する。

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
\n
\n

try ブロック内の非同期/リアクティブ処理と物理コード行数を制限します。

\n

ルール詳細

\n

このルールは try を同期例外向けの小さな境界として保ちます。デフォルトでは次を報告します。

\n
    \n
  • await、およびTypeScript型がPromiseライクな式
  • \n
  • Promise.resolve() 呼び出し全般。同期例外をPromise rejectionへ変換するチェーンも含む
  • \n
  • 型または基底型が rxjs パッケージで宣言された式。ObservableSubject の派生を含む
  • \n
  • Angularの computed() および effect() コールバック内の try
  • \n
  • 物理コード行が3行を超える try 本体
  • \n
\n

try 固有の検査では try 本体だけを見ます。catchfinally は対象外です。ネストした関数・クラス・ネストした try は別の実行境界であり、外側の try には帰属しません。Promise.resolve() の検査はファイル全体に適用されます。

\n

Promiseのrejectionは通常、.catch() のようなPromiseエラー境界で扱うべきです。同期失敗をrejectionチャネルへ移すために Promise.resolve() でその境界を作り出さないでください。

\n
// incorrect\nPromise.resolve()\n  .then(() => fallibleSynchronousWork())\n  .catch(handleError);\n

同期の try 境界は小さく保ち、その失敗を扱う責務のある層に置きます。Promise.resolve(value) で正規化するのではなく、値または既存のPromiseをそのまま返します。

\n

RxJSのエラーは、catchError() や明示的なsubscriberのエラーハンドラなど、Observableのエラーチャネルで扱います。

\n

PromiseライクとRxJSの型検出は、利用可能な場合にTypeScriptの型情報を使います。typed lintingがない場合、型依存の検査はESLintを止めずにスキップされ、構文ベースの awaitPromise.resolve()、Angular Signalコンテキスト、maxLines の検査は引き続き動作します。完全な強制にはtyped lintingを設定します。例えば次のとおりです。

\n
languageOptions: {\n  parserOptions: {\n    projectService: true,\n    tsconfigRootDir: __dirname,\n  },\n},\n

オプション

\n
{\n  allowPromise: false,\n  allowPromiseResolve: false,\n  allowRxjs: false,\n  allowInSignal: false,\n  maxLines: 3,\n}\n
    \n
  • allowPromise: try 内のPromiseライク処理と await を許可する。
  • \n
  • allowPromiseResolve: ファイル全体の専用 Promise.resolve() 検査を無効化する。try 本体内では、呼び出しが独立してPromiseライク処理でもあるため、allowPromise: true も必要になる。
  • \n
  • allowRxjs: rxjs で宣言された型に裏打ちされた値と操作を許可する。ObservableSubject、およびそのサブクラスを含む。
  • \n
  • allowInSignal: インラインのAngular computed() / effect() コールバック内の try を許可する。@angular/core からのエイリアスと名前空間importを認識する。ネストした関数・クラス本体は別の実行境界である。
  • \n
  • maxLines: try 本体の最大物理コード行数。サイズ検査を無効にする場合は false
  • \n
\n

allowPromise: falseallowRxjs: false は、typed lintingが設定されているときに完全に強制されます。型情報がない場合、それらのカテゴリでは await のような構文ベースの検査だけが残ります。

\n

Promise.resolve() の検査は、シャドウされていないグローバル Promise と明示的な globalThis.Promise(静的ブラケット記法を含む)を認識します。エイリアスは意図的に追いません。ローカル宣言・importされた Promise という名前の値や、ローカルでシャドウされた globalThis は組み込みAPIとしては扱いません。

\n

maxLines では、外側の波括弧・コメント・空行を除外します。その他のトークンを含む一意の物理行を1回だけ数えます。内側の波括弧と複数行トークンは数えるため、フォーマットは意図的に結果へ影響します。境界を論理的にも見た目にも小さく保つためです。

\n

実装

\n\n", + "html": "\n
\n

tryブロック内のPromise、RxJS、Angular Signal context、Promise.resolve() による逃げ道、物理コード行数を制限する。

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
\n
\n

try/catch は、実際にthrowする可能性がある小さな同期処理を保護するために使用してください。非同期処理、長いblock、reactive callbackを try 内に置くとerror boundaryが不明瞭になり、errorを握りつぶしたり誤った経路へ送ったりする可能性があります。このルールは、それらを制限します。

\n

ルール詳細

\n

すべての try blockを検査し、デフォルトでは次を報告します。

\n
    \n
  • try 内の await またはその他のPromise/thenable使用
  • \n
  • 逃げ道としての、try の外も含むすべての Promise.resolve()
  • \n
  • try 内のRxJS型または操作
  • \n
  • computed() または effect() callback内の try block
  • \n
  • 物理コード行が3行を超える try block
  • \n
\n

try に限定した検査では try 本体だけを調べ、catchfinally clauseは除外します。ネストした関数、class、try 文はそれぞれ別の実行境界であり、外側のblockには帰属しません。Promise.resolve() の検査はファイル全体に適用されます。

\n

Promise-likeとRxJSの検出には、利用可能な場合TypeScript型情報を使用します。typed lintingがない場合、それらの検査はESLintを停止せずskipされますが、構文ベースの awaitPromise.resolve()、Angular Signal context、行数検査は引き続き実行されます。完全に強制するには parserOptions.projectService を設定してください。

\n

オプション

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/restrict-try-block\": [\n      \"error\",\n      {\n        \"allowPromise\": false,\n        \"allowPromiseResolve\": false,\n        \"allowRxjs\": false,\n        \"allowInSignal\": false,\n        \"maxLines\": 3\n      }\n    ]\n  }\n}\n

allowPromise

\n
    \n
  • 型: boolean
  • \n
  • デフォルト: false
  • \n
\n

try 内でPromise/thenableを使用できるようにします。

\n

allowPromiseResolve

\n
    \n
  • 型: boolean
  • \n
  • デフォルト: false
  • \n
\n

ファイル全体の Promise.resolve() 検査を無効にします。try 本体内では、その呼び出しが独立してPromise-like処理でもあるため、allowPromise: true も必要です。

\n

allowRxjs

\n
    \n
  • 型: boolean
  • \n
  • デフォルト: false
  • \n
\n

try 内でRxJSを使用できるようにします。

\n

allowInSignal

\n
    \n
  • 型: boolean
  • \n
  • デフォルト: false
  • \n
\n

computed() または effect() callback内で try blockを使用できるようにします。

\n

maxLines

\n
    \n
  • 型: number | false
  • \n
  • デフォルト: 3
  • \n
\n

try block内の物理コード行数の上限です。サイズ検査を無効にするには false を指定します。外側の波括弧、comment、空行は除外され、それ以外のtokenを含む一意の行を1回数えます。

\n

\n

誤り

\n
async function run() {\n  try {\n    await work();\n  } catch {}\n}\n
try {\n  Promise.resolve(1).catch(() => 0);\n} catch {}\n
import { of } from 'rxjs';\n\ntry {\n  of(1).pipe().subscribe();\n} catch {}\n
import { computed } from '@angular/core';\n\nconst value = computed(() => {\n  try {\n    return JSON.parse('1');\n  } catch {\n    return 0;\n  }\n});\n
try {\n  first();\n  second();\n  third();\n  fourth();\n} catch {}\n

正しい

\n
function parse(source: string) {\n  try {\n    return JSON.parse(source);\n  } catch {\n    return null;\n  }\n}\n
async function run() {\n  try {\n    doWork();\n  } catch {\n    await recover();\n  } finally {\n    cleanup();\n  }\n}\n
import { of } from 'rxjs';\nimport { catchError } from 'rxjs/operators';\n\nof(1)\n  .pipe(catchError(() => of(0)))\n  .subscribe();\n

検査を緩和する

\n
{\n  \"rules\": {\n    \"@rdlabo/rules/restrict-try-block\": [\n      \"error\",\n      {\n        \"allowPromise\": true,\n        \"allowPromiseResolve\": true,\n        \"allowRxjs\": true,\n        \"allowInSignal\": true,\n        \"maxLines\": false\n      }\n    ]\n  }\n}\n

有効にする場合

\n

try/catch を小さく明示的なerror boundaryとして維持したいすべてのプロジェクトで有効にしてください。Angular Signal codeや、Promise/RxJS中心のerror handlingから移行するときに特に有効です。

\n

Promise.resolve() の検査は、shadowされていないglobal Promise と、静的bracket notationを含む明示的な globalThis.Promise を認識します。aliasは意図的に追跡しません。ローカルで宣言またはimportされた Promise や、shadowされた globalThis は組み込みAPIとして扱いません。

\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", @@ -618,6 +1096,56 @@ export const PROJECT = { "text": "オプション", "level": 2 }, + { + "id": "allowpromise", + "text": "allowPromise", + "level": 3 + }, + { + "id": "allowpromiseresolve", + "text": "allowPromiseResolve", + "level": 3 + }, + { + "id": "allowrxjs", + "text": "allowRxjs", + "level": 3 + }, + { + "id": "allowinsignal", + "text": "allowInSignal", + "level": 3 + }, + { + "id": "maxlines", + "text": "maxLines", + "level": 3 + }, + { + "id": "%E4%BE%8B", + "text": "例", + "level": 2 + }, + { + "id": "%E8%AA%A4%E3%82%8A", + "text": "誤り", + "level": 3 + }, + { + "id": "%E6%AD%A3%E3%81%97%E3%81%84", + "text": "正しい", + "level": 3 + }, + { + "id": "%E6%A4%9C%E6%9F%BB%E3%82%92%E7%B7%A9%E5%92%8C%E3%81%99%E3%82%8B", + "text": "検査を緩和する", + "level": 3 + }, + { + "id": "%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88", + "text": "有効にする場合", + "level": 2 + }, { "id": "%E5%AE%9F%E8%A3%85", "text": "実装", @@ -635,21 +1163,46 @@ export const PROJECT = { "file": "rules/signal-use-as-signal-template.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/signal-use-as-signal-template", - "html": "\n
\n

テンプレートでAngular Signalにアクセスするとき () を要求する

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
\n
\n

このルールは、テンプレートでSignalを関数呼び出し構文 () で正しくアクセスすることを要求します。AngularのSignalは現在値を得るために呼び出す必要がある関数だからです。

\n

ルール詳細

\n

❌ 誤り: 関数呼び出し構文なしでSignalを使う

\n
@Component({\n  template: `\n    <div>{{ count }}</div>\n    <div>{{ count.signal }}</div>\n    <div>{{ count + 1 }}</div>\n    @if (count) {\n      <div>{{ count }}</div>\n    }\n    @switch (count) {\n      @case (0) {\n        <div>Zero</div>\n      }\n    }\n    @defer (on viewport) {\n      <div>{{ count }}</div>\n    }\n  `,\n})\nexport class TestComponent {\n  count = signal(0);\n}\n

✅ 正しい: 関数呼び出し構文でSignalを使う

\n
@Component({\n  template: `\n    <div>{{ count() }}</div>\n    <div>{{ count() + 1 }}</div>\n    <div>{{ count() > 0 ? 'Positive' : 'Zero' }}</div>\n    @if (count()) {\n      <div>{{ count() | async }}</div>\n    }\n    @switch (count()) {\n      @case (0) {\n        <div>Zero</div>\n      }\n      @case (1) {\n        <div>One</div>\n      }\n      @default {\n        <div>Other</div>\n      }\n    }\n    @defer (on viewport) {\n      <div>{{ count() }}</div>\n    }\n  `,\n})\nexport class TestComponent {\n  count = signal(0);\n}\n

✅ 正しい: Signal参照をinput bindingとして渡す

\n

BoundAttributeでSignal名だけを渡す場合は、Signal参照のprops渡しとして許可されます。

\n
@Component({\n  template: `<child [inventorySignal]=\"inventorySignal\"></child>`,\n})\nexport class TestComponent {\n  inventorySignal = signal(0);\n}\n

値として演算する場合は () が必要です。

\n
// ❌\n<child [disabled]=\"count > 0\"></child>\n// ✅\n<child [disabled]=\"count() > 0\"></child>\n

オプション

\n

オプションなし。

\n

未対応パターン

\n

このルールはネストしたSignalパターンに対応していません。例えば次のとおりです。

\n
@Component({\n  template: `\n    <div>{{ nestedSignal().child() }}</div>\n    // Correct usage\n    <div>{{ nestedSignal().child }}</div>\n    // Incorrect: missing function call\n  `,\n})\nexport class TestComponent {\n  nestedSignal = signal({\n    child: signal<number>(0),\n  });\n}\n

ネストしたSignalが関数呼び出しで正しくアクセスされていない場合を、このルールは検出できません。

\n

実装

\n\n", + "html": "\n
\n

テンプレートでAngular Signalにアクセスするとき () を要求する

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
\n
\n

Angular Signalは関数です。テンプレートで現在値を読み取るには、Signalを () 付きで呼び出す必要があります。RxJSの BehaviorSubjectmodel() inputから移行するとき、括弧の付け忘れはよくあるミスです。このルールはAngularテンプレート内のSignal識別子を検出し、{{ count }}[hidden]=\"count\" のような裸の読み取りを報告します。

\n

ルール詳細

\n

@Component のAngularテンプレートを解析し、次からSignal識別子を収集します。

\n
    \n
  • callee名が signalmodelcomputedlinkedSignalinputtoSignal のいずれかである呼び出しによって初期化されたclass property。
  • \n
  • object literal内にネストしたSignal property(例: count = { first: signal(0) })。
  • \n
\n

検出は名前に基づき、import元は解決しません。alias付きfactory importは認識されず、逆に同名の無関係なローカル関数がSignal factoryとして扱われる場合があります。toSignal は通常 @angular/core/rxjs-interop からimportされますが、このルールはmoduleではなく名前で認識します。

\n

続いて、テンプレート内でSignalが () なしで読み取られる箇所を報告します。対象には次が含まれます。

\n
    \n
  • interpolation {{ count }}
  • \n
  • property binding [hidden]=\"count\"
  • \n
  • event binding (click)=\"count > 0 ? ...\"
  • \n
  • control flow expression @if (count)@switch (count)@for (...; track count)
  • \n
  • optional chaining count?.signal
  • \n
  • pipe使用 count | async
  • \n
\n

templatetemplateUrl の両方のcomponentに対応します。

\n

\n

誤り

\n
<div>{{ count }}</div>\n
<child [hidden]=\"count > 0\"></child>\n
@if (count) {\n<div>Positive</div>\n}\n
<ion-input [formField]=\"count.first\"></ion-input>\n

正しい

\n
<div>{{ count() }}</div>\n
<child [hidden]=\"count() > 0\"></child>\n
@if (count()) {\n<div>Positive</div>\n}\n
<ion-input [formField]=\"count.first()\"></ion-input>\n

Signal参照を子componentへ渡す

\n

子componentが値ではなくSignal objectを期待する場合は、() なしで参照を渡せます。

\n
<child [inventorySignal]=\"inventorySignal\"></child>\n

この場合を認識し、bound attributeとして渡された裸のSignalは報告しません。

\n

オプション

\n

このルールにオプションはありません。

\n

有効にする場合

\n

Signalを使用するすべてのAngularプロジェクトで有効にしてください。Observable ベースのコードから移行するときや、テンプレート内で呼び出す必要のあるSignal風objectを返す model()input() を導入するときに特に有効です。

\n

関連項目

\n\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", "text": "ルール詳細", "level": 2 }, + { + "id": "%E4%BE%8B", + "text": "例", + "level": 2 + }, + { + "id": "%E8%AA%A4%E3%82%8A", + "text": "誤り", + "level": 3 + }, + { + "id": "%E6%AD%A3%E3%81%97%E3%81%84", + "text": "正しい", + "level": 3 + }, + { + "id": "signal%E5%8F%82%E7%85%A7%E3%82%92%E5%AD%90component%E3%81%B8%E6%B8%A1%E3%81%99", + "text": "Signal参照を子componentへ渡す", + "level": 3 + }, { "id": "%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3", "text": "オプション", "level": 2 }, { - "id": "%E6%9C%AA%E5%AF%BE%E5%BF%9C%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3", - "text": "未対応パターン", + "id": "%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88", + "text": "有効にする場合", + "level": 2 + }, + { + "id": "%E9%96%A2%E9%80%A3%E9%A0%85%E7%9B%AE", + "text": "関連項目", "level": 2 }, { @@ -669,21 +1222,46 @@ export const PROJECT = { "file": "rules/signal-use-as-signal.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/signal-use-as-signal", - "html": "\n
\n

このプラグインは、SignalをSignalとして正しく使うかを検査します。

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
  • ✒️ コマンドライン--fix オプションで、このルールが報告する問題の一部を自動修正できます。
  • \n
\n
\n

このルールは、Signalが通常のプロパティとして誤って使われることを防ぎます。

\n

ルール詳細

\n

❌ 誤り: Signalを通常のプロパティとして使う

\n
@Component()\nexport class SigninPage {\n  readonly #id = signal<number>(undefined);\n\n  useMethod() {\n    if (this.#id) {\n      // error\n      this.#id() = 1; // error\n    }\n  }\n}\n

✅ 正しい: Signalを適切に使う

\n
@Component()\nexport class SigninPage {\n  readonly #id = signal<number>(undefined);\n\n  useMethod() {\n    if (this.#id()) {\n      this.#id.set(1); // error\n    }\n  }\n}\n

✅ 正しい: Signal参照をpropsとして渡す

\n

Signalを値として読むのではなく、Signal自体を渡す場合は () は不要です。

\n
@Component()\nexport class SigninPage {\n  readonly food = signal<number>(0);\n\n  openPreview() {\n    // componentProps / modal launcher などへ参照渡し\n    launchModal({ food: this.food });\n    const food = this.food;\n    return this.food;\n  }\n}\n

オプション

\n

オプションなし。

\n

未対応パターン

\n

このルールはネストしたSignalパターンに対応していません。例えば次のとおりです。

\n
@Component({...})\nexport class TestComponent {\n  nestedSignal = signal({\n    child: signal<number>(0)\n  });\n\n  ngOnInit() {\n    if (this.nestedSignal().child) {  // Incorrect: missing function call\n      ...\n    }\n  }\n}\n

ネストしたSignalが関数呼び出しで正しくアクセスされていない場合を、このルールは検出できません。

\n

実装

\n\n", + "html": "\n
\n

SignalがSignalとして正しく使われているか検査する。

\n
    \n
  • ⭐️ このルールは plugin:@rdlabo/rules/recommended プリセットに含まれます。
  • \n
  • ✒️ コマンドライン--fix オプションで、このルールが報告する問題の一部を自動修正できます。
  • \n
\n
\n

Angular Signalはgetter関数です。読み取りには () が必要で、書き込みには .set() または .update() を使う必要があります。このルールは、Signal変数を通常の値のように扱うコードを検出し、一般的な誤りの多くを自動修正できます。

\n

ルール詳細

\n

Signal factory(signalmodelinputlinkedSignaltoSignalasReadonly)で初期化されたclass propertyを追跡し、次のような誤用を報告します。

\n
    \n
  • expression contextでの this.count() ではなく this.count
  • \n
  • this.count.set(value) ではなく this.count() = value
  • \n
  • this.user.update(user => ({ ...user, name: 'Jane' })) ではなく this.user().name = 'Jane'
  • \n
  • this.items.update(items => { items.push(x); return items; }) ではなく this.items().push(x)
  • \n
  • this.#user.set(value) ではなく、Signal propertyへの直接代入 this.#user = value
  • \n
\n

Signal参照が期待されるcontextと、値が期待されるcontextを区別します。たとえば、Signal objectをpropsとして渡すことは許可されます。

\n
const props = { food: this.food };\nlaunchModal({ food: this.food });\n

\n

誤り

\n
export class SigninPage {\n  readonly #id = signal<number | undefined>(undefined);\n\n  constructor() {\n    this.#id = 1;\n  }\n\n  useMethod() {\n    if (this.#id) {\n      this.#id().hoge = 1;\n    }\n  }\n}\n
export class SigninPage {\n  readonly #user = signal<{ name: string }>({ name: 'John' });\n\n  updateUser() {\n    this.#user().name = 'Jane';\n  }\n}\n
export class SigninPage {\n  readonly #numbers = signal<number[]>([1, 2, 3]);\n\n  updateNumbers() {\n    this.#numbers().push(4);\n  }\n}\n
export class SigninPage {\n  readonly #value = signal<number>(0);\n\n  updateValue() {\n    this.#value() = 42;\n  }\n}\n

正しい

\n
export class SigninPage {\n  readonly #user = signal<{ name: string }>({ name: 'John' });\n\n  updateUser() {\n    this.#user.update((user) => ({ ...user, name: 'Jane' }));\n  }\n}\n
export class SigninPage {\n  readonly #numbers = signal<number[]>([1, 2, 3]);\n\n  updateNumbers() {\n    this.#numbers.update((numbers) => {\n      numbers.push(4);\n      return numbers;\n    });\n  }\n}\n
export class SigninPage {\n  readonly #value = signal<number>(0);\n\n  updateValue() {\n    this.#value.set(42);\n  }\n}\n
export class SigninPage {\n  readonly food = signal<number>(0);\n\n  openPreview() {\n    const props = { food: this.food };\n    launchModal({ food: this.food });\n  }\n}\n

自動修正

\n

次のパターンを自動修正できます。

\n
    \n
  • this.count = value -> this.count.set(value)
  • \n
  • this.count() = value -> this.count.set(value)
  • \n
  • this.count().x = value -> this.count.update(value => ({ ...value, x: value }))
  • \n
  • this.count().push(x) -> this.count.update(value => { value.push(x); return value; })
  • \n
\n

オプション

\n

このルールにオプションはありません。

\n

有効にする場合

\n

Signalを使用するすべてのAngularプロジェクトで有効にしてください。テンプレート内のSignal使用を検査する @rdlabo/rules/signal-use-as-signal-template と相互補完します。

\n

関連項目

\n\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", "text": "ルール詳細", "level": 2 }, + { + "id": "%E4%BE%8B", + "text": "例", + "level": 2 + }, + { + "id": "%E8%AA%A4%E3%82%8A", + "text": "誤り", + "level": 3 + }, + { + "id": "%E6%AD%A3%E3%81%97%E3%81%84", + "text": "正しい", + "level": 3 + }, + { + "id": "%E8%87%AA%E5%8B%95%E4%BF%AE%E6%AD%A3", + "text": "自動修正", + "level": 2 + }, { "id": "%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3", "text": "オプション", "level": 2 }, { - "id": "%E6%9C%AA%E5%AF%BE%E5%BF%9C%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3", - "text": "未対応パターン", + "id": "%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88", + "text": "有効にする場合", + "level": 2 + }, + { + "id": "%E9%96%A2%E9%80%A3%E9%A0%85%E7%9B%AE", + "text": "関連項目", "level": 2 }, { diff --git a/src/app/generated/projects/ionic-angular-collect-icons.en.generated.ts b/src/app/generated/projects/ionic-angular-collect-icons.en.generated.ts index 3fec55a..528744b 100644 --- a/src/app/generated/projects/ionic-angular-collect-icons.en.generated.ts +++ b/src/app/generated/projects/ionic-angular-collect-icons.en.generated.ts @@ -8,7 +8,7 @@ export const PROJECT = { "repositoryUrl": "https://github.com/rdlabo-dev/ionic-angular-collect-icons", "category": "frontend-tools", "icon": "app", - "version": "2.1.0", + "version": "3.0.0", "description": "Automate ionIcons collection and export for Ionic Angular projects.", "headline": "Collect used ionIcons before production builds", "overview": "Group unique ionIcons in a project and generate an export file—register all icons during development, then collect icons used in templates before production builds.", @@ -36,7 +36,7 @@ export const PROJECT = { "file": "readme.md", "section": "Quickstart", "path": "/projects/ionic-angular-collect-icons/docs/readme", - "html": "

What is this?

\n

This library is used to uniquely group the ionIcons in a project, and generate for export ionIcons file. In small projects, it is difficult to manage addIcons() of ionIcons each time, so we automated it.

\n
    \n
  • development: Stress-free development by add all icons at addIcons.
  • \n
  • Production: Automatically collect and update the ionIcon used in the template prior to build.
  • \n
\n

Of course, to maximize bundle size reduction, it is important to load a minimum number of icons at each Component lazy loading. This is a compromise to speed up development.

\n

This project is based ionic-team/ionic-angular-standalone-codemods .

\n

Requirements

\n
    \n
  • Node.js >= 20
  • \n
  • ionicons >= 6.0.0
  • \n
\n

Quick start

\n

After Installation, initialize addIcons and collect icons before production builds:

\n
npx @rdlabo/ionic-angular-collect-icons --initialize true\n

Details: Initialize and Usage.

\n

Installation

\n
npm install @rdlabo/ionic-angular-collect-icons --save-dev\n

Documentation

\n

Start with Installation, then Initialize and Usage.

\n
    \n
  • Initialize — wire addIcons automatically or by hand.
  • \n
  • Usage — run the collector before production builds.
  • \n
  • CLI Options--dry-run, --initialize, paths.
  • \n
  • FAQ — tests, binding, and main.ts.
  • \n
\n", + "html": "

What is this?

\n

This library is used to uniquely group the ionIcons in a project, and generate for export ionIcons file. In small projects, it is difficult to manage addIcons() of ionIcons each time, so we automated it.

\n
    \n
  • development: Stress-free development by add all icons at addIcons.
  • \n
  • Production: Automatically collect and update the ionIcon used in the template prior to build.
  • \n
\n

Of course, to maximize bundle size reduction, it is important to load a minimum number of icons at each Component lazy loading. This is a compromise to speed up development.

\n

This project is based ionic-team/ionic-angular-standalone-codemods .

\n

Requirements

\n
    \n
  • Node.js >= 22
  • \n
  • Ionic Angular >= 9.0.0
  • \n
  • Angular >= 18.0.0
  • \n
  • TypeScript >= 5.4.0
  • \n
  • ionicons >= 8.0.0
  • \n
  • @angular-eslint/template-parser 21 or 22
  • \n
\n

Quick start

\n

After Installation, initialize addIcons and collect icons before production builds:

\n
npx @rdlabo/ionic-angular-collect-icons --initialize true\n

Details: Initialize and Usage.

\n

Migrating from Ionic Angular 8

\n

Commit the consuming application's current changes, then run Ionic's official
\nmigration tool from the application root:

\n
npx @ionic/migrate\n

It applies safe automatic changes and reports items that require manual review.
\nAfter it finishes, update this package and follow the
\nIonic Angular 9 migration guide for the remaining checks.

\n

Installation

\n
npm install --save-dev \\\n  @rdlabo/ionic-angular-collect-icons \\\n  @angular-eslint/template-parser@^21\n

Use @angular-eslint/template-parser@^22 instead when the consuming project
\nuses Angular ESLint 22. The parser is a peer dependency so the collector uses
\nthe same Angular template parser major as the consuming project.

\n

Documentation

\n

Start with Installation, then Initialize and Usage.

\n
    \n
  • Initialize — wire addIcons automatically or by hand.
  • \n
  • Usage — run the collector before production builds.
  • \n
  • Migration — migrate an existing project to Ionic Angular 9.
  • \n
  • CLI Options--dry-run, --initialize, paths.
  • \n
  • FAQ — tests, binding, and main.ts.
  • \n
\n", "headings": [ { "id": "what-is-this%3F", @@ -53,6 +53,11 @@ export const PROJECT = { "text": "Quick start", "level": 2 }, + { + "id": "migrating-from-ionic-angular-8", + "text": "Migrating from Ionic Angular 8", + "level": 2 + }, { "id": "installation", "text": "Installation", @@ -178,6 +183,60 @@ export const PROJECT = { "scrollMap": [], "editUrl": "https://github.com/rdlabo-dev/ionic-angular-collect-icons/edit/main/docs/faq.md" }, + { + "title": "Migration", + "navTitle": "Migration", + "slug": "migration", + "file": "migration.md", + "section": "Guides", + "path": "/projects/ionic-angular-collect-icons/docs/migration", + "html": "

Migrating to Ionic Angular 9

\n

This version targets Ionic Angular 9 and follows the
\nIonic Framework 9 breaking changes.

\n

Requirements

\n
    \n
  • Ionic Angular 9 or later
  • \n
  • Angular 18 or later
  • \n
  • Capacitor 7 or later for native applications
  • \n
  • TypeScript 5.4 or later
  • \n
  • Ionicons 8 or later
  • \n
  • Node.js 22 or later
  • \n
\n

Run the official migrator

\n

Ionic recommends using its official migration tool. Commit the application's
\ncurrent changes first: the migrator edits files in place and requires a clean
\nGit working tree so the commit can be used to review or undo its changes.

\n

Run it from the root of the Ionic application:

\n
npx @ionic/migrate\n

The migrator detects the installed Ionic major version, updates dependencies,
\napplies safe automatic fixes, formats changed files, reinstalls dependencies,
\nand prints a checklist of changes that require manual review.

\n

To preview the migration without writing files, run:

\n
npx @ionic/migrate --dry-run\n

After the official migration finishes, update this collector and confirm that
\nthe resulting dependency versions meet the requirements above:

\n
npm install --save-dev @rdlabo/ionic-angular-collect-icons@latest\n

The remaining sections explain the important Ionic Angular 9 changes to verify
\nin the generated diff and in the migrator's manual-review checklist.

\n

Complete the standalone migration

\n

Ionic 9 exports standalone Angular components from @ionic/angular. Replace
\nthe Ionic 8 standalone entry point:

\n
- import { IonApp, IonIcon, provideIonicAngular } from '@ionic/angular/standalone';\n+ import { IonApp, IonIcon, provideIonicAngular } from '@ionic/angular';\n

The official migrator may move NgModule imports to @ionic/angular/lazy to
\npreserve the application's architecture during the framework upgrade. Treat
\nthat as an intermediate state, not the standalone destination. Complete the
\nAngular standalone migration, then import each Ionic component from
\n@ionic/angular. Do not mechanically rewrite /lazy imports before their
\nNgModule consumers have been converted.

\n

Replace IonicModule after the standalone migration

\n

IonicModule is deprecated in Ionic 9, but removing it requires
\napplication-level architectural changes. Convert the application to standalone
\nbootstrap, move the Ionic configuration to provideIonicAngular(), and import
\nthe standalone Ionic components used by each consumer:

\n
- platformBrowserDynamic().bootstrapModule(AppModule);\n+ bootstrapApplication(AppComponent, {\n+   providers: [provideIonicAngular(config)],\n+ });\n

Import provideIonicAngular from @ionic/angular. Do not replace
\nIonicModule.forRoot() with a one-line provider change inside the same
\nNgModule; finish the NgModule-to-standalone migration first.

\n

Use exports-aware module resolution

\n

Ionic 9 publishes package subpaths through exports. Applications should use
\nthe Angular default bundler resolution:

\n
{\n  \"compilerOptions\": {\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"target\": \"ES2022\"\n  }\n}\n

Replace webpack-style CSS imports that use ~:

\n
- @import '~@ionic/angular/css/core.css';\n+ @import '@ionic/angular/css/core.css';\n

Run the icon collector

\n

Initialize the generated icon registration if the application has not already
\ndone so:

\n
npx @rdlabo/ionic-angular-collect-icons --initialize true\n

Continue running the collector before production builds as described in the
\nusage guide.

\n

Review other Ionic 9 changes

\n

The collector finds ion-icon usage in Angular templates and updates its own
\nicon registration files. It does not depend on Ionic component behavior or
\ninternal DOM, so those Ionic 9 changes do not require collector-specific code
\nchanges. Consuming applications must still review the official migration notes,
\nparticularly the new browser and mobile platform minimums and these changes:

\n
    \n
  • Native applications require Capacitor 7+ and iOS 16+.
  • \n
  • Supported desktop browsers are Chrome 89+, Safari 16+, Edge 89+, and Firefox 75+.
  • \n
  • ion-input and ion-searchbar now use a boolean autocorrect property.
  • \n
  • Legacy picker components and PickerController were removed.
  • \n
  • Sheet modal handles now default to handleBehavior=\"cycle\".
  • \n
  • ion-nav no longer integrates with ion-router.
  • \n
  • ion-select emits ionChange only when its value changes.
  • \n
  • Input, select, and textarea internal DOM and styling hooks changed.
  • \n
  • Angular 21 applications use zoneless change detection by default.
  • \n
\n

After migrating, run the application's lint, test, and production build commands
\nand verify any customized Ionic component styles visually.

\n", + "headings": [ + { + "id": "migrating-to-ionic-angular-9", + "text": "Migrating to Ionic Angular 9", + "level": 2 + }, + { + "id": "requirements", + "text": "Requirements", + "level": 3 + }, + { + "id": "run-the-official-migrator", + "text": "Run the official migrator", + "level": 3 + }, + { + "id": "complete-the-standalone-migration", + "text": "Complete the standalone migration", + "level": 3 + }, + { + "id": "replace-ionicmodule-after-the-standalone-migration", + "text": "Replace IonicModule after the standalone migration", + "level": 3 + }, + { + "id": "use-exports-aware-module-resolution", + "text": "Use exports-aware module resolution", + "level": 3 + }, + { + "id": "run-the-icon-collector", + "text": "Run the icon collector", + "level": 3 + }, + { + "id": "review-other-ionic-9-changes", + "text": "Review other Ionic 9 changes", + "level": 3 + } + ], + "codes": [], + "scrollMap": [], + "editUrl": "https://github.com/rdlabo-dev/ionic-angular-collect-icons/edit/main/docs/migration.md" + }, { "title": "CLI API", "navTitle": "CLI API", diff --git a/src/app/generated/projects/ionic-angular-collect-icons.ja.generated.ts b/src/app/generated/projects/ionic-angular-collect-icons.ja.generated.ts index 262f620..b5d48d5 100644 --- a/src/app/generated/projects/ionic-angular-collect-icons.ja.generated.ts +++ b/src/app/generated/projects/ionic-angular-collect-icons.ja.generated.ts @@ -8,7 +8,7 @@ export const PROJECT = { "repositoryUrl": "https://github.com/rdlabo-dev/ionic-angular-collect-icons", "category": "frontend-tools", "icon": "app", - "version": "2.1.0", + "version": "3.0.0", "description": "Ionic Angularプロジェクト向けのionIcons収集・エクスポート自動化。", "headline": "本番ビルド前に使用中のionIconsを収集する", "overview": "プロジェクト内のionIconsを一意にまとめエクスポート用ファイルを生成します。開発時は全アイコンを登録し、本番ビルド前にテンプレートで使われているアイコンを収集します。", @@ -36,13 +36,8 @@ export const PROJECT = { "file": "readme.md", "section": "クイックスタート", "path": "/projects/ionic-angular-collect-icons/docs/readme", - "html": "

概要

\n

これは何ですか?

\n

このライブラリは、プロジェクト内の ionIcons を一意にまとめ、エクスポート用の ionIcons ファイルを生成するために使います。小さなプロジェクトでは、毎回 addIcons() を管理するのが難しいため、自動化しました。

\n
    \n
  • 開発時: addIcons に全アイコンを追加することで、ストレスのない開発ができます。
  • \n
  • 本番時: ビルド前にテンプレートで使われている ionIcon を自動収集・更新します。
  • \n
\n

もちろん、バンドルサイズ削減を最大化するには、各コンポーネントの遅延読み込み時に最小限のアイコンだけを読み込むことが重要です。これは開発速度を優先するための妥協案です。

\n

このプロジェクトは ionic-team/ionic-angular-standalone-codemods を基にしています。

\n

要件

\n
    \n
  • Node.js >= 20
  • \n
  • ionicons >= 6.0.0
  • \n
\n

クイックスタート

\n

インストール のあと、addIcons を初期化し、本番ビルド前にアイコンを収集します。

\n
npx @rdlabo/ionic-angular-collect-icons --initialize true\n

詳細は 初期化使い方 です。

\n

インストール

\n
npm install @rdlabo/ionic-angular-collect-icons --save-dev\n

ドキュメント

\n

上の インストール から始め、初期化使い方 を見てください。

\n
    \n
  • 初期化addIcons の自動または手動配線。
  • \n
  • 使い方 — 本番ビルド前のコレクター実行。
  • \n
  • CLI オプション--dry-run--initialize、パス。
  • \n
  • FAQ — テスト、バインディング、main.ts
  • \n
\n", + "html": "

これは何ですか?

\n

このライブラリは、プロジェクト内の ionIcons を一意にまとめ、エクスポート用の ionIcons ファイルを生成するために使います。小さなプロジェクトでは、毎回 addIcons() を管理するのが難しいため、自動化しました。

\n
    \n
  • 開発時: addIcons に全アイコンを追加することで、ストレスのない開発ができます。
  • \n
  • 本番時: ビルド前にテンプレートで使われている ionIcon を自動収集・更新します。
  • \n
\n

もちろん、バンドルサイズ削減を最大化するには、各コンポーネントの遅延読み込み時に最小限のアイコンだけを読み込むことが重要です。これは開発速度を優先するための妥協案です。

\n

このプロジェクトは ionic-team/ionic-angular-standalone-codemods を基にしています。

\n

要件

\n
    \n
  • Node.js >= 22
  • \n
  • Ionic Angular >= 9.0.0
  • \n
  • Angular >= 18.0.0
  • \n
  • TypeScript >= 5.4.0
  • \n
  • ionicons >= 8.0.0
  • \n
  • @angular-eslint/template-parser 21 または 22
  • \n
\n

クイックスタート

\n

インストール のあと、addIcons を初期化し、本番ビルド前にアイコンを収集します。

\n
npx @rdlabo/ionic-angular-collect-icons --initialize true\n

詳細は 初期化使い方 です。

\n

Ionic Angular 8からの移行

\n

アプリケーションの変更をcommitしてから、アプリケーションrootでIonic公式のmigration toolを実行します。

\n
npx @ionic/migrate\n

安全に自動化できる変更が適用され、手動確認が必要な項目が表示されます。完了後にこのpackageを更新し、残りの確認事項はIonic Angular 9への移行を参照してください。

\n

インストール

\n
npm install --save-dev \\\n  @rdlabo/ionic-angular-collect-icons \\\n  @angular-eslint/template-parser@^21\n

Angular ESLint 22を使うprojectでは、代わりに @angular-eslint/template-parser@^22 を指定してください。parserはpeer dependencyのため、collectorはアプリケーションと同じ世代のAngular template parserを使います。

\n

ドキュメント

\n

上の インストール から始め、初期化使い方 を見てください。

\n
    \n
  • 初期化addIcons の自動または手動配線。
  • \n
  • 使い方 — 本番ビルド前のコレクター実行。
  • \n
  • 移行 — 既存projectをIonic Angular 9へ移行する。
  • \n
  • CLI オプション--dry-run--initialize、パス。
  • \n
  • FAQ — テスト、バインディング、main.ts
  • \n
\n", "headings": [ - { - "id": "%E6%A6%82%E8%A6%81", - "text": "概要", - "level": 2 - }, { "id": "%E3%81%93%E3%82%8C%E3%81%AF%E4%BD%95%E3%81%A7%E3%81%99%E3%81%8B%EF%BC%9F", "text": "これは何ですか?", @@ -58,6 +53,11 @@ export const PROJECT = { "text": "クイックスタート", "level": 2 }, + { + "id": "ionic-angular-8%E3%81%8B%E3%82%89%E3%81%AE%E7%A7%BB%E8%A1%8C", + "text": "Ionic Angular 8からの移行", + "level": 2 + }, { "id": "%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB", "text": "インストール", @@ -183,6 +183,60 @@ export const PROJECT = { "scrollMap": [], "editUrl": "https://github.com/rdlabo-dev/docs/edit/main/src/ionic-angular-collect-icons/docs/ja/faq.md" }, + { + "title": "移行", + "navTitle": "移行", + "slug": "migration", + "file": "migration.md", + "section": "ガイド", + "path": "/projects/ionic-angular-collect-icons/docs/migration", + "html": "

Ionic Angular 9への移行

\n

このversionはIonic Angular 9を対象とし、Ionic Framework 9の破壊的変更に従います。

\n

要件

\n
    \n
  • Ionic Angular 9以降
  • \n
  • Angular 18以降
  • \n
  • nativeアプリケーションではCapacitor 7以降
  • \n
  • TypeScript 5.4以降
  • \n
  • Ionicons 8以降
  • \n
  • Node.js 22以降
  • \n
\n

公式migratorを実行する

\n

Ionicは公式migration toolの利用を推奨しています。最初にアプリケーションの現在の変更をcommitしてください。migratorはfileを直接編集するため、差分の確認や取り消しにcommitを使えるcleanなGit worktreeが必要です。

\n

Ionicアプリケーションのrootで実行します。

\n
npx @ionic/migrate\n

migratorはインストール済みのIonic major versionを検出し、依存関係を更新します。安全な自動修正とformat、依存関係の再installを実行し、手動確認が必要な変更をchecklistで表示します。

\n

fileを書き換えず移行内容をpreviewするには次を実行します。

\n
npx @ionic/migrate --dry-run\n

公式migration完了後にcollectorを更新し、依存packageのversionが上記要件を満たすことを確認します。

\n
npm install --save-dev @rdlabo/ionic-angular-collect-icons@latest\n

以降では、生成された差分とmigratorの手動確認checklistで検証すべきIonic Angular 9の重要な変更を説明します。

\n

standalone migrationを完了する

\n

Ionic 9はstandalone Angular componentを @ionic/angular からexportします。Ionic 8のstandalone entry pointを置き換えます。

\n
- import { IonApp, IonIcon, provideIonicAngular } from '@ionic/angular/standalone';\n+ import { IonApp, IonIcon, provideIonicAngular } from '@ionic/angular';\n

公式migratorはframework更新中にアプリケーションのarchitectureを維持するため、NgModule importを @ionic/angular/lazy へ移す場合があります。これは移行途中の状態であり、standalone migrationの完了形ではありません。Angular standalone migrationを完了してから、各Ionic componentを @ionic/angular からimportしてください。NgModule consumerを変換する前に /lazy importを機械的に書き換えてはいけません。

\n

standalone migration後に IonicModule を置き換える

\n

IonicModule はIonic 9でdeprecatedですが、削除にはアプリケーション単位のarchitecture変更が必要です。アプリケーションをstandalone bootstrapへ変換し、Ionic設定を provideIonicAngular() へ移し、各consumerが使うstandalone Ionic componentをimportします。

\n
- platformBrowserDynamic().bootstrapModule(AppModule);\n+ bootstrapApplication(AppComponent, {\n+   providers: [provideIonicAngular(config)],\n+ });\n

provideIonicAngular@ionic/angular からimportします。同じNgModule内で IonicModule.forRoot() を1行のprovider変更へ置き換えてはいけません。最初にNgModuleからstandaloneへの移行を完了してください。

\n

exportsを考慮するmodule resolutionを使う

\n

Ionic 9はpackage subpathを exports で公開します。アプリケーションではAngular標準のbundler resolutionを使ってください。

\n
{\n  \"compilerOptions\": {\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"target\": \"ES2022\"\n  }\n}\n

~ を使うwebpack形式のCSS importを置き換えます。

\n
- @import '~@ionic/angular/css/core.css';\n+ @import '@ionic/angular/css/core.css';\n

icon collectorを実行する

\n

まだ初期化していない場合は、生成されるicon登録を初期化します。

\n
npx @rdlabo/ionic-angular-collect-icons --initialize true\n

使い方の説明に従い、本番build前にcollectorを引き続き実行します。

\n

Ionic 9のほかの変更を確認する

\n

collectorはAngular templateから ion-icon の利用箇所を探し、専用のicon登録fileを更新します。Ionic componentの挙動や内部DOMには依存しないため、それらにcollector固有の変更はありません。ただし、アプリケーション側では公式migration note、特に新しいbrowser・mobile platformのminimum versionと次の変更を確認してください。

\n
    \n
  • nativeアプリケーションはCapacitor 7以降とiOS 16以降が必要です。
  • \n
  • 対応desktop browserはChrome 89以降、Safari 16以降、Edge 89以降、Firefox 75以降です。
  • \n
  • ion-inpution-searchbarautocorrect propertyはbooleanになりました。
  • \n
  • legacy picker componentと PickerController は削除されました。
  • \n
  • sheet modal handleのdefaultは handleBehavior=\"cycle\" になりました。
  • \n
  • ion-navion-router と連携しなくなりました。
  • \n
  • ion-select はvalueが変わった場合だけ ionChange をemitします。
  • \n
  • input、select、textareaの内部DOMとstyling hookが変わりました。
  • \n
  • Angular 21アプリケーションはzoneless change detectionをdefaultで使います。
  • \n
\n

移行後はアプリケーションのlint、test、本番buildを実行し、customizeしたIonic componentのstyleを目視確認してください。

\n", + "headings": [ + { + "id": "ionic-angular-9%E3%81%B8%E3%81%AE%E7%A7%BB%E8%A1%8C", + "text": "Ionic Angular 9への移行", + "level": 2 + }, + { + "id": "%E8%A6%81%E4%BB%B6", + "text": "要件", + "level": 3 + }, + { + "id": "%E5%85%AC%E5%BC%8Fmigrator%E3%82%92%E5%AE%9F%E8%A1%8C%E3%81%99%E3%82%8B", + "text": "公式migratorを実行する", + "level": 3 + }, + { + "id": "standalone-migration%E3%82%92%E5%AE%8C%E4%BA%86%E3%81%99%E3%82%8B", + "text": "standalone migrationを完了する", + "level": 3 + }, + { + "id": "standalone-migration%E5%BE%8C%E3%81%AB-ionicmodule-%E3%82%92%E7%BD%AE%E3%81%8D%E6%8F%9B%E3%81%88%E3%82%8B", + "text": "standalone migration後に IonicModule を置き換える", + "level": 3 + }, + { + "id": "exports%E3%82%92%E8%80%83%E6%85%AE%E3%81%99%E3%82%8Bmodule-resolution%E3%82%92%E4%BD%BF%E3%81%86", + "text": "exportsを考慮するmodule resolutionを使う", + "level": 3 + }, + { + "id": "icon-collector%E3%82%92%E5%AE%9F%E8%A1%8C%E3%81%99%E3%82%8B", + "text": "icon collectorを実行する", + "level": 3 + }, + { + "id": "ionic-9%E3%81%AE%E3%81%BB%E3%81%8B%E3%81%AE%E5%A4%89%E6%9B%B4%E3%82%92%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B", + "text": "Ionic 9のほかの変更を確認する", + "level": 3 + } + ], + "codes": [], + "scrollMap": [], + "editUrl": "https://github.com/rdlabo-dev/docs/edit/main/src/ionic-angular-collect-icons/docs/ja/migration.md" + }, { "title": "CLI API", "navTitle": "CLI API", @@ -190,7 +244,7 @@ export const PROJECT = { "file": "api.md", "section": "リファレンス", "path": "/projects/ionic-angular-collect-icons/docs/api", - "html": "

@rdlabo/ionic-angular-collect-icons v2.1.0 のcommand referenceです。

\n

Command

\n

command npx @rdlabo/ionic-angular-collect-icons

Angularのsourceとtemplateをscanし、Applicationが使用するIoniconsを既定では src/use-icons.ts へ出力します。

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
OptionTypeDescriptionDefault
--dry-runbooleanFileを書き込まず変更内容を表示します。false
--interactivebooleanPromptですべてのoptionを指定し、結果を確認します。false
--initializebooleanaddIcons の初期化を追加し、Component単位の登録を削除します。false
--project-pathstringsrc をscanするProject directoryです。現在のdirectory
--icon-pathstring生成するicon登録fileです。src/use-icons.ts
\n\n\n
", + "html": "

@rdlabo/ionic-angular-collect-icons v3.0.0 のcommand referenceです。

\n

Command

\n

command npx @rdlabo/ionic-angular-collect-icons

Angularのsourceとtemplateをscanし、Applicationが使用するIoniconsを既定では src/use-icons.ts へ出力します。

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
OptionTypeDescriptionDefault
--dry-runbooleanFileを書き込まず変更内容を表示します。false
--interactivebooleanPromptですべてのoptionを指定し、結果を確認します。false
--initializebooleanaddIcons の初期化を追加し、Component単位の登録を削除します。false
--project-pathstringsrc をscanするProject directoryです。現在のdirectory
--icon-pathstring生成するicon登録fileです。src/use-icons.ts
\n\n\n
", "headings": [ { "id": "command", diff --git a/src/app/generated/projects/ionic-theme-ios26.en.generated.ts b/src/app/generated/projects/ionic-theme-ios26.en.generated.ts index aba5f41..343433e 100644 --- a/src/app/generated/projects/ionic-theme-ios26.en.generated.ts +++ b/src/app/generated/projects/ionic-theme-ios26.en.generated.ts @@ -8,7 +8,7 @@ export const PROJECT = { "repositoryUrl": "https://github.com/rdlabo-dev/ionic-theme-ios26", "category": "frontend-tools", "icon": "theme", - "version": "2.3.2", + "version": "3.0.0", "description": "iOS 26 design styling for Ionic applications.", "headline": "Bring iOS 26 design to Ionic apps", "overview": "Apply iOS 26 CSS, transitions, and Liquid Glass interactions to Ionic components, with dark mode and selective migration support.", @@ -36,7 +36,7 @@ export const PROJECT = { "file": "readme.md", "section": "Quickstart", "path": "/projects/ionic-theme-ios26/docs/readme", - "html": "

Overview

\n

This library provides CSS/JS files that bring the iOS26 design system to Ionic applications. It updates the look and feel of Ionic components to match the latest iOS26 design guidelines.

\n

I'm also working on the Android Design (Material Design 3) theme. Be sure to catch up!

\n

👉️rdlabo-dev/ionic-theme-md3

\n

Quick start

\n

After Installation, import the theme CSS. Details are in Installation below.

\n

Installation

\n

This is a CSS theme for extending your Ionic project. It does not work on its own, so use it together with the Ionic Framework.

\n
npm install @rdlabo/ionic-theme-ios26\n

Note: If you use @ionic/core@ < 8.8.1, use @rdlabo/ionic-theme-ios26@2.2.1.

\n

And import the theme in your project's main CSS file (e.g., src/styles.scss).

\n
@import '@rdlabo/ionic-theme-ios26/dist/css/default-variables.css';\n@import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26.css';\n\n/**\n * This file is to eliminate the impact of class name changes for iOS26.\n * For example, `ion-buttons ion-button[fill=default]` is not normally implemented, but may be required for iOS26.\n * This file is to eliminate such effects.\n * Note: This is not include `@rdlabo/ionic-theme-md3`\n */\n@import '@rdlabo/ionic-theme-ios26/dist/css/md-remove-ios-class-effect.css';\n\n/**\n * If you will use the design of ion-item-group with ion-list on Android as well, import it.\n * More info: https://github.com/rdlabo-dev/ionic-theme-ios26/blob/v2.3.2/USING_ION_ITEM_GROUP.md\n * Note: This is include `@rdlabo/ionic-theme-md3`\n * @import '@rdlabo/ionic-theme-ios26/dist/css/md-ion-list-inset.css';\n */\n\n/*\n * Support Dark Mode\n * We support Ionic Dark Mode. More information is here: https://ionicframework.com/docs/theming/dark-mode\n * use Always:    @import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26-dark-always.css'\n * use System:    @import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26-dark-system.css'\n * use CSS Class: @import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26-dark-class.css'\n */\n

Next, configure the animations for iOS 26. Add the following to your Ionic configuration options.

\n
import { isPlatform } from '@ionic/core'; // or @ionic/angular/standalone, @ionic/react, @ionic/vue\nimport { iosTransitionAnimation, popoverEnterAnimation, popoverLeaveAnimation } from '@rdlabo/ionic-theme-ios26';\n\n// Angular\nprovideIonicAngular({\n    ...\n    navAnimation: isPlatform('ios') ? iosTransitionAnimation: undefined,\n    popoverEnter: isPlatform('ios') ? popoverEnterAnimation: undefined,\n    popoverLeave: isPlatform('ios') ? popoverLeaveAnimation: undefined,\n});\n\n// React\nsetupIonicReact({\n    ...\n    navAnimation: isPlatform('ios') ? iosTransitionAnimation: undefined,\n    popoverEnter: isPlatform('ios') ? popoverEnterAnimation: undefined,\n    popoverLeave: isPlatform('ios') ? popoverLeaveAnimation: undefined,\n});\n\n// Vue\ncreateApp(App)\n    .use(IonicVue, {\n        ...\n        navAnimation: isPlatform('ios') ? iosTransitionAnimation: undefined,\n        popoverEnter: isPlatform('ios') ? popoverEnterAnimation: undefined,\n        popoverLeave: isPlatform('ios') ? popoverLeaveAnimation: undefined,\n})\n

Documentation

\n

Start with Installation, then Using ion-item-group when you use inset lists.

\n\n", + "html": "

Overview

\n

This library provides CSS/JS files that bring the iOS26 design system to Ionic applications. It updates the look and feel of Ionic components to match the latest iOS26 design guidelines.

\n

I'm also working on the Android Design (Material Design 3) theme. Be sure to catch up!

\n

👉️rdlabo-dev/ionic-theme-md3

\n

Quick start

\n

After Installation, import the theme CSS. Details are in Installation below.

\n

Installation

\n

This is a CSS theme for extending your Ionic project. It does not work on its own, so use it together with the Ionic Framework.

\n
npm install @rdlabo/ionic-theme-ios26\n

Note: If you use @ionic/core@ < 8.8.1, use @rdlabo/ionic-theme-ios26@2.2.1.

\n

And import the theme in your project's main CSS file (e.g., src/styles.scss).

\n
@import '@rdlabo/ionic-theme-ios26/dist/css/default-variables.css';\n@import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26.css';\n\n/**\n * This file is to eliminate the impact of class name changes for iOS26.\n * For example, `ion-buttons ion-button[fill=default]` is not normally implemented, but may be required for iOS26.\n * This file is to eliminate such effects.\n * Note: This stylesheet is not included in `@rdlabo/ionic-theme-md3`.\n */\n@import '@rdlabo/ionic-theme-ios26/dist/css/md-remove-ios-class-effect.css';\n\n/**\n * If you will use the design of ion-item-group with ion-list on Android as well, import it.\n * More info: https://github.com/rdlabo-dev/ionic-theme-ios26/blob/v3.0.0/docs/using-ion-item-group.md\n * Note: This stylesheet is included in `@rdlabo/ionic-theme-md3`.\n * @import '@rdlabo/ionic-theme-ios26/dist/css/md-ion-list-inset.css';\n */\n\n/*\n * Support Dark Mode\n * We support Ionic Dark Mode. More information is here: https://ionicframework.com/docs/theming/dark-mode\n * use Always:    @import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26-dark-always.css'\n * use System:    @import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26-dark-system.css'\n * use CSS Class: @import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26-dark-class.css'\n */\n

Optional: use the iOS 26 and MD3 themes together

\n

Install the MD3 theme to style both Ionic modes from the same application.

\n

The current releases of both themes require @ionic/core 8.8.1 or later.

\n
npm install @rdlabo/ionic-theme-md3\n

When your global stylesheet uses Sass, initialize the themes in this order:

\n
@use '@rdlabo/ionic-theme-ios26/src/styles/default-variables.scss' as ios26-vars;\n@use '@rdlabo/ionic-theme-ios26/src/styles/ionic-theme-ios26.scss';\n@use '@rdlabo/ionic-theme-ios26/src/styles/ionic-theme-ios26-dark-class.scss';\n@use '@rdlabo/ionic-theme-ios26/src/styles/md-remove-ios-class-effect.scss';\n@use '@rdlabo/ionic-theme-md3/dist/css/default-variables.css' as md3-vars;\n@use '@rdlabo/ionic-theme-md3/dist/css/ionic-theme-md3.css';\n

The example uses Ionic's class-based dark mode. Your global stylesheet must also load Ionic's matching dark palette, such as @ionic/angular/css/palettes/dark.class.css for Angular. When using dark-system or dark-always, select the same variant for both Ionic's palette and the iOS 26 theme. See Ionic's Dark Mode documentation. The explicit ios26-vars and md3-vars namespaces prevent the two variable modules from using the same default namespace.

\n

Configure both transition implementations when both themes are installed:

\n
import { isPlatform } from '@ionic/core'; // or @ionic/angular (Ionic 9), @ionic/angular/standalone (Ionic 8), @ionic/react, @ionic/vue\nimport { iosTransitionAnimation, popoverEnterAnimation, popoverLeaveAnimation } from '@rdlabo/ionic-theme-ios26';\nimport { mdTransitionAnimation } from '@rdlabo/ionic-theme-md3';\n\n// Angular\nprovideIonicAngular({\n    ...\n    navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation,\n    popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined,\n    popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined,\n});\n\n// React\nsetupIonicReact({\n    ...\n    navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation,\n    popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined,\n    popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined,\n});\n\n// Vue\ncreateApp(App)\n    .use(IonicVue, {\n        ...\n        navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation,\n        popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined,\n        popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined,\n    });\n

If you installed only the iOS 26 theme, configure its animations as follows.

\n
import { isPlatform } from '@ionic/core'; // or @ionic/angular (Ionic 9), @ionic/angular/standalone (Ionic 8), @ionic/react, @ionic/vue\nimport { iosTransitionAnimation, popoverEnterAnimation, popoverLeaveAnimation } from '@rdlabo/ionic-theme-ios26';\n\n// Angular\nprovideIonicAngular({\n    ...\n    navAnimation: isPlatform('ios') ? iosTransitionAnimation: undefined,\n    popoverEnter: isPlatform('ios') ? popoverEnterAnimation: undefined,\n    popoverLeave: isPlatform('ios') ? popoverLeaveAnimation: undefined,\n});\n\n// React\nsetupIonicReact({\n    ...\n    navAnimation: isPlatform('ios') ? iosTransitionAnimation: undefined,\n    popoverEnter: isPlatform('ios') ? popoverEnterAnimation: undefined,\n    popoverLeave: isPlatform('ios') ? popoverLeaveAnimation: undefined,\n});\n\n// Vue\ncreateApp(App)\n    .use(IonicVue, {\n        ...\n        navAnimation: isPlatform('ios') ? iosTransitionAnimation: undefined,\n        popoverEnter: isPlatform('ios') ? popoverEnterAnimation: undefined,\n        popoverLeave: isPlatform('ios') ? popoverLeaveAnimation: undefined,\n})\n

Documentation

\n

Start with Installation, then Using ion-item-group when you use inset lists.

\n\n", "headings": [ { "id": "overview", @@ -53,6 +53,11 @@ export const PROJECT = { "text": "Installation", "level": 2 }, + { + "id": "optional%3A-use-the-ios-26-and-md3-themes-together", + "text": "Optional: use the iOS 26 and MD3 themes together", + "level": 3 + }, { "id": "documentation", "text": "Documentation", @@ -70,81 +75,110 @@ export const PROJECT = { "file": "using-ion-item-group.md", "section": "Guides", "path": "/projects/ionic-theme-ios26/docs/using-ion-item-group", - "html": "

This theme aims to bring Ionic Framework applications as close as possible to iOS 26 design. In most cases, you can use your existing Ionic code as-is, but only under specific conditions, you need to add ion-item-group.

\n

When is ion-item-group required?

\n

It is only required when the following condition is met:

\n
    \n
  • You have enabled the inset property on ion-list
  • \n
\n

Only when this condition applies, you need to wrap your list items with ion-item-group.

\n

Implementation Example

\n
  <ion-list inset=true>\n    <ion-list-header><ion-label>Label</ion-label></ion-list-header>\n+   <ion-item-group>\n      <ion-item>...</ion-item>\n      <ion-item>...</ion-item>\n+   </ion-item-group>\n  </ion-list>\n

Why is this change necessary?

\n

Background: Challenges in iOS Design Reproduction

\n

By default in Ionic Framework, ion-list has a background color, and ion-list-header is treated as part of the list. However, with this structure, it's impossible to accurately reproduce iOS's native design patterns.

\n

\"ion-list

\n

Solution: Background Color Separation

\n

To faithfully reproduce iOS design, this theme makes the following changes:

\n
    \n
  • Set ion-list background color to transparent
  • \n
  • Delegate background color to ion-item-group
  • \n
\n

This change allows ion-list-header to be treated as an independent element, achieving the native iOS appearance.

\n

Using the Same Design with Material Design

\n

If you want to use the same design pattern with Material Design theme, import the following CSS:

\n
@import '@rdlabo/ionic-theme-ios26/dist/css/md-ion-list-inset.css';\n

This will apply the same ion-item-group pattern to the Material Design theme as well.

\n

Summary

\n
    \n
  • Most cases: You can use your existing Ionic code as-is
  • \n
  • Specific conditions only: ion-item-group is only required when setting using inset on ion-list
  • \n
  • Purpose: To accurately reproduce iOS 26's native design patterns
  • \n
\n", + "html": "

Most Ionic markup works without changes. When an ion-list uses inset=\"true\", wrap its items in ion-item-group and keep ion-list-header outside the group.

\n

The examples use framework-neutral Web Component markup. In React or Vue, use the equivalent component and property syntax.

\n
<ion-list inset=\"true\">\n  <ion-list-header><ion-label>Connections</ion-label></ion-list-header>\n  <ion-item-group>\n    <ion-item>...</ion-item>\n    <ion-item>...</ion-item>\n  </ion-item-group>\n</ion-list>\n

No wrapper is required for lists that do not use inset=\"true\".

\n

Why the wrapper is required

\n

Ionic normally gives ion-list its background, which makes ion-list-header appear inside the same surface as the items. The iOS 26 layout treats the header and item surface separately.

\n

\"Inset

\n

The theme therefore:

\n
    \n
  • makes the inset ion-list background transparent;
  • \n
  • applies the item surface to ion-item-group; and
  • \n
  • leaves ion-list-header outside that surface.
  • \n
\n

Sharing the markup with Material Design

\n

@rdlabo/ionic-theme-md3 supports the same grouped markup, so one template can be used for both Ionic modes.

\n

When an application uses this package without @rdlabo/ionic-theme-md3, import the optional stylesheet to apply the same grouped layout in Material mode:

\n
@import '@rdlabo/ionic-theme-ios26/dist/css/md-ion-list-inset.css';\n

For two-line items and section-header groups, see Special markup and classes.

\n", "headings": [ { - "id": "when-is-ion-item-group-required%3F", - "text": "When is ion-item-group required?", + "id": "why-the-wrapper-is-required", + "text": "Why the wrapper is required", "level": 2 }, { - "id": "implementation-example", - "text": "Implementation Example", + "id": "sharing-the-markup-with-material-design", + "text": "Sharing the markup with Material Design", "level": 2 - }, + } + ], + "codes": [], + "scrollMap": [], + "editUrl": "https://github.com/rdlabo-dev/ionic-theme-ios26/edit/main/docs/using-ion-item-group.md" + }, + { + "title": "Features", + "navTitle": "Features", + "slug": "features", + "file": "features.md", + "section": "Guides", + "path": "/projects/ionic-theme-ios26/docs/features", + "html": "

Customize the theme with CSS variables and Sass mixins, or adopt it one component at a time. Markup-specific opt-ins are documented in Special markup and classes.

\n

CSS variables

\n

To customize the library's default styles to match your design, several CSS variables are provided. See this file for details:
\nhttps://github.com/rdlabo-dev/ionic-theme-ios26/blob/v3.0.0/src/styles/default-variables.scsshttps://github.com/rdlabo-dev/ionic-theme-ios26/blob/v3.0.0/src/styles/default-variables.scss

\n

Liquid Glass mixin

\n

Import the SCSS files from the main package to use the liquid glass mixin.

\n
@use '@rdlabo/ionic-theme-ios26/src/styles/utils/api.scss';\n\nion-textarea label.textarea-wrapper {\n  @include api.glass-background;\n}\n

Selective component imports

\n

For gradual adoption, you can import individual components instead of the full theme file.

\n
@import '@rdlabo/ionic-theme-ios26/dist/css/utils/translucent';\n@import '@rdlabo/ionic-theme-ios26/dist/css/components/ion-action-sheet';\n@import '@rdlabo/ionic-theme-ios26/dist/css/components/ion-alert';\n@import '@rdlabo/ionic-theme-ios26/dist/css/components/ion-button';\n/* Import the remaining components your application uses. */\n

Dark mode with individual components

\n

Use SCSS when selectively importing components with dark mode support because the selectors differ between Always, System, and Class modes.

\n

Always:

\n
@use '@rdlabo/ionic-theme-ios26/src/styles/utils/theme-dark';\n\n:root {\n  @include theme-dark.default-variables;\n}\n@include theme-dark.ion-button;\n@include theme-dark.ion-fab;\n@include theme-dark.ion-tabs;\n@include theme-dark.ion-segment;\n

System:

\n
@use '@rdlabo/ionic-theme-ios26/src/styles/utils/theme-dark';\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    @include theme-dark.default-variables;\n  }\n  @include theme-dark.ion-button;\n  @include theme-dark.ion-fab;\n  @include theme-dark.ion-tabs;\n  @include theme-dark.ion-segment;\n}\n

Class:

\n
@use '@rdlabo/ionic-theme-ios26/src/styles/utils/theme-dark';\n\n.ion-palette-dark {\n  @include theme-dark.default-variables;\n  @include theme-dark.ion-button;\n  @include theme-dark.ion-fab;\n  @include theme-dark.ion-tabs;\n  @include theme-dark.ion-segment;\n}\n

Interactive examples

\n

Browse rendered examples in the demo.

\n", + "headings": [ { - "id": "why-is-this-change-necessary%3F", - "text": "Why is this change necessary?", + "id": "css-variables", + "text": "CSS variables", "level": 2 }, { - "id": "background%3A-challenges-in-ios-design-reproduction", - "text": "Background: Challenges in iOS Design Reproduction", - "level": 3 + "id": "liquid-glass-mixin", + "text": "Liquid Glass mixin", + "level": 2 }, { - "id": "solution%3A-background-color-separation", - "text": "Solution: Background Color Separation", - "level": 3 + "id": "selective-component-imports", + "text": "Selective component imports", + "level": 2 }, { - "id": "using-the-same-design-with-material-design", - "text": "Using the Same Design with Material Design", - "level": 2 + "id": "dark-mode-with-individual-components", + "text": "Dark mode with individual components", + "level": 3 }, { - "id": "summary", - "text": "Summary", + "id": "interactive-examples", + "text": "Interactive examples", "level": 2 } ], "codes": [], "scrollMap": [], - "editUrl": "https://github.com/rdlabo-dev/ionic-theme-ios26/edit/main/docs/using-ion-item-group.md" + "editUrl": "https://github.com/rdlabo-dev/ionic-theme-ios26/edit/main/docs/features.md" }, { - "title": "Features", - "navTitle": "Features", - "slug": "features", - "file": "features.md", + "title": "Special markup and classes", + "navTitle": "Special markup and classes", + "slug": "special-markup", + "file": "special-markup.md", "section": "Guides", - "path": "/projects/ionic-theme-ios26/docs/features", - "html": "

CSS variables, opt-out class, and the liquid glass mixin. See Using ion-item-group for list markup.

\n

CSS Variables

\n

To customize the library's default styles to match your design, several CSS variables are provided. See this file for details:
\nhttps://github.com/rdlabo-dev/ionic-theme-ios26/blob/v2.3.2/src/styles/default-variables.scsshttps://github.com/rdlabo-dev/ionic-theme-ios26/blob/v2.3.2/src/styles/default-variables.scss

\n

.ios26-disabled Class

\n

Add the .ios26-disabled class to disable the iOS26 theme on specific components.

\n
<!-- iOS26 theme applied -->\n<ion-button>iOS26 Design</ion-button>\n\n<!-- Standard Ionic iOS styling -->\n<ion-button class=\"ios26-disabled\">Standard Ionic Design</ion-button>\n

Liquid Glass Mixin

\n

Import the SCSS files from the main package to use the liquid glass mixin.

\n
@use '@rdlabo/ionic-theme-ios26/src/styles/utils/api.scss';\n\nion-textarea label.textarea-wrapper {\n  @include api.glass-background;\n}\n

Additional Design

\n

To achieve higher fidelity to iOS26 design, you can implement additional design provided by this library. For more details, please visit:

\n

https://ionic-theme-ios26.netlify.app/main/docshttps://ionic-theme-ios26.netlify.app/main/docs

\n", + "path": "/projects/ionic-theme-ios26/docs/special-markup", + "html": "

Most Ionic markup works without changes. The combinations below are explicit opt-ins provided by the theme.

\n

Primary submit buttons

\n

Solid primary submit buttons use --ion-color-primary-brightness for their foreground and border treatment. Define a value with sufficient contrast for your primary color.

\n
:root {\n  --ion-color-primary-brightness: #96feff;\n}\n
<ion-button type=\"submit\" color=\"primary\">Submit</ion-button>\n<ion-button class=\"button-submit\" fill=\"solid\" color=\"primary\">Continue</ion-button>\n

Use .button-submit when the button needs the same treatment but cannot use type=\"submit\".

\n

Two-line inset list items

\n

Place an unslotted ion-label immediately alongside an unslotted ion-note to render a two-line item. When using the iOS-style inset-list background, wrap the items in ion-item-group; keep ion-list-header outside the group.

\n
<ion-list inset=\"true\">\n  <ion-list-header>\n    <ion-label>Connections</ion-label>\n  </ion-list-header>\n  <ion-item-group>\n    <ion-item>\n      <ion-label>Network &amp; internet</ion-label>\n      <ion-note>Mobile, Wi-Fi, hotspot</ion-note>\n    </ion-item>\n  </ion-item-group>\n</ion-list>\n

Use slot=\"end\" on ion-note when you want the standard trailing-note layout instead.

\n

Inset-list section headers

\n

Add .item-group-header to an ion-item-group to create the centered icon, title, and description used at the top of the component demo pages.

\n

This is an introductory group. Place regular list items in a separate ion-item-group that follows it.

\n
<ion-list inset=\"true\">\n  <ion-item-group class=\"item-group-header\">\n    <ion-item>\n      <ion-label>\n        <ion-icon name=\"list\" style=\"background: var(--ion-color-primary)\"></ion-icon>\n        <h2>Lists</h2>\n        <ion-text>Inset-list examples</ion-text>\n      </ion-label>\n    </ion-item>\n  </ion-item-group>\n  <ion-item-group>\n    <ion-item><ion-label>First item</ion-label></ion-item>\n  </ion-item-group>\n</ion-list>\n

Full-width segments

\n

Add .segment-expand when segment buttons should divide the available width evenly. The class also changes the Liquid Glass effect sizing when registerSegmentEffect is used.

\n
<ion-segment class=\"segment-expand\" value=\"new\">\n  <ion-segment-button value=\"new\"><ion-label>New</ion-label></ion-segment-button>\n  <ion-segment-button value=\"replied\"><ion-label>Replied</ion-label></ion-segment-button>\n</ion-segment>\n

Classic search bar in a condense header

\n

The theme gives iOS search bars the iOS 26 appearance by default. Add .searchbar-classic to the search field shown beneath a large title in an ion-header with collapse=\"condense\". It uses the conventional filled iOS appearance and collapses with the large title instead of remaining in the fixed header.

\n

Place it in a toolbar with a color, such as color=\"light\"; the classic background is derived from that color's contrast value.

\n

The example uses Ionic's standard collapsible large-title structure. Scroll the preview to collapse the large title and reveal the fixed header.

\n
<div class=\"ion-page\">\n  <ion-header translucent=\"true\">\n    <ion-toolbar color=\"light\">\n      <ion-title>Search</ion-title>\n    </ion-toolbar>\n  </ion-header>\n  <ion-content color=\"light\" fullscreen=\"true\">\n    <ion-header collapse=\"condense\">\n      <ion-toolbar color=\"light\">\n        <ion-title size=\"large\">Search</ion-title>\n      </ion-toolbar>\n      <ion-toolbar color=\"light\">\n        <ion-searchbar class=\"searchbar-classic\" placeholder=\"Filter results\"></ion-searchbar>\n      </ion-toolbar>\n    </ion-header>\n    <ion-list inset=\"true\">\n      <ion-item-group>\n        <ion-item><ion-label>Recent item 1</ion-label></ion-item>\n        <ion-item><ion-label>Recent item 2</ion-label></ion-item>\n        <ion-item><ion-label>Recent item 3</ion-label></ion-item>\n        <ion-item><ion-label>Recent item 4</ion-label></ion-item>\n        <ion-item><ion-label>Recent item 5</ion-label></ion-item>\n        <ion-item><ion-label>Recent item 6</ion-label></ion-item>\n        <ion-item><ion-label>Recent item 7</ion-label></ion-item>\n        <ion-item><ion-label>Recent item 8</ion-label></ion-item>\n        <ion-item><ion-label>Recent item 9</ion-label></ion-item>\n        <ion-item><ion-label>Recent item 10</ion-label></ion-item>\n      </ion-item-group>\n    </ion-list>\n  </ion-content>\n</div>\n

The .ion-page wrapper makes this embedded preview behave like a complete routed page. An application using ion-router-outlet normally receives that page container automatically. The inset list and its items only provide enough content to demonstrate scrolling; they are not required by .searchbar-classic.

\n

Search-bar toolbars

\n

Add .toolbar-searchbar when an ion-toolbar combines a search bar with start or end buttons. The class centers the slotted controls and adjusts the spacing around the search field.

\n
<ion-toolbar class=\"toolbar-searchbar\">\n  <ion-buttons slot=\"start\">\n    <ion-button>Cancel</ion-button>\n  </ion-buttons>\n  <ion-searchbar></ion-searchbar>\n</ion-toolbar>\n

Opting out

\n

Add .ios26-disabled to an individual Ionic component when it must retain Ionic's standard iOS styling.

\n
<ion-button>iOS 26 theme</ion-button> <ion-button class=\"ios26-disabled\">Standard Ionic button</ion-button>\n

For the background model behind inset lists, see Using ion-item-group.

\n", "headings": [ { - "id": "css-variables", - "text": "CSS Variables", - "level": 3 + "id": "primary-submit-buttons", + "text": "Primary submit buttons", + "level": 2 }, { - "id": ".ios26-disabled-class", - "text": ".ios26-disabled Class", - "level": 3 + "id": "two-line-inset-list-items", + "text": "Two-line inset list items", + "level": 2 }, { - "id": "liquid-glass-mixin", - "text": "Liquid Glass Mixin", - "level": 3 + "id": "inset-list-section-headers", + "text": "Inset-list section headers", + "level": 2 }, { - "id": "additional-design", - "text": "Additional Design", - "level": 3 + "id": "full-width-segments", + "text": "Full-width segments", + "level": 2 + }, + { + "id": "classic-search-bar-in-a-condense-header", + "text": "Classic search bar in a condense header", + "level": 2 + }, + { + "id": "search-bar-toolbars", + "text": "Search-bar toolbars", + "level": 2 + }, + { + "id": "opting-out", + "text": "Opting out", + "level": 2 } ], "codes": [], "scrollMap": [], - "editUrl": "https://github.com/rdlabo-dev/ionic-theme-ios26/edit/main/docs/features.md" + "editUrl": "https://github.com/rdlabo-dev/ionic-theme-ios26/edit/main/docs/special-markup.md" }, { "title": "Experimental Animation", @@ -153,17 +187,17 @@ export const PROJECT = { "file": "experimental-animation.md", "section": "Guides", "path": "/projects/ionic-theme-ios26/docs/experimental-animation", - "html": "

Optional gesture and animation helpers. The theme works without this feature.

\n

This feature is experimental. The library can be used without this feature.

\n

Sheet of Glass with ion-tab-button / ion-segment-button

\n

By registering ion-tab-bar / ion-segment, you can display animation effects on ion-tab-button / ion-segment-button

\n

\"Sheet

\n
import { registerTabBarEffect, registerSegmentEffect } from '@rdlabo/ionic-theme-ios26';\n\n/**\n * Register DOM elements. Effects are applied using Ionic Gesture and Ionic Animation.\n */\nconst tabBar = document.querySelector<HTMLElement>('ion-tab-bar');\nconst segment = document.querySelector<HTMLElement>('ion-segment');\nconst registeredTabBarEffect = tabBar ? registerTabBarEffect(tabBar) : undefined;\nconst registeredSegmentEffect = segment ? registerSegmentEffect(segment) : undefined;\n\nconst destroy = () => {\n  /**\n   * If the registered DOM element is removed (e.g., due to page navigation),\n   * make sure to destroy the gesture and animation. This will also remove the event listeners.\n   * You can re-register them if needed.\n   */\n  registeredTabBarEffect?.destroy();\n  registeredSegmentEffect?.destroy();\n};\n

TabBarSearchable: Searchable with ion-tab-bar and ion-fab-button

\n

Enable Searchable for the DOM structure with the specified markup inner ion-tabs.

\n

\"TabBarSearchable

\n
<ion-content>...</ion-content>\n<ion-fab vertical=\"bottom\" horizontal=\"end\" slot=\"fixed\">\n  <ion-fab-button (click)=\"present($event)\">\n    <ion-icon name=\"search\"></ion-icon>\n  </ion-fab-button>\n</ion-fab>\n<ion-footer [translucent]=\"true\">\n  <ion-toolbar>\n    <ion-buttons slot=\"start\">\n      <!-- ion-icon name is set dynamically by the animation -->\n      <ion-button fill=\"default\"><ion-icon slot=\"icon-only\"></ion-icon> </ion-button>\n    </ion-buttons>\n    <!-- User set `ionChange` or other events. -->\n    <ion-searchbar (ionChange)=\"example($event)\"></ion-searchbar>\n  </ion-toolbar>\n</ion-footer>\n
import { attachTabBarSearchable, TabBarSearchableType } from '@rdlabo/ionic-theme-ios26';\nimport type { TabBarSearchableFunction } from '@rdlabo/ionic-theme-ios26';\n\nlet searchableFun: TabBarSearchableFunction | undefined;\nconst initialize = () => {\n  // attachTabBarSearchable has state. You should initialize per page.\n  const tabBar = document.querySelector<HTMLElement>('ion-tab-bar');\n  const fabButton = document.querySelector<HTMLElement>('ion-fab-button');\n  const footer = document.querySelector<HTMLElement>('ion-footer');\n  if (!tabBar || !fabButton || !footer) {\n    return;\n  }\n  searchableFun = attachTabBarSearchable(tabBar, fabButton, footer);\n};\n\nconst present = (event: Event) => {\n  searchableFun!(event, TabBarSearchableType.Enter);\n};\n\nconst dismiss = (event: Event) => {\n  searchableFun!(event, TabBarSearchableType.Leave);\n};\n
", + "html": "

These gesture and animation helpers are experimental and optional. The theme works without them.

\n

Sheet of Glass with ion-tab-button / ion-segment-button

\n

Register an ion-tab-bar or ion-segment element to add a moving selection effect to its buttons.

\n

\"Sheet

\n
import { registerTabBarEffect, registerSegmentEffect } from '@rdlabo/ionic-theme-ios26';\n\n/**\n * Register DOM elements. Effects are applied using Ionic Gesture and Ionic Animation.\n */\nconst tabBar = document.querySelector<HTMLElement>('ion-tab-bar');\nconst segment = document.querySelector<HTMLElement>('ion-segment');\nconst registeredTabBarEffect = tabBar ? registerTabBarEffect(tabBar) : undefined;\nconst registeredSegmentEffect = segment ? registerSegmentEffect(segment) : undefined;\n\nconst destroy = () => {\n  /**\n   * If the registered DOM element is removed (e.g., due to page navigation),\n   * make sure to destroy the gesture and animation. This will also remove the event listeners.\n   * You can re-register them if needed.\n   */\n  registeredTabBarEffect?.destroy();\n  registeredSegmentEffect?.destroy();\n};\n

TabBarSearchable: Searchable with ion-tab-bar and ion-fab-button

\n

Use the following structure inside ion-tabs to animate a search button into a search toolbar.

\n

\"TabBarSearchable

\n
<ion-content>...</ion-content>\n<ion-fab vertical=\"bottom\" horizontal=\"end\" slot=\"fixed\">\n  <ion-fab-button (click)=\"present($event)\">\n    <ion-icon name=\"search\"></ion-icon>\n  </ion-fab-button>\n</ion-fab>\n<ion-footer [translucent]=\"true\">\n  <ion-toolbar>\n    <ion-buttons slot=\"start\">\n      <!-- ion-icon name is set dynamically by the animation -->\n      <ion-button fill=\"default\"><ion-icon slot=\"icon-only\"></ion-icon> </ion-button>\n    </ion-buttons>\n    <!-- User set `ionChange` or other events. -->\n    <ion-searchbar (ionChange)=\"example($event)\"></ion-searchbar>\n  </ion-toolbar>\n</ion-footer>\n
import { attachTabBarSearchable, TabBarSearchableType } from '@rdlabo/ionic-theme-ios26';\nimport type { TabBarSearchableFunction } from '@rdlabo/ionic-theme-ios26';\n\nlet searchableFun: TabBarSearchableFunction | undefined;\nconst initialize = () => {\n  // attachTabBarSearchable has state. You should initialize per page.\n  const tabBar = document.querySelector<HTMLElement>('ion-tab-bar');\n  const fabButton = document.querySelector<HTMLElement>('ion-fab-button');\n  const footer = document.querySelector<HTMLElement>('ion-footer');\n  if (!tabBar || !fabButton || !footer) {\n    return;\n  }\n  searchableFun = attachTabBarSearchable(tabBar, fabButton, footer);\n};\n\nconst present = (event: Event) => {\n  searchableFun!(event, TabBarSearchableType.Enter);\n};\n\nconst dismiss = (event: Event) => {\n  searchableFun!(event, TabBarSearchableType.Leave);\n};\n
", "headings": [ { "id": "sheet-of-glass-with-ion-tab-button-%2F-ion-segment-button", "text": "Sheet of Glass with ion-tab-button / ion-segment-button", - "level": 3 + "level": 2 }, { "id": "tabbarsearchable%3A-searchable-with-ion-tab-bar-and-ion-fab-button", "text": "TabBarSearchable: Searchable with ion-tab-bar and ion-fab-button", - "level": 3 + "level": 2 } ], "codes": [], @@ -177,12 +211,12 @@ export const PROJECT = { "file": "ios-18.md", "section": "Guides", "path": "/projects/ionic-theme-ios26/docs/ios-18", - "html": "

How to prevent loading a theme file on iOS 18

\n

If you want to load a theme file only when the user's device is running iOS 26 (and let users on iOS 18 use the default Ionic iOS theme), you can achieve this by adding a supports-condition to your import.

\n
@import '@rdlabo/ionic-theme-ios26/dist/css/default-variables.css' supports(text-wrap: pretty);\n@import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26.css' supports(text-wrap: pretty);\n@import '@rdlabo/ionic-theme-ios26/dist/css/md-remove-ios-class-effect.css'\n  supports(text-wrap: pretty);\n@import '@rdlabo/ionic-theme-ios26/dist/css/md-ion-list-inset.css' supports(text-wrap: pretty);\n
", + "html": "

How to prevent loading the theme on iOS 18

\n

If you want to load a theme file only when the user's device is running iOS 26 (and let users on iOS 18 use the default Ionic iOS theme), you can achieve this by adding a supports-condition to your import.

\n
@import '@rdlabo/ionic-theme-ios26/dist/css/default-variables.css' supports(text-wrap: pretty);\n@import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26.css' supports(text-wrap: pretty);\n@import '@rdlabo/ionic-theme-ios26/dist/css/md-remove-ios-class-effect.css' supports(text-wrap: pretty);\n@import '@rdlabo/ionic-theme-ios26/dist/css/md-ion-list-inset.css' supports(text-wrap: pretty);\n
", "headings": [ { - "id": "how-to-prevent-loading-a-theme-file-on-ios-18", - "text": "How to prevent loading a theme file on iOS 18", - "level": 3 + "id": "how-to-prevent-loading-the-theme-on-ios-18", + "text": "How to prevent loading the theme on iOS 18", + "level": 2 } ], "codes": [], @@ -196,11 +230,46 @@ export const PROJECT = { "file": "migration.md", "section": "Guides", "path": "/projects/ionic-theme-ios26/docs/migration", - "html": "

For gradual migration, you can selectively apply the iOS26 theme by importing individual components instead of the full theme file.

\n
@import '@rdlabo/ionic-theme-ios26/dist/css/utils/translucent';\n@import '@rdlabo/ionic-theme-ios26/dist/css/components/ion-action-sheet';\n@import '@rdlabo/ionic-theme-ios26/dist/css/components/ion-alert';\n@import '@rdlabo/ionic-theme-ios26/dist/css/components/ion-button';\n...\n

Dark Mode with Individual Components

\n

When importing individual components with dark mode support, use SCSS instead of CSS. This is because the selectors differ between Always, System, and Class modes.

\n
\n

Note: Currently, only ion-button has separate dark mode styling applied.

\n
\n

Always (Always Dark Mode):

\n
@use '@rdlabo/ionic-theme-ios26/src/styles/utils/theme-dark';\n\n:root {\n  @include theme-dark.default-variables;\n}\n@include theme-dark.ion-button;\n@include theme-dark.ion-fab;\n@include theme-dark.ion-tabs;\n@include theme-dark.ion-segment;\n

System (Follow System Settings):

\n
@use '@rdlabo/ionic-theme-ios26/src/styles/utils/theme-dark';\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    @include theme-dark.default-variables;\n  }\n  @include theme-dark.ion-button;\n  @include theme-dark.ion-fab;\n  @include theme-dark.ion-tabs;\n  @include theme-dark.ion-segment;\n}\n

Class (Toggle with CSS Class):

\n
@use '@rdlabo/ionic-theme-ios26/src/styles/utils/theme-dark';\n\n.ion-palette-dark {\n  @include theme-dark.default-variables;\n  @include theme-dark.ion-button;\n  @include theme-dark.ion-fab;\n  @include theme-dark.ion-tabs;\n  @include theme-dark.ion-segment;\n}\n
", + "html": "

Use the section for the version you are upgrading to. Each section lists only the changes that require application code or configuration updates.

\n

Migrating to 3.0.0

\n

Rename .header-item-group to .item-group-header

\n

The class for an ion-item-group used as a section header has been renamed for consistency with the element it modifies. Replace every occurrence of .header-item-group in application templates and styles.

\n
- <ion-item-group class=\"header-item-group\">\n+ <ion-item-group class=\"item-group-header\">\n    ...\n  </ion-item-group>\n

The old class is no longer styled by the theme. This rename applies to markup shared with @rdlabo/ionic-theme-md3 as well.

\n

Migrating to 2.0.0

\n

Configure iosTransitionAnimation

\n

Version 2 requires the package navigation transition. It follows Ionic's default iOS transition without the obsolete animateBackButton() behavior that animated a Large Title into the back-button label.

\n
import { isPlatform } from '@ionic/core'; // or @ionic/angular/standalone, @ionic/react, @ionic/vue\nimport { iosTransitionAnimation } from '@rdlabo/ionic-theme-ios26';\n\n// Angular\nprovideIonicAngular({\n  // ...\n  navAnimation: isPlatform('ios') ? iosTransitionAnimation : undefined,\n});\n\n// React\nsetupIonicReact({\n  // ...\n  navAnimation: isPlatform('ios') ? iosTransitionAnimation : undefined,\n});\n\n// Vue\ncreateApp(App).use(IonicVue, {\n  // ...\n  navAnimation: isPlatform('ios') ? iosTransitionAnimation : undefined,\n});\n

With this transition configured, <ion-buttons><ion-back-button></ion-back-button></ion-buttons> can be used without the unwanted transition side effects caused by the old animation.

\n

Migrating to 1.0.0

\n

Update SCSS import paths

\n

The source files moved under src/styles when JavaScript files were added to the package.

\n
- @import '@rdlabo/ionic-theme-ios26/src/default-variables.scss';\n+ @import '@rdlabo/ionic-theme-ios26/src/styles/default-variables.scss';\n

Generated CSS paths under dist did not change.

\n

Rename --ios26-color-background-rgb

\n
  :root {\n-   --ios26-color-background-rgb: 255, 255, 255;\n+   --ios26-content-box-shadow-rgb: 255, 255, 255;\n  }\n

Rename brightness variables

\n

Replace each --ion-color-*-brightness-rgb variable with --ion-color-*-brightness and use a color value instead of an RGB channel list.

\n
  :root {\n-   --ion-color-primary-brightness-rgb: 130, 255, 255;\n+   --ion-color-primary-brightness: #96feff;\n  }\n
", "headings": [ { - "id": "dark-mode-with-individual-components", - "text": "Dark Mode with Individual Components", + "id": "migrating-to-3.0.0", + "text": "Migrating to 3.0.0", + "level": 2 + }, + { + "id": "rename-.header-item-group-to-.item-group-header", + "text": "Rename .header-item-group to .item-group-header", + "level": 3 + }, + { + "id": "migrating-to-2.0.0", + "text": "Migrating to 2.0.0", + "level": 2 + }, + { + "id": "configure-iostransitionanimation", + "text": "Configure iosTransitionAnimation", + "level": 3 + }, + { + "id": "migrating-to-1.0.0", + "text": "Migrating to 1.0.0", + "level": 2 + }, + { + "id": "update-scss-import-paths", + "text": "Update SCSS import paths", + "level": 3 + }, + { + "id": "rename---ios26-color-background-rgb", + "text": "Rename --ios26-color-background-rgb", + "level": 3 + }, + { + "id": "rename-brightness-variables", + "text": "Rename brightness variables", "level": 3 } ], diff --git a/src/app/generated/projects/ionic-theme-ios26.ja.generated.ts b/src/app/generated/projects/ionic-theme-ios26.ja.generated.ts index a8b544b..659b7eb 100644 --- a/src/app/generated/projects/ionic-theme-ios26.ja.generated.ts +++ b/src/app/generated/projects/ionic-theme-ios26.ja.generated.ts @@ -8,7 +8,7 @@ export const PROJECT = { "repositoryUrl": "https://github.com/rdlabo-dev/ionic-theme-ios26", "category": "frontend-tools", "icon": "theme", - "version": "2.3.2", + "version": "3.0.0", "description": "Ionicアプリ向けのiOS 26デザインスタイル。", "headline": "IonicアプリにiOS 26デザインを取り入れる", "overview": "IonicコンポーネントへiOS 26のCSS、トランジション、Liquid Glassインタラクションを適用し、ダークモードと段階的な移行にも対応します。", @@ -36,7 +36,7 @@ export const PROJECT = { "file": "readme.md", "section": "クイックスタート", "path": "/projects/ionic-theme-ios26/docs/readme", - "html": "

IonicアプリケーションにiOS26デザインシステムを適用するCSS/JSテーマライブラリです。

\n

\"iOS

\n

DEMOはこちら: https://ionic-theme-ios26.netlify.app/

\n

概要

\n

このライブラリは、IonicアプリケーションにiOS26デザインシステムをもたらすCSS/JSファイルを提供します。Ionicコンポーネントの見た目を、最新のiOS26デザインガイドラインに合わせて更新します。

\n

Android Design(Material Design 3)テーマも作成中です。ぜひチェックしてください!

\n

👉️rdlabo-dev/ionic-theme-md3

\n

クイックスタート

\n

インストール のあと、テーマ CSS をインポートします。詳細は下のインストールです。

\n

インストール

\n

これはIonicプロジェクトを拡張するためのCSSテーマです。単体では動作しないため、Ionic Frameworkと一緒に使ってください。

\n
npm install @rdlabo/ionic-theme-ios26\n

Note: @ionic/core@ < 8.8.1 を使う場合は、@rdlabo/ionic-theme-ios26@2.2.1 を使ってください。

\n

プロジェクトのメインCSSファイル(例: src/styles.scss)でテーマをインポートします。

\n
@import '@rdlabo/ionic-theme-ios26/dist/css/default-variables.css';\n@import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26.css';\n\n/**\n * This file is to eliminate the impact of class name changes for iOS26.\n * For example, `ion-buttons ion-button[fill=default]` is not normally implemented, but may be required for iOS26.\n * This file is to eliminate such effects.\n * Note: This is not include `@rdlabo/ionic-theme-md3`\n */\n@import '@rdlabo/ionic-theme-ios26/dist/css/md-remove-ios-class-effect.css';\n\n/**\n * If you will use the design of ion-item-group with ion-list on Android as well, import it.\n * More info: https://github.com/rdlabo-dev/ionic-theme-ios26/blob/v2.3.2/USING_ION_ITEM_GROUP.md\n * Note: This is include `@rdlabo/ionic-theme-md3`\n * @import '@rdlabo/ionic-theme-ios26/dist/css/md-ion-list-inset.css';\n */\n\n/*\n * Support Dark Mode\n * We support Ionic Dark Mode. More information is here: https://ionicframework.com/docs/theming/dark-mode\n * use Always:    @import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26-dark-always.css'\n * use System:    @import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26-dark-system.css'\n * use CSS Class: @import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26-dark-class.css'\n */\n

次に、iOS 26向けのアニメーションを設定します。Ionicの設定オプションに次を追加してください。

\n
import { isPlatform } from '@ionic/core'; // or @ionic/angular/standalone, @ionic/react, @ionic/vue\nimport { iosTransitionAnimation, popoverEnterAnimation, popoverLeaveAnimation } from '@rdlabo/ionic-theme-ios26';\n\n// Angular\nprovideIonicAngular({\n    ...\n    navAnimation: isPlatform('ios') ? iosTransitionAnimation: undefined,\n    popoverEnter: isPlatform('ios') ? popoverEnterAnimation: undefined,\n    popoverLeave: isPlatform('ios') ? popoverLeaveAnimation: undefined,\n});\n\n// React\nsetupIonicReact({\n    ...\n    navAnimation: isPlatform('ios') ? iosTransitionAnimation: undefined,\n    popoverEnter: isPlatform('ios') ? popoverEnterAnimation: undefined,\n    popoverLeave: isPlatform('ios') ? popoverLeaveAnimation: undefined,\n});\n\n// Vue\ncreateApp(App)\n    .use(IonicVue, {\n        ...\n        navAnimation: isPlatform('ios') ? iosTransitionAnimation: undefined,\n        popoverEnter: isPlatform('ios') ? popoverEnterAnimation: undefined,\n        popoverLeave: isPlatform('ios') ? popoverLeaveAnimation: undefined,\n})\n

ドキュメント

\n

上の インストール から始め、inset リストでは ion-item-groupの使用方法 を見てください。

\n\n", + "html": "

IonicアプリケーションにiOS26デザインシステムを適用するCSS/JSテーマライブラリです。

\n

\"iOS

\n

DEMOはこちら: https://ionic-theme-ios26.rdlabo.dev/

\n

概要

\n

このライブラリは、IonicアプリケーションにiOS26デザインシステムをもたらすCSS/JSファイルを提供します。Ionicコンポーネントの見た目を、最新のiOS26デザインガイドラインに合わせて更新します。

\n

Android Design(Material Design 3)テーマも作成中です。ぜひチェックしてください!

\n

👉️rdlabo-dev/ionic-theme-md3

\n

クイックスタート

\n

インストール のあと、テーマ CSS をインポートします。詳細は下のインストールです。

\n

インストール

\n

これはIonicプロジェクトを拡張するためのCSSテーマです。単体では動作しないため、Ionic Frameworkと一緒に使ってください。

\n
npm install @rdlabo/ionic-theme-ios26\n

Note: @ionic/core@ < 8.8.1 を使う場合は、@rdlabo/ionic-theme-ios26@2.2.1 を使ってください。

\n

プロジェクトのメインCSSファイル(例: src/styles.scss)でテーマをインポートします。

\n
@import '@rdlabo/ionic-theme-ios26/dist/css/default-variables.css';\n@import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26.css';\n\n/**\n * This file is to eliminate the impact of class name changes for iOS26.\n * For example, `ion-buttons ion-button[fill=default]` is not normally implemented, but may be required for iOS26.\n * This file is to eliminate such effects.\n * Note: This stylesheet is not included in `@rdlabo/ionic-theme-md3`.\n */\n@import '@rdlabo/ionic-theme-ios26/dist/css/md-remove-ios-class-effect.css';\n\n/**\n * If you will use the design of ion-item-group with ion-list on Android as well, import it.\n * More info: https://github.com/rdlabo-dev/ionic-theme-ios26/blob/v3.0.0/docs/using-ion-item-group.md\n * Note: This stylesheet is included in `@rdlabo/ionic-theme-md3`.\n * @import '@rdlabo/ionic-theme-ios26/dist/css/md-ion-list-inset.css';\n */\n\n/*\n * Support Dark Mode\n * We support Ionic Dark Mode. More information is here: https://ionicframework.com/docs/theming/dark-mode\n * use Always:    @import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26-dark-always.css'\n * use System:    @import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26-dark-system.css'\n * use CSS Class: @import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26-dark-class.css'\n */\n

オプション: iOS 26テーマとMD3テーマを併用する

\n

同じアプリケーションでIonicの両モードをスタイルするには、MD3テーマをインストールします。

\n

両テーマの現行リリースには、@ionic/core 8.8.1以降が必要です。

\n
npm install @rdlabo/ionic-theme-md3\n

グローバルスタイルシートでSassを使っている場合は、次の順序でテーマを初期化します。

\n
@use '@rdlabo/ionic-theme-ios26/src/styles/default-variables.scss' as ios26-vars;\n@use '@rdlabo/ionic-theme-ios26/src/styles/ionic-theme-ios26.scss';\n@use '@rdlabo/ionic-theme-ios26/src/styles/ionic-theme-ios26-dark-class.scss';\n@use '@rdlabo/ionic-theme-ios26/src/styles/md-remove-ios-class-effect.scss';\n@use '@rdlabo/ionic-theme-md3/dist/css/default-variables.css' as md3-vars;\n@use '@rdlabo/ionic-theme-md3/dist/css/ionic-theme-md3.css';\n

この例ではIonicのclassベースのダークモードを使います。グローバルスタイルシートでは、Angularの @ionic/angular/css/palettes/dark.class.css など、Ionic側の対応するダークパレットも読み込んでください。dark-system または dark-always を使う場合は、IonicのパレットとiOS 26テーマの両方で同じvariantを選びます。詳しくはIonicのダークモードのドキュメントを参照してください。ios26-varsmd3-vars を明示することで、2つの変数モジュールが同じデフォルトnamespaceを使うことを防ぎます。

\n

両テーマをインストールした場合は、両方のtransition実装を設定します。

\n
import { isPlatform } from '@ionic/core'; // or @ionic/angular (Ionic 9), @ionic/angular/standalone (Ionic 8), @ionic/react, @ionic/vue\nimport { iosTransitionAnimation, popoverEnterAnimation, popoverLeaveAnimation } from '@rdlabo/ionic-theme-ios26';\nimport { mdTransitionAnimation } from '@rdlabo/ionic-theme-md3';\n\n// Angular\nprovideIonicAngular({\n    ...\n    navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation,\n    popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined,\n    popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined,\n});\n\n// React\nsetupIonicReact({\n    ...\n    navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation,\n    popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined,\n    popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined,\n});\n\n// Vue\ncreateApp(App)\n    .use(IonicVue, {\n        ...\n        navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation,\n        popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined,\n        popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined,\n    });\n

iOS 26テーマだけをインストールした場合は、次のようにアニメーションを設定します。

\n
import { isPlatform } from '@ionic/core'; // or @ionic/angular (Ionic 9), @ionic/angular/standalone (Ionic 8), @ionic/react, @ionic/vue\nimport { iosTransitionAnimation, popoverEnterAnimation, popoverLeaveAnimation } from '@rdlabo/ionic-theme-ios26';\n\n// Angular\nprovideIonicAngular({\n    ...\n    navAnimation: isPlatform('ios') ? iosTransitionAnimation: undefined,\n    popoverEnter: isPlatform('ios') ? popoverEnterAnimation: undefined,\n    popoverLeave: isPlatform('ios') ? popoverLeaveAnimation: undefined,\n});\n\n// React\nsetupIonicReact({\n    ...\n    navAnimation: isPlatform('ios') ? iosTransitionAnimation: undefined,\n    popoverEnter: isPlatform('ios') ? popoverEnterAnimation: undefined,\n    popoverLeave: isPlatform('ios') ? popoverLeaveAnimation: undefined,\n});\n\n// Vue\ncreateApp(App)\n    .use(IonicVue, {\n        ...\n        navAnimation: isPlatform('ios') ? iosTransitionAnimation: undefined,\n        popoverEnter: isPlatform('ios') ? popoverEnterAnimation: undefined,\n        popoverLeave: isPlatform('ios') ? popoverLeaveAnimation: undefined,\n})\n

ドキュメント

\n

上の インストール から始め、inset リストでは ion-item-groupの使用方法 を見てください。

\n\n", "headings": [ { "id": "%E6%A6%82%E8%A6%81", @@ -53,6 +53,11 @@ export const PROJECT = { "text": "インストール", "level": 2 }, + { + "id": "%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3%3A-ios-26%E3%83%86%E3%83%BC%E3%83%9E%E3%81%A8md3%E3%83%86%E3%83%BC%E3%83%9E%E3%82%92%E4%BD%B5%E7%94%A8%E3%81%99%E3%82%8B", + "text": "オプション: iOS 26テーマとMD3テーマを併用する", + "level": 3 + }, { "id": "%E3%83%89%E3%82%AD%E3%83%A5%E3%83%A1%E3%83%B3%E3%83%88", "text": "ドキュメント", @@ -70,81 +75,110 @@ export const PROJECT = { "file": "using-ion-item-group.md", "section": "ガイド", "path": "/projects/ionic-theme-ios26/docs/using-ion-item-group", - "html": "\n

このテーマは、Ionic Frameworkアプリケーションを可能な限りiOS 26デザインに近づけることを目指しています。多くの場合、既存のIonicコードをそのまま使えますが、特定の条件下でのみ ion-item-group の追加が必要です。

\n

ion-item-group が必要な場合

\n

次の条件を満たす場合にのみ必要です:

\n
    \n
  • ion-listinset プロパティを有効にしている
  • \n
\n

この条件に当てはまる場合のみ、リスト項目を ion-item-group で囲む必要があります。

\n

実装例

\n
  <ion-list inset=true>\n    <ion-list-header><ion-label>Label</ion-label></ion-list-header>\n+   <ion-item-group>\n      <ion-item>...</ion-item>\n      <ion-item>...</ion-item>\n+   </ion-item-group>\n  </ion-list>\n

なぜこの変更が必要か

\n

背景: iOSデザイン再現の課題

\n

Ionic Frameworkのデフォルトでは、ion-list に背景色があり、ion-list-header はリストの一部として扱われます。しかし、この構造では iOSのネイティブなデザインパターン を正確に再現できません。

\n

\"ion-list

\n

解決策: 背景色の分離

\n

iOSデザインを忠実に再現するため、このテーマは次の変更を行います:

\n
    \n
  • ion-list の背景色を透明にする
  • \n
  • 背景色を ion-item-group に委ねる
  • \n
\n

この変更により、ion-list-header を独立した要素として扱え、ネイティブなiOSの見た目を実現できます。

\n

Material Designで同じデザインを使う

\n

同じデザインパターンをMaterial Designテーマでも使いたい場合は、次のCSSをインポートします:

\n
@import '@rdlabo/ionic-theme-ios26/dist/css/md-ion-list-inset.css';\n

これにより、Material Designテーマにも同じ ion-item-group パターンが適用されます。

\n

まとめ

\n
    \n
  • ほとんどの場合: 既存のIonicコードをそのまま使えます
  • \n
  • 特定条件のみ: ion-listinset を使う場合にのみ ion-item-group が必要です
  • \n
  • 目的: iOS 26のネイティブなデザインパターンを正確に再現するため
  • \n
\n", + "html": "

ほとんどのIonicマークアップは変更せずに使えます。ion-listinset=\"true\" を使う場合は、itemを ion-item-group で囲み、ion-list-header はgroupの外に置きます。

\n

以下はframeworkに依存しないWeb Component形式のマークアップです。ReactまたはVueでは、各frameworkに対応するcomponentとpropertyの構文に置き換えてください。

\n
<ion-list inset=\"true\">\n  <ion-list-header><ion-label>Connections</ion-label></ion-list-header>\n  <ion-item-group>\n    <ion-item>...</ion-item>\n    <ion-item>...</ion-item>\n  </ion-item-group>\n</ion-list>\n

inset=\"true\" を使わないlistでは、このwrapperは不要です。

\n

wrapperが必要な理由

\n

Ionicは通常、ion-list 自体に背景を設定するため、ion-list-header もitemと同じsurface内に表示されます。iOS 26のlayoutではheaderとitemのsurfaceを分離します。

\n

\"ion-item-groupが必要な理由を示すinset

\n

そのため、このテーマは次のようにstyleを適用します。

\n
    \n
  • inset ion-list の背景を透明にする
  • \n
  • itemのsurfaceを ion-item-group に適用する
  • \n
  • ion-list-header をsurfaceの外に置く
  • \n
\n

Material Designとのマークアップ共有

\n

@rdlabo/ionic-theme-md3 も同じgroup構造に対応しているため、両方のIonic modeで1つのtemplateを共有できます。

\n

このpackageを @rdlabo/ionic-theme-md3 なしで使うapplicationでは、Material modeにも同じgroup layoutを適用するため、任意のstylesheetをimportします。

\n
@import '@rdlabo/ionic-theme-ios26/dist/css/md-ion-list-inset.css';\n

2行itemとsection header groupについては 特別なマークアップとクラス を参照してください。

\n", "headings": [ { - "id": "ion-item-group-%E3%81%8C%E5%BF%85%E8%A6%81%E3%81%AA%E5%A0%B4%E5%90%88", - "text": "ion-item-group が必要な場合", + "id": "wrapper%E3%81%8C%E5%BF%85%E8%A6%81%E3%81%AA%E7%90%86%E7%94%B1", + "text": "wrapperが必要な理由", "level": 2 }, { - "id": "%E5%AE%9F%E8%A3%85%E4%BE%8B", - "text": "実装例", + "id": "material-design%E3%81%A8%E3%81%AE%E3%83%9E%E3%83%BC%E3%82%AF%E3%82%A2%E3%83%83%E3%83%97%E5%85%B1%E6%9C%89", + "text": "Material Designとのマークアップ共有", "level": 2 - }, + } + ], + "codes": [], + "scrollMap": [], + "editUrl": "https://github.com/rdlabo-dev/docs/edit/main/src/ionic-theme-ios26/docs/ja/using-ion-item-group.md" + }, + { + "title": "機能", + "navTitle": "機能", + "slug": "features", + "file": "features.md", + "section": "ガイド", + "path": "/projects/ionic-theme-ios26/docs/features", + "html": "

CSS変数とSass mixinでテーマをカスタマイズしたり、コンポーネント単位で段階的に導入したりできます。マークアップ固有のopt-inは 特別なマークアップとクラス を参照してください。

\n

CSS変数

\n

ライブラリのデフォルトスタイルをデザインに合わせてカスタマイズできるよう、複数のCSS変数を提供しています。詳細は次のファイルを参照してください。
\nhttps://github.com/rdlabo-dev/ionic-theme-ios26/blob/v3.0.0/src/styles/default-variables.scsshttps://github.com/rdlabo-dev/ionic-theme-ios26/blob/v3.0.0/src/styles/default-variables.scss

\n

Liquid Glass mixin

\n

Liquid Glass mixinを使うには、メインパッケージからSCSSファイルをimportします。

\n
@use '@rdlabo/ionic-theme-ios26/src/styles/utils/api.scss';\n\nion-textarea label.textarea-wrapper {\n  @include api.glass-background;\n}\n

コンポーネント単位のimport

\n

段階的に導入する場合は、テーマ全体ではなく個別のコンポーネントをimportできます。

\n
@import '@rdlabo/ionic-theme-ios26/dist/css/utils/translucent';\n@import '@rdlabo/ionic-theme-ios26/dist/css/components/ion-action-sheet';\n@import '@rdlabo/ionic-theme-ios26/dist/css/components/ion-alert';\n@import '@rdlabo/ionic-theme-ios26/dist/css/components/ion-button';\n/* Import the remaining components your application uses. */\n

コンポーネント単位でのDark Mode

\n

Dark Mode対応のコンポーネントを個別にimportする場合は、Always、System、Class modeでselectorが異なるためSCSSを使います。

\n

Always:

\n
@use '@rdlabo/ionic-theme-ios26/src/styles/utils/theme-dark';\n\n:root {\n  @include theme-dark.default-variables;\n}\n@include theme-dark.ion-button;\n@include theme-dark.ion-fab;\n@include theme-dark.ion-tabs;\n@include theme-dark.ion-segment;\n

System:

\n
@use '@rdlabo/ionic-theme-ios26/src/styles/utils/theme-dark';\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    @include theme-dark.default-variables;\n  }\n  @include theme-dark.ion-button;\n  @include theme-dark.ion-fab;\n  @include theme-dark.ion-tabs;\n  @include theme-dark.ion-segment;\n}\n

Class:

\n
@use '@rdlabo/ionic-theme-ios26/src/styles/utils/theme-dark';\n\n.ion-palette-dark {\n  @include theme-dark.default-variables;\n  @include theme-dark.ion-button;\n  @include theme-dark.ion-fab;\n  @include theme-dark.ion-tabs;\n  @include theme-dark.ion-segment;\n}\n

インタラクティブな例

\n

デモでrender済みの例を見る

\n", + "headings": [ { - "id": "%E3%81%AA%E3%81%9C%E3%81%93%E3%81%AE%E5%A4%89%E6%9B%B4%E3%81%8C%E5%BF%85%E8%A6%81%E3%81%8B", - "text": "なぜこの変更が必要か", + "id": "css%E5%A4%89%E6%95%B0", + "text": "CSS変数", "level": 2 }, { - "id": "%E8%83%8C%E6%99%AF%3A-ios%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3%E5%86%8D%E7%8F%BE%E3%81%AE%E8%AA%B2%E9%A1%8C", - "text": "背景: iOSデザイン再現の課題", - "level": 3 + "id": "liquid-glass-mixin", + "text": "Liquid Glass mixin", + "level": 2 }, { - "id": "%E8%A7%A3%E6%B1%BA%E7%AD%96%3A-%E8%83%8C%E6%99%AF%E8%89%B2%E3%81%AE%E5%88%86%E9%9B%A2", - "text": "解決策: 背景色の分離", - "level": 3 + "id": "%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E5%8D%98%E4%BD%8D%E3%81%AEimport", + "text": "コンポーネント単位のimport", + "level": 2 }, { - "id": "material-design%E3%81%A7%E5%90%8C%E3%81%98%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3%E3%82%92%E4%BD%BF%E3%81%86", - "text": "Material Designで同じデザインを使う", - "level": 2 + "id": "%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E5%8D%98%E4%BD%8D%E3%81%A7%E3%81%AEdark-mode", + "text": "コンポーネント単位でのDark Mode", + "level": 3 }, { - "id": "%E3%81%BE%E3%81%A8%E3%82%81", - "text": "まとめ", + "id": "%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%A9%E3%82%AF%E3%83%86%E3%82%A3%E3%83%96%E3%81%AA%E4%BE%8B", + "text": "インタラクティブな例", "level": 2 } ], "codes": [], "scrollMap": [], - "editUrl": "https://github.com/rdlabo-dev/docs/edit/main/src/ionic-theme-ios26/docs/ja/using-ion-item-group.md" + "editUrl": "https://github.com/rdlabo-dev/docs/edit/main/src/ionic-theme-ios26/docs/ja/features.md" }, { - "title": "機能", - "navTitle": "機能", - "slug": "features", - "file": "features.md", + "title": "特別なマークアップとクラス", + "navTitle": "特別なマークアップとクラス", + "slug": "special-markup", + "file": "special-markup.md", "section": "ガイド", - "path": "/projects/ionic-theme-ios26/docs/features", - "html": "

CSS 変数、オプトアウトクラス、Liquid Glass mixin です。リストのマークアップは ion-item-groupの使用方法 を見てください。

\n

CSS変数

\n

ライブラリのデフォルトスタイルをデザインに合わせてカスタマイズできるよう、いくつかのCSS変数が用意されています。詳細はこのファイルを参照してください:
\nhttps://github.com/rdlabo-dev/ionic-theme-ios26/blob/v2.3.2/src/styles/default-variables.scsshttps://github.com/rdlabo-dev/ionic-theme-ios26/blob/v2.3.2/src/styles/default-variables.scss

\n

.ios26-disabled クラス

\n

特定のコンポーネントでiOS26テーマを無効にするには、.ios26-disabled クラスを追加します。

\n
<!-- iOS26 theme applied -->\n<ion-button>iOS26 Design</ion-button>\n\n<!-- Standard Ionic iOS styling -->\n<ion-button class=\"ios26-disabled\">Standard Ionic Design</ion-button>\n

Liquid Glass Mixin

\n

liquid glass mixinを使うには、メインパッケージからSCSSファイルをインポートします。

\n
@use '@rdlabo/ionic-theme-ios26/src/styles/utils/api.scss';\n\nion-textarea label.textarea-wrapper {\n  @include api.glass-background;\n}\n

追加デザイン

\n

iOS26デザインへの忠実度をさらに高めるため、このライブラリが提供する追加デザインを実装できます。詳細は次を参照してください:

\n

https://ionic-theme-ios26.netlify.app/main/docshttps://ionic-theme-ios26.netlify.app/main/docs

\n", + "path": "/projects/ionic-theme-ios26/docs/special-markup", + "html": "

ほとんどのIonicマークアップは変更せずに使えます。以下はテーマが提供する明示的なopt-inです。

\n

Primaryのsubmit button

\n

solidのPrimary submit buttonは、foregroundとborderの表現に --ion-color-primary-brightness を使います。Primary colorに対して十分なcontrastを確保できる値を定義してください。

\n
:root {\n  --ion-color-primary-brightness: #96feff;\n}\n
<ion-button type=\"submit\" color=\"primary\">Submit</ion-button>\n<ion-button class=\"button-submit\" fill=\"solid\" color=\"primary\">Continue</ion-button>\n

buttonに type=\"submit\" を指定できない場合に同じstyleを適用するには、.button-submit を使います。

\n

2行のinset list item

\n

slotを指定しない ion-labelion-note を隣接させると、2行のitemとして表示します。iOS styleのinset list背景を使う場合はitemを ion-item-group で囲み、ion-list-header はgroupの外に置きます。

\n
<ion-list inset=\"true\">\n  <ion-list-header>\n    <ion-label>Connections</ion-label>\n  </ion-list-header>\n  <ion-item-group>\n    <ion-item>\n      <ion-label>Network &amp; internet</ion-label>\n      <ion-note>Mobile, Wi-Fi, hotspot</ion-note>\n    </ion-item>\n  </ion-item-group>\n</ion-list>\n

通常の末尾noteとして表示したい場合は、ion-noteslot=\"end\" を指定します。

\n

inset listのsection header

\n

.item-group-headerion-item-group に追加すると、component demo pageの先頭で使われている中央揃えのicon、title、descriptionを表示できます。

\n

これは導入用のgroupです。通常のlist itemは、その後に置く別の ion-item-group に入れてください。

\n
<ion-list inset=\"true\">\n  <ion-item-group class=\"item-group-header\">\n    <ion-item>\n      <ion-label>\n        <ion-icon name=\"list\" style=\"background: var(--ion-color-primary)\"></ion-icon>\n        <h2>Lists</h2>\n        <ion-text>Inset-list examples</ion-text>\n      </ion-label>\n    </ion-item>\n  </ion-item-group>\n  <ion-item-group>\n    <ion-item><ion-label>First item</ion-label></ion-item>\n  </ion-item-group>\n</ion-list>\n

幅いっぱいのsegment

\n

segment buttonを利用可能な幅に均等配置する場合は .segment-expand を追加します。registerSegmentEffect を使う場合、このclassはLiquid Glass effectのsizeも変更します。

\n
<ion-segment class=\"segment-expand\" value=\"new\">\n  <ion-segment-button value=\"new\"><ion-label>New</ion-label></ion-segment-button>\n  <ion-segment-button value=\"replied\"><ion-label>Replied</ion-label></ion-segment-button>\n</ion-segment>\n

condense header内のclassic search bar

\n

Themeはdefaultでsearch barにiOS 26の外観を適用します。collapse=\"condense\" を指定した ion-header のlarge titleの下に表示するsearch fieldには、.searchbar-classic を追加します。従来の塗りつぶされたiOSの外観になり、固定headerに残らずlarge titleと一緒にcollapseします。

\n

color=\"light\" など、colorを指定したtoolbar内に配置してください。classic背景は、そのcolorのcontrast値から生成されます。

\n

次の例はIonic標準のcollapse可能なlarge title構造です。previewをscrollするとlarge titleがcollapseし、固定headerが表示されます。

\n
<div class=\"ion-page\">\n  <ion-header translucent=\"true\">\n    <ion-toolbar color=\"light\">\n      <ion-title>Search</ion-title>\n    </ion-toolbar>\n  </ion-header>\n  <ion-content color=\"light\" fullscreen=\"true\">\n    <ion-header collapse=\"condense\">\n      <ion-toolbar color=\"light\">\n        <ion-title size=\"large\">Search</ion-title>\n      </ion-toolbar>\n      <ion-toolbar color=\"light\">\n        <ion-searchbar class=\"searchbar-classic\" placeholder=\"Filter results\"></ion-searchbar>\n      </ion-toolbar>\n    </ion-header>\n    <ion-list inset=\"true\">\n      <ion-item-group>\n        <ion-item><ion-label>Recent item 1</ion-label></ion-item>\n        <ion-item><ion-label>Recent item 2</ion-label></ion-item>\n        <ion-item><ion-label>Recent item 3</ion-label></ion-item>\n        <ion-item><ion-label>Recent item 4</ion-label></ion-item>\n        <ion-item><ion-label>Recent item 5</ion-label></ion-item>\n        <ion-item><ion-label>Recent item 6</ion-label></ion-item>\n        <ion-item><ion-label>Recent item 7</ion-label></ion-item>\n        <ion-item><ion-label>Recent item 8</ion-label></ion-item>\n        <ion-item><ion-label>Recent item 9</ion-label></ion-item>\n        <ion-item><ion-label>Recent item 10</ion-label></ion-item>\n      </ion-item-group>\n    </ion-list>\n  </ion-content>\n</div>\n

.ion-page wrapperによって、埋め込みpreviewが完全なrouted pageとして動作します。ion-router-outlet を使うapplicationでは通常、このpage containerは自動的に追加されます。Inset listとitemはscrollを実演するための十分なcontentを用意しているだけで、.searchbar-classic の必須要素ではありません。

\n

search barを含むtoolbar

\n

search barとstartまたはend buttonを組み合わせる ion-toolbar には、.toolbar-searchbar を追加します。このclassはslot付きcontrolを中央揃えにし、search field周辺のspacingを調整します。

\n
<ion-toolbar class=\"toolbar-searchbar\">\n  <ion-buttons slot=\"start\">\n    <ion-button>Cancel</ion-button>\n  </ion-buttons>\n  <ion-searchbar></ion-searchbar>\n</ion-toolbar>\n

Themeを無効にする

\n

個別のIonic componentで標準のiOS styleを維持する場合は .ios26-disabled を追加します。

\n
<ion-button>iOS 26 theme</ion-button> <ion-button class=\"ios26-disabled\">Standard Ionic button</ion-button>\n

Inset listの背景modelについては ion-item-groupの使用方法 を参照してください。

\n", "headings": [ { - "id": "css%E5%A4%89%E6%95%B0", - "text": "CSS変数", - "level": 3 + "id": "primary%E3%81%AEsubmit-button", + "text": "Primaryのsubmit button", + "level": 2 }, { - "id": ".ios26-disabled-%E3%82%AF%E3%83%A9%E3%82%B9", - "text": ".ios26-disabled クラス", - "level": 3 + "id": "2%E8%A1%8C%E3%81%AEinset-list-item", + "text": "2行のinset list item", + "level": 2 }, { - "id": "liquid-glass-mixin", - "text": "Liquid Glass Mixin", - "level": 3 + "id": "inset-list%E3%81%AEsection-header", + "text": "inset listのsection header", + "level": 2 }, { - "id": "%E8%BF%BD%E5%8A%A0%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3", - "text": "追加デザイン", - "level": 3 + "id": "%E5%B9%85%E3%81%84%E3%81%A3%E3%81%B1%E3%81%84%E3%81%AEsegment", + "text": "幅いっぱいのsegment", + "level": 2 + }, + { + "id": "condense-header%E5%86%85%E3%81%AEclassic-search-bar", + "text": "condense header内のclassic search bar", + "level": 2 + }, + { + "id": "search-bar%E3%82%92%E5%90%AB%E3%82%80toolbar", + "text": "search barを含むtoolbar", + "level": 2 + }, + { + "id": "theme%E3%82%92%E7%84%A1%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B", + "text": "Themeを無効にする", + "level": 2 } ], "codes": [], "scrollMap": [], - "editUrl": "https://github.com/rdlabo-dev/docs/edit/main/src/ionic-theme-ios26/docs/ja/features.md" + "editUrl": "https://github.com/rdlabo-dev/docs/edit/main/src/ionic-theme-ios26/docs/ja/special-markup.md" }, { "title": "実験的なアニメーション", @@ -153,17 +187,17 @@ export const PROJECT = { "file": "experimental-animation.md", "section": "ガイド", "path": "/projects/ionic-theme-ios26/docs/experimental-animation", - "html": "

任意のジェスチャーとアニメーション補助です。この機能なしでもテーマは使えます。

\n

この機能は実験的です。この機能なしでもライブラリは利用できます。

\n

ion-tab-button / ion-segment-button の Sheet of Glass

\n

ion-tab-bar / ion-segment を登録すると、ion-tab-button / ion-segment-button にアニメーション効果を表示できます。

\n

\"ion-tab-button

\n
import { registerTabBarEffect, registerSegmentEffect } from '@rdlabo/ionic-theme-ios26';\n\n/**\n * Register DOM elements. Effects are applied using Ionic Gesture and Ionic Animation.\n */\nconst tabBar = document.querySelector<HTMLElement>('ion-tab-bar');\nconst segment = document.querySelector<HTMLElement>('ion-segment');\nconst registeredTabBarEffect = tabBar ? registerTabBarEffect(tabBar) : undefined;\nconst registeredSegmentEffect = segment ? registerSegmentEffect(segment) : undefined;\n\nconst destroy = () => {\n  /**\n   * If the registered DOM element is removed (e.g., due to page navigation),\n   * make sure to destroy the gesture and animation. This will also remove the event listeners.\n   * You can re-register them if needed.\n   */\n  registeredTabBarEffect?.destroy();\n  registeredSegmentEffect?.destroy();\n};\n

TabBarSearchable: ion-tab-barion-fab-button の Searchable

\n

ion-tabs 内の指定マークアップ構成で Searchable を有効にします。

\n

\"ion-fab-button

\n
<ion-content>...</ion-content>\n<ion-fab vertical=\"bottom\" horizontal=\"end\" slot=\"fixed\">\n  <ion-fab-button (click)=\"present($event)\">\n    <ion-icon name=\"search\"></ion-icon>\n  </ion-fab-button>\n</ion-fab>\n<ion-footer [translucent]=\"true\">\n  <ion-toolbar>\n    <ion-buttons slot=\"start\">\n      <!-- ion-icon name is set dynamically by the animation -->\n      <ion-button fill=\"default\"><ion-icon slot=\"icon-only\"></ion-icon> </ion-button>\n    </ion-buttons>\n    <!-- User set `ionChange` or other events. -->\n    <ion-searchbar (ionChange)=\"example($event)\"></ion-searchbar>\n  </ion-toolbar>\n</ion-footer>\n
import { attachTabBarSearchable, TabBarSearchableType } from '@rdlabo/ionic-theme-ios26';\nimport type { TabBarSearchableFunction } from '@rdlabo/ionic-theme-ios26';\n\nlet searchableFun: TabBarSearchableFunction | undefined;\nconst initialize = () => {\n  // attachTabBarSearchable has state. You should initialize per page.\n  const tabBar = document.querySelector<HTMLElement>('ion-tab-bar');\n  const fabButton = document.querySelector<HTMLElement>('ion-fab-button');\n  const footer = document.querySelector<HTMLElement>('ion-footer');\n  if (!tabBar || !fabButton || !footer) {\n    return;\n  }\n  searchableFun = attachTabBarSearchable(tabBar, fabButton, footer);\n};\n\nconst present = (event: Event) => {\n  searchableFun!(event, TabBarSearchableType.Enter);\n};\n\nconst dismiss = (event: Event) => {\n  searchableFun!(event, TabBarSearchableType.Leave);\n};\n
", + "html": "

以下のgestureとanimation helperは実験的な任意機能です。これらを使わなくてもテーマは動作します。

\n

ion-tab-button / ion-segment-button の Sheet of Glass

\n

ion-tab-bar または ion-segment elementを登録すると、そのbuttonに移動するselection effectを追加できます。

\n

\"ion-tab-button

\n
import { registerTabBarEffect, registerSegmentEffect } from '@rdlabo/ionic-theme-ios26';\n\n/**\n * Register DOM elements. Effects are applied using Ionic Gesture and Ionic Animation.\n */\nconst tabBar = document.querySelector<HTMLElement>('ion-tab-bar');\nconst segment = document.querySelector<HTMLElement>('ion-segment');\nconst registeredTabBarEffect = tabBar ? registerTabBarEffect(tabBar) : undefined;\nconst registeredSegmentEffect = segment ? registerSegmentEffect(segment) : undefined;\n\nconst destroy = () => {\n  /**\n   * If the registered DOM element is removed (e.g., due to page navigation),\n   * make sure to destroy the gesture and animation. This will also remove the event listeners.\n   * You can re-register them if needed.\n   */\n  registeredTabBarEffect?.destroy();\n  registeredSegmentEffect?.destroy();\n};\n

TabBarSearchable: ion-tab-barion-fab-button の Searchable

\n

ion-tabs 内で次の構造を使うと、search buttonからsearch toolbarへのanimationを適用できます。

\n

\"ion-fab-button

\n
<ion-content>...</ion-content>\n<ion-fab vertical=\"bottom\" horizontal=\"end\" slot=\"fixed\">\n  <ion-fab-button (click)=\"present($event)\">\n    <ion-icon name=\"search\"></ion-icon>\n  </ion-fab-button>\n</ion-fab>\n<ion-footer [translucent]=\"true\">\n  <ion-toolbar>\n    <ion-buttons slot=\"start\">\n      <!-- ion-icon name is set dynamically by the animation -->\n      <ion-button fill=\"default\"><ion-icon slot=\"icon-only\"></ion-icon> </ion-button>\n    </ion-buttons>\n    <!-- User set `ionChange` or other events. -->\n    <ion-searchbar (ionChange)=\"example($event)\"></ion-searchbar>\n  </ion-toolbar>\n</ion-footer>\n
import { attachTabBarSearchable, TabBarSearchableType } from '@rdlabo/ionic-theme-ios26';\nimport type { TabBarSearchableFunction } from '@rdlabo/ionic-theme-ios26';\n\nlet searchableFun: TabBarSearchableFunction | undefined;\nconst initialize = () => {\n  // attachTabBarSearchable has state. You should initialize per page.\n  const tabBar = document.querySelector<HTMLElement>('ion-tab-bar');\n  const fabButton = document.querySelector<HTMLElement>('ion-fab-button');\n  const footer = document.querySelector<HTMLElement>('ion-footer');\n  if (!tabBar || !fabButton || !footer) {\n    return;\n  }\n  searchableFun = attachTabBarSearchable(tabBar, fabButton, footer);\n};\n\nconst present = (event: Event) => {\n  searchableFun!(event, TabBarSearchableType.Enter);\n};\n\nconst dismiss = (event: Event) => {\n  searchableFun!(event, TabBarSearchableType.Leave);\n};\n
", "headings": [ { "id": "ion-tab-button-%2F-ion-segment-button-%E3%81%AE-sheet-of-glass", "text": "ion-tab-button / ion-segment-button の Sheet of Glass", - "level": 3 + "level": 2 }, { "id": "tabbarsearchable%3A-ion-tab-bar-%E3%81%A8-ion-fab-button-%E3%81%AE-searchable", "text": "TabBarSearchable: ion-tab-bar と ion-fab-button の Searchable", - "level": 3 + "level": 2 } ], "codes": [], @@ -177,12 +211,12 @@ export const PROJECT = { "file": "ios-18.md", "section": "ガイド", "path": "/projects/ionic-theme-ios26/docs/ios-18", - "html": "

iOS 18でテーマファイルの読み込みを防ぐ方法

\n

テーマファイルをユーザーの端末がiOS 26のときだけ読み込みたい場合(iOS 18のユーザーにはデフォルトのIonic iOSテーマを使わせる場合)、import に supports-condition を追加することで実現できます。

\n
@import '@rdlabo/ionic-theme-ios26/dist/css/default-variables.css' supports(text-wrap: pretty);\n@import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26.css' supports(text-wrap: pretty);\n@import '@rdlabo/ionic-theme-ios26/dist/css/md-remove-ios-class-effect.css'\n  supports(text-wrap: pretty);\n@import '@rdlabo/ionic-theme-ios26/dist/css/md-ion-list-inset.css' supports(text-wrap: pretty);\n
", + "html": "

iOS 18でテーマファイルの読み込みを防ぐ方法

\n

テーマファイルをユーザーの端末がiOS 26のときだけ読み込みたい場合(iOS 18のユーザーにはデフォルトのIonic iOSテーマを使わせる場合)、import に supports-condition を追加することで実現できます。

\n
@import '@rdlabo/ionic-theme-ios26/dist/css/default-variables.css' supports(text-wrap: pretty);\n@import '@rdlabo/ionic-theme-ios26/dist/css/ionic-theme-ios26.css' supports(text-wrap: pretty);\n@import '@rdlabo/ionic-theme-ios26/dist/css/md-remove-ios-class-effect.css' supports(text-wrap: pretty);\n@import '@rdlabo/ionic-theme-ios26/dist/css/md-ion-list-inset.css' supports(text-wrap: pretty);\n
", "headings": [ { "id": "ios-18%E3%81%A7%E3%83%86%E3%83%BC%E3%83%9E%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF%E3%82%92%E9%98%B2%E3%81%90%E6%96%B9%E6%B3%95", "text": "iOS 18でテーマファイルの読み込みを防ぐ方法", - "level": 3 + "level": 2 } ], "codes": [], @@ -196,11 +230,46 @@ export const PROJECT = { "file": "migration.md", "section": "ガイド", "path": "/projects/ionic-theme-ios26/docs/migration", - "html": "

段階的な移行のために、テーマ全体のファイルではなく個別コンポーネントをインポートして、iOS26テーマを選択的に適用できます。

\n
@import '@rdlabo/ionic-theme-ios26/dist/css/utils/translucent';\n@import '@rdlabo/ionic-theme-ios26/dist/css/components/ion-action-sheet';\n@import '@rdlabo/ionic-theme-ios26/dist/css/components/ion-alert';\n@import '@rdlabo/ionic-theme-ios26/dist/css/components/ion-button';\n...\n

個別コンポーネントでのダークモード

\n

個別コンポーネントをダークモード対応でインポートする場合は、CSSではなくSCSSを使います。AlwaysSystemClass モードでセレクタが異なるためです。

\n
\n

Note: 現時点では、ion-button にのみ個別のダークモードスタイルが適用されています。

\n
\n

Always(常時ダークモード):

\n
@use '@rdlabo/ionic-theme-ios26/src/styles/utils/theme-dark';\n\n:root {\n  @include theme-dark.default-variables;\n}\n@include theme-dark.ion-button;\n@include theme-dark.ion-fab;\n@include theme-dark.ion-tabs;\n@include theme-dark.ion-segment;\n

System(システム設定に追従):

\n
@use '@rdlabo/ionic-theme-ios26/src/styles/utils/theme-dark';\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    @include theme-dark.default-variables;\n  }\n  @include theme-dark.ion-button;\n  @include theme-dark.ion-fab;\n  @include theme-dark.ion-tabs;\n  @include theme-dark.ion-segment;\n}\n

Class(CSSクラスで切り替え):

\n
@use '@rdlabo/ionic-theme-ios26/src/styles/utils/theme-dark';\n\n.ion-palette-dark {\n  @include theme-dark.default-variables;\n  @include theme-dark.ion-button;\n  @include theme-dark.ion-fab;\n  @include theme-dark.ion-tabs;\n  @include theme-dark.ion-segment;\n}\n
", + "html": "

アップグレード対象のversionに対応するsectionを参照してください。各sectionには、アプリケーションのコードまたは設定の変更が必要な項目のみを記載しています。

\n

3.0.0への移行

\n

.header-item-group.item-group-header に変更する

\n

section headerとして使う ion-item-group のclass名を、対象elementとの一貫性を保つため変更しました。アプリケーションのtemplateとstyleにある .header-item-group をすべて置き換えてください。

\n
- <ion-item-group class=\"header-item-group\">\n+ <ion-item-group class=\"item-group-header\">\n    ...\n  </ion-item-group>\n

旧classはthemeでstyleされなくなりました。この変更は、@rdlabo/ionic-theme-md3 と共有するmarkupにも適用されます。

\n

2.0.0への移行

\n

iosTransitionAnimation を設定する

\n

version 2ではpackageのnavigation transitionが必要です。このtransitionは、Large Titleをback buttonのlabelへ動かしていた古い animateBackButton() の挙動を除き、Ionicのdefault iOS transitionに従います。

\n
import { isPlatform } from '@ionic/core'; // or @ionic/angular/standalone, @ionic/react, @ionic/vue\nimport { iosTransitionAnimation } from '@rdlabo/ionic-theme-ios26';\n\n// Angular\nprovideIonicAngular({\n  // ...\n  navAnimation: isPlatform('ios') ? iosTransitionAnimation : undefined,\n});\n\n// React\nsetupIonicReact({\n  // ...\n  navAnimation: isPlatform('ios') ? iosTransitionAnimation : undefined,\n});\n\n// Vue\ncreateApp(App).use(IonicVue, {\n  // ...\n  navAnimation: isPlatform('ios') ? iosTransitionAnimation : undefined,\n});\n

このtransitionを設定すると、旧animationによる不要なtransitionの副作用なしに <ion-buttons><ion-back-button></ion-back-button></ion-buttons> を利用できます。

\n

1.0.0への移行

\n

SCSSのimport pathを更新する

\n

JavaScript fileがpackageに追加された際、source fileは src/styles 以下へ移動しました。

\n
- @import '@rdlabo/ionic-theme-ios26/src/default-variables.scss';\n+ @import '@rdlabo/ionic-theme-ios26/src/styles/default-variables.scss';\n

dist 以下の生成済みCSS pathは変更されていません。

\n

--ios26-color-background-rgb の名前を変更する

\n
  :root {\n-   --ios26-color-background-rgb: 255, 255, 255;\n+   --ios26-content-box-shadow-rgb: 255, 255, 255;\n  }\n

brightness変数の名前を変更する

\n

--ion-color-*-brightness-rgb 変数を --ion-color-*-brightness に置き換え、RGB channel listではなくcolor valueを指定してください。

\n
  :root {\n-   --ion-color-primary-brightness-rgb: 130, 255, 255;\n+   --ion-color-primary-brightness: #96feff;\n  }\n
", "headings": [ { - "id": "%E5%80%8B%E5%88%A5%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%A7%E3%81%AE%E3%83%80%E3%83%BC%E3%82%AF%E3%83%A2%E3%83%BC%E3%83%89", - "text": "個別コンポーネントでのダークモード", + "id": "3.0.0%E3%81%B8%E3%81%AE%E7%A7%BB%E8%A1%8C", + "text": "3.0.0への移行", + "level": 2 + }, + { + "id": ".header-item-group-%E3%82%92-.item-group-header-%E3%81%AB%E5%A4%89%E6%9B%B4%E3%81%99%E3%82%8B", + "text": ".header-item-group を .item-group-header に変更する", + "level": 3 + }, + { + "id": "2.0.0%E3%81%B8%E3%81%AE%E7%A7%BB%E8%A1%8C", + "text": "2.0.0への移行", + "level": 2 + }, + { + "id": "iostransitionanimation-%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B", + "text": "iosTransitionAnimation を設定する", + "level": 3 + }, + { + "id": "1.0.0%E3%81%B8%E3%81%AE%E7%A7%BB%E8%A1%8C", + "text": "1.0.0への移行", + "level": 2 + }, + { + "id": "scss%E3%81%AEimport-path%E3%82%92%E6%9B%B4%E6%96%B0%E3%81%99%E3%82%8B", + "text": "SCSSのimport pathを更新する", + "level": 3 + }, + { + "id": "--ios26-color-background-rgb-%E3%81%AE%E5%90%8D%E5%89%8D%E3%82%92%E5%A4%89%E6%9B%B4%E3%81%99%E3%82%8B", + "text": "--ios26-color-background-rgb の名前を変更する", + "level": 3 + }, + { + "id": "brightness%E5%A4%89%E6%95%B0%E3%81%AE%E5%90%8D%E5%89%8D%E3%82%92%E5%A4%89%E6%9B%B4%E3%81%99%E3%82%8B", + "text": "brightness変数の名前を変更する", "level": 3 } ], @@ -215,7 +284,7 @@ export const PROJECT = { "file": "api.md", "section": "リファレンス", "path": "/projects/ionic-theme-ios26/docs/api", - "html": "

@rdlabo/ionic-theme-ios26 v2.3.2 が公開するJavaScript APIのリファレンスです。CSSとSassのentry pointはREADMEで説明します。

\n

Effect

\n

function registerTabBarEffect

(targetElement: HTMLElement) => registeredEffect | undefined

Ionic Tab BarにLiquid Glassの選択effectを登録します。

\n\n\n

function registerSegmentEffect

(targetElement: HTMLElement) => registeredEffect | undefined

Ionic SegmentにLiquid Glassの選択effectを登録します。

\n\n\n

interface registeredEffect

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
MemberTypeDescription
destroy() => void登録時に作成したlistenerとeffect elementを削除します。
\n\n

interface EffectScales

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
PropTypeDescription
smallstringSmall effectのscaleです。
mediumstringMedium effectのscaleです。
largestringLarge effectのscaleです。
xlargestringExtra Large effectのscaleです。
\n\n

Searchable Tab Bar

\n

function attachTabBarSearchable

(ionTabBar: HTMLElement, ionFabButton: HTMLElement, ionFooter: HTMLElement) => TabBarSearchableFunction

Searchable Tab Barのtransitionを設定し、event handlerを返します。

\n\n\n

enum TabBarSearchableType

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
MemberValueDescription
Enter\"enter\"Searchable modeへ入ります。
Leave\"leave\"Searchable modeから戻ります。
\n\n

type alias TabBarSearchableFunction

(event: Event, type: TabBarSearchableType) => Promise<void>

\n\n

Animation

\n

function iosTransitionAnimation

(navEl: HTMLElement, opts: TransitionOptions) => Animation

PackageのiOS navigation transitionを生成します。

\n\n\n

function popoverEnterAnimation

(baseEl: HTMLElement, opts?: any) => Animation

iOS Popoverのenter animationを生成します。

\n\n\n

function popoverLeaveAnimation

(baseEl: HTMLElement) => Animation

iOS Popoverのleave animationを生成します。

\n\n\n
", + "html": "

@rdlabo/ionic-theme-ios26 v3.0.0 が公開するJavaScript APIのリファレンスです。CSSとSassのentry pointはREADMEで説明します。

\n

Effect

\n

function registerTabBarEffect

(targetElement: HTMLElement) => registeredEffect | undefined

Ionic Tab BarにLiquid Glassの選択effectを登録します。

\n\n\n

function registerSegmentEffect

(targetElement: HTMLElement) => registeredEffect | undefined

Ionic SegmentにLiquid Glassの選択effectを登録します。

\n\n\n

interface registeredEffect

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
MemberTypeDescription
destroy() => void登録時に作成したlistenerとeffect elementを削除します。
\n\n

interface EffectScales

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
PropTypeDescription
smallstringSmall effectのscaleです。
mediumstringMedium effectのscaleです。
largestringLarge effectのscaleです。
xlargestringExtra Large effectのscaleです。
\n\n

Searchable Tab Bar

\n

function attachTabBarSearchable

(ionTabBar: HTMLElement, ionFabButton: HTMLElement, ionFooter: HTMLElement) => TabBarSearchableFunction

Searchable Tab Barのtransitionを設定し、event handlerを返します。

\n\n\n

enum TabBarSearchableType

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
MemberValueDescription
Enter\"enter\"Searchable modeへ入ります。
Leave\"leave\"Searchable modeから戻ります。
\n\n

type alias TabBarSearchableFunction

(event: Event, type: TabBarSearchableType) => Promise<void>

\n\n

Animation

\n

function iosTransitionAnimation

(navEl: HTMLElement, opts: TransitionOptions) => Animation

PackageのiOS navigation transitionを生成します。

\n\n\n

function popoverEnterAnimation

(baseEl: HTMLElement, opts?: any) => Animation

iOS Popoverのenter animationを生成します。

\n\n\n

function popoverLeaveAnimation

(baseEl: HTMLElement) => Animation

iOS Popoverのleave animationを生成します。

\n\n\n
", "headings": [ { "id": "effect", diff --git a/src/app/generated/projects/ionic-theme-md3.en.generated.ts b/src/app/generated/projects/ionic-theme-md3.en.generated.ts index 1003a32..e67a4a1 100644 --- a/src/app/generated/projects/ionic-theme-md3.en.generated.ts +++ b/src/app/generated/projects/ionic-theme-md3.en.generated.ts @@ -8,7 +8,7 @@ export const PROJECT = { "repositoryUrl": "https://github.com/rdlabo-dev/ionic-theme-md3", "category": "frontend-tools", "icon": "theme", - "version": "1.1.0", + "version": "2.0.0", "description": "Material Design 3 styling for Ionic applications.", "headline": "Bring Material Design 3 to Ionic apps", "overview": "Apply Material Design 3 styling to Ionic while keeping markup compatible with the iOS 26 theme and shared transition animations.", @@ -36,7 +36,7 @@ export const PROJECT = { "file": "readme.md", "section": "Documentation", "path": "/projects/ionic-theme-md3/docs/readme", - "html": "

Overview

\n

This library provides CSS/JS files that bring the Material Design 3 design system to Ionic applications. It updates the look and feel of Ionic components to match the latest Material Design 3 guidelines.

\n

This project aims to follow the core concepts of Ionic as closely as possible, while placing a strong emphasis on compatibility with @rdlabo/ionic-theme-ios26. Just as Ionic provides beautiful styling whether it displays the ios or md theme from a single HTML structure, our goal is to ensure that this theme and @rdlabo/ionic-theme-ios26 are fully compatible.

\n

If you don't know about @rdlabo/ionic-theme-ios26, you should definitely give it a try!

\n

👉️rdlabo-dev/ionic-theme-ios26

\n

Related Projects

\n

If you need a more comprehensive Material Design 3 implementation, you may also be interested in:

\n\n
\n

Note: This theme is purpose-built for compatibility with Ionic's design approach and @rdlabo/ionic-theme-ios26; it is not intended as a strict, full MD3 recreation.

\n
\n

Quick start

\n

After Installation, import the theme CSS and set navAnimation as shown below.

\n

Installation

\n

This is a CSS theme for extending your Ionic project. It does not work on its own, so use it together with the Ionic Framework.

\n
npm install @rdlabo/ionic-theme-md3\n

Note: If you use @ionic/core@ < 8.8.0, use @rdlabo/ionic-theme-md3@1.0.2.

\n

And import the theme in your project's main CSS file (e.g., src/styles.scss).

\n
@import '@rdlabo/ionic-theme-md3/dist/css/default-variables.css';\n@import '@rdlabo/ionic-theme-md3/dist/css/ionic-theme-md3.css';\n

Next, configure the animations for MD3. Add the following to your Ionic configuration options.

\n
import { isPlatform } from '@ionic/core'; // or @ionic/angular/standalone, @ionic/react, @ionic/vue\nimport { mdTransitionAnimation } from '@rdlabo/ionic-theme-md3';\n\n// Angular\nprovideIonicAngular({\n    ...\n    navAnimation: isPlatform('ios') ? undefined: mdTransitionAnimation,\n});\n\n// React\nsetupIonicReact({\n    ...\n    navAnimation: isPlatform('ios') ? undefined: mdTransitionAnimation,\n});\n\n// Vue\ncreateApp(App)\n    .use(IonicVue, {\n        ...\n        navAnimation: isPlatform('ios') ? undefined: mdTransitionAnimation,\n})\n

Documentation

\n

Start with Installation. Pair this theme with @rdlabo/ionic-theme-ios26 when you need both platforms from one markup tree.

\n", + "html": "

Overview

\n

This library provides CSS/JS files that bring the Material Design 3 design system to Ionic applications. It updates the look and feel of Ionic components to match the latest Material Design 3 guidelines.

\n

This project aims to follow the core concepts of Ionic as closely as possible, while placing a strong emphasis on compatibility with @rdlabo/ionic-theme-ios26. Just as Ionic provides beautiful styling whether it displays the ios or md theme from a single HTML structure, our goal is to ensure that this theme and @rdlabo/ionic-theme-ios26 are fully compatible.

\n

If you don't know about @rdlabo/ionic-theme-ios26, you should definitely give it a try!

\n

👉️rdlabo-dev/ionic-theme-ios26

\n

Related Projects

\n

If you need a more comprehensive Material Design 3 implementation, you may also be interested in:

\n\n
\n

Note: This theme is purpose-built for compatibility with Ionic's design approach and @rdlabo/ionic-theme-ios26; it is not intended as a strict, full MD3 recreation.

\n
\n

Quick start

\n

After Installation, import the theme CSS and set navAnimation as shown below.

\n

Installation

\n

This is a CSS theme for extending your Ionic project. It does not work on its own, so use it together with the Ionic Framework.

\n
npm install @rdlabo/ionic-theme-md3\n

Note: If you use @ionic/core@ < 8.8.0, use @rdlabo/ionic-theme-md3@1.0.2.

\n

And import the theme in your project's main CSS file (e.g., src/styles.scss).

\n
@import '@rdlabo/ionic-theme-md3/dist/css/default-variables.css';\n@import '@rdlabo/ionic-theme-md3/dist/css/ionic-theme-md3.css';\n

Optional: use the MD3 and iOS 26 themes together

\n

Install the iOS 26 theme to style both Ionic modes from the same application.

\n

The current releases of both themes require @ionic/core 8.8.1 or later. Upgrade Ionic before using this setup if your application is on 8.8.0 or earlier.

\n
npm install @rdlabo/ionic-theme-ios26\n

When your global stylesheet uses Sass, initialize the themes in this order:

\n
@use '@rdlabo/ionic-theme-ios26/src/styles/default-variables.scss' as ios26-vars;\n@use '@rdlabo/ionic-theme-ios26/src/styles/ionic-theme-ios26.scss';\n@use '@rdlabo/ionic-theme-ios26/src/styles/ionic-theme-ios26-dark-class.scss';\n@use '@rdlabo/ionic-theme-ios26/src/styles/md-remove-ios-class-effect.scss';\n@use '@rdlabo/ionic-theme-md3/dist/css/default-variables.css' as md3-vars;\n@use '@rdlabo/ionic-theme-md3/dist/css/ionic-theme-md3.css';\n

The example uses Ionic's class-based dark mode. Your global stylesheet must also load Ionic's matching dark palette, such as @ionic/angular/css/palettes/dark.class.css for Angular. When using dark-system or dark-always, select the same variant for both Ionic's palette and the iOS 26 theme. See Ionic's Dark Mode documentation. The explicit ios26-vars and md3-vars namespaces prevent the two variable modules from using the same default namespace.

\n

Configure both transition implementations when both themes are installed:

\n
import { isPlatform } from '@ionic/core'; // or @ionic/angular (Ionic 9), @ionic/angular/standalone (Ionic 8), @ionic/react, @ionic/vue\nimport { iosTransitionAnimation, popoverEnterAnimation, popoverLeaveAnimation } from '@rdlabo/ionic-theme-ios26';\nimport { mdTransitionAnimation } from '@rdlabo/ionic-theme-md3';\n\n// Angular\nprovideIonicAngular({\n    ...\n    navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation,\n    popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined,\n    popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined,\n});\n\n// React\nsetupIonicReact({\n    ...\n    navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation,\n    popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined,\n    popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined,\n});\n\n// Vue\ncreateApp(App)\n    .use(IonicVue, {\n        ...\n        navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation,\n        popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined,\n        popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined,\n    });\n

If you installed only the MD3 theme, configure its animation as follows.

\n
import { isPlatform } from '@ionic/core'; // or @ionic/angular (Ionic 9), @ionic/angular/standalone (Ionic 8), @ionic/react, @ionic/vue\nimport { mdTransitionAnimation } from '@rdlabo/ionic-theme-md3';\n\n// Angular\nprovideIonicAngular({\n    ...\n    navAnimation: isPlatform('ios') ? undefined: mdTransitionAnimation,\n});\n\n// React\nsetupIonicReact({\n    ...\n    navAnimation: isPlatform('ios') ? undefined: mdTransitionAnimation,\n});\n\n// Vue\ncreateApp(App)\n    .use(IonicVue, {\n        ...\n        navAnimation: isPlatform('ios') ? undefined: mdTransitionAnimation,\n})\n

Documentation

\n

Start with Installation. Pair this theme with @rdlabo/ionic-theme-ios26 when you need both platforms from one markup tree.

\n\n", "headings": [ { "id": "overview", @@ -58,6 +58,11 @@ export const PROJECT = { "text": "Installation", "level": 2 }, + { + "id": "optional%3A-use-the-md3-and-ios-26-themes-together", + "text": "Optional: use the MD3 and iOS 26 themes together", + "level": 3 + }, { "id": "documentation", "text": "Documentation", @@ -68,6 +73,83 @@ export const PROJECT = { "scrollMap": [], "editUrl": "https://github.com/rdlabo-dev/ionic-theme-md3/edit/main/README.md" }, + { + "title": "Special markup", + "navTitle": "Special markup", + "slug": "special-markup", + "file": "special-markup.md", + "section": "Guides", + "path": "/projects/ionic-theme-md3/docs/special-markup", + "html": "

Most Ionic markup works without changes. The combinations below are explicit opt-ins and are useful when the same template also uses @rdlabo/ionic-theme-ios26.

\n

Two-line inset list items

\n

Place an unslotted ion-label immediately alongside an unslotted ion-note to render a two-line item. Use slot=\"end\" on ion-note when you want the standard trailing-note layout instead.

\n
<ion-list inset=\"true\">\n  <ion-item-group>\n    <ion-item>\n      <ion-label>Network &amp; internet</ion-label>\n      <ion-note>Mobile, Wi-Fi, hotspot</ion-note>\n    </ion-item>\n  </ion-item-group>\n</ion-list>\n

Square buttons

\n

Add .button-square when a button should use more squared corners. It works with text buttons and icon-only buttons.

\n
<ion-button class=\"button-square\" fill=\"solid\">Continue</ion-button>\n<ion-button class=\"button-square\" fill=\"solid\">\n  <ion-icon name=\"add\" slot=\"icon-only\"></ion-icon>\n</ion-button>\n

Inset-list section headers

\n

Add .item-group-header to an ion-item-group to create the centered icon, title, and description used at the top of the component demo pages.

\n

This is an introductory group. Place regular list items in a separate ion-item-group that follows it.

\n
<ion-list inset=\"true\">\n  <ion-item-group class=\"item-group-header\">\n    <ion-item>\n      <ion-label>\n        <ion-icon name=\"list\" style=\"background: var(--ion-color-primary)\"></ion-icon>\n        <h2>Lists</h2>\n        <ion-text>Inset-list examples</ion-text>\n      </ion-label>\n    </ion-item>\n  </ion-item-group>\n  <ion-item-group>\n    <ion-item><ion-label>First item</ion-label></ion-item>\n  </ion-item-group>\n</ion-list>\n

Opting out

\n

Add .md3-disabled to an individual Ionic component when it must retain Ionic's standard Material styling.

\n
<ion-button fill=\"solid\">MD3 theme</ion-button> <ion-button class=\"md3-disabled\" fill=\"solid\">Standard Ionic</ion-button>\n
", + "headings": [ + { + "id": "two-line-inset-list-items", + "text": "Two-line inset list items", + "level": 2 + }, + { + "id": "square-buttons", + "text": "Square buttons", + "level": 2 + }, + { + "id": "inset-list-section-headers", + "text": "Inset-list section headers", + "level": 2 + }, + { + "id": "opting-out", + "text": "Opting out", + "level": 2 + } + ], + "codes": [], + "scrollMap": [], + "editUrl": "https://github.com/rdlabo-dev/ionic-theme-md3/edit/main/docs/special-markup.md" + }, + { + "title": "Using ion-item-group", + "navTitle": "Using ion-item-group", + "slug": "using-ion-item-group", + "file": "using-ion-item-group.md", + "section": "Guides", + "path": "/projects/ionic-theme-md3/docs/using-ion-item-group", + "html": "

The MD3 theme uses the same inset-list structure as @rdlabo/ionic-theme-ios26, allowing one template to work across Ionic modes. When an ion-list uses inset=\"true\", wrap its items in ion-item-group and keep ion-list-header outside the group.

\n

The examples use framework-neutral Web Component markup. In React or Vue, use the equivalent component and property syntax.

\n
<ion-list inset=\"true\">\n  <ion-list-header><ion-label>Connections</ion-label></ion-list-header>\n  <ion-item-group>\n    <ion-item>...</ion-item>\n    <ion-item>...</ion-item>\n  </ion-item-group>\n</ion-list>\n

No wrapper is required for lists that do not use inset=\"true\".

\n

Why the wrapper is required

\n

The shared structure keeps ion-list-header separate from the item surface. This matches the iOS 26 layout while allowing MD3 to style the same markup without platform-specific templates.

\n

The theme therefore:

\n
    \n
  • makes the inset ion-list background transparent;
  • \n
  • applies the item surface to ion-item-group; and
  • \n
  • leaves ion-list-header outside that surface.
  • \n
\n

For two-line items and section-header groups, see Special markup.

\n", + "headings": [ + { + "id": "why-the-wrapper-is-required", + "text": "Why the wrapper is required", + "level": 2 + } + ], + "codes": [], + "scrollMap": [], + "editUrl": "https://github.com/rdlabo-dev/ionic-theme-md3/edit/main/docs/using-ion-item-group.md" + }, + { + "title": "Migration", + "navTitle": "Migration", + "slug": "migration", + "file": "migration.md", + "section": "Guides", + "path": "/projects/ionic-theme-md3/docs/migration", + "html": "

Use the section for the version you are upgrading to. Each section lists only the changes that require application code or configuration updates.

\n

Migrating to 2.0.0

\n

Rename .header-item-group to .item-group-header

\n

The class for an ion-item-group used as a section header has been renamed for consistency with the element it modifies. Replace every occurrence of .header-item-group in application templates and styles.

\n
- <ion-item-group class=\"header-item-group\">\n+ <ion-item-group class=\"item-group-header\">\n    ...\n  </ion-item-group>\n

The old class is no longer styled by the theme. This rename applies to markup shared with @rdlabo/ionic-theme-ios26 as well.

\n", + "headings": [ + { + "id": "migrating-to-2.0.0", + "text": "Migrating to 2.0.0", + "level": 2 + }, + { + "id": "rename-.header-item-group-to-.item-group-header", + "text": "Rename .header-item-group to .item-group-header", + "level": 3 + } + ], + "codes": [], + "scrollMap": [], + "editUrl": "https://github.com/rdlabo-dev/ionic-theme-md3/edit/main/docs/migration.md" + }, { "title": "API", "navTitle": "API", diff --git a/src/app/generated/projects/ionic-theme-md3.ja.generated.ts b/src/app/generated/projects/ionic-theme-md3.ja.generated.ts index 3de5682..f65c964 100644 --- a/src/app/generated/projects/ionic-theme-md3.ja.generated.ts +++ b/src/app/generated/projects/ionic-theme-md3.ja.generated.ts @@ -8,7 +8,7 @@ export const PROJECT = { "repositoryUrl": "https://github.com/rdlabo-dev/ionic-theme-md3", "category": "frontend-tools", "icon": "theme", - "version": "1.1.0", + "version": "2.0.0", "description": "Ionicアプリ向けのMaterial Design 3スタイル。", "headline": "IonicアプリにMaterial Design 3を取り入れる", "overview": "iOS 26テーマと共通のマークアップ互換性を保ちつつ、IonicへMaterial Design 3スタイルとトランジションアニメーションを適用します。", @@ -36,7 +36,7 @@ export const PROJECT = { "file": "readme.md", "section": "ドキュメント", "path": "/projects/ionic-theme-md3/docs/readme", - "html": "

IonicアプリケーションにMaterial Design 3デザインシステムを適用するCSS/JSテーマライブラリです。

\n

\"Material

\n

DEMOはこちら: https://ionic-theme-md3.netlify.app/

\n

概要

\n

このライブラリは、IonicアプリケーションにMaterial Design 3デザインシステムをもたらすCSS/JSファイルを提供します。Ionicコンポーネントの見た目を、最新のMaterial Design 3ガイドラインに合わせて更新します。

\n

このプロジェクトはIonicのコアコンセプトに可能な限り沿いながら、@rdlabo/ionic-theme-ios26 との互換性を特に重視しています。Ionicが単一のHTML構造からiosテーマでもmdテーマでも美しいスタイルを提供するのと同じように、このテーマと @rdlabo/ionic-theme-ios26 が十分に互換であることを目指しています。

\n

@rdlabo/ionic-theme-ios26 をまだご存じでない方は、ぜひ試してみてください!

\n

👉️rdlabo-dev/ionic-theme-ios26

\n

関連プロジェクト

\n

より包括的なMaterial Design 3実装が必要な場合は、次も参考になるかもしれません:

\n\n
\n

Note: このテーマは Ionic の設計方針と @rdlabo/ionic-theme-ios26 との互換性を目的に作られており、厳密で完全な MD3 再現を意図したものではありません。

\n
\n

クイックスタート

\n

インストール のあと、テーマ CSS をインポートし、下のとおり navAnimation を設定します。

\n

インストール

\n

これはIonicプロジェクトを拡張するためのCSSテーマです。単体では動作しないため、Ionic Frameworkと一緒に使ってください。

\n
npm install @rdlabo/ionic-theme-md3\n

Note: @ionic/core@ < 8.8.0 を使う場合は、@rdlabo/ionic-theme-md3@1.0.2 を使ってください。

\n

プロジェクトのメインCSSファイル(例: src/styles.scss)でテーマをインポートします。

\n
@import '@rdlabo/ionic-theme-md3/dist/css/default-variables.css';\n@import '@rdlabo/ionic-theme-md3/dist/css/ionic-theme-md3.css';\n

次に、MD3向けのアニメーションを設定します。Ionicの設定オプションに次を追加してください。

\n
import { isPlatform } from '@ionic/core'; // or @ionic/angular/standalone, @ionic/react, @ionic/vue\nimport { mdTransitionAnimation } from '@rdlabo/ionic-theme-md3';\n\n// Angular\nprovideIonicAngular({\n    ...\n    navAnimation: isPlatform('ios') ? undefined: mdTransitionAnimation,\n});\n\n// React\nsetupIonicReact({\n    ...\n    navAnimation: isPlatform('ios') ? undefined: mdTransitionAnimation,\n});\n\n// Vue\ncreateApp(App)\n    .use(IonicVue, {\n        ...\n        navAnimation: isPlatform('ios') ? undefined: mdTransitionAnimation,\n})\n

ドキュメント

\n

上の インストール から始めてください。同じマークアップで両プラットフォームが必要なときは @rdlabo/ionic-theme-ios26 と組み合わせます。

\n", + "html": "

IonicアプリケーションにMaterial Design 3デザインシステムを適用するCSS/JSテーマライブラリです。

\n

\"Material

\n

DEMOはこちら: https://ionic-theme-md3.rdlabo.dev/

\n

概要

\n

このライブラリは、IonicアプリケーションにMaterial Design 3デザインシステムをもたらすCSS/JSファイルを提供します。Ionicコンポーネントの見た目を、最新のMaterial Design 3ガイドラインに合わせて更新します。

\n

このプロジェクトはIonicのコアコンセプトに可能な限り沿いながら、@rdlabo/ionic-theme-ios26 との互換性を特に重視しています。Ionicが単一のHTML構造からiosテーマでもmdテーマでも美しいスタイルを提供するのと同じように、このテーマと @rdlabo/ionic-theme-ios26 が十分に互換であることを目指しています。

\n

@rdlabo/ionic-theme-ios26 をまだご存じでない方は、ぜひ試してみてください!

\n

👉️rdlabo-dev/ionic-theme-ios26

\n

関連プロジェクト

\n

より包括的なMaterial Design 3実装が必要な場合は、次も参考になるかもしれません:

\n\n
\n

Note: このテーマは Ionic の設計方針と @rdlabo/ionic-theme-ios26 との互換性を目的に作られており、厳密で完全な MD3 再現を意図したものではありません。

\n
\n

クイックスタート

\n

インストール のあと、テーマ CSS をインポートし、下のとおり navAnimation を設定します。

\n

インストール

\n

これはIonicプロジェクトを拡張するためのCSSテーマです。単体では動作しないため、Ionic Frameworkと一緒に使ってください。

\n
npm install @rdlabo/ionic-theme-md3\n

Note: @ionic/core@ < 8.8.0 を使う場合は、@rdlabo/ionic-theme-md3@1.0.2 を使ってください。

\n

プロジェクトのメインCSSファイル(例: src/styles.scss)でテーマをインポートします。

\n
@import '@rdlabo/ionic-theme-md3/dist/css/default-variables.css';\n@import '@rdlabo/ionic-theme-md3/dist/css/ionic-theme-md3.css';\n

オプション: MD3テーマとiOS 26テーマを併用する

\n

同じアプリケーションでIonicの両モードをスタイルするには、iOS 26テーマをインストールします。

\n

両テーマの現行リリースには、@ionic/core 8.8.1以降が必要です。アプリケーションが8.8.0以前の場合は、この設定を使う前にIonicをアップグレードしてください。

\n
npm install @rdlabo/ionic-theme-ios26\n

グローバルスタイルシートでSassを使っている場合は、次の順序でテーマを初期化します。

\n
@use '@rdlabo/ionic-theme-ios26/src/styles/default-variables.scss' as ios26-vars;\n@use '@rdlabo/ionic-theme-ios26/src/styles/ionic-theme-ios26.scss';\n@use '@rdlabo/ionic-theme-ios26/src/styles/ionic-theme-ios26-dark-class.scss';\n@use '@rdlabo/ionic-theme-ios26/src/styles/md-remove-ios-class-effect.scss';\n@use '@rdlabo/ionic-theme-md3/dist/css/default-variables.css' as md3-vars;\n@use '@rdlabo/ionic-theme-md3/dist/css/ionic-theme-md3.css';\n

この例ではIonicのclassベースのダークモードを使います。グローバルスタイルシートでは、Angularの @ionic/angular/css/palettes/dark.class.css など、Ionic側の対応するダークパレットも読み込んでください。dark-system または dark-always を使う場合は、IonicのパレットとiOS 26テーマの両方で同じvariantを選びます。詳しくはIonicのダークモードのドキュメントを参照してください。ios26-varsmd3-vars を明示することで、2つの変数モジュールが同じデフォルトnamespaceを使うことを防ぎます。

\n

両テーマをインストールした場合は、両方のtransition実装を設定します。

\n
import { isPlatform } from '@ionic/core'; // or @ionic/angular (Ionic 9), @ionic/angular/standalone (Ionic 8), @ionic/react, @ionic/vue\nimport { iosTransitionAnimation, popoverEnterAnimation, popoverLeaveAnimation } from '@rdlabo/ionic-theme-ios26';\nimport { mdTransitionAnimation } from '@rdlabo/ionic-theme-md3';\n\n// Angular\nprovideIonicAngular({\n    ...\n    navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation,\n    popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined,\n    popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined,\n});\n\n// React\nsetupIonicReact({\n    ...\n    navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation,\n    popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined,\n    popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined,\n});\n\n// Vue\ncreateApp(App)\n    .use(IonicVue, {\n        ...\n        navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation,\n        popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined,\n        popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined,\n    });\n

MD3テーマだけをインストールした場合は、次のようにアニメーションを設定します。

\n
import { isPlatform } from '@ionic/core'; // or @ionic/angular (Ionic 9), @ionic/angular/standalone (Ionic 8), @ionic/react, @ionic/vue\nimport { mdTransitionAnimation } from '@rdlabo/ionic-theme-md3';\n\n// Angular\nprovideIonicAngular({\n    ...\n    navAnimation: isPlatform('ios') ? undefined: mdTransitionAnimation,\n});\n\n// React\nsetupIonicReact({\n    ...\n    navAnimation: isPlatform('ios') ? undefined: mdTransitionAnimation,\n});\n\n// Vue\ncreateApp(App)\n    .use(IonicVue, {\n        ...\n        navAnimation: isPlatform('ios') ? undefined: mdTransitionAnimation,\n})\n

ドキュメント

\n

上の インストール から始めてください。同じマークアップで両プラットフォームが必要なときは @rdlabo/ionic-theme-ios26 と組み合わせます。

\n\n", "headings": [ { "id": "%E6%A6%82%E8%A6%81", @@ -58,6 +58,11 @@ export const PROJECT = { "text": "インストール", "level": 2 }, + { + "id": "%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3%3A-md3%E3%83%86%E3%83%BC%E3%83%9E%E3%81%A8ios-26%E3%83%86%E3%83%BC%E3%83%9E%E3%82%92%E4%BD%B5%E7%94%A8%E3%81%99%E3%82%8B", + "text": "オプション: MD3テーマとiOS 26テーマを併用する", + "level": 3 + }, { "id": "%E3%83%89%E3%82%AD%E3%83%A5%E3%83%A1%E3%83%B3%E3%83%88", "text": "ドキュメント", @@ -68,6 +73,83 @@ export const PROJECT = { "scrollMap": [], "editUrl": "https://github.com/rdlabo-dev/docs/edit/main/src/ionic-theme-md3/docs/ja/readme.md" }, + { + "title": "特別なマークアップ", + "navTitle": "特別なマークアップ", + "slug": "special-markup", + "file": "special-markup.md", + "section": "ガイド", + "path": "/projects/ionic-theme-md3/docs/special-markup", + "html": "

ほとんどのIonicマークアップは変更せずに使えます。以下は明示的なopt-inで、同じtemplateを @rdlabo/ionic-theme-ios26 でも使う場合に役立ちます。

\n

2行のinset list item

\n

slotを指定しない ion-labelion-note を隣接させると、2行のitemとして表示します。通常の末尾noteとして表示したい場合は、ion-noteslot=\"end\" を指定します。

\n
<ion-list inset=\"true\">\n  <ion-item-group>\n    <ion-item>\n      <ion-label>Network &amp; internet</ion-label>\n      <ion-note>Mobile, Wi-Fi, hotspot</ion-note>\n    </ion-item>\n  </ion-item-group>\n</ion-list>\n

square button

\n

buttonのcornerをよりsquareにする場合は .button-square を追加します。text buttonとicon-only buttonの両方で使えます。

\n
<ion-button class=\"button-square\" fill=\"solid\">Continue</ion-button>\n<ion-button class=\"button-square\" fill=\"solid\">\n  <ion-icon name=\"add\" slot=\"icon-only\"></ion-icon>\n</ion-button>\n

inset listのsection header

\n

.item-group-headerion-item-group に追加すると、component demo pageの先頭で使われている中央揃えのicon、title、descriptionを表示できます。

\n

これは導入用のgroupです。通常のlist itemは、その後に置く別の ion-item-group に入れてください。

\n
<ion-list inset=\"true\">\n  <ion-item-group class=\"item-group-header\">\n    <ion-item>\n      <ion-label>\n        <ion-icon name=\"list\" style=\"background: var(--ion-color-primary)\"></ion-icon>\n        <h2>Lists</h2>\n        <ion-text>Inset-list examples</ion-text>\n      </ion-label>\n    </ion-item>\n  </ion-item-group>\n  <ion-item-group>\n    <ion-item><ion-label>First item</ion-label></ion-item>\n  </ion-item-group>\n</ion-list>\n

Themeを無効にする

\n

個別のIonic componentで標準のMaterial styleを維持する場合は .md3-disabled を追加します。

\n
<ion-button fill=\"solid\">MD3 theme</ion-button> <ion-button class=\"md3-disabled\" fill=\"solid\">Standard Ionic</ion-button>\n
", + "headings": [ + { + "id": "2%E8%A1%8C%E3%81%AEinset-list-item", + "text": "2行のinset list item", + "level": 2 + }, + { + "id": "square-button", + "text": "square button", + "level": 2 + }, + { + "id": "inset-list%E3%81%AEsection-header", + "text": "inset listのsection header", + "level": 2 + }, + { + "id": "theme%E3%82%92%E7%84%A1%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B", + "text": "Themeを無効にする", + "level": 2 + } + ], + "codes": [], + "scrollMap": [], + "editUrl": "https://github.com/rdlabo-dev/docs/edit/main/src/ionic-theme-md3/docs/ja/special-markup.md" + }, + { + "title": "ion-item-groupの使用方法", + "navTitle": "ion-item-groupの使用方法", + "slug": "using-ion-item-group", + "file": "using-ion-item-group.md", + "section": "ガイド", + "path": "/projects/ionic-theme-md3/docs/using-ion-item-group", + "html": "

MD3テーマは @rdlabo/ionic-theme-ios26 と同じinset list構造を使うため、Ionic modeをまたいで1つのtemplateを利用できます。ion-listinset=\"true\" を使う場合は、itemを ion-item-group で囲み、ion-list-header はgroupの外に置きます。

\n

以下はframeworkに依存しないWeb Component形式のマークアップです。ReactまたはVueでは、各frameworkに対応するcomponentとpropertyの構文に置き換えてください。

\n
<ion-list inset=\"true\">\n  <ion-list-header><ion-label>Connections</ion-label></ion-list-header>\n  <ion-item-group>\n    <ion-item>...</ion-item>\n    <ion-item>...</ion-item>\n  </ion-item-group>\n</ion-list>\n

inset=\"true\" を使わないlistでは、このwrapperは不要です。

\n

wrapperが必要な理由

\n

共有構造では、ion-list-header をitemのsurfaceから分離します。これにより、platform固有のtemplateを用意せずに、iOS 26のlayoutとMD3のstyleを同じマークアップへ適用できます。

\n

そのため、このテーマは次のようにstyleを適用します。

\n
    \n
  • inset ion-list の背景を透明にする
  • \n
  • itemのsurfaceを ion-item-group に適用する
  • \n
  • ion-list-header をsurfaceの外に置く
  • \n
\n

2行itemとsection header groupについては 特別なマークアップ を参照してください。

\n", + "headings": [ + { + "id": "wrapper%E3%81%8C%E5%BF%85%E8%A6%81%E3%81%AA%E7%90%86%E7%94%B1", + "text": "wrapperが必要な理由", + "level": 2 + } + ], + "codes": [], + "scrollMap": [], + "editUrl": "https://github.com/rdlabo-dev/docs/edit/main/src/ionic-theme-md3/docs/ja/using-ion-item-group.md" + }, + { + "title": "移行", + "navTitle": "移行", + "slug": "migration", + "file": "migration.md", + "section": "ガイド", + "path": "/projects/ionic-theme-md3/docs/migration", + "html": "

アップグレード対象のversionに対応するsectionを参照してください。各sectionには、アプリケーションのコードまたは設定の変更が必要な項目のみを記載しています。

\n

2.0.0への移行

\n

.header-item-group.item-group-header に変更する

\n

section headerとして使う ion-item-group のclass名を、対象elementとの一貫性を保つため変更しました。アプリケーションのtemplateとstyleにある .header-item-group をすべて置き換えてください。

\n
- <ion-item-group class=\"header-item-group\">\n+ <ion-item-group class=\"item-group-header\">\n    ...\n  </ion-item-group>\n

旧classはthemeでstyleされなくなりました。この変更は、@rdlabo/ionic-theme-ios26 と共有するmarkupにも適用されます。

\n", + "headings": [ + { + "id": "2.0.0%E3%81%B8%E3%81%AE%E7%A7%BB%E8%A1%8C", + "text": "2.0.0への移行", + "level": 2 + }, + { + "id": ".header-item-group-%E3%82%92-.item-group-header-%E3%81%AB%E5%A4%89%E6%9B%B4%E3%81%99%E3%82%8B", + "text": ".header-item-group を .item-group-header に変更する", + "level": 3 + } + ], + "codes": [], + "scrollMap": [], + "editUrl": "https://github.com/rdlabo-dev/docs/edit/main/src/ionic-theme-md3/docs/ja/migration.md" + }, { "title": "API", "navTitle": "API", @@ -75,7 +157,7 @@ export const PROJECT = { "file": "api.md", "section": "リファレンス", "path": "/projects/ionic-theme-md3/docs/api", - "html": "

@rdlabo/ionic-theme-md3 v1.1.0 が公開するJavaScript APIのリファレンスです。CSS entry pointはREADMEで説明します。

\n

Animation

\n

function mdTransitionAnimation

(_: HTMLElement, opts: TransitionOptions) => Animation

Ionic向けのMaterial Design 3 navigation transitionを生成します。

\n\n\n
", + "html": "

@rdlabo/ionic-theme-md3 v2.0.0 が公開するJavaScript APIのリファレンスです。CSS entry pointはREADMEで説明します。

\n

Animation

\n

function mdTransitionAnimation

(_: HTMLElement, opts: TransitionOptions) => Animation

Ionic向けのMaterial Design 3 navigation transitionを生成します。

\n\n\n
", "headings": [ { "id": "animation", diff --git a/src/app/generated/projects/stripe-terminal.en.generated.ts b/src/app/generated/projects/stripe-terminal.en.generated.ts index 3b05a60..75de851 100644 --- a/src/app/generated/projects/stripe-terminal.en.generated.ts +++ b/src/app/generated/projects/stripe-terminal.en.generated.ts @@ -40,7 +40,7 @@ export const PROJECT = { "file": "configuration.md", "section": "Quickstart", "path": "/projects/capacitor-stripe-terminal/docs/configuration", - "html": "

Install Stripe Terminal and synchronize the native Capacitor projects.

\n
npm install @capacitor-community/stripe-terminal\nnpx cap sync\n

The plugin is @capacitor-community/stripe-terminal v8.2.1. Official demos:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
RequirementMinimum
Capacitor8
iOS15.0
Android minSdkVersion26
\n

Platform and connection types

\n

discoverReaders takes a TerminalConnectTypes value. Support is not the same on every platform.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
TerminalConnectTypesWebiOSAndroid
InternetYes — the only supported typeYesYes
BluetoothNoYesYes
TapToPayNoYesYes
UsbNoUnimplementedYes
HandOffNoUnimplementedYes (Apps on Devices)
SimulatedNoUnimplemented as a discover typeTreated as Bluetooth discovery
\n

On every platform, pass isTest: true to initialize when you want simulated readers for a supported connection type. Do not rely on TerminalConnectTypes.Simulated on iOS or web; use Internet, Bluetooth, or TapToPay with isTest: true instead.

\n

Web discoverReaders rejects with an unavailable error for any type other than Internet.

\n

Platform-only APIs

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
APIWebiOSAndroid
setTapToPayUxConfigurationNo-op (logs only)UnimplementedYes — call after initialize, before connectReader
isTapToPayAccountLinkedUnavailable (throws)Yes — iOS 16.4+, after initializeUnimplemented
\n

See Tap to Pay for the setup sequence and limitations.

\n

Web no-op and unsupported lifecycle methods

\n

These methods exist on the plugin interface but do not drive the Stripe Terminal JS SDK on web:

\n
    \n
  • cancelDiscoverReaders — no-op
  • \n
  • setSimulatorConfiguration — no-op
  • \n
  • installAvailableUpdate — no-op
  • \n
  • cancelInstallUpdate — no-op
  • \n
  • rebootReader — no-op
  • \n
  • cancelReaderReconnection — no-op
  • \n
  • setTapToPayUxConfiguration — no-op
  • \n
\n

isTapToPayAccountLinked throws unavailable on web.

\n

Internet readers on web still support initialize, discoverReaders, connectReader, getConnectedReader, disconnectReader, collectPaymentMethod, cancelCollectPaymentMethod, confirmPaymentIntent, setReaderDisplay, clearReaderDisplay, setConnectionToken, and the connection / payment status listeners.

\n

Web configuration

\n

No additional steps are necessary. Only Internet readers are available.

\n

iOS configuration

\n

No additional steps are necessary for the plugin. USB, HandOff, and setTapToPayUxConfiguration are unimplemented on iOS.

\n

Android configuration

\n

Add permissions to your android/app/src/main/AndroidManifest.xml file:

\n
+ <uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\" />\n+ <uses-permission android:name=\"android.permission.BLUETOOTH\" android:maxSdkVersion=\"30\" />\n+ <uses-permission android:name=\"android.permission.BLUETOOTH_ADMIN\" android:maxSdkVersion=\"30\" />\n+ <uses-permission android:name=\"android.permission.BLUETOOTH_SCAN\" />\n+ <uses-permission android:name=\"android.permission.BLUETOOTH_ADVERTISE\" />\n+ <uses-permission android:name=\"android.permission.BLUETOOTH_CONNECT\" />\n

discoverReaders rejects when ACCESS_FINE_LOCATION has not been granted at runtime.

\n

And update minSdkVersion to 26 in your android/variables.gradle file:

\n
  ext {\n-    minSdkVersion = 24\n+    minSdkVersion = 26\n

If you are developing apps for Stripe Android devices (for example Stripe Reader S700) and using TerminalConnectTypes.HandOff, follow Stripe's client-side setup guide.

\n", + "html": "

Install Stripe Terminal and synchronize the native Capacitor projects.

\n
npm install @capacitor-community/stripe-terminal\nnpx cap sync\n

The plugin is @capacitor-community/stripe-terminal v8.2.1. Official demos:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
RequirementMinimum
Capacitor8
iOS15.0
Android minSdkVersion26
\n

Platform and connection types

\n

discoverReaders takes a TerminalConnectTypes value. Support is not the same on every platform.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
TerminalConnectTypesWebiOSAndroid
InternetYes — the only supported typeYesYes
BluetoothNoYesYes
TapToPayNoYesYes
UsbNoUnimplementedYes
HandOffNoUnimplementedYes (Apps on Devices)
SimulatedNoUnimplemented as a discover typeTreated as Bluetooth discovery
\n

On every platform, pass isTest: true to initialize when you want simulated readers for a supported connection type. Do not rely on TerminalConnectTypes.Simulated on iOS or web; use Internet, Bluetooth, or TapToPay with isTest: true instead.

\n

Web discoverReaders rejects with an unavailable error for any type other than Internet.

\n

Platform-only APIs

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
APIWebiOSAndroid
setTapToPayUxConfigurationNo-op (logs only)UnimplementedYes — call after initialize, before connectReader
isTapToPayAccountLinkedUnavailable (throws)Yes — iOS 16.4+, after initializeUnimplemented
\n

See Tap to Pay for the setup sequence and limitations.

\n

Web no-op and unsupported lifecycle methods

\n

These methods exist on the plugin interface but do not drive the Stripe Terminal JS SDK on web:

\n
    \n
  • cancelDiscoverReaders — no-op
  • \n
  • setSimulatorConfiguration — no-op
  • \n
  • installAvailableUpdate — no-op
  • \n
  • cancelInstallUpdate — no-op
  • \n
  • rebootReader — no-op
  • \n
  • cancelReaderReconnection — no-op
  • \n
  • setTapToPayUxConfiguration — no-op
  • \n
\n

isTapToPayAccountLinked throws unavailable on web.

\n

Internet readers on web still support initialize, discoverReaders, connectReader, getConnectedReader, disconnectReader, collectPaymentMethod, cancelCollectPaymentMethod, confirmPaymentIntent, setReaderDisplay, clearReaderDisplay, setConnectionToken, and the connection / payment status listeners.

\n

Web configuration

\n

No additional steps are necessary. Only Internet readers are available.

\n

iOS configuration

\n

No additional steps are necessary for the plugin. USB, HandOff, and setTapToPayUxConfiguration are unimplemented on iOS.

\n

Android configuration

\n

Add permissions to your android/app/src/main/AndroidManifest.xml file:

\n
+ <uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\" />\n+ <uses-permission android:name=\"android.permission.BLUETOOTH\" android:maxSdkVersion=\"30\" />\n+ <uses-permission android:name=\"android.permission.BLUETOOTH_ADMIN\" android:maxSdkVersion=\"30\" />\n+ <uses-permission android:name=\"android.permission.BLUETOOTH_SCAN\" />\n+ <uses-permission android:name=\"android.permission.BLUETOOTH_ADVERTISE\" />\n+ <uses-permission android:name=\"android.permission.BLUETOOTH_CONNECT\" />\n

discoverReaders rejects when ACCESS_FINE_LOCATION has not been granted at runtime.

\n

And update minSdkVersion to 26 in your android/variables.gradle file:

\n
  ext {\n-    minSdkVersion = 24\n+    minSdkVersion = 26\n

If you are developing apps for Stripe Android devices (for example Stripe Reader S700) and using TerminalConnectTypes.HandOff, follow Stripe's client-side setup guide.

\n", "headings": [ { "id": "platform-and-connection-types", @@ -691,7 +691,7 @@ export const PROJECT = { "file": "tap-to-pay.md", "section": "Guides", "path": "/projects/capacitor-stripe-terminal/docs/tap-to-pay", - "html": "

Tap to Pay collects contactless payments on a compatible phone or tablet without a separate card reader. Use TerminalConnectTypes.TapToPay after configuration and a working connection token.

\n

The official demo exercises Tap to Pay, Internet, and Bluetooth in demo/angular.

\n

Platform prerequisites

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
PlatformSupportedNotes
AndroidYesNFC-capable device, location permission, Stripe Tap to Pay on Android eligibility. minSdkVersion 26.
iOSYesTap to Pay on iPhone, iOS 16.4+ for the account-link check. setTapToPayUxConfiguration is unimplemented.
WebNodiscoverReaders({ type: TapToPay }) is unavailable.
\n

Complete Stripe Dashboard Terminal setup and create a Location. Pass that locationId into discoverReaders; the plugin uses it when connecting the Tap to Pay reader.

\n

Android initialize requests the location permission listed in Configuration. Bluetooth permissions are requested only when discovering Bluetooth or Simulated readers; Tap to Pay discovery itself does not request them.

\n

Setup sequence

\n
    \n
  1. Register application-level listeners.
  2. \n
  3. Register an authenticated connection-token provider with RequestedConnectionToken + setConnectionToken, then call initialize.
  4. \n
  5. On iOS, call isTapToPayAccountLinked (do not cache the result).
  6. \n
  7. On Android, optionally call setTapToPayUxConfiguration.
  8. \n
  9. discoverReaders with type: TerminalConnectTypes.TapToPay and locationId.
  10. \n
  11. connectReader with the discovered reader.
  12. \n
  13. Collect and confirm a card_present PaymentIntent as in Collect a Payment.
  14. \n
\n

method initialize(...)

Initializes the Stripe Terminal SDK and its connection-token provider.
\nCall this once before discovering readers.

When tokenProviderEndpoint is provided, the plugin sends a POST request
\nand expects { secret: string }. When it is omitted, handle
\nRequestedConnectionToken and call setConnectionToken() instead.

initialize(options: StripeTerminalInitializationOptions) => Promise<void>

\n\n\n\n\n

isTapToPayAccountLinked is iOS only and requires iOS 16.4 or later. initialize() must have run so the SDK has a connection token provider. No reader connection is required and the call does not activate NFC.

\n

The answer is read from Apple on every call. Do not cache isLinked. For Stripe Connect, pass onBehalfOf as the connected account ID; omit it to check the account that owns the API key.

\n

Android and web reject the call (unimplemented / unavailable). Guard with a platform check or .catch() like the official demo does for Android-only UX configuration.

\n

method isTapToPayAccountLinked(...)

Check whether the merchant has accepted Apple's Tap to Pay on iPhone
\nTerms and Conditions.

iOS only, and requires iOS 16.4 or later. initialize() must have been
\ncalled first because the SDK needs a connection token provider, but no
\nreader connection is required and the call does not activate the device.

The answer is read from Apple on every call. Apple's Tap to Pay on iPhone
\nrequirements state that acceptance state must be retrieved from Apple
\nrather than from a local variable, so do not cache the result.

Stripe docs reference

isTapToPayAccountLinked(options?: IsTapToPayAccountLinkedOptions | undefined) => Promise<{ isLinked: boolean; }>

\n\n\n\n\n\n

interface IsTapToPayAccountLinkedOptions

Options for isTapToPayAccountLinked.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
PropTypeDescriptionSince
onBehalfOfstringConnected account ID, for Stripe Connect platforms. Omit to check the account that owns the API key.8.2.0
\n\n\n

UX configuration

\n

setTapToPayUxConfiguration is Android only. Call it after initialize() and before connectReader(). iOS returns unimplemented; web logs and returns.

\n

The installed Android implementation applies colors (primary, success, error as 'default' or a hex string such as '#FF5733') and darkMode (SYSTEM, DARK, LIGHT). The TypeScript tapZone field is declared but not applied on the current Android Terminal SDK used by v8.2.1.

\n

method setTapToPayUxConfiguration(...)

Configure the Tap to Pay UX appearance (Android only).
\nCall this after initialize() but before connectReader().
\nHas no effect on iOS or web platforms.

setTapToPayUxConfiguration(options: TapToPayUxConfiguration) => Promise<void>

\n\n\n

interface TapToPayUxConfiguration

Configuration for the Tap to Pay UX (Android only).

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
PropTypeDescriptionSince
colorsTapToPayColorSchemeColor scheme for the Tap to Pay screen.8.1.0
darkModeTapToPayDarkModeDark-mode setting for the Tap to Pay screen.8.1.0
tapZoneTapToPayTapZonePosition of the tap indicator on screen.8.1.0
\n\n\n

interface TapToPayColorScheme

Color scheme for the Tap to Pay screen.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
PropTypeDescriptionSince
primaryTapToPayColorPrimary color for the tap-zone indicator. Use a hex string or default.8.1.0
successTapToPayColorSuccess-state color. Use a hex string or default.8.1.0
errorTapToPayColorError-state color. Use a hex string or default.8.1.0
\n\n\n

type alias TapToPayColor

'default' | string

\n\n

type alias TapToPayTapZone

{ type: 'default' } | { type: 'front'; xBias: number; yBias: number } | { type: 'behind'; xBias: number; yBias: number } | { type: 'above'; bias?: number } | { type: 'below'; bias?: number } | { type: 'left'; bias?: number } | { type: 'right'; bias?: number }

\n\n

enum TapToPayDarkMode

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
MemberValue
System'SYSTEM'
Dark'DARK'
Light'LIGHT'
\n\n

Discover and connect

\n

Discover with TerminalConnectTypes.TapToPay and a locationId. Simulated Tap to Pay uses isTest: true on initialize, not TerminalConnectTypes.Simulated.

\n

Connect the reader from the discovery result. autoReconnectOnUnexpectedDisconnect defaults to false and is supported for Tap to Pay. On iOS, merchantDisplayName and onBehalfOf are passed into the Tap to Pay connection configuration. On Android, set those values on the PaymentIntent instead.

\n

method discoverReaders(...)

Discovers readers using the requested transport. The returned readers are
\nsnapshots; listen for DiscoveredReaders when continuous discovery can
\nproduce additional results.

discoverReaders(options: DiscoverReadersOptions) => Promise<{ readers: ReaderInterface[]; }>

\n\n\n

method connectReader(...)

Connects to a reader returned by discoverReaders().

connectReader(options: ConnectReaderOptions) => Promise<void>

After connect, use collectPaymentMethod and confirmPaymentIntent with a server-created card_present PaymentIntent.

\n\n\n\n

Limitations

\n
    \n
  • Web cannot discover or connect Tap to Pay.
  • \n
  • UX colors and dark mode are Android-only; iOS uses the system Tap to Pay on iPhone UI.
  • \n
  • Account-link status is iOS-only and must be re-fetched from Apple each time.
  • \n
  • tapZone is part of the TypeScript API but is not wired through on the installed Android SDK.
  • \n
  • Optional reader software updates still follow Reader Lifecycle rules: do not install during checkout.
  • \n
  • Keep Stripe secret keys and connection-token creation on the backend.
  • \n
\n", + "html": "

Tap to Pay collects contactless payments on a compatible phone or tablet without a separate card reader. Use TerminalConnectTypes.TapToPay after configuration and a working connection token.

\n

The official demo exercises Tap to Pay, Internet, and Bluetooth in demo/angular.

\n

Platform prerequisites

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
PlatformSupportedNotes
AndroidYesNFC-capable device, location permission, Stripe Tap to Pay on Android eligibility. minSdkVersion 26.
iOSYesTap to Pay on iPhone, iOS 16.4+ for the account-link check. setTapToPayUxConfiguration is unimplemented.
WebNodiscoverReaders({ type: TapToPay }) is unavailable.
\n

Complete Stripe Dashboard Terminal setup and create a Location. Pass that locationId into discoverReaders; the plugin uses it when connecting the Tap to Pay reader.

\n

Android initialize requests the location permission listed in Configuration. Bluetooth permissions are requested only when discovering Bluetooth or Simulated readers; Tap to Pay discovery itself does not request them.

\n

Setup sequence

\n
    \n
  1. Register application-level listeners.
  2. \n
  3. Register an authenticated connection-token provider with RequestedConnectionToken + setConnectionToken, then call initialize.
  4. \n
  5. On iOS, call isTapToPayAccountLinked (do not cache the result).
  6. \n
  7. On Android, optionally call setTapToPayUxConfiguration.
  8. \n
  9. discoverReaders with type: TerminalConnectTypes.TapToPay and locationId.
  10. \n
  11. connectReader with the discovered reader.
  12. \n
  13. Collect and confirm a card_present PaymentIntent as in Collect a Payment.
  14. \n
\n

method initialize(...)

Initializes the Stripe Terminal SDK and its connection-token provider.
\nCall this once before discovering readers.

When tokenProviderEndpoint is provided, the plugin sends a POST request
\nand expects { secret: string }. When it is omitted, handle
\nRequestedConnectionToken and call setConnectionToken() instead.

initialize(options: StripeTerminalInitializationOptions) => Promise<void>

\n\n\n\n\n

isTapToPayAccountLinked is iOS only and requires iOS 16.4 or later. initialize() must have run so the SDK has a connection token provider. No reader connection is required and the call does not activate NFC.

\n

The answer is read from Apple on every call. Do not cache isLinked. For Stripe Connect, pass onBehalfOf as the connected account ID; omit it to check the account that owns the API key.

\n

Android and web reject the call (unimplemented / unavailable). Guard with a platform check or .catch() like the official demo does for Android-only UX configuration.

\n

method isTapToPayAccountLinked(...)

Check whether the merchant has accepted Apple's Tap to Pay on iPhone
\nTerms and Conditions.

iOS only, and requires iOS 16.4 or later. initialize() must have been
\ncalled first because the SDK needs a connection token provider, but no
\nreader connection is required and the call does not activate the device.

The answer is read from Apple on every call. Apple's Tap to Pay on iPhone
\nrequirements state that acceptance state must be retrieved from Apple
\nrather than from a local variable, so do not cache the result.

Stripe docs reference

isTapToPayAccountLinked(options?: IsTapToPayAccountLinkedOptions | undefined) => Promise<{ isLinked: boolean; }>

\n\n\n\n\n\n

interface IsTapToPayAccountLinkedOptions

Options for isTapToPayAccountLinked.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
PropTypeDescriptionSince
onBehalfOfstringConnected account ID, for Stripe Connect platforms. Omit to check the account that owns the API key.8.2.0
\n\n\n

UX configuration

\n

setTapToPayUxConfiguration is Android only. Call it after initialize() and before connectReader(). iOS returns unimplemented; web logs and returns.

\n

The installed Android implementation applies colors (primary, success, error as 'default' or a hex string such as '#FF5733') and darkMode (SYSTEM, DARK, LIGHT). The TypeScript tapZone field is declared but not applied on the current Android Terminal SDK used by v8.2.1.

\n

method setTapToPayUxConfiguration(...)

Configure the Tap to Pay UX appearance (Android only).
\nCall this after initialize() but before connectReader().
\nHas no effect on iOS or web platforms.

setTapToPayUxConfiguration(options: TapToPayUxConfiguration) => Promise<void>

\n\n\n

interface TapToPayUxConfiguration

Configuration for the Tap to Pay UX (Android only).

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
PropTypeDescriptionSince
colorsTapToPayColorSchemeColor scheme for the Tap to Pay screen.8.1.0
darkModeTapToPayDarkModeDark-mode setting for the Tap to Pay screen.8.1.0
tapZoneTapToPayTapZonePosition of the tap indicator on screen.8.1.0
\n\n\n

interface TapToPayColorScheme

Color scheme for the Tap to Pay screen.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
PropTypeDescriptionSince
primaryTapToPayColorPrimary color for the tap-zone indicator. Use a hex string or default.8.1.0
successTapToPayColorSuccess-state color. Use a hex string or default.8.1.0
errorTapToPayColorError-state color. Use a hex string or default.8.1.0
\n\n\n

type alias TapToPayColor

'default' | string

\n\n

type alias TapToPayTapZone

{ type: 'default' } | { type: 'front'; xBias: number; yBias: number } | { type: 'behind'; xBias: number; yBias: number } | { type: 'above'; bias?: number } | { type: 'below'; bias?: number } | { type: 'left'; bias?: number } | { type: 'right'; bias?: number }

\n\n

enum TapToPayDarkMode

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
MemberValue
System'SYSTEM'
Dark'DARK'
Light'LIGHT'
\n\n

Discover and connect

\n

Discover with TerminalConnectTypes.TapToPay and a locationId. Simulated Tap to Pay uses isTest: true on initialize, not TerminalConnectTypes.Simulated.

\n

Connect the reader from the discovery result. autoReconnectOnUnexpectedDisconnect defaults to false and is supported for Tap to Pay. On iOS, merchantDisplayName and onBehalfOf are passed into the Tap to Pay connection configuration. On Android, set those values on the PaymentIntent instead.

\n

method discoverReaders(...)

Discovers readers using the requested transport. The returned readers are
\nsnapshots; listen for DiscoveredReaders when continuous discovery can
\nproduce additional results.

discoverReaders(options: DiscoverReadersOptions) => Promise<{ readers: ReaderInterface[]; }>

\n\n\n

method connectReader(...)

Connects to a reader returned by discoverReaders().

connectReader(options: ConnectReaderOptions) => Promise<void>

After connect, use collectPaymentMethod and confirmPaymentIntent with a server-created card_present PaymentIntent.

\n\n\n\n

Limitations

\n
    \n
  • Web cannot discover or connect Tap to Pay.
  • \n
  • UX colors and dark mode are Android-only; iOS uses the system Tap to Pay on iPhone UI.
  • \n
  • Account-link status is iOS-only and must be re-fetched from Apple each time.
  • \n
  • tapZone is part of the TypeScript API but is not wired through on the installed Android SDK.
  • \n
  • Optional reader software updates still follow Reader Lifecycle rules: do not install during checkout.
  • \n
  • Keep Stripe secret keys and connection-token creation on the backend.
  • \n
\n", "headings": [ { "id": "platform-prerequisites", diff --git a/src/app/generated/projects/stripe.en.generated.ts b/src/app/generated/projects/stripe.en.generated.ts index 3207e0e..a1b406f 100644 --- a/src/app/generated/projects/stripe.en.generated.ts +++ b/src/app/generated/projects/stripe.en.generated.ts @@ -105,7 +105,7 @@ export const PROJECT = { "file": "react.md", "section": "Quickstart", "path": "/projects/capacitor-stripe/docs/react", - "html": "

Wrap the application with CapacitorStripeProvider. The provider calls Stripe.initialize, checks Apple Pay and Google Pay availability, and registers stripe-pwa-elements on web.

\n
App.tsx
import { CapacitorStripeProvider } from '@capacitor-community/stripe/react';\n\nconst App: React.FC = () => (\n  <CapacitorStripeProvider\n    publishableKey=\"Your Publishable Key\"\n    fallback={<p>Loading...</p>}\n  >\n    <IonApp>{/* ... */}</IonApp>\n  </CapacitorStripeProvider>\n);\n\nexport default App;\n

CapacitorStripeProvider also accepts optional stripeAccount for Stripe Connect.

\n

Use the Stripe client

\n

Read the initialized client with useCapacitorStripe. The returned stripe object is the same plugin instance as Stripe from @capacitor-community/stripe.

\n
import { useCapacitorStripe } from '@capacitor-community/stripe/react';\n\nexport const PaymentSheet: React.FC = () => {\n  const { stripe, isApplePayAvailable, isGooglePayAvailable } = useCapacitorStripe();\n  // ...\n};\n
export const PaymentSheet: React.FC = () => {\n  const { stripe } = useCapacitorStripe();\n  return (\n    <button\n      onClick={async () => {\n        await stripe.createPaymentSheet({\n          paymentIntentClientSecret,\n          merchantDisplayName: 'App Name',\n        });\n        await stripe.presentPaymentSheet();\n      }}\n    >\n      Pay\n    </button>\n  );\n};\n

Register result listeners once during application startup, not inside a payment button handler. See Event Listeners.

\n

The official React demo is at capacitor-community/stripe/demo/react.

\n", + "html": "

Wrap the application with CapacitorStripeProvider. The provider calls Stripe.initialize, checks Apple Pay and Google Pay availability, and registers stripe-pwa-elements on web.

\n
App.tsx
import { CapacitorStripeProvider } from '@capacitor-community/stripe/react';\n\nconst App: React.FC = () => (\n  <CapacitorStripeProvider\n    publishableKey=\"Your Publishable Key\"\n    fallback={<p>Loading...</p>}\n  >\n    <IonApp>{/* ... */}</IonApp>\n  </CapacitorStripeProvider>\n);\n\nexport default App;\n

CapacitorStripeProvider also accepts optional stripeAccount for Stripe Connect.

\n

Use the Stripe client

\n

Read the initialized client with useCapacitorStripe. The returned stripe object is the same plugin instance as Stripe from @capacitor-community/stripe.

\n
import { useCapacitorStripe } from '@capacitor-community/stripe/react';\n\nexport const PaymentSheet: React.FC = () => {\n  const { stripe, isApplePayAvailable, isGooglePayAvailable } = useCapacitorStripe();\n  // ...\n};\n
export const PaymentSheet: React.FC = () => {\n  const { stripe } = useCapacitorStripe();\n  return (\n    <button\n      onClick={async () => {\n        await stripe.createPaymentSheet({\n          paymentIntentClientSecret,\n          merchantDisplayName: 'App Name',\n        });\n        await stripe.presentPaymentSheet();\n      }}\n    >\n      Pay\n    </button>\n  );\n};\n

Register result listeners once during application startup, not inside a payment button handler. See Event Listeners.

\n

The official React demo is at capacitor-community/stripe/demo/react.

\n", "headings": [ { "id": "use-the-stripe-client", @@ -308,7 +308,7 @@ export const PROJECT = { "file": "server-integration.md", "section": "Learn", "path": "/projects/capacitor-stripe/docs/server-integration", - "html": "

@capacitor-community/stripe only accepts client-safe values. Your backend creates PaymentIntents, SetupIntents, Customers, and ephemeral keys with the Stripe secret key. The plugin never calls the secret API.

\n

Secret-key confinement

\n

Keep sk_live_... and sk_test_... on the server. Ship only the publishable key to the app, through Stripe.initialize or Android Google Pay metadata. Do not embed the secret key in Capacitor config, source control, or client logs.

\n

Client secrets

\n

Create a PaymentIntent to charge now, or a SetupIntent to save a method for later. Return the Intent client secret to the app, not the secret key and not a raw charge.

\n

Customer ephemeral keys are optional. Use them with a Customer id when PaymentSheet or PaymentFlow should show saved methods. If you pass customerId to the plugin, you must also pass customerEphemeralKeySecret. A PaymentIntent without a Customer is valid.

\n

Map server fields to plugin options:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
Server fieldPlugin option
paymentIntentpaymentIntentClientSecret
setupIntentsetupIntentClientSecret
ephemeralKeycustomerEphemeralKeySecret
customercustomerId
\n

Response shapes

\n

PaymentIntent with a Customer:

\n
{\n  \"paymentIntent\": \"pi_..._secret_...\",\n  \"ephemeralKey\": \"ek_...\",\n  \"customer\": \"cus_...\"\n}\n

SetupIntent with a Customer:

\n
{\n  \"setupIntent\": \"seti_..._secret_...\",\n  \"ephemeralKey\": \"ek_...\",\n  \"customer\": \"cus_...\"\n}\n

PaymentIntent without a Customer:

\n
{\n  \"paymentIntent\": \"pi_..._secret_...\"\n}\n

Apple Pay uses a PaymentIntent client secret. Google Pay uses a PaymentIntent client secret on web; Android also accepts a SetupIntent client secret through the historically named paymentIntentClientSecret option. Native PaymentSheet and PaymentFlow accept either Intent secret, with or without Customer fields. The current web PaymentSheet accepts PaymentIntents only; web PaymentFlow accepts either Intent type.

\n

Webhook authority

\n

Completed on the device is a UI signal. It is not proof that Stripe captured funds. Fulfill orders from verified Stripe webhooks such as payment_intent.succeeded or setup_intent.succeeded.

\n

Treat Canceled as the customer dismissing the sheet. Treat Failed and FailedToLoad as errors. Retry only after you create a new Intent when the previous one can no longer be confirmed.

\n

The official demo server that returns the shapes above is capacitor-community/stripe/demo/server.

\n", + "html": "

@capacitor-community/stripe only accepts client-safe values. Your backend creates PaymentIntents, SetupIntents, Customers, and ephemeral keys with the Stripe secret key. The plugin never calls the secret API.

\n

Secret-key confinement

\n

Keep sk_live_... and sk_test_... on the server. Ship only the publishable key to the app, through Stripe.initialize or Android Google Pay metadata. Do not embed the secret key in Capacitor config, source control, or client logs.

\n

Client secrets

\n

Create a PaymentIntent to charge now, or a SetupIntent to save a method for later. Return the Intent client secret to the app, not the secret key and not a raw charge.

\n

Customer ephemeral keys are optional. Use them with a Customer id when PaymentSheet or PaymentFlow should show saved methods. If you pass customerId to the plugin, you must also pass customerEphemeralKeySecret. A PaymentIntent without a Customer is valid.

\n

Map server fields to plugin options:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
Server fieldPlugin option
paymentIntentpaymentIntentClientSecret
setupIntentsetupIntentClientSecret
ephemeralKeycustomerEphemeralKeySecret
customercustomerId
\n

Response shapes

\n

PaymentIntent with a Customer:

\n
{\n  \"paymentIntent\": \"pi_..._secret_...\",\n  \"ephemeralKey\": \"ek_...\",\n  \"customer\": \"cus_...\"\n}\n

SetupIntent with a Customer:

\n
{\n  \"setupIntent\": \"seti_..._secret_...\",\n  \"ephemeralKey\": \"ek_...\",\n  \"customer\": \"cus_...\"\n}\n

PaymentIntent without a Customer:

\n
{\n  \"paymentIntent\": \"pi_..._secret_...\"\n}\n

Apple Pay uses a PaymentIntent client secret. Google Pay uses a PaymentIntent client secret on web; Android also accepts a SetupIntent client secret through the historically named paymentIntentClientSecret option. Native PaymentSheet and PaymentFlow accept either Intent secret, with or without Customer fields. The current web PaymentSheet accepts PaymentIntents only; web PaymentFlow accepts either Intent type.

\n

Webhook authority

\n

Completed on the device is a UI signal. It is not proof that Stripe captured funds. Fulfill orders from verified Stripe webhooks such as payment_intent.succeeded or setup_intent.succeeded.

\n

Treat Canceled as the customer dismissing the sheet. Treat Failed and FailedToLoad as errors. Retry only after you create a new Intent when the previous one can no longer be confirmed.

\n

The official demo server that returns the shapes above is capacitor-community/stripe/demo/server.

\n", "headings": [ { "id": "secret-key-confinement", diff --git a/src/eslint-plugin-rules/docs/api.md b/src/eslint-plugin-rules/docs/api.md index dd0880a..38fca9e 100644 --- a/src/eslint-plugin-rules/docs/api.md +++ b/src/eslint-plugin-rules/docs/api.md @@ -2,7 +2,7 @@ title: API --- -Public plugin surface for `@rdlabo/eslint-plugin-rules` v21.3.0. Detailed options and examples are available on each rule page. +Public plugin surface for `@rdlabo/eslint-plugin-rules` v22.0.0. Detailed options and examples are available on each rule page. ## Module @@ -20,6 +20,6 @@ Public plugin surface for `@rdlabo/eslint-plugin-rules` v21.3.0. Detailed option | Group | Rules | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Component boundaries | `component-property-use-readonly`, `deny-constructor-di`, `deny-soft-private-modifier`, `no-component-method-except-lifecycle`, `no-component-writable-signal`, `require-viewmodel` | -| Ionic APIs | `deny-element`, `deny-import-from-ionic-module`, `deny-overlay-create`, `implements-ionic-lifecycle`, `ionic-attr-type-check`, `prefer-disable-handler`, `prefer-modal-launcher` | +| Ionic APIs | `deny-element`, `deny-overlay-create`, `implements-ionic-lifecycle`, `ionic-attr-type-check`, `prefer-disable-handler`, `prefer-ionic-standalone`, `prefer-modal-launcher`, `require-ion-item-group` | | Forms and signals | `no-reactive-forms`, `no-template-driven-forms`, `signal-use-as-signal`, `signal-use-as-signal-template` | | Control flow | `restrict-try-block` | diff --git a/src/eslint-plugin-rules/docs/ja/api.md b/src/eslint-plugin-rules/docs/ja/api.md index 1c51892..44ede3d 100644 --- a/src/eslint-plugin-rules/docs/ja/api.md +++ b/src/eslint-plugin-rules/docs/ja/api.md @@ -2,7 +2,7 @@ title: API --- -`@rdlabo/eslint-plugin-rules` v21.3.0 のpublic plugin surfaceです。詳細なoptionと例は各Ruleページにあります。 +`@rdlabo/eslint-plugin-rules` v22.0.0 のpublic plugin surfaceです。詳細なoptionと例は各Ruleページにあります。 ## Module @@ -20,6 +20,6 @@ title: API | Group | Rules | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Component境界 | `component-property-use-readonly`, `deny-constructor-di`, `deny-soft-private-modifier`, `no-component-method-except-lifecycle`, `no-component-writable-signal`, `require-viewmodel` | -| Ionic API | `deny-element`, `deny-import-from-ionic-module`, `deny-overlay-create`, `implements-ionic-lifecycle`, `ionic-attr-type-check`, `prefer-disable-handler`, `prefer-modal-launcher` | +| Ionic API | `deny-element`, `deny-overlay-create`, `implements-ionic-lifecycle`, `ionic-attr-type-check`, `prefer-disable-handler`, `prefer-ionic-standalone`, `prefer-modal-launcher`, `require-ion-item-group` | | Form・Signal | `no-reactive-forms`, `no-template-driven-forms`, `signal-use-as-signal`, `signal-use-as-signal-template` | | Control flow | `restrict-try-block` | diff --git a/src/eslint-plugin-rules/docs/ja/configuration.md b/src/eslint-plugin-rules/docs/ja/configuration.md index ceb3f0d..d02a40b 100644 --- a/src/eslint-plugin-rules/docs/ja/configuration.md +++ b/src/eslint-plugin-rules/docs/ja/configuration.md @@ -22,12 +22,7 @@ module.exports = tseslint.config( languageOptions: { parserOptions: { projectService: true, tsconfigRootDir: __dirname }, }, - extends: [ - eslint.configs.recommended, - ...tseslint.configs.recommended, - ...tseslint.configs.stylistic, - ...angular.configs.tsRecommended, - ], + extends: [eslint.configs.recommended, ...tseslint.configs.recommended, ...tseslint.configs.stylistic, ...angular.configs.tsRecommended], processor: angular.processInlineTemplates, }, { @@ -68,6 +63,6 @@ export default tseslint.config({ ## 推奨プリセット -TypeScriptではSignal、Component境界、ライフサイクル、Overlay、readonly、tryブロックの共通ルールを有効にします。HTMLではIonic属性型、禁止Overlay要素、非同期操作の二重実行防止を有効にします。 +TypeScriptではSignal、Component境界、ライフサイクル、Overlay、Ionic 9 standalone import、readonly、tryブロックの共通ルールを有効にします。HTMLではIonic属性型、禁止Overlay要素、非同期操作の二重実行防止、iOS 26・MD3向けのgroup化されたlist構造を有効にします。 非推奨の `deny-constructor-di` はプリセットに含まれません。Angularの `inject()` migrationを利用してください。 diff --git a/src/eslint-plugin-rules/docs/ja/getting-started.md b/src/eslint-plugin-rules/docs/ja/getting-started.md index 6e3dd36..8ba2666 100644 --- a/src/eslint-plugin-rules/docs/ja/getting-started.md +++ b/src/eslint-plugin-rules/docs/ja/getting-started.md @@ -8,7 +8,7 @@ title: はじめに npm install --save-dev @rdlabo/eslint-plugin-rules ``` -パッケージルートはAngular・Ionic向けルールを公開します。これらを使う場合は `@angular-eslint/template-parser` と `@ionic/core` もインストールしてください。Angular・Ionicに依存しないTypeScriptプロジェクトでは、両者を読み込まない `/typescript` エントリポイントを利用できます。 +パッケージルートはAngular・Ionic向けルールを公開します。これらを使う場合は `@angular-eslint/template-parser`、`@ionic/angular`、`@ionic/core` もインストールしてください。Angular・Ionicに依存しないTypeScriptプロジェクトでは、これらを読み込まない `/typescript` エントリポイントを利用できます。 ## 要件 @@ -17,8 +17,9 @@ npm install --save-dev @rdlabo/eslint-plugin-rules | Node.js | 20以降 | | ESLint | 9以降 | | `@typescript-eslint/utils` | 8.33以上9未満 | -| `@angular-eslint/template-parser` | テンプレートルール利用時は21.x | -| `@ionic/core` | Ionicルール利用時は8.x | +| `@angular-eslint/template-parser` | 21.xまたは22.x | +| `@ionic/angular` | Ionicルール利用時は9.x | +| `@ionic/core` | Ionicルール利用時は9.x | ## エントリポイントを選ぶ @@ -27,4 +28,6 @@ npm install --save-dev @rdlabo/eslint-plugin-rules 推奨プリセットはESLint Flat Config向けです。TypeScriptとHTMLの対象指定を維持するため、設定のトップレベルへ追加します。 +Ionic templateでは、推奨プリセットが `ion-list` 内の `ion-item` に対し、`ion-item-group`、`ion-reorder-group`、`ion-radio-group`、または `ion-accordion-group` 内の `ion-accordion` を使うことも要求します。これはiOS 26とMaterial Design 3のlist構造に対応するためです。 + 次は[設定](/eslint-plugin-rules/docs/configuration)で推奨プリセットまたは個別ルールを有効にします。 diff --git a/src/eslint-plugin-rules/docs/ja/migration.md b/src/eslint-plugin-rules/docs/ja/migration.md new file mode 100644 index 0000000..7a7938a --- /dev/null +++ b/src/eslint-plugin-rules/docs/ja/migration.md @@ -0,0 +1,66 @@ +--- +title: 移行 +--- + +## 21.xから22.xへ + +version 22はAngular 21・22とIonic Framework 9を対象にします。Ionic 8のアプリケーションでは、このpluginのversion 21を使い続けてください。 + +### 依存関係 + +まずアプリケーションの変更をcommitし、アプリケーションrootでIonic公式の [`@ionic/migrate`](https://www.npmjs.com/package/@ionic/migrate) を実行します。 + +```sh +npx @ionic/migrate --dry-run +npx @ionic/migrate +``` + +migratorはインストール済みのIonic major versionを検出し、`@ionic/angular` と `@ionic/core` を同時に更新します。安全なv8からv9への変更を適用し、手動判断が必要な項目をchecklistとして表示します。続行前に差分を確認してtestしてください。このpluginのversion 22はAngularおよびAngular ESLint 21から22に対応します。 + +### Ionic Angular import + +削除された `deny-import-from-ionic-module` ruleを `prefer-ionic-standalone` に置き換えます。 + +```diff +- '@rdlabo/rules/deny-import-from-ionic-module': 'error' ++ '@rdlabo/rules/prefer-ionic-standalone': 'error' +``` + +Angularアプリケーションでは、公式migratorが既存のNgModule importを `@ionic/angular` から `@ionic/angular/lazy` へ、standalone importを `@ionic/angular/standalone` からpackage rootへ移します。これによりframework更新中も現在のarchitectureが維持されます。 + +たとえば、migratorは次の安全なstandalone importの書き換えを自動で行います。 + +```diff +- import { IonButton } from '@ionic/angular/standalone'; ++ import { IonButton } from '@ionic/angular'; +``` + +このpluginはIonic 9のstandaloneアプリケーションだけをサポートします。NgModuleアプリケーションの変換にはarchitecture上の判断が必要なため、公式migratorは `IonicModule` を自動修正せず報告します。実行後にAngular standalone migrationを完了し、Ionic componentをpackage rootからimportしてください。`@ionic/angular/lazy` pathを機械的に置換してはいけません。まず各NgModule consumerをstandaloneへ変換し、その後 `IonicModule` を実際に使う個別のIonic componentへ置き換えます。 + +新しいruleはNgModule向けの `@ionic/angular/lazy` entry pointと `IonicModule` を禁止します。`provideIonicAngular()` を使ったstandalone bootstrapへ移行し、standalone Ionic componentを直接importします。 + +```diff +- platformBrowserDynamic().bootstrapModule(AppModule); ++ bootstrapApplication(AppComponent, { ++ providers: [provideIonicAngular(config)], ++ }); +``` + +`provideIonicAngular` は `@ionic/angular` からimportします。`IonicModule` を削除する前にAngular NgModuleからstandaloneへの移行を完了してください。NgModule内で安全に1行置換することはできません。 + +### recommended presetのlist構造 + +version 22ではrecommended presetに `require-ion-item-group` も追加されます。そのため、`ion-list` 内の `ion-item` が `ion-item-group`、`ion-reorder-group`、`ion-radio-group`、または `ion-accordion-group` 内の `ion-accordion` で囲まれていない既存のIonic templateでは、新しいerrorが報告される場合があります。 + +意図したgroup境界を判断できる場合だけ安全な自動修正を適用します。再利用可能または曖昧なtemplateは変更せずに報告します。wrapper componentはそれ自身のtemplateを通して検査されるため、正しいgroup化済みlistをrenderするcustom elementが、caller側でgroup化されていない `ion-item` として扱われることはありません。対応構造と修正条件は [`require-ion-item-group`](/eslint-plugin-rules/docs/rules/require-ion-item-group) を参照してください。 + +### booleanのautocorrect + +Ionic 9では `ion-input` と `ion-searchbar` の `autocorrect` が `'on' | 'off'` からbooleanへ変わります。`ionic-attr-type-check` ruleは古いstring形式を修正します。 + +```diff +- ++ +``` + +Ionic公式migratorもこのv8からv9への変更を自動処理します。このruleは移行後に残った値や新たに追加された古いstring値の検出にも利用でき、Ionic 9のcomponent type定義から、ほかのproperty typeや許容値の変更も追従します。ESLintを `--fix` 付きで実行し、templateの変更を確認してからAngular buildとtestを実行してください。 diff --git a/src/eslint-plugin-rules/docs/ja/rules.md b/src/eslint-plugin-rules/docs/ja/rules.md index 4ea7f2a..19fba59 100644 --- a/src/eslint-plugin-rules/docs/ja/rules.md +++ b/src/eslint-plugin-rules/docs/ja/rules.md @@ -2,14 +2,13 @@ title: ルール一覧 --- -パッケージは18個のルールを公開します。「推奨」がYesのルールは `rdlabo.configs.recommended` で有効になり、それ以外は個別に設定します。 +パッケージは19個のルールを公開します。「推奨」がYesのルールは `rdlabo.configs.recommended` で有効になり、それ以外は個別に設定します。 | ルール | 目的 | Fix | 推奨 | | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | :-: | :--: | | [`component-property-use-readonly`](/eslint-plugin-rules/docs/rules/component-property-use-readonly) | Angular Componentの不変プロパティに `readonly` を要求する。 | Yes | Yes | | [`deny-constructor-di`](/eslint-plugin-rules/docs/rules/deny-constructor-di) | constructor DIを禁止する。`inject()`を推奨する非推奨ルール。 | No | No | | [`deny-element`](/eslint-plugin-rules/docs/rules/deny-element) | インラインIonic Overlayなど、設定したHTML要素を禁止する。 | No | Yes | -| [`deny-import-from-ionic-module`](/eslint-plugin-rules/docs/rules/deny-import-from-ionic-module) | `@ionic/angular`からの誤importをstandalone importへ置換する。 | Yes | Yes | | [`deny-overlay-create`](/eslint-plugin-rules/docs/rules/deny-overlay-create) | Modal・Popover Controllerの直接 `.create()` を禁止する。 | No | Yes | | [`deny-soft-private-modifier`](/eslint-plugin-rules/docs/rules/deny-soft-private-modifier) | TypeScriptの `private` をハードプライベート `#` へ置換する。 | Yes | Yes | | [`implements-ionic-lifecycle`](/eslint-plugin-rules/docs/rules/implements-ionic-lifecycle) | Angular・Ionic lifecycle methodに対応するinterfaceを要求する。 | Yes | Yes | @@ -19,7 +18,9 @@ title: ルール一覧 | [`no-reactive-forms`](/eslint-plugin-rules/docs/rules/no-reactive-forms) | Reactive Formsを禁止し、Angular Signal Formsへ移行する。 | No | No | | [`no-template-driven-forms`](/eslint-plugin-rules/docs/rules/no-template-driven-forms) | 許可した相互運用要素を除きtemplate-driven formsを禁止する。 | No | No | | [`prefer-disable-handler`](/eslint-plugin-rules/docs/rules/prefer-disable-handler) | 非同期操作の二重実行を防ぐwrapperを要求する。 | No | Yes | +| [`prefer-ionic-standalone`](/eslint-plugin-rules/docs/rules/prefer-ionic-standalone) | Ionic 9 standalone importを優先し、`IonicModule` を禁止する。 | Yes | Yes | | [`prefer-modal-launcher`](/eslint-plugin-rules/docs/rules/prefer-modal-launcher) | `presentModal` 呼び出しを `launch*` 関数に限定する。 | No | Yes | +| [`require-ion-item-group`](/eslint-plugin-rules/docs/rules/require-ion-item-group) | iOS 26・MD3向けにIonic list itemのgroup化を要求する。 | Yes | Yes | | [`require-viewmodel`](/eslint-plugin-rules/docs/rules/require-viewmodel) | Component所有と `ViewModelStore` 境界を検査する。 | No | Yes | | [`restrict-try-block`](/eslint-plugin-rules/docs/rules/restrict-try-block) | `try` を短く保ち、Promise・RxJS・Signal contextをポリシーで制限する。 | No | Yes | | [`signal-use-as-signal-template`](/eslint-plugin-rules/docs/rules/signal-use-as-signal-template) | テンプレートでSignalを読むときに `()` を要求する。 | No | Yes | @@ -27,7 +28,7 @@ title: ルール一覧 ## ルール別ドキュメント -各ルールページに詳細・オプション・正誤例があります。上の表はv21.3.0を反映しています。 +各ルールページに詳細・オプション・正誤例があります。 ## 型情報を使うルール diff --git a/src/eslint-plugin-rules/docs/ja/rules/component-property-use-readonly.md b/src/eslint-plugin-rules/docs/ja/rules/component-property-use-readonly.md index c9b971e..64d2534 100644 --- a/src/eslint-plugin-rules/docs/ja/rules/component-property-use-readonly.md +++ b/src/eslint-plugin-rules/docs/ja/rules/component-property-use-readonly.md @@ -9,11 +9,37 @@ title: component-property-use-readonly > - ⭐️ このルールは `plugin:@rdlabo/rules/recommended` プリセットに含まれます。 > - ✒️ [コマンドライン](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems)の `--fix` オプションで、このルールが報告する問題の一部を自動修正できます。 -このルールは、不変であるべきAngularコンポーネントのプロパティに `readonly` 修飾子の使用を強制します。予期しない変更を防ぎ、コンポーネントの状態管理をより安全にします。 +このルールは、Angular Componentで宣言された関数以外のプロパティに `readonly` 修飾子を要求します。初期化済み・未初期化・static・computed・decorator付き・soft private・hard privateの各プロパティを報告し、`readonly` を自動的に追加できます。 ## ルール詳細 -❌ 誤り: `readonly` 修飾子のないプロパティ +`@Component()` で装飾されたクラスだけを検査します。method、getter、setter、arrow functionプロパティ、function expressionプロパティ、すでに `readonly` のプロパティ、および他のクラスのプロパティは無視します。 + +## オプション + +```json +{ + "rules": { + "@rdlabo/rules/component-property-use-readonly": [ + "error", + { + "ignorePrivateProperties": true + } + ] + } +} +``` + +### `ignorePrivateProperties` + +- 型: `boolean` +- デフォルト: `false` + +`true` の場合、TypeScriptの `private` 修飾子を指定したプロパティとECMAScriptの `#` privateプロパティを無視します。public、protected、staticプロパティは引き続き検査します。 + +## 例 + +### 誤り ```ts @Component({ @@ -33,7 +59,7 @@ export class ExampleComponent { } ``` -✅ 正しい: `readonly` 修飾子付きのプロパティ +### 正しい ```ts @Component({ @@ -53,62 +79,9 @@ export class ExampleComponent { } ``` -## ルール設定 - -```json -{ - "rules": { - "@rdlabo/rules/component-property-use-readonly": [ - "error", - { - "ignorePrivateProperties": true - } - ] - } -} -``` - -## オプション - -```ts -const options: { - ignorePrivateProperties?: boolean; // Whether to ignore private properties (default: false) -}; -``` - -### ignorePrivateProperties - -`true` にすると、ソフトプライベートプロパティ(`private` 修飾子)とハードプライベートプロパティ(`#` 接頭辞)の両方を無視します。プライベートプロパティは通常コンポーネント外からアクセスされないため、`readonly` 修飾子の重要度が下がる場合に有用です。 - -❌ 誤り: `ignorePrivateProperties: true` なしのプライベートプロパティ - -```ts -@Component({ - selector: 'app-example', - template: '
example
', -}) -export class ExampleComponent { - private privateProp = 1; // error - #secretProp = 2; // error -} -``` - -✅ 正しい: `ignorePrivateProperties: true` ありのプライベートプロパティ +`ignorePrivateProperties: true` の場合、privateプロパティは書き込み可能なままでも構いません。 ```ts -// .eslintrc.json -{ - "rules": { - "@rdlabo/rules/component-property-use-readonly": [ - "error", - { - "ignorePrivateProperties": true - } - ] - } -} - -// Component code @Component({ selector: 'app-example', template: '
example
', @@ -116,11 +89,15 @@ export class ExampleComponent { export class ExampleComponent { private privateProp = 1; // no error #secretProp = 2; // no error - public publicProp = 3; // still requires readonly + public readonly publicProp = 3; } ``` +## 有効にする場面 + +Componentプロパティに安定した参照を公開させ、書き込み可能な状態をSignalsまたはViewModelで管理する場合に、このルールを有効にします。 + ## 実装 -- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/src/rules/component-property-use-readonly.ts) -- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/tests/rules/component-property-use-readonly.ts) +- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v22.0.0/src/rules/component-property-use-readonly.ts) +- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v22.0.0/tests/rules/component-property-use-readonly.ts) diff --git a/src/eslint-plugin-rules/docs/ja/rules/deny-constructor-di.md b/src/eslint-plugin-rules/docs/ja/rules/deny-constructor-di.md index c420623..b488da7 100644 --- a/src/eslint-plugin-rules/docs/ja/rules/deny-constructor-di.md +++ b/src/eslint-plugin-rules/docs/ja/rules/deny-constructor-di.md @@ -6,43 +6,65 @@ title: deny-constructor-di > このプラグインはconstructor内のDependency Injectionを禁止します。 -このルールは `inject` 関数を使う、現代的なAngularの依存性注入の作法を促します。 +このルールは、`constructor(private readonly auth: AuthService)` のようにDependency Injectionに使われるconstructor parameter propertyを報告します。Angularの `inject()` 関数は、standalone Componentやserviceで依存関係を取得する現代的な方法です。constructorの定型コードをなくし、DIを明示的にします。 ## ルール詳細 -❌ 誤り: constructorベースの依存性注入を使う +クラスのconstructorを検査し、`TSParameterProperty`(`public`、`private`、`readonly` などの修飾子を持つparameter)を報告します。これらはクラスフィールドになり、DIに使われるparameterです。 + +- 修飾子のない通常のconstructor parameterは許可されます。 +- このルールは自動修正しません。constructor DIを手動で `inject()` に置き換える必要があります。 + +## 例 + +### 誤り ```ts @Component({ - selector: 'app-confirm', - templateUrl: './confirm.page.html', - styleUrls: ['./confirm.page.scss'], + selector: 'app-signin', + templateUrl: './signin.page.html', }) export class SigninPage { - constructor(public platform: Platform) {} + constructor( + private store: Store, + public readonly navCtrl: NavController, + ) {} } ``` -✅ 正しい: 依存性注入に `inject` 関数を使う +### 正しい ```ts +import { inject } from '@angular/core'; + @Component({ - selector: 'app-confirm', - templateUrl: './confirm.page.html', - styleUrls: ['./confirm.page.scss'], + selector: 'app-signin', + templateUrl: './signin.page.html', }) export class SigninPage { - public platform = inject(Platform); + private readonly store = inject(Store); + private readonly navCtrl = inject(NavController); +} +``` - constructor() {} +```ts +// Non-DI constructor parameters are allowed +export class LogManager { + constructor(logDomain: string) { + this.logDomain = logDomain; + } } ``` ## オプション -オプションなし。 +このルールにオプションはありません。 + +## 有効にする場面 + +constructor parameter propertyではなく `inject()` でAngularの依存関係を取得することをプロジェクトで要求する場合に、このopt-inルールを有効にします。このルールは `TSParameterProperty` nodeだけを報告するため、通常のconstructor parameterは引き続き許可されます。 ## 実装 -- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/src/rules/deny-constructor-di.ts) -- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/tests/rules/deny-costructor-di.ts) +- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v22.0.0/src/rules/deny-constructor-di.ts) +- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v22.0.0/tests/rules/deny-constructor-di.ts) diff --git a/src/eslint-plugin-rules/docs/ja/rules/deny-element.md b/src/eslint-plugin-rules/docs/ja/rules/deny-element.md index 8fdf58c..ce320f7 100644 --- a/src/eslint-plugin-rules/docs/ja/rules/deny-element.md +++ b/src/eslint-plugin-rules/docs/ja/rules/deny-element.md @@ -8,20 +8,16 @@ title: deny-element > > - ⭐️ このルールは `plugin:@rdlabo/rules/recommended` プリセットに含まれます。 -このルールは、インラインコンポーネントではなくControllerベースのmodalやaction sheetの利用を強制したいIonicアプリで特に有用です。 +このルールは、Angular templateで特定のelementが使われることを防ぎます。一般的には、templateで宣言する代わりにlauncher methodや専用serviceを通じて表示すべき ``、``、``、``、``、``、`` などのinline overlay componentを禁止するために使います。 ## ルール詳細 -❌ 誤り: テンプレートで禁止された要素を使う +このルールは `.html` template fileで実行され、tag nameが設定済みの `elements` listに含まれるelementを報告します。template ASTを走査し、`@if`、`@for`、`@else` と、ネストした `then` / `else` branchなどのAngular control flow構文にも対応します。 -```html - - -``` - -✅ 正しい: `.eslintrc.json` で禁止する要素を指定するようルールを設定する +- testに影響しないよう `.spec.html` fileは無視します。 +- 明示的なオプションがなければ、デフォルトのIonic overlay element listを使います。option objectを指定する場合、そのschemaでは `elements` arrayが必須です。 -## ルール設定 +## オプション ```json { @@ -29,22 +25,63 @@ title: deny-element "@rdlabo/rules/deny-element": [ "error", { - "elements": ["ion-modal"] + "elements": ["ion-modal", "ion-popover", "ion-toast", "ion-alert", "ion-loading", "ion-picker", "ion-action-sheet"] } ] } } ``` -## オプション +### `elements` + +- 型: `string[]` +- デフォルト: `ion-modal`, `ion-popover`, `ion-toast`, `ion-alert`, `ion-loading`, `ion-picker`, `ion-action-sheet` + +禁止するelement tag nameの配列です。このルールはこれらの名前をAngular template ASTの `Element` node typeと比較するため、element自体とcontrol flow branch内の存在の両方を検査します。 -```ts -const options: { - elements: string[]; // Array of element names to disallow -}; +## 例 + +### 誤り + +```html + + +
+ + +
``` +```html +@if (showModal) { +Modal content +} +``` + +### 正しい + +```html +Open +``` + +```html +@for (item of items; track item.id) { + + {{ item.name }} + +} +``` + +## 有効にする場面 + +overlayにlauncher patternを使うプロジェクトで、このルールを有効にします。[`@rdlabo/rules/prefer-modal-launcher`](./prefer-modal-launcher.md)および[`@rdlabo/rules/prefer-disable-handler`](./prefer-disable-handler.md)と組み合わせることで、modalとoverlayのlogicをtemplateから分離できます。 + +## 関連項目 + +- [`@rdlabo/rules/prefer-modal-launcher`](./prefer-modal-launcher.md) +- [`@rdlabo/rules/prefer-disable-handler`](./prefer-disable-handler.md) + ## 実装 -- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/src/rules/deny-element.ts) -- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/tests/rules/deny-element.ts) +- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v22.0.0/src/rules/deny-element.ts) +- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v22.0.0/tests/rules/deny-element.ts) diff --git a/src/eslint-plugin-rules/docs/ja/rules/deny-import-from-ionic-module.md b/src/eslint-plugin-rules/docs/ja/rules/deny-import-from-ionic-module.md deleted file mode 100644 index f4667cb..0000000 --- a/src/eslint-plugin-rules/docs/ja/rules/deny-import-from-ionic-module.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: deny-import-from-ionic-module ---- - -# @rdlabo/rules/deny-import-from-ionic-module - -> このプラグインは、@ionic/angular/standalone ではなく @ionic/angular からの誤ったimportを防ぎます。 -> -> - ⭐️ このルールは `plugin:@rdlabo/rules/recommended` プリセットに含まれます。 -> - ✒️ [コマンドライン](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems)の `--fix` オプションで、このルールが報告する問題の一部を自動修正できます。 - -`@ionic/angular` と `@ionic/angular/standalone` のimportを混在させるとランタイム問題が起き得ます。ビルドは成功しても、ブラウザ実行時に失敗することがあります。このルールは一貫したimportパスを強制することで、そうした問題を防ぎます。 - -## ルール詳細 - -❌ 誤り: `@ionic/angular` からimportする - -```ts -import { ModalController } from '@ionic/angular'; -``` - -✅ 正しい: `@ionic/angular/standalone` からimportする - -```ts -import { ModalController } from '@ionic/angular/standalone'; -``` - -## オプション - -オプションなし。 - -## 実装 - -- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/src/rules/deny-import-from-ionic-module.ts) -- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/tests/rules/deny-import-from-ionic-module.ts) diff --git a/src/eslint-plugin-rules/docs/ja/rules/deny-overlay-create.md b/src/eslint-plugin-rules/docs/ja/rules/deny-overlay-create.md index 44316a7..b7420a0 100644 --- a/src/eslint-plugin-rules/docs/ja/rules/deny-overlay-create.md +++ b/src/eslint-plugin-rules/docs/ja/rules/deny-overlay-create.md @@ -4,96 +4,114 @@ title: deny-overlay-create # @rdlabo/rules/deny-overlay-create -> ModalController / PopoverController の `.create()` を禁止し、launcher経由でoverlayを開く。 +> ModalController / PopoverControllerの `.create()` を禁止し、launcher経由でoverlayを開く。 > > - ⭐️ このルールは `plugin:@rdlabo/rules/recommended` プリセットに含まれます。 -Ionicアプリでは、modalとpopoverは共有ヘルパー(`presentModal`)とエクスポートした `launch*` 関数経由で開くべきであり、`ModalController.create()` / `PopoverController.create()` を直接呼んではいけません。 - -次のルールと併用します。 - -- `@rdlabo/rules/deny-element` — テンプレート内のインライン `` / `` を禁止する -- `@rdlabo/rules/prefer-modal-launcher` — `presentModal` を `launch*` 内に置くことを要求する - -`LoadingController`、`AlertController`、`ToastController`、`ActionSheetController` はデフォルトで許可されたままです。`ModalController` の `dismiss()` も許可されます。 +このルールは、controllerの `.create()` 呼び出しによるIonic overlayの直接生成を防ぎます。rdlabo architectureでは、overlayはlauncher functionと共有の `presentModal` / `presentPopover` helperを通じて開きます。これによりoverlay logicを一元化し、呼び出し側をcontroller APIから分離できます。 ## ルール詳細 -❌ 誤り: controller経由でmodal / popoverをcreateする +receiverが `ModalController`、`PopoverController`(または設定した他のcontroller)である `.create()` 呼び出しを検出します。次のような複数のpatternからcontrollerを解決します。 -```ts -import { inject } from '@angular/core'; -import { ModalController } from '@ionic/angular/standalone'; +- `this.modalCtrl.create()` +- `modalCtrl.create()`(`modalCtrl` が `inject(ModalController)` の場合) +- `inject(ModalController).create()` +- constructor parameter `constructor(private modalCtrl: ModalController)` +- `ModalController` 型のclass property -export class ExamplePage { - readonly #modalCtrl = inject(ModalController); +`LoadingController`、`AlertController`、`ToastController`、`ActionSheetController` など、その他のoverlay controllerは直接使うことが意図されている場合があるため、デフォルトでは禁止しません。 - async open() { - await this.#modalCtrl.create({ component: OtherPage }); // error +## オプション + +```json +{ + "rules": { + "@rdlabo/rules/deny-overlay-create": [ + "error", + { + "deny": ["ModalController", "PopoverController"] + } + ] } } ``` -次も同様です。 +### `deny` -- `this.modalCtrl.create(...)` -- `inject(ModalController).create(...)` -- `ModalController` / `PopoverController` 型のパラメータ +- 型: `string[]` +- デフォルト: `["ModalController", "PopoverController"]` -✅ 正しい: launcher経由で開き、`ModalController` はdismiss用にだけ持つ +`.create()` 呼び出しを禁止するcontroller class nameです。空の配列を指定するとルールを無効にできます。 -```ts -export const launchOtherPage = (helper: HelperService, props: OtherProps) => { - return helper.presentModal(OtherPage, props, { watchKeyboard: false }); -}; +## 例 + +### 誤り +```ts export class ExamplePage { readonly #modalCtrl = inject(ModalController); - readonly helper = inject(HelperService); async open() { - await launchOtherPage(this.helper, {}); - } - - dismiss() { - this.#modalCtrl.dismiss(); + await this.#modalCtrl.create({ component: OtherPage }); } } ``` -✅ 正しい: Loading / Alert / Toast / ActionSheet のcreate +```ts +export async function open(modalCtrl: ModalController) { + await modalCtrl.create({ component: OtherPage }); +} +``` ```ts -readonly #loadingCtrl = inject(LoadingController); -await this.#loadingCtrl.create({ message: '...' }); +export class ExamplePage { + constructor(private modalCtrl: ModalController) {} + + async open() { + await this.modalCtrl.create({ component: OtherPage }); + } +} ``` -## オプション +### 正しい ```ts -{ - // Controllers whose `.create()` is denied. - // default: ['ModalController', 'PopoverController'] - deny?: string[]; -} +export const launchOtherPage = (overlay: Helper, props: Props) => { + return overlay.presentModal(OtherPage, props); +}; ``` -```js -'@rdlabo/rules/deny-overlay-create': [ - 'error', - { deny: ['ModalController', 'PopoverController'] }, -], +```ts +export class ExamplePage { + readonly #loadingCtrl = inject(LoadingController); + + async showLoading() { + await this.#loadingCtrl.create({ message: '...' }); + } +} ``` -alertのcreateも禁止するには次のようにします。 +```ts +export class ExamplePage { + readonly #modalCtrl = inject(ModalController); -```js -{ - deny: ['ModalController', 'PopoverController', 'AlertController']; + dismiss(data?: unknown) { + this.#modalCtrl.dismiss(data); + } } ``` +## 有効にする場面 + +launcher patternと共有overlay helperを使うIonicプロジェクトで、このルールを有効にします。[`@rdlabo/rules/prefer-modal-launcher`](./prefer-modal-launcher.md)および[`@rdlabo/rules/deny-element`](./deny-element.md)と組み合わせて使います。 + +## 関連項目 + +- [`@rdlabo/rules/prefer-modal-launcher`](./prefer-modal-launcher.md) +- [`@rdlabo/rules/deny-element`](./deny-element.md) + ## 実装 -- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/src/rules/deny-overlay-create.ts) -- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/tests/rules/deny-overlay-create.ts) +- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v22.0.0/src/rules/deny-overlay-create.ts) +- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v22.0.0/tests/rules/deny-overlay-create.ts) diff --git a/src/eslint-plugin-rules/docs/ja/rules/deny-soft-private-modifier.md b/src/eslint-plugin-rules/docs/ja/rules/deny-soft-private-modifier.md index 27585c1..6e08d67 100644 --- a/src/eslint-plugin-rules/docs/ja/rules/deny-soft-private-modifier.md +++ b/src/eslint-plugin-rules/docs/ja/rules/deny-soft-private-modifier.md @@ -14,22 +14,28 @@ title: deny-soft-private-modifier ❌ 誤り: クラスフィールドに `private` 修飾子を使う ```ts -@Component({}) -export class SigninPage { - private platform = inject(Platform); +class TokenStore { + private token = ''; + + private refresh() { + this.token = 'new-token'; + } } ``` ✅ 正しい: ハードプライベートフィールド構文(#)を使う ```ts -@Component({}) -export class SigninPage { - #platform = inject(Platform); +class TokenStore { + #token = ''; + + #refresh() { + this.#token = 'new-token'; + } } ``` ## 実装 -- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/src/rules/deny-soft-private-modifier.ts) -- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/tests/rules/deny-soft-private-modifier.ts) +- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v22.0.0/src/rules/deny-soft-private-modifier.ts) +- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v22.0.0/tests/rules/deny-soft-private-modifier.ts) diff --git a/src/eslint-plugin-rules/docs/ja/rules/implements-ionic-lifecycle.md b/src/eslint-plugin-rules/docs/ja/rules/implements-ionic-lifecycle.md index fb53fbe..30d11d0 100644 --- a/src/eslint-plugin-rules/docs/ja/rules/implements-ionic-lifecycle.md +++ b/src/eslint-plugin-rules/docs/ja/rules/implements-ionic-lifecycle.md @@ -9,39 +9,84 @@ title: implements-ionic-lifecycle > - ⭐️ このルールは `plugin:@rdlabo/rules/recommended` プリセットに含まれます。 > - ✒️ [コマンドライン](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems)の `--fix` オプションで、このルールが報告する問題の一部を自動修正できます。 +Ionicは `ionViewWillEnter` や `ionViewDidLeave` などのframework-level lifecycle methodを提供します。Componentでこれらのmethodを宣言する場合、TypeScriptがcontractを型検査できるよう、対応するinterface(`ViewWillEnter`、`ViewDidEnter`、`ViewWillLeave`、`ViewDidLeave`)もimplementsする必要があります。このルールはその組み合わせを強制し、`implements` clauseを自動修正できます。 + ## ルール詳細 -❌ 誤り: interfaceをimplementsせずにIonic Lifecycle methodを使う +`@Component` で装飾されたクラスを検査し、次のIonic lifecycle method名を持つmethod definitionを探します。 + +- `ionViewWillEnter` -> `ViewWillEnter` +- `ionViewDidEnter` -> `ViewDidEnter` +- `ionViewWillLeave` -> `ViewWillLeave` +- `ionViewDidLeave` -> `ViewDidLeave` + +methodが存在し、対応するinterfaceがなければ報告します。欠けているinterfaceを修正する際は、`implements` clause全体を、使用中のmethodに対応するIonic lifecycle interfaceで置き換えます。この処理で `OnInit` など無関係なinterfaceが削除される場合があるため、修正内容を確認し、クラスに引き続き必要なIonic以外のinterfaceを戻してください。必要なinterfaceがすべて存在する場合、余分なlifecycle interfaceは報告も削除もされません。 + +- Component以外のクラスは検査しません。 +- class bodyが空でもlifecycle interfaceをimplementsしている場合、古い `implements` clauseを削除します。 +- fixの重複を避けるため、修正可能なgroupごとに一度だけ報告します。 + +## 例 + +### 誤り + +```ts +@Component({ + selector: 'app-scanner', + standalone: true, +}) +export class ScannerPage { + ionViewWillEnter() {} + ionViewWillLeave() {} +} +``` ```ts @Component({ - selector: 'app-confirm', - templateUrl: './confirm.page.html', - styleUrls: ['./confirm.page.scss'], + selector: 'app-scanner', + standalone: true, }) -export class SigninPage { +export class ScannerPage implements ViewDidEnter, ViewDidLeave { ionViewWillEnter() {} + ionViewWillLeave() {} } ``` -✅ 正しい: 適切なinterfaceのimplementsとともにIonic Lifecycle methodを使う +### 正しい ```ts +import { ViewWillEnter, ViewWillLeave } from '@ionic/angular'; + @Component({ - selector: 'app-confirm', - templateUrl: './confirm.page.html', - styleUrls: ['./confirm.page.scss'], + selector: 'app-scanner', + standalone: true, }) -export class SigninPage implements ionViewWillEnter { +export class ScannerPage implements ViewWillEnter, ViewWillLeave { ionViewWillEnter() {} + ionViewWillLeave() {} +} +``` + +```ts +@Component({ + selector: 'app-scanner', + standalone: true, +}) +export class ScannerPage implements ViewDidEnter, ViewDidLeave { + ionViewDidEnter() {} + ionViewDidLeave() {} } ``` ## オプション -オプションなし。 +このルールにオプションはありません。 + +## 有効にする場面 + +すべてのIonic Angularプロジェクトで、このルールを有効にします。lifecycle methodを追加・改名・削除したときに `implements` clauseを正確に保つのに役立ち、`--fix` と組み合わせて利用できます。 ## 実装 -- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/src/rules/implements-ionic-lifecycle.ts) -- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/tests/rules/implements-ionic-lifecycle.ts) +- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v22.0.0/src/rules/implements-ionic-lifecycle.ts) +- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v22.0.0/tests/rules/implements-ionic-lifecycle.ts) diff --git a/src/eslint-plugin-rules/docs/ja/rules/ionic-attr-type-check.md b/src/eslint-plugin-rules/docs/ja/rules/ionic-attr-type-check.md index 2f63661..fc5e4eb 100644 --- a/src/eslint-plugin-rules/docs/ja/rules/ionic-attr-type-check.md +++ b/src/eslint-plugin-rules/docs/ja/rules/ionic-attr-type-check.md @@ -4,79 +4,81 @@ title: ionic-attr-type-check # @rdlabo/rules/ionic-attr-type-check -> Ionicコンポーネントの非string属性への文字列値を禁止し、適切なproperty bindingを提案する。boolean・number・object型属性に対応する。 +> 対応するstring以外のIonic属性にproperty bindingを要求し、string literal属性を検証する。 > > - ⭐️ このルールは `plugin:@rdlabo/rules/recommended` プリセットに含まれます。 > - ✒️ [コマンドライン](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems)の `--fix` オプションで、このルールが報告する問題の一部を自動修正できます。 -このルールは、Ionicコンポーネントのテンプレートで非string属性(boolean、number、object、complex)に文字列値が割り当てられている箇所を検出し、適切なproperty bindingを提案することで、TypeScriptのビルドエラーを防ぎます。 +Ionic componentの属性はboolean、number、object、stringのいずれかです。boolean propertyに `button="true"` のようなstringを渡すのはよくある誤りで、予期しない動作を招く場合があります。このルールは `@ionic/core` の型定義を読み取り、不一致を報告します。 ## ルール詳細 -このルールは、Ionicコンポーネントのテンプレートで非string属性(boolean、number、object、complex)に文字列値が割り当てられている箇所を検出します。 +Angular templateで実行されます。各Ionic elementについて `@ionic/core` の型定義を参照し、属性を次のいずれかに分類します。 -❌ 誤り: 非string属性に文字列値を使う +- `string` — string literalを許可 +- `string literal` — 特定の値だけを許可 +- `boolean` — `[attr]="true"` または `[attr]="false"` を使用 +- `number` — `[attr]="50"` を使用 +- `object` — `[attr]="..."` を使用 +- `skip` / `unknown` — 検査しない + +boolean属性では、string値 `true`、`false`、`1`、`0`、`yes`、`no`、`on`、`off` を認識します。それ以外のstringはboolean検査では報告しません。対応するboolean、number、objectの不一致はproperty bindingへ自動修正されます。 + +- `button="true"` -> `[button]="true"` +- `value="50"` -> `[value]="50"` +- Ionic 9の `autocorrect="off"` -> `[autocorrect]="false"` + +string literal属性に無効なstring値が指定されている場合、許容される値を報告します。 + +## 例 + +### 誤り ```html - +``` + +```html - - ``` -✅ 正しい: property bindingを使う +```html + +``` + +### 正しい ```html - - - - ``` -## オプション +```html + +``` -オプションなし。 +```html + +``` -## サポートする属性型 +```html + + +Click me +``` -このルールはIonicコンポーネントの型定義から非string属性を自動で特定し、次のような属性を検出します。 +## オプション -### Ionicコンポーネント属性の例 +このルールにオプションはありません。 -- `ion-item`: `button`, `disabled`, `detail` -- `ion-list`: `inset`, `lines` -- `ion-button`: `disabled`, `expand`, `fill`, `strong` -- `ion-checkbox`: `checked`, `disabled`, `indeterminate` -- `ion-toggle`: `checked`, `disabled` -- `ion-radio`: `checked`, `disabled` -- `ion-input`: `disabled`, `readonly`, `required` -- `ion-textarea`: `disabled`, `readonly`, `required` -- `ion-select`: `disabled`, `multiple`, `required` -- `ion-datetime`: `disabled`, `readonly` -- `ion-range`: `disabled`, `pin`, `snaps` -- `ion-segment`: `disabled` -- `ion-slides`: `pager`, `scrollbar` -- `ion-tab`: `selected` -- `ion-menu`: `disabled`, `swipeGesture` -- `ion-modal`: `animated`, `backdropDismiss`, `showBackdrop` -- `ion-popover`: `animated`, `backdropDismiss`, `showBackdrop` -- `ion-alert`: `animated`, `backdropDismiss` -- `ion-loading`: `animated`, `backdropDismiss` -- `ion-toast`: `animated` -- `ion-action-sheet`: `animated`, `backdropDismiss` +## 有効にする場面 -## エラーメッセージ +すべてのIonic Angularプロジェクトで、このルールを有効にします。古いIonic構文から移行するときや、通常のHTML属性に慣れたdeveloperが参加するときに特に役立ちます。 -このルールは次のメッセージを表示します。 +## 要件 -``` -boolean attribute 'button' should not have a string value 'true'. Use property binding [button]="true" instead. -number attribute 'value' should not have a string value '50'. Use property binding [value]="50" instead. -``` +`node_modules/@ionic/core/dist/types/components.d.ts` を読み取れるよう、同じプロジェクトに `@ionic/core` がインストールされている必要があります。packageが存在しない場合、ルールは空の結果を返し、何も報告しません。 ## 実装 -- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/src/rules/ionic-attr-type-check.ts) -- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/tests/rules/ionic-attr-type-check.ts) +- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v22.0.0/src/rules/ionic-attr-type-check.ts) +- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v22.0.0/tests/rules/ionic-attr-type-check.ts) diff --git a/src/eslint-plugin-rules/docs/ja/rules/no-component-method-except-lifecycle.md b/src/eslint-plugin-rules/docs/ja/rules/no-component-method-except-lifecycle.md index 18379c8..b3dac66 100644 --- a/src/eslint-plugin-rules/docs/ja/rules/no-component-method-except-lifecycle.md +++ b/src/eslint-plugin-rules/docs/ja/rules/no-component-method-except-lifecycle.md @@ -24,19 +24,14 @@ title: no-component-method-except-lifecycle ```ts @Component({ selector: 'app-example', template: '' }) -export class ExamplePage implements ViewWillEnter, ViewWillLeave, OnDestroy { - readonly vm = new ViewModel(this); - readonly open = () => launchOtherPage(this.helper, {}); - - constructor() {} +export class ExamplePage { + open() { + launchOtherPage(this.helper, {}); + } - ionViewWillEnter() { + reload() { this.vm.reload$.next(); } - - ionViewWillLeave() {} - - ngOnDestroy() {} } ``` @@ -45,8 +40,7 @@ export class ExamplePage implements ViewWillEnter, ViewWillLeave, OnDestroy { ```ts @Component({ selector: 'app-example', template: '' }) export class ExamplePage { - ionViewWillEnter() {} // error — needs implements ViewWillEnter - ngOnDestroy() {} // error — needs implements OnDestroy + ionViewWillEnter() {} // missing implements ViewWillEnter } ``` @@ -54,9 +48,16 @@ export class ExamplePage { ```ts @Component({ selector: 'app-example', template: '' }) -export class ExamplePage implements ViewWillEnter { - ionViewWillEnter() {} - ionViewWillLeave() {} // error — needs implements ViewWillLeave +export class ExamplePage implements ViewWillEnter, ViewWillLeave, OnDestroy { + readonly vm = new ViewModel(this); + readonly open = () => launchOtherPage(this.helper, {}); + + ionViewWillEnter() { + this.vm.reload$.next(); + } + + ionViewWillLeave() {} + ngOnDestroy() {} } ``` @@ -67,10 +68,11 @@ export class ExamplePage implements ViewWillEnter { export class ExamplePage implements ViewWillEnter { ionViewWillEnter() {} - open() { - // error - launchOtherPage(this.helper, {}); + trackById(_index: number, item: { id: number }) { + return item.id; } + + customHook() {} } ``` @@ -103,21 +105,33 @@ export class ExamplePage implements ViewWillEnter { ## オプション -```ts +```json { - // Extra method names to allow (e.g. trackBy helpers during migration). - additionalAllowedMethods?: string[]; + "rules": { + "@rdlabo/rules/no-component-method-except-lifecycle": [ + "error", + { + "additionalAllowedMethods": ["trackById", "customHook"] + } + ] + } } ``` -```js -'@rdlabo/rules/no-component-method-except-lifecycle': [ - 'warn', - { additionalAllowedMethods: ['trackById'] }, -], +```json +{ + "rules": { + "@rdlabo/rules/no-component-method-except-lifecycle": [ + "error", + { + "additionalAllowedMethods": [] + } + ] + } +} ``` ## 実装 -- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/src/rules/no-component-method-except-lifecycle.ts) -- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/tests/rules/no-component-method-except-lifecycle.ts) +- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v22.0.0/src/rules/no-component-method-except-lifecycle.ts) +- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v22.0.0/tests/rules/no-component-method-except-lifecycle.ts) diff --git a/src/eslint-plugin-rules/docs/ja/rules/no-component-writable-signal.md b/src/eslint-plugin-rules/docs/ja/rules/no-component-writable-signal.md index c1678b9..0a0a2e9 100644 --- a/src/eslint-plugin-rules/docs/ja/rules/no-component-writable-signal.md +++ b/src/eslint-plugin-rules/docs/ja/rules/no-component-writable-signal.md @@ -6,20 +6,86 @@ title: no-component-writable-signal > 書き込み可能なComponent状態はViewModelに置く。ただしAngular Signal Formsの `form()` に渡すmodelは例外とする。 -書き込み可能なComponent状態をViewModelに置きます。Componentローカルの `signal()` と `linkedSignal()` は、`profileForm = form(this.model)` のようにSignal Formsのフィールド初期化へ直接渡すmodelプロパティでない限り拒否されます。 +このルールは、Angular ComponentとViewModelの間に明確な境界を強制します。Componentはtemplateに読み取り専用の派生状態を公開し、書き込み可能な状態はViewModelに置くことで、変更を一元化しtest可能にします。Componentで許可される唯一の書き込み可能なSignalは、Signal Formsの `form()` にmodelとして直接渡されるものです。 + +## ルール詳細 + +`@Component` で装飾されたクラスを検査し、`@angular/forms/signals` の `form()` の第1引数に同じpropertyが渡されている場合を除き、`@angular/core` の `signal()` または `linkedSignal()` で初期化されたclass propertyを報告します。 + +- `computed()` と `effect()` は引き続きComponentの責務であり、報告しません。 +- Component以外のクラスは無視します。 +- `@angular/core` と `@angular/forms/signals` のalias importとnamespace importを認識します。 +- import元を検証するため、同名のlocal helperは無視します。 + +Signal Formsの例外は、`readonly pageForm = form(this.model)` のようなComponent property initializerだけを認識します。method内でSignalを `form()` に渡しても例外にはならないため、書き込み可能なSignal propertyは引き続き報告されます。 + +## 例 + +### 誤り ```ts -readonly model = signal({ name: '' }); -readonly profileForm = form(this.model); // valid +import { Component, signal } from '@angular/core'; -readonly isLoading = signal(false); // invalid: move to ViewModel +@Component({ template: '' }) +class Page { + readonly isLoading = signal(false); // reported: move to ViewModel +} ``` -`computed()` と `effect()` は引き続きComponentの責務であり、このルールでは制限しません。Component以外のクラスは無視します。 +```ts +import { Component, signal } from '@angular/core'; +import { form } from '@angular/forms/signals'; + +@Component({ template: '' }) +class Page { + readonly model = signal({ name: '' }); + readonly loading = signal(false); // reported + readonly pageForm = form(this.model); +} +``` + +### 正しい + +```ts +import { Component, computed } from '@angular/core'; +import { form } from '@angular/forms/signals'; +import { PageViewModel } from './page.viewmodel'; + +@Component({ template: '' }) +class Page { + private readonly vm = new PageViewModel(this); + readonly isLoading = this.vm.isLoading; // read-only view of ViewModel state + readonly model = this.vm.model; + readonly pageForm = form(this.model); + readonly title = computed(() => this.model().name); +} +``` + +```ts +import { Component, signal as writable } from '@angular/core'; +import { form as signalForm } from '@angular/forms/signals'; + +@Component({ template: '' }) +class Page { + readonly data = writable({ name: '' }); + readonly pageForm = signalForm(this.data); // data is the Signal Forms model +} +``` + +## オプション + +このルールにオプションはありません。 + +## 有効にする場面 + +`@rdlabo/rules/require-viewmodel` とともにViewModel patternを使うプロジェクトで、このルールを有効にします。Component propertyを共有状態への読み取り専用viewにすることで、Componentによる状態の直接変更を防ぎます。 + +## 関連項目 -`@angular/core` および `@angular/forms/signals` からの名前付きエイリアスと名前空間importに対応します。同名のローカルヘルパーは、ルールがimport元を検証するため無視されます。 +- [`@rdlabo/rules/require-viewmodel`](./require-viewmodel.md) +- [`@rdlabo/rules/no-reactive-forms`](./no-reactive-forms.md) ## 実装 -- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/src/rules/no-component-writable-signal.ts) -- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/tests/rules/no-component-writable-signal.ts) +- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v22.0.0/src/rules/no-component-writable-signal.ts) +- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v22.0.0/tests/rules/no-component-writable-signal.ts) diff --git a/src/eslint-plugin-rules/docs/ja/rules/no-reactive-forms.md b/src/eslint-plugin-rules/docs/ja/rules/no-reactive-forms.md index 9958c9c..284adf0 100644 --- a/src/eslint-plugin-rules/docs/ja/rules/no-reactive-forms.md +++ b/src/eslint-plugin-rules/docs/ja/rules/no-reactive-forms.md @@ -6,13 +6,78 @@ title: no-reactive-forms > Angular Reactive Formsを禁止し、Signal Formsを推奨する。 -`@angular/forms/signals` を優先するため、Angular Reactive FormsのAPIとテンプレートバインディングを禁止します。 +このルールは、Angular Reactive Formsから `@angular/forms/signals` への移行を支援します。Reactive Formsでは、Componentとservice間で共有されることの多い書き込み可能な `FormControl` / `FormGroup` 状態が必要なため、状態変更の発生元を追いにくくなります。Signal Formsではform状態をSignalsに保持するため、依存graphが明示的になり、デフォルトでreactiveになります。 -このルールは `ReactiveFormsModule`、型付き・型なしのcontrol/group/array/builder、reactive directive、`Validators`、および `@angular/forms` からの関連importを拒否します。名前付きAPI検査を迂回できるため、名前空間importとdefault importも拒否します。テンプレートでは `formControl`、`formControlName`、`formGroup`、`formGroupName`、`formArrayName` を拒否します。 +プロジェクトがSignal Formsを採用する間に、新しいReactive Forms codeが追加されるのを防ぎたい場合に使います。 -`FormsModule` と `ngModel` は意図的にこのルールの対象外です。それらのバインディングには `no-template-driven-forms` を使います。 +## ルール詳細 + +このルールは3つのpatternを報告します。 + +1. **`@angular/forms` からのReactive Forms APIのnamed import** + 次の名前のimportをすべて報告します。 + + `AbstractControl`, `FormArray`, `FormArrayName`, `FormBuilder`, `FormControl`, `FormControlDirective`, `FormControlName`, `FormGroup`, `FormGroupDirective`, `FormGroupName`, `FormRecord`, `NonNullableFormBuilder`, `ReactiveFormsModule`, `UntypedFormArray`, `UntypedFormBuilder`, `UntypedFormControl`, `UntypedFormGroup`, `Validators`. + +2. **`@angular/forms` からのnamespace importまたはdefault import** + named APIの検査を迂回できるため、`import * as forms from '@angular/forms'` と `import forms from '@angular/forms'` を報告します。 + +3. **Reactive Formsのtemplate binding** + Angular templateで次のbindingを報告します。 + `formControl`, `formControlName`, `formGroup`, `formGroupName`, `formArrayName`. + +`FormsModule` と `ngModel` は意図的にこのルールの対象外です。これらを制限するには[`@rdlabo/rules/no-template-driven-forms`](./no-template-driven-forms.md)を使います。 + +## 例 + +### 誤り + +```ts +// TypeScript: importing Reactive Forms APIs +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; + +import * as forms from '@angular/forms'; +const control = new forms.FormControl(''); +``` + +```html + +
+ +
+``` + +### 正しい + +```ts +import { signal } from '@angular/core'; +import { form, required } from '@angular/forms/signals'; + +const userModel = signal({ name: '' }); +const userForm = form(userModel, (path) => { + required(path.name); +}); +``` + +```html + + +``` + +## オプション + +このルールにオプションはありません。 + +## 有効にする場面 + +Signal Formsを採用済み、またはReactive Formsから移行中のAngularプロジェクトで、このルールを有効にします。両方のform styleを対象にするため、`@rdlabo/rules/no-template-driven-forms` と同時に安全に有効化できます。 + +## 関連項目 + +- [`@rdlabo/rules/no-template-driven-forms`](./no-template-driven-forms.md) +- [`@rdlabo/rules/no-component-writable-signal`](./no-component-writable-signal.md) ## 実装 -- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/src/rules/no-reactive-forms.ts) -- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/tests/rules/no-reactive-forms.ts) +- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v22.0.0/src/rules/no-reactive-forms.ts) +- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v22.0.0/tests/rules/no-reactive-forms.ts) diff --git a/src/eslint-plugin-rules/docs/ja/rules/no-template-driven-forms.md b/src/eslint-plugin-rules/docs/ja/rules/no-template-driven-forms.md index 31d3e87..11df530 100644 --- a/src/eslint-plugin-rules/docs/ja/rules/no-template-driven-forms.md +++ b/src/eslint-plugin-rules/docs/ja/rules/no-template-driven-forms.md @@ -4,31 +4,83 @@ title: no-template-driven-forms # @rdlabo/rules/no-template-driven-forms -> 明示的に許可した要素上の `ngModel` バインディングを除き、template-driven formsを禁止する。 - -Signal Formsに適さないIonic Viewバインディングとして、要素名が明示的に許可されていない限り `ngModel` を禁止します。`ngForm` と `ngModelGroup` は常に拒否されます。 - -```js -'@rdlabo/rules/no-template-driven-forms': [ - 'error', - { - allowedElements: [ - 'ion-searchbar', - 'ion-segment', - 'ion-radio-group', - 'ion-select', - 'ion-range', - 'ion-toggle', - 'ion-checkbox', - 'ion-input-otp', - ], - }, -]; +> 明示的に許可された要素の `ngModel` バインディングを除き、template-driven formsを禁止する。 + +このルールはAngularテンプレート内のtemplate-driven formsを制限します。`ngForm` と `ngModelGroup` はテンプレート内に可変フォーム状態を保持するため、常に拒否されます。`ngModel` も、Signal Formsに適さないIonic Viewバインディング向けに明示的に許可された要素でない限り拒否されます。 + +許可要素は相互運用のための例外であり、template-driven formsの利用を推奨するものではありません。送信フォームでは、許可要素を含む場合でもSignal Formsを使用してください。 + +## ルール詳細 + +このルールはAngularテンプレートに対して次の3パターンを検査します。 + +1. **`allowedElements` に含まれない要素上の `ngModel`** + 許可リストにないタグの `ngModel`、`[(ngModel)]`、`[ngModel]` を報告します。単独の `(ngModelChange)` outputは検査しません。 + +2. **`ngModelGroup` 属性** + すべての要素上の `ngModelGroup` 属性を報告します。 + +3. **`ngForm` referenceまたはdirective** + `
` と `
` を報告します。 + +型情報は使用せず、parse済みのtemplate ASTだけを検査します。 + +## 例 + +### 誤り + +```html + + + + + + + +
``` -許可要素は相互運用のための例外であり、template-driven formsの利用を推奨するものではありません。送信フォームは、許可要素を含んでいてもSignal Formsを使うべきです。 +### 正しい + +```html + + + + + +``` + +## オプション + +```json +{ + "rules": { + "@rdlabo/rules/no-template-driven-forms": [ + "error", + { + "allowedElements": ["ion-searchbar", "ion-segment", "ion-radio-group", "ion-select", "ion-range", "ion-toggle", "ion-checkbox", "ion-input-otp"] + } + ] + } +} +``` + +### `allowedElements` + +- 型: `string[]` +- デフォルト: `[]` + +`ngModel` の使用を許可する要素のタグ名です。`ion-searchbar` や `ion-toggle` のように、View上の便宜として `ngModel` で値を公開するIonicコンポーネントを想定しています。要素が許可されていても、`ngModelGroup` と `ngForm` は報告されます。 + +## 有効にする場合 + +Angular Signal Formsへ移行しながら、特定のIonic Viewコンポーネントに限定して `ngModel` バインディングが必要なプロジェクトで有効にしてください。Reactive Formsを全面的に採用し、Signal Formsを導入する予定がない場合にのみ無効にします。 + +## 関連項目 + +- [`@rdlabo/rules/no-reactive-forms`](./no-reactive-forms.md) ## 実装 -- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/src/rules/no-template-driven-forms.ts) -- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v21.3.0/tests/rules/no-template-driven-forms.ts) +- [Rule source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v22.0.0/src/rules/no-template-driven-forms.ts) +- [Test source](https://github.com/rdlabo-dev/eslint-plugin-rules/blob/v22.0.0/tests/rules/no-template-driven-forms.ts) diff --git a/src/eslint-plugin-rules/docs/ja/rules/prefer-disable-handler.md b/src/eslint-plugin-rules/docs/ja/rules/prefer-disable-handler.md index 085e524..7a67178 100644 --- a/src/eslint-plugin-rules/docs/ja/rules/prefer-disable-handler.md +++ b/src/eslint-plugin-rules/docs/ja/rules/prefer-disable-handler.md @@ -4,112 +4,137 @@ title: prefer-disable-handler # @rdlabo/rules/prefer-disable-handler -> 設定した要素/イベントバインディングで、非同期処理中の二重タップを防ぐwrapper method(デフォルト: disableHandler($event, work))を要求する +> 非同期処理中の二重タップを防ぐため、設定した要素とイベントのバインディングにwrapper method(デフォルト: disableHandler($event, work))を要求する > > - ⭐️ このルールは `plugin:@rdlabo/rules/recommended` プリセットに含まれます。 -`disableHandler`(`@rdlabo/ionic-angular-kit` 由来)は、非同期操作の実行中にきっかけとなったコントロールを無効化し、Promiseがsettledしたときに復元します。同期処理に使っても害はありません。コントロールは一瞬無効化されて戻され、無効化可能な対象が見つからない場合は何もしません。 +非同期処理を開始するbuttonをユーザーがtapしたら、処理がsettleするまでcontrolを無効にする必要があります。そうしなければ、2回目のtapで同じactionが再実行される可能性があります。このルールは、設定した `(event)` bindingにwrapper呼び出し構文を強制します。UIの無効化とwork値の適切な処理はwrapper実装の責務です。 -要素名・イベント名・wrapper method名はすべて設定可能なので、フリート共通のデフォルトと、リポジトリ固有のバインディング(例: `(ionComplete)`)の両方を同じルールでカバーできます。 +## ルール詳細 -`recommended` でこのルールを有効にすると、wrapperなしで非同期処理を呼ぶ既存テンプレートが指摘されます。プラグインを上げるときは移行パスを見込んでください。 +Angularテンプレートを検査します。設定対象に一致する各 `BoundEvent` のhandler expressionは、2つ以上の引数を持つwrapper method呼び出しでなければなりません。 -## ルール詳細 +1. event parameter(デフォルトは `$event`)。 +2. wrapperへ渡すwork expression。 -❌ 誤り(デフォルト) +たとえば `(click)="vm.disableHandler($event, vm.save())"` は有効です。`(click)="vm.save()"` は報告されます。第2引数の型やPromiseを返すかどうかは検査しません。 -```html -Save -
-``` +`$event.stopPropagation()` や `$event.preventDefault()` のようなevent methodの単独呼び出しも許可します(`allowEventMethods` で設定可能)。 -次も誤りです。wrapperは `$event` と第2引数の `work` を受け取る必要があります。 +デフォルトの対象は次のとおりです。 -```html -Save Save +- `` と `