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..b98e802 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:
\nnpm 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\nPackage \nSupported version \n \n\n\n\nNode.js \n20 or later \n \n\nESLint \n9 or later \n \n\n@typescript-eslint/utils \n8.33 or later, before 9 \n \n\n@angular-eslint/template-parser \n21.x when template rules are used \n \n\n@ionic/core \n8.x when Ionic rules are used \n \n\n
\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
\nThe 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
\nContinue to Configuration to enable the recommended preset or individual rules.
\n Documentation
\n\n- Configuration
\n- Rules
\n
\n",
+ "html": "Install the plugin as a development dependency:
\nnpm 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\nPackage \nSupported version \n \n\n\n\nNode.js \n20 or later \n \n\nESLint \n9 or later \n \n\n@typescript-eslint/utils \n8.33 or later, before 9 \n \n\n@angular-eslint/template-parser \n21.x or 22.x \n \n\n@ionic/angular \n9.x when Ionic rules are used \n \n\n@ionic/core \n9.x when Ionic rules are used \n \n\n
\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
\nThe recommended preset is designed for ESLint Flat Config. Add it at the top level so its TypeScript and HTML file selectors remain intact.
\nFor 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
\nContinue 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
\nRegister the plugin, spread its recommended configs at the top level, then add the standard Angular and TypeScript configs for your project.
\nconst 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
\nimport 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
\nThe 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.
\ndeny-constructor-di is deprecated and is not in the preset. Prefer Angular's inject() migration.
\n",
+ "html": " Angular and Ionic
\nPlugin 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.
\nRegister the plugin, spread its recommended configs at the top level, then add the standard Angular and TypeScript configs for your project.
\nconst 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
\nimport 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
\nThe 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.
\nThe TypeScript preset includes prefer-ionic-standalone, which requires Ionic 9 root imports and rejects IonicModule and NgModule-based lazy imports.
\ndeny-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
\nVersion 22 targets Angular 21 and 22 with Ionic Framework 9. Ionic 8 applications must remain on version 21 of this plugin.
\n Dependencies
\nCommit your application changes first, then run Ionic's official @ionic/migrate tool from the application root:
\nnpx @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
\nReplace 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.
\nFor 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.
\nThe 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
\nVersion 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.
\nThe 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
\nIonic 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\nRule \nPurpose \nFix \nPreset \n \n\n\n\ncomponent-property-use-readonly \nRequire readonly on immutable Angular component properties. \nYes \nYes \n \n\ndeny-constructor-di \nDisallow constructor dependency injection. Deprecated in favor of inject(). \nNo \nNo \n \n\ndeny-element \nReject configured HTML elements, such as inline Ionic overlays. \nNo \nYes \n \n\ndeny-import-from-ionic-module \nReplace accidental @ionic/angular imports with standalone imports. \nYes \nYes \n \n\ndeny-overlay-create \nDisallow direct .create() calls on modal and popover controllers. \nNo \nYes \n \n\ndeny-soft-private-modifier \nReplace TypeScript private with hard-private # fields. \nYes \nYes \n \n\nimplements-ionic-lifecycle \nRequire the matching interface for Angular and Ionic lifecycle methods. \nYes \nYes \n \n\nionic-attr-type-check \nRequire property binding for non-string Ionic attributes. \nYes \nYes \n \n\nno-component-method-except-lifecycle \nKeep arbitrary methods out of Angular components. \nNo \nYes \n \n\nno-component-writable-signal \nKeep writable component state in a ViewModel, with a Signal Forms model exception. \nNo \nNo \n \n\nno-reactive-forms \nDisallow Reactive Forms in favor of Angular Signal Forms. \nNo \nNo \n \n\nno-template-driven-forms \nDisallow template-driven forms except configured interoperability elements. \nNo \nNo \n \n\nprefer-disable-handler \nWrap configured event handlers to prevent duplicate async actions. \nNo \nYes \n \n\nprefer-modal-launcher \nRestrict presentModal calls to launch* functions. \nNo \nYes \n \n\nrequire-viewmodel \nEnforce component ownership and the ViewModelStore boundary. \nNo \nYes \n \n\nrestrict-try-block \nKeep try blocks small and exclude Promise, RxJS, and Signal contexts by policy. \nNo \nYes \n \n\nsignal-use-as-signal-template \nRequire () when reading Angular Signals in templates. \nNo \nYes \n \n\nsignal-use-as-signal \nRequire correct Signal reads and writes in TypeScript. \nYes \nYes \n \n\n
\n Rule documentation
\nEach rule page in this documentation contains options and correct/incorrect examples. The table above reflects v21.3.0.
\n Typed rules
\nEnable 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\nRule \nPurpose \nFix \nPreset \n \n\n\n\ncomponent-property-use-readonly \nRequire readonly on immutable Angular component properties. \nYes \nYes \n \n\ndeny-constructor-di \nDisallow constructor dependency injection. Deprecated in favor of inject(). \nNo \nNo \n \n\ndeny-element \nReject configured HTML elements, such as inline Ionic overlays. \nNo \nYes \n \n\ndeny-overlay-create \nDisallow direct .create() calls on modal and popover controllers. \nNo \nYes \n \n\ndeny-soft-private-modifier \nReplace TypeScript private with hard-private # fields. \nYes \nYes \n \n\nimplements-ionic-lifecycle \nRequire the matching interface for Angular and Ionic lifecycle methods. \nYes \nYes \n \n\nionic-attr-type-check \nRequire property binding for non-string Ionic attributes. \nYes \nYes \n \n\nno-component-method-except-lifecycle \nKeep arbitrary methods out of Angular components. \nNo \nYes \n \n\nno-component-writable-signal \nKeep writable component state in a ViewModel, with a Signal Forms model exception. \nNo \nNo \n \n\nno-reactive-forms \nDisallow Reactive Forms in favor of Angular Signal Forms. \nNo \nNo \n \n\nno-template-driven-forms \nDisallow template-driven forms except configured interoperability elements. \nNo \nNo \n \n\nprefer-disable-handler \nWrap configured event handlers to prevent duplicate async actions. \nNo \nYes \n \n\nprefer-ionic-standalone \nPrefer Ionic 9 standalone imports and disallow IonicModule. \nYes \nYes \n \n\nprefer-modal-launcher \nRestrict presentModal calls to launch* functions. \nNo \nYes \n \n\nrequire-ion-item-group \nRequire grouped Ionic list items for iOS 26 and Material Design 3. \nYes \nYes \n \n\nrequire-viewmodel \nEnforce component ownership and the ViewModelStore boundary. \nNo \nYes \n \n\nrestrict-try-block \nKeep try blocks small and exclude Promise, RxJS, and Signal contexts by policy. \nNo \nYes \n \n\nsignal-use-as-signal-template \nRequire () when reading Angular Signals in templates. \nNo \nYes \n \n\nsignal-use-as-signal \nRequire correct Signal reads and writes in TypeScript. \nYes \nYes \n \n\n
\n Rule documentation
\nEach rule page in this documentation contains options and correct/incorrect examples.
\n Typed rules
\nEnable 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",
@@ -127,7 +166,7 @@ export const PROJECT = {
"file": "api.md",
"section": "Reference",
"path": "/projects/eslint-plugin-rules/docs/api",
- "html": "Public plugin surface for @rdlabo/eslint-plugin-rules v21.3.0. Detailed options and examples are available on each rule page.
\n Module
\n module @rdlabo/eslint-plugin-rules
\n\n\nExport \nDescription \n \n\n\n\nrules \nAll rule implementations keyed by rule name. \n \n\nconfigs \nShareable plugin configurations. \n \n\n
\n\n Rules
\n rule Rule set
\n\n\nGroup \nRules \n \n\n\n\nComponent boundaries \ncomponent-property-use-readonly, deny-constructor-di, deny-soft-private-modifier, no-component-method-except-lifecycle, no-component-writable-signal, require-viewmodel \n \n\nIonic APIs \ndeny-element, deny-import-from-ionic-module, deny-overlay-create, implements-ionic-lifecycle, ionic-attr-type-check, prefer-disable-handler, prefer-modal-launcher \n \n\nForms and signals \nno-reactive-forms, no-template-driven-forms, signal-use-as-signal, signal-use-as-signal-template \n \n\nControl flow \nrestrict-try-block \n \n\n
\n\n",
+ "html": "Public plugin surface for @rdlabo/eslint-plugin-rules v22.0.0. Detailed options and examples are available on each rule page.
\n Module
\n module @rdlabo/eslint-plugin-rules
\n\n\nExport \nDescription \n \n\n\n\nrules \nAll rule implementations keyed by rule name. \n \n\nconfigs \nShareable plugin configurations. \n \n\n
\n\n Rules
\n rule Rule set
\n\n\nGroup \nRules \n \n\n\n\nComponent boundaries \ncomponent-property-use-readonly, deny-constructor-di, deny-soft-private-modifier, no-component-method-except-lifecycle, no-component-writable-signal, require-viewmodel \n \n\nIonic APIs \ndeny-element, deny-overlay-create, implements-ionic-lifecycle, ionic-attr-type-check, prefer-disable-handler, prefer-ionic-standalone, prefer-modal-launcher, require-ion-item-group \n \n\nForms and signals \nno-reactive-forms, no-template-driven-forms, signal-use-as-signal, signal-use-as-signal-template \n \n\nControl flow \nrestrict-try-block \n \n\n
\n\n",
"headings": [
{
"id": "module",
@@ -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": "\nWarns 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
\nThis 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
\nconst options: {\n ignorePrivateProperties?: boolean; // Whether to ignore private properties (default: false)\n};\n
ignorePrivateProperties
\nWhen 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- Rule source
\n- Test source
\n
\n",
+ "html": "\nWarns 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
\nThis 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
\nOnly 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
\nWhen 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
\nEnable this rule when component properties should expose stable references and writable state is managed through Signals or a ViewModel.
\n Implementation
\n\n- Rule source
\n- Test source
\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": "\nThis plugin disallows Dependency Injection within the constructor.
\n
\nThis 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
\nNo Options.
\n Implementation
\n\n- Rule source
\n- Test source
\n
\n",
+ "html": "\nThis plugin disallows Dependency Injection within the constructor.
\n
\nThis 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
\nThe 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
\nimport { 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
\nThis rule has no options.
\n When to enable
\nEnable 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- Rule source
\n- Test source
\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": "\nThis plugin disallows the use of certain HTML tags.
\n\n- ⭐️ This rule is included in
plugin:@rdlabo/rules/recommended preset. \n
\n
\nThis 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
\nconst options: {\n elements: string[]; // Array of element names to disallow\n};\n
Implementation
\n\n- Rule source
\n- Test source
\n
\n",
+ "html": "\nThis plugin disallows the use of certain HTML tags.
\n\n- ⭐️ This rule is included in
plugin:@rdlabo/rules/recommended preset. \n
\n
\nThis 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
\nThe 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
\nArray 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
\nEnable 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- Rule source
\n- Test source
\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": "\nThis 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
\nMixing 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
\nimport { ModalController } from '@ionic/angular';\n
✅ Correct: Importing from @ionic/angular/standalone
\nimport { ModalController } from '@ionic/angular/standalone';\n
Options
\nNo Options.
\n Implementation
\n\n- Rule source
\n- Test source
\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": "\nDisallow .create() on ModalController / PopoverController; open overlays via launchers instead.
\n\n- ⭐️ This rule is included in
plugin:@rdlabo/rules/recommended preset. \n
\n
\nIn 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.
\nUse 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
\nLoadingController, 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
\nimport { 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\nthis.modalCtrl.create(...) \ninject(ModalController).create(...) \n- parameters typed as
ModalController / PopoverController \n
\n✅ Correct: open via launcher; keep ModalController only for dismiss
\nexport 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
\nreadonly #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- Rule source
\n- Test source
\n
\n",
+ "html": "\nDisallow .create() on ModalController / PopoverController; open overlays via launchers instead.
\n\n- ⭐️ This rule is included in
plugin:@rdlabo/rules/recommended preset. \n
\n
\nThis 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
\nThe rule detects .create() calls where the receiver is a ModalController or PopoverController (or other configured controllers). It resolves the controller through several patterns:
\n\nthis.modalCtrl.create() \nmodalCtrl.create() (where modalCtrl is inject(ModalController)) \ninject(ModalController).create() \n- Constructor parameter
constructor(private modalCtrl: ModalController) \n- Class property typed as
ModalController \n
\nOther 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
\nController class names whose .create() calls should be disallowed. Use an empty array to disable the rule.
\n Examples
\n Incorrect
\nexport 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
\nexport 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
\nEnable 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- Rule source
\n- Test source
\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": "\nThis 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- Rule source
\n- Test source
\n
\n",
+ "html": "\nThis 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
\nTypeScript'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
\nThis 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
\nIt 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.
\nThe rule auto-fixes by:
\n\n- Removing the
private keyword. \n- Inserting
# before the property or method name. \n- Updating all
this.field or this.method() references in the class to this.#field or this.#method(). \n
\n Examples
\n Incorrect
\nclass TokenStore {\n private token = '';\n\n private refresh() {\n this.token = 'new-token';\n }\n}\n
Correct
\nclass TokenStore {\n #token = '';\n\n #refresh() {\n this.#token = 'new-token';\n }\n}\n
Options
\nThis rule has no options.
\n When to enable
\nEnable 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- Rule source
\n- Test source
\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": "\nThis 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
\nNo Options.
\n Implementation
\n\n- Rule source
\n- Test source
\n
\n",
+ "html": "\nThis 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
\nIonic 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
\nThis rule checks @Component decorated classes. It looks for method definitions named after Ionic lifecycle methods:
\n\nionViewWillEnter -> ViewWillEnter \nionViewDidEnter -> ViewDidEnter \nionViewWillLeave -> ViewWillLeave \nionViewDidLeave -> ViewDidLeave \n
\nIf 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
\nimport { 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
\nThis rule has no options.
\n When to enable
\nEnable 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- Rule source
\n- Test source
\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": "\nDisallows 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
\nThis 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
\nThis 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
\nNo Options.
\n Supported Attribute Types
\nThis rule automatically identifies non-string attributes from Ionic component type definitions and detects attributes such as:
\n Ionic Component Attribute Examples
\n\nion-item: button, disabled, detail \nion-list: inset, lines \nion-button: disabled, expand, fill, strong \nion-checkbox: checked, disabled, indeterminate \nion-toggle: checked, disabled \nion-radio: checked, disabled \nion-input: disabled, readonly, required \nion-textarea: disabled, readonly, required \nion-select: disabled, multiple, required \nion-datetime: disabled, readonly \nion-range: disabled, pin, snaps \nion-segment: disabled \nion-slides: pager, scrollbar \nion-tab: selected \nion-menu: disabled, swipeGesture \nion-modal: animated, backdropDismiss, showBackdrop \nion-popover: animated, backdropDismiss, showBackdrop \nion-alert: animated, backdropDismiss \nion-loading: animated, backdropDismiss \nion-toast: animated \nion-action-sheet: animated, backdropDismiss \n
\n Error Message
\nThis rule displays the following message:
\nboolean 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- Rule source
\n- Test source
\n
\n",
+ "html": "\nRequire 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
\nIonic 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
\nThe 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\nstring — string literals are allowed \nstring literal — only a specific set of values is allowed \nboolean — use [attr]=\"true\" or [attr]=\"false\" \nnumber — use [attr]=\"50\" \nobject — use [attr]=\"...\" \nskip / unknown — not checked \n
\nFor 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\nbutton=\"true\" -> [button]=\"true\" \nvalue=\"50\" -> [value]=\"50\" \nautocorrect=\"off\" -> [autocorrect]=\"false\" on Ionic 9 \n
\nWhen 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
\nThis rule has no options.
\n When to enable
\nEnable 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
\nThe 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- Rule source
\n- Test source
\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": "\nDisallow 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
\nKeep @Component classes thin. Behavior belongs on ViewModel (or modal launch* helpers).
\nAllowed methods are only those that match lifecycle interfaces listed in implements. A lifecycle method without the corresponding interface is also an error.
\nProperties 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\nExisting 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\nimplements \nmethod \n \n\n\n\nOnChanges \nngOnChanges \n \n\nOnInit \nngOnInit \n \n\nDoCheck \nngDoCheck \n \n\nAfterContentInit \nngAfterContentInit \n \n\nAfterContentChecked \nngAfterContentChecked \n \n\nAfterViewInit \nngAfterViewInit \n \n\nAfterViewChecked \nngAfterViewChecked \n \n\nOnDestroy \nngOnDestroy \n \n\n
\n Ionic
\n\n\n\nimplements \nmethod \n \n\n\n\nViewWillEnter \nionViewWillEnter \n \n\nViewDidEnter \nionViewDidEnter \n \n\nViewWillLeave \nionViewWillLeave \n \n\nViewDidLeave \nionViewDidLeave \n \n\nViewWillUnload \nionViewWillUnload \n \n\n
\nAlso 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- Rule source
\n- Test source
\n
\n",
+ "html": "\nDisallow 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
\nThis 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
\nThe rule checks methods inside @Component decorated classes:
\n\nconstructor, 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
\nThe 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\nInterface \nMethod \n \n\n\n\nOnChanges \nngOnChanges \n \n\nOnInit \nngOnInit \n \n\nDoCheck \nngDoCheck \n \n\nAfterContentInit \nngAfterContentInit \n \n\nAfterContentChecked \nngAfterContentChecked \n \n\nAfterViewInit \nngAfterViewInit \n \n\nAfterViewChecked \nngAfterViewChecked \n \n\nOnDestroy \nngOnDestroy \n \n\nViewWillEnter \nionViewWillEnter \n \n\nViewDidEnter \nionViewDidEnter \n \n\nViewWillLeave \nionViewWillLeave \n \n\nViewDidLeave \nionViewDidLeave \n \n\nViewWillUnload \nionViewWillUnload \n \n\n
\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
\nMethod 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
\nEnable 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- Rule source
\n- Test source
\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": "\nKeep writable component state in ViewModel, except models passed to Angular Signal Forms form().
\n
\nKeeps 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).
\nreadonly 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.
\nNamed 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- Rule source
\n- Test source
\n
\n",
+ "html": "\nKeep writable component state in ViewModel, except models passed to Angular Signal Forms form().
\n
\nThis 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
\nThis 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\ncomputed() 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
\nThe 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
\nimport { 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
\nimport { 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
\nThis rule has no options.
\n When to enable
\nEnable 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- Rule source
\n- Test source
\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": "\nDisallow Angular Reactive Forms in favor of Signal Forms.
\n
\nDisallows Angular Reactive Forms APIs and template bindings in favor of @angular/forms/signals.
\nThe 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.
\nFormsModule and ngModel are intentionally outside this rule; use no-template-driven-forms for those bindings.
\n Implementation
\n\n- Rule source
\n- Test source
\n
\n",
+ "html": "\nDisallow Angular Reactive Forms in favor of Signal Forms.
\n
\nThis 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.
\nUse this rule when you want to prevent new Reactive Forms code from being introduced while a project is adopting Signal Forms.
\n Rule Details
\nThis rule reports three patterns:
\n\n- \n
Named imports of Reactive Forms APIs from @angular/forms
\nAny import of the following names is reported:
\nAbstractControl, FormArray, FormArrayName, FormBuilder, FormControl, FormControlDirective, FormControlName, FormGroup, FormGroupDirective, FormGroupName, FormRecord, NonNullableFormBuilder, ReactiveFormsModule, UntypedFormArray, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators.
\n \n- \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 \n- \n
Reactive Forms template bindings
\nThe following bindings are reported in Angular templates:
\nformControl, formControlName, formGroup, formGroupName, formArrayName.
\n \n
\nFormsModule 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
\nimport { 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
\nThis rule has no options.
\n When to enable
\nEnable 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- Rule source
\n- Test source
\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": "\nDisallow template-driven forms except ngModel bindings on explicitly allowed elements.
\n
\nDisallows 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- Rule source
\n- Test source
\n
\n",
+ "html": "\nDisallow template-driven forms except ngModel bindings on explicitly allowed elements.
\n
\nThis 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.
\nAn 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
\nThe rule runs against Angular templates and checks three patterns:
\n\n- \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 \n- \n
ngModelGroup attribute
\nReports any ngModelGroup attribute on any element.
\n \n- \n
ngForm reference or directive
\nReports <form #form=\"ngForm\"> and <div ngForm>.
\n \n
\nThe 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
\nElement 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
\nEnable 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- Rule source
\n- Test source
\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": "\nRequire 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
\ndisableHandler (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.
\nElement 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)).
\nEnabling 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
\nRequire 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- Rule source
\n- Test source
\n
\n",
+ "html": "\nRequire 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
\nWhen 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
\nThe 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- The event parameter (default
$event). \n- A work expression passed to the wrapper.
\n
\nFor 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.
\nThe rule also allows bare event method calls such as $event.stopPropagation() and $event.preventDefault() (configurable with allowEventMethods).
\nBy default, the rule targets:
\n\nclick on <ion-button> and <button> \nsubmit on any element \n
\nIt 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
\nThe wrapper method name expected in the handler expression.
\n eventParam
\n\n- Type:
string \n- Default:
\"$event\" \n
\nThe 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
\nEach 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
\nEvent 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
\nEnable 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- Rule source
\n- Test source
\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": "\nPrefer 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
\nIonic 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
\nThe 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
\nimport { IonButton } from '@ionic/angular/standalone';\nimport { IonInput } from '@ionic/angular/lazy';\nimport { IonicModule } from '@ionic/angular';\n
Correct
\nimport { 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
\nThis rule has no options. Configure its severity as warn or error in ESLint configuration.
\n When to enable
\nEnable 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- Rule source
\n- Test source
\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": "\nRequire presentModal calls to live inside a launch* launcher function.
\n\n- ⭐️ This rule is included in
plugin:@rdlabo/rules/recommended preset. \n
\n
\nCall sites must use await launchXxxPage(helper, props) instead of inlining helper.presentModal(...).
\nEach modal page should export:
\n\nXxxProps / XxxDismiss types \nlaunchXxxPage(helper, props) that wraps presentModal \n
\nPair with @rdlabo/rules/deny-overlay-create and @rdlabo/rules/deny-element.
\n Rule Details
\n❌ Incorrect: inline presentModal at the call site
\nexport class ExamplePage {\n readonly helper = inject(HelperService);\n\n async open() {\n await this.helper.presentModal(OtherPage, {}); // error\n }\n}\n
Also incorrect:
\nexport 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
\nexport 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:
\nexport 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- Rule source
\n- Test source
\n
\n",
+ "html": "\nRequire presentModal calls to live inside a launch* launcher function.
\n\n- ⭐️ This rule is included in
plugin:@rdlabo/rules/recommended preset. \n
\n
\nModals 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
\nThe 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.
\nA launcher function is one whose name matches the configured regular expression (default ^launch). The rule looks at:
\n\nfunction launchXxx(...) \nconst launchXxx = (...) \nclass Foo { launchXxx = (...) } \nclass 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
\nThe present method names to restrict.
\n launcherNamePattern
\n\n- Type:
string \n- Default:
\"^launch\" \n
\nA regular expression string. Present method calls must be inside a function whose name matches this pattern.
\n Examples
\n Incorrect
\nexport 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
\nexport 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
\nexport 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
\nEnable 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- Rule source
\n- Test source
\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": "\nRequire 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
\nIonic'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
\nAn ion-item within ion-list must use exactly one of these structures:
\n\nion-list > ion-item-group > ion-item \nion-list > ion-reorder-group > ion-item \nion-list > ion-accordion-group > ion-accordion > ion-item \nion-list > ion-radio-group > ion-item \n
\nAngular 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.
\nThe 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
\nThis rule has no options.
\n Automatic fixes
\nWhen 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.
\nThe 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.
\nNo 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
\nEnable 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- Rule source
\n- Test source
\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": "\nEnforce 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
\nEnforces the Ionic Angular Component / ViewModel split in one rule. The co-located class name is ViewModel by default.
\n\n\n\nCheck \nRequirement \n \n\n\n\nComponent ownership \nEvery @Component has a field initialized with new ViewModel(this) \n \n\nConstruction \nFirst argument must be this \n \n\nHost boundary \nViewModel extends ViewModelStore<ComponentType, Keys>; Keys optionally exposes explicit non-Signal Component properties \n \n\nConstructor / host \nViewModel inherits both by default; an optional constructor must forward host with super(host) \n \n\nView APIs \nviewChild / viewChildren / contentChild / contentChildren / effect / computed / render lifecycle APIs must not appear inside ViewModel \n \n\n
\nNon-@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
\nclass 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
\nclass 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.
\nA generic ViewModel may use a default Component host. The default is used for the ownership check:
\nclass ViewModel<THost = MainPage> extends ViewModelStore<THost> {}\n
Hard-private ViewModel ownership is also fine:
\nreadonly #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
\nclass 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.
\nA 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:
\nclass 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:
\nclass ViewModel extends ViewModelStore<ExamplePage> {\n constructor(host: ExamplePage) {\n super(host);\n registerCleanup();\n }\n}\n
❌ View APIs on ViewModel
\nclass 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- Rule source
\n- Test source
\n
\n",
+ "html": "\nEnforce 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
\nThis 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
\nThe rule performs three checks:
\n 1. Component must own a ViewModel
\nA @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>
\nThe 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
\nThe ViewModel class must not call the following APIs:
\nviewChild, viewChildren, contentChild, contentChildren, effect, computed, afterNextRender, afterEveryRender, afterRenderEffect.
\nThis 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
\nimport { 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
\nThe 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
\nThe 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
\nAPIs 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
\nEnable 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- Rule source
\n- Test source
\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": "\nRestrict 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
\nRestricts asynchronous/reactive processing and physical code lines inside try blocks.
\n Rule Details
\nThis rule keeps try as a small boundary for synchronous exceptions. By default it reports:
\n\nawait and expressions whose TypeScript type is Promise-like \nPromise.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 \ntry statements inside Angular computed() and effect() callbacks \ntry bodies containing more than three physical code lines \n
\nFor 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.
\nPromise 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).
\nRxJS errors should be handled through the Observable error channel, such as catchError() or an explicit subscriber error handler.
\nPromise-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:
\nlanguageOptions: {\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
\nallowPromise: Allow Promise-like processing and await inside try. \nallowPromiseResolve: 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. \nallowRxjs: Allow values and operations backed by types declared by rxjs. This includes Observable, Subject, and their subclasses. \nallowInSignal: 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. \nmaxLines: Maximum physical code lines in the try body, or false to disable the size check. \n
\nallowPromise: 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.
\nThe 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.
\nFor 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- Rule source
\n- Test source
\n
\n",
+ "html": "\nRestrict 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
\ntry/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
\nThe rule checks every try block and reports the following by default:
\n\nawait or other Promise/thenable usage inside try \nPromise.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
\nFor 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.
\nPromise-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
\nAllow Promise/thenable usage inside try.
\n allowPromiseResolve
\n\n- Type:
boolean \n- Default:
false \n
\nDisable 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
\nAllow RxJS usage inside try.
\n allowInSignal
\n\n- Type:
boolean \n- Default:
false \n
\nAllow try blocks inside computed() or effect() callbacks.
\n maxLines
\n\n- Type:
number | false \n- Default:
3 \n
\nMaximum 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
\nasync 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
\nfunction 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
\nEnable 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.
\nThe 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- Rule source
\n- Test source
\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": "\nRequire () when accessing Angular Signals in templates
\n\n- ⭐️ This rule is included in
plugin:@rdlabo/rules/recommended preset. \n
\n
\nThis 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
\nBoundAttribute で 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
\nNo Options.
\n Unsupport Pattern
\nThis 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- Rule source
\n- Test source
\n
\n",
+ "html": "\nRequire () when accessing Angular Signals in templates
\n\n- ⭐️ This rule is included in
plugin:@rdlabo/rules/recommended preset. \n
\n
\nAngular 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
\nThe 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
\nDetection 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.
\nIt 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
\nThe 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
\nIf 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
\nThis rule has no options.
\n When to enable
\nEnable 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- Rule source
\n- Test source
\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": "\nThis 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
\nThis 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
\nSignal を値として読むのではなく、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
\nNo Options.
\n Unsupport Pattern
\nThis 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- Rule source
\n- Test source
\n
\n",
+ "html": "\nThis 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
\nAngular 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
\nThe rule tracks class properties initialized with Signal factories (signal, model, input, linkedSignal, toSignal, asReadonly) and reports misuse such as:
\n\nthis.count instead of this.count() in an expression context \nthis.count() = value instead of this.count.set(value) \nthis.user().name = 'Jane' instead of this.user.update(user => ({ ...user, name: 'Jane' })) \nthis.items().push(x) instead of this.items.update(items => { items.push(x); return items; }) \nthis.#user = value (direct assignment to a Signal property) instead of this.#user.set(value) \n
\nThe 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:
\nconst props = { food: this.food };\nlaunchModal({ food: this.food });\n
Examples
\n Incorrect
\nexport 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
\nexport 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
\nThe rule provides auto-fix for the patterns above:
\n\nthis.count = value -> this.count.set(value) \nthis.count() = value -> this.count.set(value) \nthis.count().x = value -> this.count.update(value => ({ ...value, x: value })) \nthis.count().push(x) -> this.count.update(value => { value.push(x); return value; }) \n
\n Options
\nThis rule has no options.
\n When to enable
\nEnable 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- Rule source
\n- Test source
\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": "開発依存関係としてプラグインをインストールします。
\nnpm 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\nNode.js \n20以降 \n \n\nESLint \n9以降 \n \n\n@typescript-eslint/utils \n8.33以上9未満 \n \n\n@angular-eslint/template-parser \nテンプレートルール利用時は21.x \n \n\n@ionic/core \nIonicルール利用時は8.x \n \n\n
\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": "開発依存関係としてプラグインをインストールします。
\nnpm 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\nNode.js \n20以降 \n \n\nESLint \n9以降 \n \n\n@typescript-eslint/utils \n8.33以上9未満 \n \n\n@angular-eslint/template-parser \n21.xまたは22.x \n \n\n@ionic/angular \nIonicルール利用時は9.x \n \n\n@ionic/core \nIonicルール利用時は9.x \n \n\n
\n エントリポイントを選ぶ
\n\n- Angular・Ionicアプリでは
@rdlabo/eslint-plugin-rules を使います。 \n- バックエンドなどの汎用TypeScriptでは
@rdlabo/eslint-plugin-rules/typescript を使います。 \n
\n推奨プリセットはESLint Flat Config向けです。TypeScriptとHTMLの対象指定を維持するため、設定のトップレベルへ追加します。
\nIonic 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構造に対応するためです。
\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設定を追加します。
\nconst 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
\nimport 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 推奨プリセット
\nTypeScriptではSignal、Component境界、ライフサイクル、Overlay、readonly、tryブロックの共通ルールを有効にします。HTMLではIonic属性型、禁止Overlay要素、非同期操作の二重実行防止を有効にします。
\n非推奨の deny-constructor-di はプリセットに含まれません。Angularの inject() migrationを利用してください。
\n",
+ "html": " Angular・Ionic
\nプラグインを登録し、推奨設定をトップレベルで展開してから、プロジェクトで使うAngular・TypeScript設定を追加します。
\nconst 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
\nimport 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 推奨プリセット
\nTypeScriptでは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へ
\nversion 22はAngular 21・22とIonic Framework 9を対象にします。Ionic 8のアプリケーションでは、このpluginのversion 21を使い続けてください。
\n 依存関係
\nまずアプリケーションの変更をcommitし、アプリケーションrootでIonic公式の @ionic/migrate を実行します。
\nnpx @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構造
\nversion 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が報告される場合があります。
\n意図したgroup境界を判断できる場合だけ安全な自動修正を適用します。再利用可能または曖昧なtemplateは変更せずに報告します。wrapper componentはそれ自身のtemplateを通して検査されるため、正しいgroup化済みlistをrenderするcustom elementが、caller側でgroup化されていない ion-item として扱われることはありません。対応構造と修正条件は require-ion-item-group を参照してください。
\n booleanのautocorrect
\nIonic 9では ion-input と ion-searchbar の autocorrect が '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目的 \nFix \n推奨 \n \n\n\n\ncomponent-property-use-readonly \nAngular Componentの不変プロパティに readonly を要求する。 \nYes \nYes \n \n\ndeny-constructor-di \nconstructor DIを禁止する。inject()を推奨する非推奨ルール。 \nNo \nNo \n \n\ndeny-element \nインラインIonic Overlayなど、設定したHTML要素を禁止する。 \nNo \nYes \n \n\ndeny-import-from-ionic-module \n@ionic/angularからの誤importをstandalone importへ置換する。 \nYes \nYes \n \n\ndeny-overlay-create \nModal・Popover Controllerの直接 .create() を禁止する。 \nNo \nYes \n \n\ndeny-soft-private-modifier \nTypeScriptの private をハードプライベート # へ置換する。 \nYes \nYes \n \n\nimplements-ionic-lifecycle \nAngular・Ionic lifecycle methodに対応するinterfaceを要求する。 \nYes \nYes \n \n\nionic-attr-type-check \n文字列以外のIonic属性へproperty bindingを要求する。 \nYes \nYes \n \n\nno-component-method-except-lifecycle \nlifecycle以外の任意methodをComponentへ置かない。 \nNo \nYes \n \n\nno-component-writable-signal \nSignal Forms modelの例外を除き、書き込み可能状態をViewModelへ置く。 \nNo \nNo \n \n\nno-reactive-forms \nReactive Formsを禁止し、Angular Signal Formsへ移行する。 \nNo \nNo \n \n\nno-template-driven-forms \n許可した相互運用要素を除きtemplate-driven formsを禁止する。 \nNo \nNo \n \n\nprefer-disable-handler \n非同期操作の二重実行を防ぐwrapperを要求する。 \nNo \nYes \n \n\nprefer-modal-launcher \npresentModal 呼び出しを launch* 関数に限定する。 \nNo \nYes \n \n\nrequire-viewmodel \nComponent所有と ViewModelStore 境界を検査する。 \nNo \nYes \n \n\nrestrict-try-block \ntry を短く保ち、Promise・RxJS・Signal contextをポリシーで制限する。 \nNo \nYes \n \n\nsignal-use-as-signal-template \nテンプレートでSignalを読むときに () を要求する。 \nNo \nYes \n \n\nsignal-use-as-signal \nTypeScriptで正しいSignalの読み書きを要求する。 \nYes \nYes \n \n\n
\n ルール別ドキュメント
\n各ルールページに詳細・オプション・正誤例があります。上の表はv21.3.0を反映しています。
\n 型情報を使うルール
\nTypeScript型を調べるルールでは parserOptions.projectService を有効にします。typed lintingがない場合も restrict-try-block の構文検査は動作しますが、Promise・RxJSの型依存検査はスキップされます。
\n",
+ "html": "パッケージは19個のルールを公開します。「推奨」がYesのルールは rdlabo.configs.recommended で有効になり、それ以外は個別に設定します。
\n\n\n\nルール \n目的 \nFix \n推奨 \n \n\n\n\ncomponent-property-use-readonly \nAngular Componentの不変プロパティに readonly を要求する。 \nYes \nYes \n \n\ndeny-constructor-di \nconstructor DIを禁止する。inject()を推奨する非推奨ルール。 \nNo \nNo \n \n\ndeny-element \nインラインIonic Overlayなど、設定したHTML要素を禁止する。 \nNo \nYes \n \n\ndeny-overlay-create \nModal・Popover Controllerの直接 .create() を禁止する。 \nNo \nYes \n \n\ndeny-soft-private-modifier \nTypeScriptの private をハードプライベート # へ置換する。 \nYes \nYes \n \n\nimplements-ionic-lifecycle \nAngular・Ionic lifecycle methodに対応するinterfaceを要求する。 \nYes \nYes \n \n\nionic-attr-type-check \n文字列以外のIonic属性へproperty bindingを要求する。 \nYes \nYes \n \n\nno-component-method-except-lifecycle \nlifecycle以外の任意methodをComponentへ置かない。 \nNo \nYes \n \n\nno-component-writable-signal \nSignal Forms modelの例外を除き、書き込み可能状態をViewModelへ置く。 \nNo \nNo \n \n\nno-reactive-forms \nReactive Formsを禁止し、Angular Signal Formsへ移行する。 \nNo \nNo \n \n\nno-template-driven-forms \n許可した相互運用要素を除きtemplate-driven formsを禁止する。 \nNo \nNo \n \n\nprefer-disable-handler \n非同期操作の二重実行を防ぐwrapperを要求する。 \nNo \nYes \n \n\nprefer-ionic-standalone \nIonic 9 standalone importを優先し、IonicModule を禁止する。 \nYes \nYes \n \n\nprefer-modal-launcher \npresentModal 呼び出しを launch* 関数に限定する。 \nNo \nYes \n \n\nrequire-ion-item-group \niOS 26・MD3向けにIonic list itemのgroup化を要求する。 \nYes \nYes \n \n\nrequire-viewmodel \nComponent所有と ViewModelStore 境界を検査する。 \nNo \nYes \n \n\nrestrict-try-block \ntry を短く保ち、Promise・RxJS・Signal contextをポリシーで制限する。 \nNo \nYes \n \n\nsignal-use-as-signal-template \nテンプレートでSignalを読むときに () を要求する。 \nNo \nYes \n \n\nsignal-use-as-signal \nTypeScriptで正しいSignalの読み書きを要求する。 \nYes \nYes \n \n\n
\n ルール別ドキュメント
\n各ルールページに詳細・オプション・正誤例があります。
\n 型情報を使うルール
\nTypeScript型を調べるルールでは 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\nExport \nDescription \n \n\n\n\nrules \nRule名をkeyにした全Rule実装です。 \n \n\nconfigs \n共有可能なPlugin設定です。 \n \n\n
\n\n Rule
\n rule Rule set
\n\n\nGroup \nRules \n \n\n\n\nComponent境界 \ncomponent-property-use-readonly, deny-constructor-di, deny-soft-private-modifier, no-component-method-except-lifecycle, no-component-writable-signal, require-viewmodel \n \n\nIonic API \ndeny-element, deny-import-from-ionic-module, deny-overlay-create, implements-ionic-lifecycle, ionic-attr-type-check, prefer-disable-handler, prefer-modal-launcher \n \n\nForm・Signal \nno-reactive-forms, no-template-driven-forms, signal-use-as-signal, signal-use-as-signal-template \n \n\nControl flow \nrestrict-try-block \n \n\n
\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\nExport \nDescription \n \n\n\n\nrules \nRule名をkeyにした全Rule実装です。 \n \n\nconfigs \n共有可能なPlugin設定です。 \n \n\n
\n\n Rule
\n rule Rule set
\n\n\nGroup \nRules \n \n\n\n\nComponent境界 \ncomponent-property-use-readonly, deny-constructor-di, deny-soft-private-modifier, no-component-method-except-lifecycle, no-component-writable-signal, require-viewmodel \n \n\nIonic API \ndeny-element, deny-overlay-create, implements-ionic-lifecycle, ionic-attr-type-check, prefer-disable-handler, prefer-ionic-standalone, prefer-modal-launcher, require-ion-item-group \n \n\nForm・Signal \nno-reactive-forms, no-template-driven-forms, signal-use-as-signal, signal-use-as-signal-template \n \n\nControl flow \nrestrict-try-block \n \n\n
\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
オプション
\nconst options: {\n ignorePrivateProperties?: boolean; // Whether to ignore private properties (default: false)\n};\n
ignorePrivateProperties
\ntrue にすると、ソフトプライベートプロパティ(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- Rule source
\n- Test source
\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
\ntrue の場合、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
有効にする場面
\nComponentプロパティに安定した参照を公開させ、書き込み可能な状態をSignalsまたはViewModelで管理する場合に、このルールを有効にします。
\n 実装
\n\n- Rule source
\n- Test source
\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- Rule source
\n- Test source
\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を検査し、TSParameterProperty(public、private、readonly などの修飾子を持つ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
正しい
\nimport { 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 有効にする場面
\nconstructor parameter propertyではなく inject() でAngularの依存関係を取得することをプロジェクトで要求する場合に、このopt-inルールを有効にします。このルールは TSParameterProperty nodeだけを報告するため、通常のconstructor parameterは引き続き許可されます。
\n 実装
\n\n- Rule source
\n- Test source
\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
オプション
\nconst options: {\n elements: string[]; // Array of element names to disallow\n};\n
実装
\n\n- Rule source
\n- Test source
\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
有効にする場面
\noverlayにlauncher patternを使うプロジェクトで、このルールを有効にします。@rdlabo/rules/prefer-modal-launcherおよび@rdlabo/rules/prefer-disable-handlerと組み合わせることで、modalとoverlayのlogicをtemplateから分離できます。
\n 関連項目
\n\n 実装
\n\n- Rule source
\n- Test source
\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する
\nimport { ModalController } from '@ionic/angular';\n
✅ 正しい: @ionic/angular/standalone からimportする
\nimport { ModalController } from '@ionic/angular/standalone';\n
オプション
\nオプションなし。
\n 実装
\n\n- Rule source
\n- Test source
\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\nModalController / PopoverController の .create() を禁止し、launcher経由でoverlayを開く。
\n\n- ⭐️ このルールは
plugin:@rdlabo/rules/recommended プリセットに含まれます。 \n
\n
\nIonicアプリでは、modalとpopoverは共有ヘルパー(presentModal)とエクスポートした launch* 関数経由で開くべきであり、ModalController.create() / PopoverController.create() を直接呼んではいけません。
\n次のルールと併用します。
\n\n@rdlabo/rules/deny-element — テンプレート内のインライン <ion-modal> / <ion-popover> を禁止する \n@rdlabo/rules/prefer-modal-launcher — presentModal を launch* 内に置くことを要求する \n
\nLoadingController、AlertController、ToastController、ActionSheetController はデフォルトで許可されたままです。ModalController の dismiss() も許可されます。
\n ルール詳細
\n❌ 誤り: controller経由でmodal / popoverをcreateする
\nimport { 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\nthis.modalCtrl.create(...) \ninject(ModalController).create(...) \nModalController / PopoverController 型のパラメータ \n
\n✅ 正しい: launcher経由で開き、ModalController はdismiss用にだけ持つ
\nexport 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
\nreadonly #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- Rule source
\n- Test source
\n
\n",
+ "html": "\n\nModalController / 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 ルール詳細
\nreceiverが ModalController、PopoverController(または設定した他のcontroller)である .create() 呼び出しを検出します。次のような複数のpatternからcontrollerを解決します。
\n\nthis.modalCtrl.create() \nmodalCtrl.create()(modalCtrl が inject(ModalController) の場合) \ninject(ModalController).create() \n- constructor parameter
constructor(private modalCtrl: ModalController) \nModalController 型のclass property \n
\nLoadingController、AlertController、ToastController、ActionSheetController など、その他の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 誤り
\nexport 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
正しい
\nexport 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
有効にする場面
\nlauncher patternと共有overlay helperを使うIonicプロジェクトで、このルールを有効にします。@rdlabo/rules/prefer-modal-launcherおよび@rdlabo/rules/deny-elementと組み合わせて使います。
\n 関連項目
\n\n 実装
\n\n- Rule source
\n- Test source
\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- Rule source
\n- Test source
\n
\n",
+ "html": "\n\nこのプラグインはソフトプライベート修飾子の使用を禁止します。
\n\n- ⭐️ このルールは
plugin:@rdlabo/rules/recommended プリセットに含まれます。 \n- ✒️ コマンドラインの
--fix オプションで、このルールが報告する問題の一部を自動修正できます。 \n
\n
\n ルール詳細
\n❌ 誤り: クラスフィールドに private 修飾子を使う
\nclass TokenStore {\n private token = '';\n\n private refresh() {\n this.token = 'new-token';\n }\n}\n
✅ 正しい: ハードプライベートフィールド構文(#)を使う
\nclass TokenStore {\n #token = '';\n\n #refresh() {\n this.#token = 'new-token';\n }\n}\n
実装
\n\n- Rule source
\n- Test source
\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- Rule source
\n- Test source
\n
\n",
+ "html": "\n\nこのプラグインはIonic Lifecycleのimplementsを推奨します。
\n\n- ⭐️ このルールは
plugin:@rdlabo/rules/recommended プリセットに含まれます。 \n- ✒️ コマンドラインの
--fix オプションで、このルールが報告する問題の一部を自動修正できます。 \n
\n
\nIonicは ionViewWillEnter や ionViewDidLeave などのframework-level lifecycle methodを提供します。Componentでこれらのmethodを宣言する場合、TypeScriptがcontractを型検査できるよう、対応するinterface(ViewWillEnter、ViewDidEnter、ViewWillLeave、ViewDidLeave)もimplementsする必要があります。このルールはその組み合わせを強制し、implements clauseを自動修正できます。
\n ルール詳細
\n@Component で装飾されたクラスを検査し、次のIonic lifecycle method名を持つmethod definitionを探します。
\n\nionViewWillEnter -> ViewWillEnter \nionViewDidEnter -> ViewDidEnter \nionViewWillLeave -> ViewWillLeave \nionViewDidLeave -> ViewDidLeave \n
\nmethodが存在し、対応する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
正しい
\nimport { 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- Rule source
\n- Test source
\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\nIonicコンポーネントの非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\nion-item: button, disabled, detail \nion-list: inset, lines \nion-button: disabled, expand, fill, strong \nion-checkbox: checked, disabled, indeterminate \nion-toggle: checked, disabled \nion-radio: checked, disabled \nion-input: disabled, readonly, required \nion-textarea: disabled, readonly, required \nion-select: disabled, multiple, required \nion-datetime: disabled, readonly \nion-range: disabled, pin, snaps \nion-segment: disabled \nion-slides: pager, scrollbar \nion-tab: selected \nion-menu: disabled, swipeGesture \nion-modal: animated, backdropDismiss, showBackdrop \nion-popover: animated, backdropDismiss, showBackdrop \nion-alert: animated, backdropDismiss \nion-loading: animated, backdropDismiss \nion-toast: animated \nion-action-sheet: animated, backdropDismiss \n
\n エラーメッセージ
\nこのルールは次のメッセージを表示します。
\nboolean 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- Rule source
\n- Test source
\n
\n",
+ "html": "\n\n対応するstring以外のIonic属性にproperty bindingを要求し、string literal属性を検証する。
\n\n- ⭐️ このルールは
plugin:@rdlabo/rules/recommended プリセットに含まれます。 \n- ✒️ コマンドラインの
--fix オプションで、このルールが報告する問題の一部を自動修正できます。 \n
\n
\nIonic componentの属性はboolean、number、object、stringのいずれかです。boolean propertyに button=\"true\" のようなstringを渡すのはよくある誤りで、予期しない動作を招く場合があります。このルールは @ionic/core の型定義を読み取り、不一致を報告します。
\n ルール詳細
\nAngular templateで実行されます。各Ionic elementについて @ionic/core の型定義を参照し、属性を次のいずれかに分類します。
\n\nstring — string literalを許可 \nstring literal — 特定の値だけを許可 \nboolean — [attr]=\"true\" または [attr]=\"false\" を使用 \nnumber — [attr]=\"50\" を使用 \nobject — [attr]=\"...\" を使用 \nskip / unknown — 検査しない \n
\nboolean属性では、string値 true、false、1、0、yes、no、on、off を認識します。それ以外のstringはboolean検査では報告しません。対応するboolean、number、objectの不一致はproperty bindingへ自動修正されます。
\n\nbutton=\"true\" -> [button]=\"true\" \nvalue=\"50\" -> [value]=\"50\" \n- Ionic 9の
autocorrect=\"off\" -> [autocorrect]=\"false\" \n
\nstring 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 要件
\nnode_modules/@ionic/core/dist/types/components.d.ts を読み取れるよう、同じプロジェクトに @ionic/core がインストールされている必要があります。packageが存在しない場合、ルールは空の結果を返し、何も報告しません。
\n 実装
\n\n- Rule source
\n- Test source
\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\nimplements \nメソッド \n \n\n\n\nOnChanges \nngOnChanges \n \n\nOnInit \nngOnInit \n \n\nDoCheck \nngDoCheck \n \n\nAfterContentInit \nngAfterContentInit \n \n\nAfterContentChecked \nngAfterContentChecked \n \n\nAfterViewInit \nngAfterViewInit \n \n\nAfterViewChecked \nngAfterViewChecked \n \n\nOnDestroy \nngOnDestroy \n \n\n
\n Ionic
\n\n\n\nimplements \nメソッド \n \n\n\n\nViewWillEnter \nionViewWillEnter \n \n\nViewDidEnter \nionViewDidEnter \n \n\nViewWillLeave \nionViewWillLeave \n \n\nViewDidLeave \nionViewDidLeave \n \n\nViewWillUnload \nionViewWillUnload \n \n\n
\n許可(報告しない): constructor、get / 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- Rule source
\n- Test source
\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\nimplements \nメソッド \n \n\n\n\nOnChanges \nngOnChanges \n \n\nOnInit \nngOnInit \n \n\nDoCheck \nngDoCheck \n \n\nAfterContentInit \nngAfterContentInit \n \n\nAfterContentChecked \nngAfterContentChecked \n \n\nAfterViewInit \nngAfterViewInit \n \n\nAfterViewChecked \nngAfterViewChecked \n \n\nOnDestroy \nngOnDestroy \n \n\n
\n Ionic
\n\n\n\nimplements \nメソッド \n \n\n\n\nViewWillEnter \nionViewWillEnter \n \n\nViewDidEnter \nionViewDidEnter \n \n\nViewWillLeave \nionViewWillLeave \n \n\nViewDidLeave \nionViewDidLeave \n \n\nViewWillUnload \nionViewWillUnload \n \n\n
\n許可(報告しない): constructor、get / 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- Rule source
\n- Test source
\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プロパティでない限り拒否されます。
\nreadonly 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- Rule source
\n- Test source
\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/signals の form() の第1引数に同じpropertyが渡されている場合を除き、@angular/core の signal() または linkedSignal() で初期化されたclass propertyを報告します。
\n\ncomputed() と effect() は引き続きComponentの責務であり、報告しません。 \n- Component以外のクラスは無視します。
\n@angular/core と @angular/forms/signals のalias importとnamespace importを認識します。 \n- import元を検証するため、同名のlocal helperは無視します。
\n
\nSignal Formsの例外は、readonly pageForm = form(this.model) のようなComponent property initializerだけを認識します。method内でSignalを form() に渡しても例外にはならないため、書き込み可能なSignal propertyは引き続き報告されます。
\n 例
\n 誤り
\nimport { 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
正しい
\nimport { 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- Rule source
\n- Test source
\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\nAngular 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も拒否します。テンプレートでは formControl、formControlName、formGroup、formGroupName、formArrayName を拒否します。
\nFormsModule と ngModel は意図的にこのルールの対象外です。それらのバインディングには no-template-driven-forms を使います。
\n 実装
\n\n- Rule source
\n- Test source
\n
\n",
+ "html": "\n\nAngular 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- \n
@angular/forms からのReactive Forms APIのnamed import
\n次の名前のimportをすべて報告します。
\nAbstractControl, FormArray, FormArrayName, FormBuilder, FormControl, FormControlDirective, FormControlName, FormGroup, FormGroupDirective, FormGroupName, FormRecord, NonNullableFormBuilder, ReactiveFormsModule, UntypedFormArray, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators.
\n \n- \n
@angular/forms からのnamespace importまたはdefault import
\nnamed APIの検査を迂回できるため、import * as forms from '@angular/forms' と import forms from '@angular/forms' を報告します。
\n \n- \n
Reactive Formsのtemplate binding
\nAngular templateで次のbindingを報告します。
\nformControl, formControlName, formGroup, formGroupName, formArrayName.
\n \n
\nFormsModule と ngModel は意図的にこのルールの対象外です。これらを制限するには@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
正しい
\nimport { 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 有効にする場面
\nSignal Formsを採用済み、またはReactive Formsから移行中のAngularプロジェクトで、このルールを有効にします。両方のform styleを対象にするため、@rdlabo/rules/no-template-driven-forms と同時に安全に有効化できます。
\n 関連項目
\n\n 実装
\n\n- Rule source
\n- Test source
\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
\nSignal Formsに適さないIonic Viewバインディングとして、要素名が明示的に許可されていない限り ngModel を禁止します。ngForm と ngModelGroup は常に拒否されます。
\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- Rule source
\n- Test source
\n
\n",
+ "html": "\n\n明示的に許可された要素の ngModel バインディングを除き、template-driven formsを禁止する。
\n
\nこのルールはAngularテンプレート内のtemplate-driven formsを制限します。ngForm と ngModelGroup はテンプレート内に可変フォーム状態を保持するため、常に拒否されます。ngModel も、Signal Formsに適さないIonic Viewバインディング向けに明示的に許可された要素でない限り拒否されます。
\n許可要素は相互運用のための例外であり、template-driven formsの利用を推奨するものではありません。送信フォームでは、許可要素を含む場合でもSignal Formsを使用してください。
\n ルール詳細
\nこのルールはAngularテンプレートに対して次の3パターンを検査します。
\n\n- \n
allowedElements に含まれない要素上の ngModel
\n許可リストにないタグの ngModel、[(ngModel)]、[ngModel] を報告します。単独の (ngModelChange) outputは検査しません。
\n \n- \n
ngModelGroup 属性
\nすべての要素上の ngModelGroup 属性を報告します。
\n \n- \n
ngForm referenceまたはdirective
\n<form #form=\"ngForm\"> と <div ngForm> を報告します。
\n \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
\nngModel の使用を許可する要素のタグ名です。ion-searchbar や ion-toggle のように、View上の便宜として ngModel で値を公開するIonicコンポーネントを想定しています。要素が許可されていても、ngModelGroup と ngForm は報告されます。
\n 有効にする場合
\nAngular Signal Formsへ移行しながら、特定のIonic Viewコンポーネントに限定して ngModel バインディングが必要なプロジェクトで有効にしてください。Reactive Formsを全面的に採用し、Signal Formsを導入する予定がない場合にのみ無効にします。
\n 関連項目
\n\n 実装
\n\n- Rule source
\n- Test source
\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
\ndisableHandler(@rdlabo/ionic-angular-kit 由来)は、非同期操作の実行中にきっかけとなったコントロールを無効化し、Promiseがsettledしたときに復元します。同期処理に使っても害はありません。コントロールは一瞬無効化されて戻され、無効化可能な対象が見つからない場合は何もしません。
\n要素名・イベント名・wrapper method名はすべて設定可能なので、フリート共通のデフォルトと、リポジトリ固有のバインディング(例: (ionComplete))の両方を同じルールでカバーできます。
\nrecommended でこのルールを有効にすると、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- Rule source
\n- Test source
\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 ルール詳細
\nAngularテンプレートを検査します。設定対象に一致する各 BoundEvent のhandler expressionは、2つ以上の引数を持つwrapper method呼び出しでなければなりません。
\n\n- event parameter(デフォルトは
$event)。 \n- wrapperへ渡すwork expression。
\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
\nhandler expressionに要求するwrapper method名です。
\n eventParam
\n\n- 型:
string \n- デフォルト:
\"$event\" \n
\nwrapper 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
\nwrapperなしで許可する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
有効にする場合
\nAPI呼び出し、navigation、modal表示などの非同期処理をユーザー操作から開始するIonic/Angularプロジェクトで有効にしてください。@rdlabo/rules/prefer-modal-launcher および @rdlabo/rules/deny-element と組み合わせることで、overlay logicを一元化できます。
\n 関連項目
\n\n 実装
\n\n- Rule source
\n- Test source
\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\nIonic 9のstandalone APIを優先し、IonicModuleおよび廃止済み・NgModuleベースのentry pointを禁止します。
\n\n- ⭐️ このruleは
plugin:@rdlabo/rules/recommended presetに含まれます。 \n- ✒️ command lineの
--fix optionで、報告された問題の一部を自動修正できます。 \n
\n
\nIonic 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
\nimport、named re-export、export-all declaration、namespace import経由の IonicModule accessを検査します。namespace accessはscopeから解決するため、同名のlocal variableでshadowされている場合は報告しません。
\n Examples
\n Incorrect
\nimport { IonButton } from '@ionic/angular/standalone';\nimport { IonInput } from '@ionic/angular/lazy';\nimport { IonicModule } from '@ionic/angular';\n
Correct
\nimport { 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
\nstandalone bootstrapを採用したIonic 9 Angularアプリケーションで有効にしてください。@ionic/angular/lazy と IonicModule は常に禁止されるため、NgModuleアプリケーションは有効化前にstandalone migrationを完了してください。
\n Implementation
\n\n- Rule source
\n- Test source
\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\npresentModal 呼び出しを launch* launcher関数内に置くことを要求する。
\n\n- ⭐️ このルールは
plugin:@rdlabo/rules/recommended プリセットに含まれます。 \n
\n
\n呼び出し側は helper.presentModal(...) をインラインで書かず、await launchXxxPage(helper, props) を使う必要があります。
\n各modalページは次をエクスポートするべきです。
\n\nXxxProps / XxxDismiss 型 \npresentModal を包む launchXxxPage(helper, props) \n
\n@rdlabo/rules/deny-overlay-create および @rdlabo/rules/deny-element と併用します。
\n ルール詳細
\n❌ 誤り: 呼び出し側で presentModal をインライン実行する
\nexport class ExamplePage {\n readonly helper = inject(HelperService);\n\n async open() {\n await this.helper.presentModal(OtherPage, {}); // error\n }\n}\n
次も誤りです。
\nexport 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内だけに置く
\nexport 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内のネストした呼び出しは問題ありません。
\nexport 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- Rule source
\n- Test source
\n
\n",
+ "html": "\n\npresentModal 呼び出しを launch* launcher関数内に置くことを要求する。
\n\n- ⭐️ このルールは
plugin:@rdlabo/rules/recommended プリセットに含まれます。 \n
\n
\nmodalとsheetは、対象pageからexportされた専用launcher関数を介して表示してください。これにより、呼び出し側をmodal構築の詳細から分離し、application全体でmodal APIを統一できます。このルールは、presentModal(または設定した他のpresent method)がlauncher patternに一致する名前の関数内でのみ呼び出されることを保証します。
\n ルール詳細
\npresentModal、helper.presentModal(...)、overlay.presentSheet(...) などの呼び出しについて CallExpression nodeを検査します。launcher関数内にない呼び出しは報告されます。
\nlauncher関数とは、設定した正規表現(デフォルトは ^launch)に名前が一致する関数です。次の形式を検査します。
\n\nfunction launchXxx(...) \nconst launchXxx = (...) \nclass Foo { launchXxx = (...) } \nclass 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 誤り
\nexport 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
正しい
\nexport 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
カスタム設定
\nexport 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
有効にする場合
\nmodal、sheet、その他のoverlayにlauncher patternを採用するIonic/Angularプロジェクトで有効にしてください。@rdlabo/rules/deny-element および @rdlabo/rules/prefer-disable-handler と組み合わせて使用します。
\n 関連項目
\n\n 実装
\n\n- Rule source
\n- Test source
\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\nion-list 内の ion-item を、対応するIonic item groupで囲むことを要求します。
\n\n- ⭐️ このruleは
plugin:@rdlabo/rules/recommended presetに含まれます。 \n- ✒️ command lineの
--fix optionで、報告された問題の一部を自動修正できます。 \n
\n
\nIonicのiOS 26とMaterial Design 3のlist styleでは、itemをその挙動に対応するgroup componentで構成する必要があります。このruleは、groupで囲まれていない ion-item が ion-list 直下にrenderされることを防ぎます。
\n Rule Details
\nion-list 内の ion-item は、次のいずれかの構造を正確に使う必要があります。
\n\nion-list > ion-item-group > ion-item \nion-list > ion-reorder-group > ion-item \nion-list > ion-accordion-group > ion-accordion > ion-item \nion-list > ion-radio-group > ion-item \n
\n@if、@for、@empty、@switch、@defer などのAngular control-flow blockはelementをrenderしないため、この構造検査ではtransparentとして扱います。ng-container と ng-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
\nlistに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を提供します。
\ngroup化済み・未group化の内容が混在する場合、ほかのrendered content、再利用可能な ng-template 定義、nested list、間に入るrendered element、不正なaccordion構造がある場合は、修正もsuggestionも提供しません。これらのケースでは意図したgroup境界を安全に判断できません。
\n When to enable
\niOS 26とMaterial Design 3のlist designを対象とするIonic Angularアプリケーションで有効にしてください。recommended presetに含まれ、template内の ion-list に ion-item がなければ影響しません。
\n Implementation
\n\n- Rule source
\n- Test source
\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\nComponentの new ViewModel(this)、ViewModelStore<ComponentType, Keys> 継承を強制し、View APIをViewModelから排除する。
\n\n- ⭐️ このルールは
plugin:@rdlabo/rules/recommended プリセットに含まれます。 \n
\n
\nIonic AngularのComponent / ViewModel分割を1つのルールで強制します。同居するクラス名のデフォルトは ViewModel です。
\n\n\n\n検査 \n要件 \n \n\n\n\nComponent所有 \nすべての @Component が new ViewModel(this) で初期化されたフィールドを持つ \n \n\n構築 \n第1引数は this でなければならない \n \n\nホスト境界 \nViewModelは ViewModelStore<ComponentType, Keys> を継承する。Keys は任意で、明示的な非SignalのComponentプロパティを公開する \n \n\nConstructor / host \nViewModelはデフォルトで両者を継承する。任意のconstructorは super(host) で host を転送しなければならない \n \n\nView API \nviewChild / viewChildren / contentChild / contentChildren / effect / computed / 描画lifecycle APIは ViewModel 内に現れてはならない \n \n\n
\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-kit の ViewModelHost<T, K> 型を使うため、クラスフィールド初期化時にコピーされるのではなく、ViewModel method実行時に値が読まれます。基底constructorはホストの絞り込みと一度きりの描画フックを、kitの mountViewModel() ヘルパーへ委譲します。
\n✅ 明示的な非Signal依存
\nclass 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基底
\nclass ViewModel extends MainViewModel<FoodsPage> {}\nclass ViewModel extends ListViewModel<WineListPage> {}\nclass ViewModel extends ModelSearch<SearchPage, SearchCondition> {}\n
名前が ViewModel で終わる中間基底と、確立された ModelSearch 基底は、第1型引数が所有Componentと一致するときに受け入れられます。中間基底自体は ViewModelStore を継承している必要があります。
\nジェネリックなViewModelはデフォルトのComponentホストを使えます。所有検査にはそのデフォルトが使われます。
\nclass ViewModel<THost = MainPage> extends ViewModelStore<THost> {}\n
ハードプライベートなViewModel所有も問題ありません。
\nreadonly #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パターン
\nclass 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を転送する必要があります。
\nclass ViewModel extends ViewModelStore<ExamplePage, 'inventoryModel'> {\n protected override onMount(): void {\n this.host.inventoryModel.initialize();\n }\n}\n
互換のために即時constructorも有効です。
\nclass ViewModel extends ViewModelStore<ExamplePage> {\n constructor(host: ExamplePage) {\n super(host);\n registerCleanup();\n }\n}\n
❌ ViewModel上のView API
\nclass 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- Rule source
\n- Test source
\n
\n",
+ "html": "\n\nComponentの new ViewModel(this)、ViewModelStore<ComponentType, Keys> 継承を強制し、View APIをViewModelから排除する。
\n\n- ⭐️ このルールは
plugin:@rdlabo/rules/recommended プリセットに含まれます。 \n
\n
\nViewModel architecture patternを強制します。Angular Componentは new ViewModel(this) で初期化したViewModelを所有しなければなりません。少なくとも1つの一致するpropertyを要求しますが、追加のViewModel instanceは拒否しません。ViewModelは ViewModelStore<ComponentType> を継承し、host を再宣言したり、viewChild、effect、computed、afterNextRender などのView固有APIを含めたりしないでください。
\n ルール詳細
\n次の3つを検査します。
\n 1. ComponentはViewModelを所有する
\n@Component classには new ViewModel(this) で初期化したpropertyが必要です。constructor呼び出しの第1引数は this でなければなりません。
\n 2. ViewModelは ViewModelStore<ComponentType> を継承する
\nViewModel(または設定した viewModelClassName)というclassは、ViewModelStore<...>、名前が ViewModel で終わるbase、または ModelSearch を継承しなければなりません。最初のgeneric引数はhost Component型でなければなりません。中間classのgeneric defaultも解決します。
\n\nViewModelStore<ExamplePage, 'model' | 'form'> を使う場合、第2引数以降の型引数が許可されます。 \nViewModelStore を直接継承するときに型引数が2つを超えると報告されます。 \n- host型はViewModelを所有するComponentと一致する必要があります。
\n
\n 3. ViewModelにView APIを含めない
\nViewModel classでは次のAPIを呼び出せません。
\nviewChild, 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
正しい
\nimport { 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
\nComponent内で検索するclass名です。PageState など別の命名規則を使うプロジェクトで指定します。
\n viewModelStoreClassName
\n\n- 型:
string \n- デフォルト:
\"ViewModelStore\" \n
\nViewModelが継承すべきbase class名、または名前が ViewModel で終わる中間base class名です。
\n bannedApis
\n\n- 型:
string[] \n- デフォルト: 上記の一覧
\n
\nViewModel内で許可しない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- Rule source
\n- Test source
\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\ntryブロック内のPromise・RxJS・Angular Signalコンテキスト、Promise.resolve() による逃げ道、物理行数を制限する。
\n\n- ⭐️ このルールは
plugin:@rdlabo/rules/recommended プリセットに含まれます。 \n
\n
\ntry ブロック内の非同期/リアクティブ処理と物理コード行数を制限します。
\n ルール詳細
\nこのルールは try を同期例外向けの小さな境界として保ちます。デフォルトでは次を報告します。
\n\nawait、およびTypeScript型がPromiseライクな式 \nPromise.resolve() 呼び出し全般。同期例外をPromise rejectionへ変換するチェーンも含む \n- 型または基底型が
rxjs パッケージで宣言された式。Observable と Subject の派生を含む \n- Angularの
computed() および effect() コールバック内の try 文 \n- 物理コード行が3行を超える
try 本体 \n
\ntry 固有の検査では try 本体だけを見ます。catch と finally は対象外です。ネストした関数・クラス・ネストした try は別の実行境界であり、外側の try には帰属しません。Promise.resolve() の検査はファイル全体に適用されます。
\nPromiseのrejectionは通常、.catch() のようなPromiseエラー境界で扱うべきです。同期失敗をrejectionチャネルへ移すために Promise.resolve() でその境界を作り出さないでください。
\n// incorrect\nPromise.resolve()\n .then(() => fallibleSynchronousWork())\n .catch(handleError);\n
同期の try 境界は小さく保ち、その失敗を扱う責務のある層に置きます。Promise.resolve(value) で正規化するのではなく、値または既存のPromiseをそのまま返します。
\nRxJSのエラーは、catchError() や明示的なsubscriberのエラーハンドラなど、Observableのエラーチャネルで扱います。
\nPromiseライクとRxJSの型検出は、利用可能な場合にTypeScriptの型情報を使います。typed lintingがない場合、型依存の検査はESLintを止めずにスキップされ、構文ベースの await、Promise.resolve()、Angular Signalコンテキスト、maxLines の検査は引き続き動作します。完全な強制にはtyped lintingを設定します。例えば次のとおりです。
\nlanguageOptions: {\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
\nallowPromise: try 内のPromiseライク処理と await を許可する。 \nallowPromiseResolve: ファイル全体の専用 Promise.resolve() 検査を無効化する。try 本体内では、呼び出しが独立してPromiseライク処理でもあるため、allowPromise: true も必要になる。 \nallowRxjs: rxjs で宣言された型に裏打ちされた値と操作を許可する。Observable、Subject、およびそのサブクラスを含む。 \nallowInSignal: インラインのAngular computed() / effect() コールバック内の try を許可する。@angular/core からのエイリアスと名前空間importを認識する。ネストした関数・クラス本体は別の実行境界である。 \nmaxLines: try 本体の最大物理コード行数。サイズ検査を無効にする場合は false。 \n
\nallowPromise: false と allowRxjs: false は、typed lintingが設定されているときに完全に強制されます。型情報がない場合、それらのカテゴリでは await のような構文ベースの検査だけが残ります。
\nPromise.resolve() の検査は、シャドウされていないグローバル Promise と明示的な globalThis.Promise(静的ブラケット記法を含む)を認識します。エイリアスは意図的に追いません。ローカル宣言・importされた Promise という名前の値や、ローカルでシャドウされた globalThis は組み込みAPIとしては扱いません。
\nmaxLines では、外側の波括弧・コメント・空行を除外します。その他のトークンを含む一意の物理行を1回だけ数えます。内側の波括弧と複数行トークンは数えるため、フォーマットは意図的に結果へ影響します。境界を論理的にも見た目にも小さく保つためです。
\n 実装
\n\n- Rule source
\n- Test source
\n
\n",
+ "html": "\n\ntryブロック内のPromise、RxJS、Angular Signal context、Promise.resolve() による逃げ道、物理コード行数を制限する。
\n\n- ⭐️ このルールは
plugin:@rdlabo/rules/recommended プリセットに含まれます。 \n
\n
\ntry/catch は、実際にthrowする可能性がある小さな同期処理を保護するために使用してください。非同期処理、長いblock、reactive callbackを try 内に置くとerror boundaryが不明瞭になり、errorを握りつぶしたり誤った経路へ送ったりする可能性があります。このルールは、それらを制限します。
\n ルール詳細
\nすべての try blockを検査し、デフォルトでは次を報告します。
\n\ntry 内の await またはその他のPromise/thenable使用 \n- 逃げ道としての、
try の外も含むすべての Promise.resolve() \ntry 内のRxJS型または操作 \ncomputed() または effect() callback内の try block \n- 物理コード行が3行を超える
try block \n
\ntry に限定した検査では try 本体だけを調べ、catch と finally clauseは除外します。ネストした関数、class、try 文はそれぞれ別の実行境界であり、外側のblockには帰属しません。Promise.resolve() の検査はファイル全体に適用されます。
\nPromise-likeとRxJSの検出には、利用可能な場合TypeScript型情報を使用します。typed lintingがない場合、それらの検査はESLintを停止せずskipされますが、構文ベースの await、Promise.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
\ntry 内で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
\ntry 内でRxJSを使用できるようにします。
\n allowInSignal
\n\n- 型:
boolean \n- デフォルト:
false \n
\ncomputed() または effect() callback内で try blockを使用できるようにします。
\n maxLines
\n\n- 型:
number | false \n- デフォルト:
3 \n
\ntry block内の物理コード行数の上限です。サイズ検査を無効にするには false を指定します。外側の波括弧、comment、空行は除外され、それ以外のtokenを含む一意の行を1回数えます。
\n 例
\n 誤り
\nasync 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
正しい
\nfunction 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
有効にする場合
\ntry/catch を小さく明示的なerror boundaryとして維持したいすべてのプロジェクトで有効にしてください。Angular Signal codeや、Promise/RxJS中心のerror handlingから移行するときに特に有効です。
\nPromise.resolve() の検査は、shadowされていないglobal Promise と、静的bracket notationを含む明示的な globalThis.Promise を認識します。aliasは意図的に追跡しません。ローカルで宣言またはimportされた Promise や、shadowされた globalThis は組み込みAPIとして扱いません。
\n 実装
\n\n- Rule source
\n- Test source
\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として渡す
\nBoundAttributeで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- Rule source
\n- Test source
\n
\n",
+ "html": "\n\nテンプレートでAngular Signalにアクセスするとき () を要求する
\n\n- ⭐️ このルールは
plugin:@rdlabo/rules/recommended プリセットに含まれます。 \n
\n
\nAngular Signalは関数です。テンプレートで現在値を読み取るには、Signalを () 付きで呼び出す必要があります。RxJSの BehaviorSubject や model() inputから移行するとき、括弧の付け忘れはよくあるミスです。このルールはAngularテンプレート内のSignal識別子を検出し、{{ count }} や [hidden]=\"count\" のような裸の読み取りを報告します。
\n ルール詳細
\n各 @Component のAngularテンプレートを解析し、次からSignal識別子を収集します。
\n\n- callee名が
signal、model、computed、linkedSignal、input、toSignal のいずれかである呼び出しによって初期化された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
\ntemplate と templateUrl の両方の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 有効にする場合
\nSignalを使用するすべてのAngularプロジェクトで有効にしてください。Observable ベースのコードから移行するときや、テンプレート内で呼び出す必要のあるSignal風objectを返す model() と input() を導入するときに特に有効です。
\n 関連項目
\n\n 実装
\n\n- Rule source
\n- Test source
\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として渡す
\nSignalを値として読むのではなく、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- Rule source
\n- Test source
\n
\n",
+ "html": "\n\nSignalがSignalとして正しく使われているか検査する。
\n\n- ⭐️ このルールは
plugin:@rdlabo/rules/recommended プリセットに含まれます。 \n- ✒️ コマンドラインの
--fix オプションで、このルールが報告する問題の一部を自動修正できます。 \n
\n
\nAngular Signalはgetter関数です。読み取りには () が必要で、書き込みには .set() または .update() を使う必要があります。このルールは、Signal変数を通常の値のように扱うコードを検出し、一般的な誤りの多くを自動修正できます。
\n ルール詳細
\nSignal factory(signal、model、input、linkedSignal、toSignal、asReadonly)で初期化されたclass propertyを追跡し、次のような誤用を報告します。
\n\n- expression contextでの
this.count() ではなく this.count \nthis.count.set(value) ではなく this.count() = value \nthis.user.update(user => ({ ...user, name: 'Jane' })) ではなく this.user().name = 'Jane' \nthis.items.update(items => { items.push(x); return items; }) ではなく this.items().push(x) \nthis.#user.set(value) ではなく、Signal propertyへの直接代入 this.#user = value \n
\nSignal参照が期待されるcontextと、値が期待されるcontextを区別します。たとえば、Signal objectをpropsとして渡すことは許可されます。
\nconst props = { food: this.food };\nlaunchModal({ food: this.food });\n
例
\n 誤り
\nexport 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
正しい
\nexport 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\nthis.count = value -> this.count.set(value) \nthis.count() = value -> this.count.set(value) \nthis.count().x = value -> this.count.update(value => ({ ...value, x: value })) \nthis.count().push(x) -> this.count.update(value => { value.push(x); return value; }) \n
\n オプション
\nこのルールにオプションはありません。
\n 有効にする場合
\nSignalを使用するすべてのAngularプロジェクトで有効にしてください。テンプレート内のSignal使用を検査する @rdlabo/rules/signal-use-as-signal-template と相互補完します。
\n 関連項目
\n\n 実装
\n\n- Rule source
\n- Test source
\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..613e3e6 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?
\nThis 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
\nOf 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.
\nThis 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
\nAfter Installation, initialize addIcons and collect icons before production builds:
\nnpx @rdlabo/ionic-angular-collect-icons --initialize true\n
Details: Initialize and Usage.
\n Installation
\nnpm install @rdlabo/ionic-angular-collect-icons --save-dev\n
Documentation
\nStart 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?
\nThis 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
\nOf 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.
\nThis 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
\nAfter Installation, initialize addIcons and collect icons before production builds:
\nnpx @rdlabo/ionic-angular-collect-icons --initialize true\n
Details: Initialize and Usage.
\n Migrating from Ionic Angular 8
\nCommit the consuming application's current changes, then run Ionic's official
\nmigration tool from the application root:
\nnpx @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
\nnpm 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
\nStart 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
\nThis 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
\nIonic 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.
\nRun it from the root of the Ionic application:
\nnpx @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.
\nTo preview the migration without writing files, run:
\nnpx @ionic/migrate --dry-run\n
After the official migration finishes, update this collector and confirm that
\nthe resulting dependency versions meet the requirements above:
\nnpm 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
\nIonic 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
\nIonicModule 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
\nIonic 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
\nInitialize the generated icon registration if the application has not already
\ndone so:
\nnpx @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
\nThe 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+.
\nion-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\". \nion-nav no longer integrates with ion-router. \nion-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
\nAfter 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",
@@ -185,7 +244,7 @@ export const PROJECT = {
"file": "api.md",
"section": "Reference",
"path": "/projects/ionic-angular-collect-icons/docs/api",
- "html": "Command reference for @rdlabo/ionic-angular-collect-icons v2.1.0.
\n Command
\n command npx @rdlabo/ionic-angular-collect-icons
Scans Angular source and templates, then writes the Ionicons used by the application to src/use-icons.ts by default.
\n\n\nOption \nType \nDescription \nDefault \n \n\n\n\n--dry-run \nboolean \nReports changes without writing files. \nfalse \n \n\n--interactive \nboolean \nCollects all options through prompts and enables result inspection. \nfalse \n \n\n--initialize \nboolean \nAdds the addIcons initialization and removes component-level registrations. \nfalse \n \n\n--project-path \nstring \nProject directory whose src tree is scanned. \ncurrent directory \n \n\n--icon-path \nstring \nGenerated icon registration file. \nsrc/use-icons.ts \n \n\n
\n\n\n",
+ "html": "Command reference for @rdlabo/ionic-angular-collect-icons v3.0.0.
\n Command
\n command npx @rdlabo/ionic-angular-collect-icons
Scans Angular source and templates, then writes the Ionicons used by the application to src/use-icons.ts by default.
\n\n\nOption \nType \nDescription \nDefault \n \n\n\n\n--dry-run \nboolean \nReports changes without writing files. \nfalse \n \n\n--interactive \nboolean \nCollects all options through prompts and enables result inspection. \nfalse \n \n\n--initialize \nboolean \nAdds the addIcons initialization and removes component-level registrations. \nfalse \n \n\n--project-path \nstring \nProject directory whose src tree is scanned. \ncurrent directory \n \n\n--icon-path \nstring \nGenerated icon registration file. \nsrc/use-icons.ts \n \n\n
\n\n\n",
"headings": [
{
"id": "command",
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 を初期化し、本番ビルド前にアイコンを収集します。
\nnpx @rdlabo/ionic-angular-collect-icons --initialize true\n
\n インストール
\nnpm 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 を初期化し、本番ビルド前にアイコンを収集します。
\nnpx @rdlabo/ionic-angular-collect-icons --initialize true\n
\n Ionic Angular 8からの移行
\nアプリケーションの変更をcommitしてから、アプリケーションrootでIonic公式のmigration toolを実行します。
\nnpx @ionic/migrate\n
安全に自動化できる変更が適用され、手動確認が必要な項目が表示されます。完了後にこのpackageを更新し、残りの確認事項はIonic Angular 9への移行を参照してください。
\n インストール
\nnpm 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を実行する
\nIonicは公式migration toolの利用を推奨しています。最初にアプリケーションの現在の変更をcommitしてください。migratorはfileを直接編集するため、差分の確認や取り消しにcommitを使えるcleanなGit worktreeが必要です。
\nIonicアプリケーションのrootで実行します。
\nnpx @ionic/migrate\n
migratorはインストール済みのIonic major versionを検出し、依存関係を更新します。安全な自動修正とformat、依存関係の再installを実行し、手動確認が必要な変更をchecklistで表示します。
\nfileを書き換えず移行内容をpreviewするには次を実行します。
\nnpx @ionic/migrate --dry-run\n
公式migration完了後にcollectorを更新し、依存packageのversionが上記要件を満たすことを確認します。
\nnpm install --save-dev @rdlabo/ionic-angular-collect-icons@latest\n
以降では、生成された差分とmigratorの手動確認checklistで検証すべきIonic Angular 9の重要な変更を説明します。
\n standalone migrationを完了する
\nIonic 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 を置き換える
\nIonicModule は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を使う
\nIonic 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登録を初期化します。
\nnpx @rdlabo/ionic-angular-collect-icons --initialize true\n
使い方の説明に従い、本番build前にcollectorを引き続き実行します。
\n Ionic 9のほかの変更を確認する
\ncollectorは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以降です。
\nion-input と ion-searchbar の autocorrect propertyはbooleanになりました。 \n- legacy picker componentと
PickerController は削除されました。 \n- sheet modal handleのdefaultは
handleBehavior=\"cycle\" になりました。 \nion-nav は ion-router と連携しなくなりました。 \nion-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\nOption \nType \nDescription \nDefault \n \n\n\n\n--dry-run \nboolean \nFileを書き込まず変更内容を表示します。 \nfalse \n \n\n--interactive \nboolean \nPromptですべてのoptionを指定し、結果を確認します。 \nfalse \n \n\n--initialize \nboolean \naddIcons の初期化を追加し、Component単位の登録を削除します。 \nfalse \n \n\n--project-path \nstring \nsrc をscanするProject directoryです。 \n現在のdirectory \n \n\n--icon-path \nstring \n生成するicon登録fileです。 \nsrc/use-icons.ts \n \n\n
\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\nOption \nType \nDescription \nDefault \n \n\n\n\n--dry-run \nboolean \nFileを書き込まず変更内容を表示します。 \nfalse \n \n\n--interactive \nboolean \nPromptですべてのoptionを指定し、結果を確認します。 \nfalse \n \n\n--initialize \nboolean \naddIcons の初期化を追加し、Component単位の登録を削除します。 \nfalse \n \n\n--project-path \nstring \nsrc をscanするProject directoryです。 \n現在のdirectory \n \n\n--icon-path \nstring \n生成するicon登録fileです。 \nsrc/use-icons.ts \n \n\n
\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..54908d1 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
\nThis 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.
\nI'm also working on the Android Design (Material Design 3) theme. Be sure to catch up!
\n\n Quick start
\nAfter Installation, import the theme CSS. Details are in Installation below.
\n Installation
\nThis is a CSS theme for extending your Ionic project. It does not work on its own, so use it together with the Ionic Framework.
\nnpm install @rdlabo/ionic-theme-ios26\n
Note: If you use @ionic/core@ < 8.8.1, use @rdlabo/ionic-theme-ios26@2.2.1.
\nAnd 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.
\nimport { 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
\nStart with Installation, then Using ion-item-group when you use inset lists.
\n\n- Using ion-item-group — required markup for inset lists.
\n- Features — CSS variables,
.ios26-disabled, liquid glass. \n- Experimental Animation — tab bar and searchable effects.
\n- iOS 18 — load the theme only on iOS 26.
\n- Migration — selective component imports and dark mode.
\n
\n",
+ "html": " Overview
\nThis 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.
\nI'm also working on the Android Design (Material Design 3) theme. Be sure to catch up!
\n\n Quick start
\nAfter Installation, import the theme CSS. Details are in Installation below.
\n Installation
\nThis is a CSS theme for extending your Ionic project. It does not work on its own, so use it together with the Ionic Framework.
\nnpm install @rdlabo/ionic-theme-ios26\n
Note: If you use @ionic/core@ < 8.8.1, use @rdlabo/ionic-theme-ios26@2.2.1.
\nAnd 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
\nInstall the MD3 theme to style both Ionic modes from the same application.
\nThe current releases of both themes require @ionic/core 8.8.1 or later.
\nnpm 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.
\nConfigure both transition implementations when both themes are installed:
\nimport { 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.
\nimport { 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
\nStart with Installation, then Using ion-item-group when you use inset lists.
\n\n- Using ion-item-group — required markup for inset lists.
\n- Special markup and classes — opt-in markup and utility classes used by the theme.
\n- Features — CSS variables, Liquid Glass, selective imports, and dark mode.
\n- Experimental Animation — tab bar and searchable effects.
\n- iOS 18 — load the theme only on iOS 26.
\n- Migration — required changes when upgrading major versions.
\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?
\nIt is only required when the following condition is met:
\n\n- You have enabled the
inset property on ion-list \n
\nOnly 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
\nBy 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
\n Solution: Background Color Separation
\nTo 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
\nThis 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
\nIf 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.
\nThe 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
\nIonic 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
\nThe 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.
\nWhen 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
\nTo 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.scss
\n Liquid Glass mixin
\nImport 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
\nFor 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
\nUse SCSS when selectively importing components with dark mode support because the selectors differ between Always, System, and Class modes.
\nAlways:
\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
\nBrowse 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
\nTo 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.scss
\n .ios26-disabled Class
\nAdd 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
\nImport 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
\nTo achieve higher fidelity to iOS26 design, you can implement additional design provided by this library. For more details, please visit:
\nhttps://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
\nSolid 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
\nPlace 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 & 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
\nAdd .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.
\nThis 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
\nAdd .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
\nThe 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.
\nPlace it in a toolbar with a color, such as color=\"light\"; the classic background is derived from that color's contrast value.
\nThe 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
\nAdd .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
\nAdd .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.
\nThis feature is experimental. The library can be used without this feature.
\n Sheet of Glass with ion-tab-button / ion-segment-button
\nBy registering ion-tab-bar / ion-segment, you can display animation effects on ion-tab-button / ion-segment-button
\n\nimport { 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
\nEnable Searchable for the DOM structure with the specified markup inner ion-tabs.
\n\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
\nRegister an ion-tab-bar or ion-segment element to add a moving selection effect to its buttons.
\n\nimport { 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
\nUse the following structure inside ion-tabs to animate a search button into a search toolbar.
\n\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
\nIf 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
\nIf 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
\nWhen 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\nNote: Currently, only ion-button has separate dark mode styling applied.
\n
\nAlways (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
\nThe 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
\nVersion 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.
\nimport { 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
\nThe 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
\nReplace 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
}
],
@@ -215,7 +284,7 @@ export const PROJECT = {
"file": "api.md",
"section": "Reference",
"path": "/projects/ionic-theme-ios26/docs/api",
- "html": "Reference for the JavaScript API exported by @rdlabo/ionic-theme-ios26 v2.3.2. CSS and Sass entry points remain documented in the README.
\n Effects
\n function registerTabBarEffect
(targetElement: HTMLElement) => registeredEffect | undefined
Registers the liquid-glass selection effect for an Ionic tab bar.
\n\n\n function registerSegmentEffect
(targetElement: HTMLElement) => registeredEffect | undefined
Registers the liquid-glass selection effect for an Ionic segment.
\n\n\n interface registeredEffect
\n\n\nMember \nType \nDescription \n \n\n\n\ndestroy \n() => void \nRemoves listeners and effect elements created by registration. \n \n\n
\n\n interface EffectScales
\n\n\nProp \nType \nDescription \n \n\n\n\nsmall \nstring \nSmall effect scale. \n \n\nmedium \nstring \nMedium effect scale. \n \n\nlarge \nstring \nLarge effect scale. \n \n\nxlarge \nstring \nExtra-large effect scale. \n \n\n
\n\n Searchable tab bar
\n function attachTabBarSearchable
(ionTabBar: HTMLElement, ionFabButton: HTMLElement, ionFooter: HTMLElement) => TabBarSearchableFunction
Attaches the searchable tab-bar transition and returns its event handler.
\n\n\n enum TabBarSearchableType
\n\n\nMember \nValue \nDescription \n \n\n\n\nEnter \n\"enter\" \nEnters searchable mode. \n \n\nLeave \n\"leave\" \nLeaves searchable mode. \n \n\n
\n\n type alias TabBarSearchableFunction
(event: Event, type: TabBarSearchableType) => Promise<void>
\n\n Animations
\n function iosTransitionAnimation
(navEl: HTMLElement, opts: TransitionOptions) => Animation
Builds the package's iOS navigation transition.
\n\n\n function popoverEnterAnimation
(baseEl: HTMLElement, opts?: any) => Animation
Builds the iOS popover enter animation.
\n\n\n function popoverLeaveAnimation
(baseEl: HTMLElement) => Animation
Builds the iOS popover leave animation.
\n\n\n",
+ "html": "Reference for the JavaScript API exported by @rdlabo/ionic-theme-ios26 v3.0.0. CSS and Sass entry points remain documented in the README.
\n Effects
\n function registerTabBarEffect
(targetElement: HTMLElement) => registeredEffect | undefined
Registers the liquid-glass selection effect for an Ionic tab bar.
\n\n\n function registerSegmentEffect
(targetElement: HTMLElement) => registeredEffect | undefined
Registers the liquid-glass selection effect for an Ionic segment.
\n\n\n interface registeredEffect
\n\n\nMember \nType \nDescription \n \n\n\n\ndestroy \n() => void \nRemoves listeners and effect elements created by registration. \n \n\n
\n\n interface EffectScales
\n\n\nProp \nType \nDescription \n \n\n\n\nsmall \nstring \nSmall effect scale. \n \n\nmedium \nstring \nMedium effect scale. \n \n\nlarge \nstring \nLarge effect scale. \n \n\nxlarge \nstring \nExtra-large effect scale. \n \n\n
\n\n Searchable tab bar
\n function attachTabBarSearchable
(ionTabBar: HTMLElement, ionFabButton: HTMLElement, ionFooter: HTMLElement) => TabBarSearchableFunction
Attaches the searchable tab-bar transition and returns its event handler.
\n\n\n enum TabBarSearchableType
\n\n\nMember \nValue \nDescription \n \n\n\n\nEnter \n\"enter\" \nEnters searchable mode. \n \n\nLeave \n\"leave\" \nLeaves searchable mode. \n \n\n
\n\n type alias TabBarSearchableFunction
(event: Event, type: TabBarSearchableType) => Promise<void>
\n\n Animations
\n function iosTransitionAnimation
(navEl: HTMLElement, opts: TransitionOptions) => Animation
Builds the package's iOS navigation transition.
\n\n\n function popoverEnterAnimation
(baseEl: HTMLElement, opts?: any) => Animation
Builds the iOS popover enter animation.
\n\n\n function popoverLeaveAnimation
(baseEl: HTMLElement) => Animation
Builds the iOS popover leave animation.
\n\n\n",
"headings": [
{
"id": "effects",
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
\nDEMOはこちら: https://ionic-theme-ios26.netlify.app/
\n 概要
\nこのライブラリは、IonicアプリケーションにiOS26デザインシステムをもたらすCSS/JSファイルを提供します。Ionicコンポーネントの見た目を、最新のiOS26デザインガイドラインに合わせて更新します。
\nAndroid Design(Material Design 3)テーマも作成中です。ぜひチェックしてください!
\n\n クイックスタート
\nインストール のあと、テーマ CSS をインポートします。詳細は下のインストールです。
\n インストール
\nこれはIonicプロジェクトを拡張するためのCSSテーマです。単体では動作しないため、Ionic Frameworkと一緒に使ってください。
\nnpm 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の設定オプションに次を追加してください。
\nimport { 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- ion-item-groupの使用方法 — inset リストに必要なマークアップ。
\n- 機能 — CSS 変数、
.ios26-disabled、Liquid Glass。 \n- 実験的なアニメーション — タブバーと Searchable。
\n- iOS 18 — iOS 26 だけでテーマを読む。
\n- 移行 — コンポーネント単位の import とダークモード。
\n
\n",
+ "html": "IonicアプリケーションにiOS26デザインシステムを適用するCSS/JSテーマライブラリです。
\n
\nDEMOはこちら: https://ionic-theme-ios26.rdlabo.dev/
\n 概要
\nこのライブラリは、IonicアプリケーションにiOS26デザインシステムをもたらすCSS/JSファイルを提供します。Ionicコンポーネントの見た目を、最新のiOS26デザインガイドラインに合わせて更新します。
\nAndroid Design(Material Design 3)テーマも作成中です。ぜひチェックしてください!
\n\n クイックスタート
\nインストール のあと、テーマ CSS をインポートします。詳細は下のインストールです。
\n インストール
\nこれはIonicプロジェクトを拡張するためのCSSテーマです。単体では動作しないため、Ionic Frameworkと一緒に使ってください。
\nnpm 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以降が必要です。
\nnpm 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-vars と md3-vars を明示することで、2つの変数モジュールが同じデフォルトnamespaceを使うことを防ぎます。
\n両テーマをインストールした場合は、両方のtransition実装を設定します。
\nimport { 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テーマだけをインストールした場合は、次のようにアニメーションを設定します。
\nimport { 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- ion-item-groupの使用方法 — inset リストに必要なマークアップ。
\n- 特別なマークアップとクラス — テーマで使う任意のマークアップとutility class。
\n- 機能 — CSS変数、Liquid Glass、選択的import、ダークモード。
\n- 実験的なアニメーション — タブバーと Searchable。
\n- iOS 18 — iOS 26 だけでテーマを読む。
\n- 移行 — major version更新時に必要な変更。
\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\nion-list で inset プロパティを有効にしている \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デザイン再現の課題
\nIonic Frameworkのデフォルトでは、ion-list に背景色があり、ion-list-header はリストの一部として扱われます。しかし、この構造では iOSのネイティブなデザインパターン を正確に再現できません。
\n
\n 解決策: 背景色の分離
\niOSデザインを忠実に再現するため、このテーマは次の変更を行います:
\n\nion-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-list で inset を使う場合にのみ ion-item-group が必要です \n- 目的: iOS 26のネイティブなデザインパターンを正確に再現するため
\n
\n",
+ "html": "ほとんどのIonicマークアップは変更せずに使えます。ion-list で inset=\"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が必要な理由
\nIonicは通常、ion-list 自体に背景を設定するため、ion-list-header もitemと同じsurface内に表示されます。iOS 26のlayoutではheaderとitemのsurfaceを分離します。
\n
\nそのため、このテーマは次のようにstyleを適用します。
\n\n- inset
ion-list の背景を透明にする \n- itemのsurfaceを
ion-item-group に適用する \nion-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.scss
\n Liquid Glass mixin
\nLiquid 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
\nDark Mode対応のコンポーネントを個別にimportする場合は、Always、System、Class modeでselectorが異なるためSCSSを使います。
\nAlways:
\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\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.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
\nliquid 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
追加デザイン
\niOS26デザインへの忠実度をさらに高めるため、このライブラリが提供する追加デザインを実装できます。詳細は次を参照してください:
\nhttps://ionic-theme-ios26.netlify.app/main/docs
\n",
+ "path": "/projects/ionic-theme-ios26/docs/special-markup",
+ "html": "ほとんどのIonicマークアップは変更せずに使えます。以下はテーマが提供する明示的なopt-inです。
\n Primaryのsubmit button
\nsolidの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
\nslotを指定しない ion-label と ion-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 & 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-note に slot=\"end\" を指定します。
\n inset listのsection header
\n.item-group-header を ion-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
\nsegment 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
\nThemeはdefaultでsearch barにiOS 26の外観を適用します。collapse=\"condense\" を指定した ion-header のlarge titleの下に表示するsearch fieldには、.searchbar-classic を追加します。従来の塗りつぶされたiOSの外観になり、固定headerに残らずlarge titleと一緒にcollapseします。
\ncolor=\"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
\nsearch 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
\nion-tab-bar / ion-segment を登録すると、ion-tab-button / ion-segment-button にアニメーション効果を表示できます。
\n\nimport { 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-bar と ion-fab-button の Searchable
\nion-tabs 内の指定マークアップ構成で Searchable を有効にします。
\n\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
\nion-tab-bar または ion-segment elementを登録すると、そのbuttonに移動するselection effectを追加できます。
\n\nimport { 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-bar と ion-fab-button の Searchable
\nion-tabs 内で次の構造を使うと、search buttonからsearch toolbarへのanimationを適用できます。
\n\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を使います。Always、System、Class モードでセレクタが異なるためです。
\n\nNote: 現時点では、ion-button にのみ個別のダークモードスタイルが適用されています。
\n
\nAlways(常時ダークモード):
\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 に変更する
\nsection 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 を設定する
\nversion 2ではpackageのnavigation transitionが必要です。このtransitionは、Large Titleをback buttonのlabelへ動かしていた古い animateBackButton() の挙動を除き、Ionicのdefault iOS transitionに従います。
\nimport { 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を更新する
\nJavaScript 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\nMember \nType \nDescription \n \n\n\n\ndestroy \n() => void \n登録時に作成したlistenerとeffect elementを削除します。 \n \n\n
\n\n interface EffectScales
\n\n\nProp \nType \nDescription \n \n\n\n\nsmall \nstring \nSmall effectのscaleです。 \n \n\nmedium \nstring \nMedium effectのscaleです。 \n \n\nlarge \nstring \nLarge effectのscaleです。 \n \n\nxlarge \nstring \nExtra Large effectのscaleです。 \n \n\n
\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\nMember \nValue \nDescription \n \n\n\n\nEnter \n\"enter\" \nSearchable modeへ入ります。 \n \n\nLeave \n\"leave\" \nSearchable modeから戻ります。 \n \n\n
\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\nMember \nType \nDescription \n \n\n\n\ndestroy \n() => void \n登録時に作成したlistenerとeffect elementを削除します。 \n \n\n
\n\n interface EffectScales
\n\n\nProp \nType \nDescription \n \n\n\n\nsmall \nstring \nSmall effectのscaleです。 \n \n\nmedium \nstring \nMedium effectのscaleです。 \n \n\nlarge \nstring \nLarge effectのscaleです。 \n \n\nxlarge \nstring \nExtra Large effectのscaleです。 \n \n\n
\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\nMember \nValue \nDescription \n \n\n\n\nEnter \n\"enter\" \nSearchable modeへ入ります。 \n \n\nLeave \n\"leave\" \nSearchable modeから戻ります。 \n \n\n
\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..e909150 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
\nThis 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.
\nThis 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.
\nIf 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
\nIf you need a more comprehensive Material Design 3 implementation, you may also be interested in:
\n\n- md3-for-ionic by danielkleebinder
\n
\n\nNote: 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
\nAfter Installation, import the theme CSS and set navAnimation as shown below.
\n Installation
\nThis is a CSS theme for extending your Ionic project. It does not work on its own, so use it together with the Ionic Framework.
\nnpm install @rdlabo/ionic-theme-md3\n
Note: If you use @ionic/core@ < 8.8.0, use @rdlabo/ionic-theme-md3@1.0.2.
\nAnd 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.
\nimport { 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
\nStart with Installation. Pair this theme with @rdlabo/ionic-theme-ios26 when you need both platforms from one markup tree.
\n",
+ "html": " Overview
\nThis 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.
\nThis 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.
\nIf 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
\nIf you need a more comprehensive Material Design 3 implementation, you may also be interested in:
\n\n- md3-for-ionic by danielkleebinder
\n
\n\nNote: 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
\nAfter Installation, import the theme CSS and set navAnimation as shown below.
\n Installation
\nThis is a CSS theme for extending your Ionic project. It does not work on its own, so use it together with the Ionic Framework.
\nnpm install @rdlabo/ionic-theme-md3\n
Note: If you use @ionic/core@ < 8.8.0, use @rdlabo/ionic-theme-md3@1.0.2.
\nAnd 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
\nInstall the iOS 26 theme to style both Ionic modes from the same application.
\nThe 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.
\nnpm 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.
\nConfigure both transition implementations when both themes are installed:
\nimport { 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.
\nimport { 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
\nStart with Installation. Pair this theme with @rdlabo/ionic-theme-ios26 when you need both platforms from one markup tree.
\n\n- Special markup — opt-in component combinations used by the demo.
\n- Using ion-item-group — shared inset-list markup for iOS 26 and MD3.
\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
\nPlace 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 & 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
\nAdd .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
\nAdd .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.
\nThis 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
\nAdd .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.
\nThe 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
\nThe 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.
\nThe 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
\nFor 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
\nThe 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",
@@ -75,7 +157,7 @@ export const PROJECT = {
"file": "api.md",
"section": "Reference",
"path": "/projects/ionic-theme-md3/docs/api",
- "html": "Reference for the JavaScript API exported by @rdlabo/ionic-theme-md3 v1.1.0. CSS entry points remain documented in the README.
\n Animation
\n function mdTransitionAnimation
(_: HTMLElement, opts: TransitionOptions) => Animation
Builds the Material Design 3 navigation transition for Ionic.
\n\n\n",
+ "html": "Reference for the JavaScript API exported by @rdlabo/ionic-theme-md3 v2.0.0. CSS entry points remain documented in the README.
\n Animation
\n function mdTransitionAnimation
(_: HTMLElement, opts: TransitionOptions) => Animation
Builds the Material Design 3 navigation transition for Ionic.
\n\n\n",
"headings": [
{
"id": "animation",
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
\nDEMOはこちら: 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- md3-for-ionic by danielkleebinder
\n
\n\nNote: このテーマは Ionic の設計方針と @rdlabo/ionic-theme-ios26 との互換性を目的に作られており、厳密で完全な MD3 再現を意図したものではありません。
\n
\n クイックスタート
\nインストール のあと、テーマ CSS をインポートし、下のとおり navAnimation を設定します。
\n インストール
\nこれはIonicプロジェクトを拡張するためのCSSテーマです。単体では動作しないため、Ionic Frameworkと一緒に使ってください。
\nnpm 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の設定オプションに次を追加してください。
\nimport { 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
\nDEMOはこちら: 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- md3-for-ionic by danielkleebinder
\n
\n\nNote: このテーマは Ionic の設計方針と @rdlabo/ionic-theme-ios26 との互換性を目的に作られており、厳密で完全な MD3 再現を意図したものではありません。
\n
\n クイックスタート
\nインストール のあと、テーマ CSS をインポートし、下のとおり navAnimation を設定します。
\n インストール
\nこれはIonicプロジェクトを拡張するためのCSSテーマです。単体では動作しないため、Ionic Frameworkと一緒に使ってください。
\nnpm 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をアップグレードしてください。
\nnpm 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-vars と md3-vars を明示することで、2つの変数モジュールが同じデフォルトnamespaceを使うことを防ぎます。
\n両テーマをインストールした場合は、両方のtransition実装を設定します。
\nimport { 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テーマだけをインストールした場合は、次のようにアニメーションを設定します。
\nimport { 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- 特別なマークアップ — demoで使う任意のコンポーネント構造。
\n- ion-item-groupの使用方法 — iOS 26とMD3で共有するinset listのマークアップ。
\n- 移行 — テーマのマークアップ更新時に必要な変更。
\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
\nslotを指定しない ion-label と ion-note を隣接させると、2行のitemとして表示します。通常の末尾noteとして表示したい場合は、ion-note に slot=\"end\" を指定します。
\n<ion-list inset=\"true\">\n <ion-item-group>\n <ion-item>\n <ion-label>Network & 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
\nbuttonの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-header を ion-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-list で inset=\"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 に適用する \nion-list-header をsurfaceの外に置く \n
\n2行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 に変更する
\nsection 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.
\nnpm 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\nRequirement \nMinimum \n \n\n\n\nCapacitor \n8 \n \n\niOS \n15.0 \n \n\nAndroid minSdkVersion \n26 \n \n\n
\n Platform and connection types
\ndiscoverReaders takes a TerminalConnectTypes value. Support is not the same on every platform.
\n\n\n\nTerminalConnectTypes \nWeb \niOS \nAndroid \n \n\n\n\nInternet \nYes — the only supported type \nYes \nYes \n \n\nBluetooth \nNo \nYes \nYes \n \n\nTapToPay \nNo \nYes \nYes \n \n\nUsb \nNo \nUnimplemented \nYes \n \n\nHandOff \nNo \nUnimplemented \nYes (Apps on Devices) \n \n\nSimulated \nNo \nUnimplemented as a discover type \nTreated as Bluetooth discovery \n \n\n
\nOn 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.
\nWeb discoverReaders rejects with an unavailable error for any type other than Internet.
\n Platform-only APIs
\n\n\n\nAPI \nWeb \niOS \nAndroid \n \n\n\n\nsetTapToPayUxConfiguration \nNo-op (logs only) \nUnimplemented \nYes — call after initialize, before connectReader \n \n\nisTapToPayAccountLinked \nUnavailable (throws) \nYes — iOS 16.4+, after initialize \nUnimplemented \n \n\n
\nSee Tap to Pay for the setup sequence and limitations.
\n Web no-op and unsupported lifecycle methods
\nThese methods exist on the plugin interface but do not drive the Stripe Terminal JS SDK on web:
\n\ncancelDiscoverReaders — no-op \nsetSimulatorConfiguration — no-op \ninstallAvailableUpdate — no-op \ncancelInstallUpdate — no-op \nrebootReader — no-op \ncancelReaderReconnection — no-op \nsetTapToPayUxConfiguration — no-op \n
\nisTapToPayAccountLinked throws unavailable on web.
\nInternet 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
\nNo additional steps are necessary. Only Internet readers are available.
\n iOS configuration
\nNo additional steps are necessary for the plugin. USB, HandOff, and setTapToPayUxConfiguration are unimplemented on iOS.
\n Android configuration
\nAdd 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.
\nAnd 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.
\nnpm 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\nRequirement \nMinimum \n \n\n\n\nCapacitor \n8 \n \n\niOS \n15.0 \n \n\nAndroid minSdkVersion \n26 \n \n\n
\n Platform and connection types
\ndiscoverReaders takes a TerminalConnectTypes value. Support is not the same on every platform.
\n\n\n\nTerminalConnectTypes \nWeb \niOS \nAndroid \n \n\n\n\nInternet \nYes — the only supported type \nYes \nYes \n \n\nBluetooth \nNo \nYes \nYes \n \n\nTapToPay \nNo \nYes \nYes \n \n\nUsb \nNo \nUnimplemented \nYes \n \n\nHandOff \nNo \nUnimplemented \nYes (Apps on Devices) \n \n\nSimulated \nNo \nUnimplemented as a discover type \nTreated as Bluetooth discovery \n \n\n
\nOn 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.
\nWeb discoverReaders rejects with an unavailable error for any type other than Internet.
\n Platform-only APIs
\n\n\n\nAPI \nWeb \niOS \nAndroid \n \n\n\n\nsetTapToPayUxConfiguration \nNo-op (logs only) \nUnimplemented \nYes — call after initialize, before connectReader \n \n\nisTapToPayAccountLinked \nUnavailable (throws) \nYes — iOS 16.4+, after initialize \nUnimplemented \n \n\n
\nSee Tap to Pay for the setup sequence and limitations.
\n Web no-op and unsupported lifecycle methods
\nThese methods exist on the plugin interface but do not drive the Stripe Terminal JS SDK on web:
\n\ncancelDiscoverReaders — no-op \nsetSimulatorConfiguration — no-op \ninstallAvailableUpdate — no-op \ncancelInstallUpdate — no-op \nrebootReader — no-op \ncancelReaderReconnection — no-op \nsetTapToPayUxConfiguration — no-op \n
\nisTapToPayAccountLinked throws unavailable on web.
\nInternet 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
\nNo additional steps are necessary. Only Internet readers are available.
\n iOS configuration
\nNo additional steps are necessary for the plugin. USB, HandOff, and setTapToPayUxConfiguration are unimplemented on iOS.
\n Android configuration
\nAdd 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.
\nAnd 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.
\nThe official demo exercises Tap to Pay, Internet, and Bluetooth in demo/angular.
\n Platform prerequisites
\n\n\n\nPlatform \nSupported \nNotes \n \n\n\n\nAndroid \nYes \nNFC-capable device, location permission, Stripe Tap to Pay on Android eligibility. minSdkVersion 26. \n \n\niOS \nYes \nTap to Pay on iPhone, iOS 16.4+ for the account-link check. setTapToPayUxConfiguration is unimplemented. \n \n\nWeb \nNo \ndiscoverReaders({ type: TapToPay }) is unavailable. \n \n\n
\nComplete Stripe Dashboard Terminal setup and create a Location. Pass that locationId into discoverReaders; the plugin uses it when connecting the Tap to Pay reader.
\nAndroid 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- Register application-level listeners.
\n- Register an authenticated connection-token provider with
RequestedConnectionToken + setConnectionToken, then call initialize. \n- On iOS, call
isTapToPayAccountLinked (do not cache the result). \n- On Android, optionally call
setTapToPayUxConfiguration. \ndiscoverReaders with type: TerminalConnectTypes.TapToPay and locationId. \nconnectReader with the discovered reader. \n- Collect and confirm a
card_present PaymentIntent as in Collect a Payment. \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 Account-link check
\nisTapToPayAccountLinked 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.
\nThe 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.
\nAndroid 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.
isTapToPayAccountLinked(options?: IsTapToPayAccountLinkedOptions | undefined) => Promise<{ isLinked: boolean; }>
\n\n\n\n\n\n interface IsTapToPayAccountLinkedOptions
Options for isTapToPayAccountLinked.
\n\n\nProp \nType \nDescription \nSince \n \n\n\n\nonBehalfOf \nstring \nConnected account ID, for Stripe Connect platforms. Omit to check the account that owns the API key. \n8.2.0 \n \n\n
\n\n\n UX configuration
\nsetTapToPayUxConfiguration is Android only. Call it after initialize() and before connectReader(). iOS returns unimplemented; web logs and returns.
\nThe 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\nProp \nType \nDescription \nSince \n \n\n\n\ncolors \nTapToPayColorScheme \nColor scheme for the Tap to Pay screen. \n8.1.0 \n \n\ndarkMode \nTapToPayDarkMode \nDark-mode setting for the Tap to Pay screen. \n8.1.0 \n \n\ntapZone \nTapToPayTapZone \nPosition of the tap indicator on screen. \n8.1.0 \n \n\n
\n\n\n interface TapToPayColorScheme
Color scheme for the Tap to Pay screen.
\n\n\nProp \nType \nDescription \nSince \n \n\n\n\nprimary \nTapToPayColor \nPrimary color for the tap-zone indicator. Use a hex string or default. \n8.1.0 \n \n\nsuccess \nTapToPayColor \nSuccess-state color. Use a hex string or default. \n8.1.0 \n \n\nerror \nTapToPayColor \nError-state color. Use a hex string or default. \n8.1.0 \n \n\n
\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\nMember \nValue \n \n\n\n\nSystem \n'SYSTEM' \n \n\nDark \n'DARK' \n \n\nLight \n'LIGHT' \n \n\n
\n\n Discover and connect
\nDiscover with TerminalConnectTypes.TapToPay and a locationId. Simulated Tap to Pay uses isTest: true on initialize, not TerminalConnectTypes.Simulated.
\nConnect 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.
\ntapZone 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.
\nThe official demo exercises Tap to Pay, Internet, and Bluetooth in demo/angular.
\n Platform prerequisites
\n\n\n\nPlatform \nSupported \nNotes \n \n\n\n\nAndroid \nYes \nNFC-capable device, location permission, Stripe Tap to Pay on Android eligibility. minSdkVersion 26. \n \n\niOS \nYes \nTap to Pay on iPhone, iOS 16.4+ for the account-link check. setTapToPayUxConfiguration is unimplemented. \n \n\nWeb \nNo \ndiscoverReaders({ type: TapToPay }) is unavailable. \n \n\n
\nComplete Stripe Dashboard Terminal setup and create a Location. Pass that locationId into discoverReaders; the plugin uses it when connecting the Tap to Pay reader.
\nAndroid 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- Register application-level listeners.
\n- Register an authenticated connection-token provider with
RequestedConnectionToken + setConnectionToken, then call initialize. \n- On iOS, call
isTapToPayAccountLinked (do not cache the result). \n- On Android, optionally call
setTapToPayUxConfiguration. \ndiscoverReaders with type: TerminalConnectTypes.TapToPay and locationId. \nconnectReader with the discovered reader. \n- Collect and confirm a
card_present PaymentIntent as in Collect a Payment. \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 Account-link check
\nisTapToPayAccountLinked 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.
\nThe 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.
\nAndroid 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.
isTapToPayAccountLinked(options?: IsTapToPayAccountLinkedOptions | undefined) => Promise<{ isLinked: boolean; }>
\n\n\n\n\n\n interface IsTapToPayAccountLinkedOptions
Options for isTapToPayAccountLinked.
\n\n\nProp \nType \nDescription \nSince \n \n\n\n\nonBehalfOf \nstring \nConnected account ID, for Stripe Connect platforms. Omit to check the account that owns the API key. \n8.2.0 \n \n\n
\n\n\n UX configuration
\nsetTapToPayUxConfiguration is Android only. Call it after initialize() and before connectReader(). iOS returns unimplemented; web logs and returns.
\nThe 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\nProp \nType \nDescription \nSince \n \n\n\n\ncolors \nTapToPayColorScheme \nColor scheme for the Tap to Pay screen. \n8.1.0 \n \n\ndarkMode \nTapToPayDarkMode \nDark-mode setting for the Tap to Pay screen. \n8.1.0 \n \n\ntapZone \nTapToPayTapZone \nPosition of the tap indicator on screen. \n8.1.0 \n \n\n
\n\n\n interface TapToPayColorScheme
Color scheme for the Tap to Pay screen.
\n\n\nProp \nType \nDescription \nSince \n \n\n\n\nprimary \nTapToPayColor \nPrimary color for the tap-zone indicator. Use a hex string or default. \n8.1.0 \n \n\nsuccess \nTapToPayColor \nSuccess-state color. Use a hex string or default. \n8.1.0 \n \n\nerror \nTapToPayColor \nError-state color. Use a hex string or default. \n8.1.0 \n \n\n
\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\nMember \nValue \n \n\n\n\nSystem \n'SYSTEM' \n \n\nDark \n'DARK' \n \n\nLight \n'LIGHT' \n \n\n
\n\n Discover and connect
\nDiscover with TerminalConnectTypes.TapToPay and a locationId. Simulated Tap to Pay uses isTest: true on initialize, not TerminalConnectTypes.Simulated.
\nConnect 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.
\ntapZone 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.
\nApp.tsximport { 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
\nRead the initialized client with useCapacitorStripe. The returned stripe object is the same plugin instance as Stripe from @capacitor-community/stripe.
\nimport { 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.
\nThe 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.
\nApp.tsximport { 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
\nRead the initialized client with useCapacitorStripe. The returned stripe object is the same plugin instance as Stripe from @capacitor-community/stripe.
\nimport { 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.
\nThe 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
\nKeep 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
\nCreate 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.
\nCustomer 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.
\nMap server fields to plugin options:
\n\n\n\nServer field \nPlugin option \n \n\n\n\npaymentIntent \npaymentIntentClientSecret \n \n\nsetupIntent \nsetupIntentClientSecret \n \n\nephemeralKey \ncustomerEphemeralKeySecret \n \n\ncustomer \ncustomerId \n \n\n
\n Response shapes
\nPaymentIntent 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
\nCompleted 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.
\nTreat 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.
\nThe 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
\nKeep 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
\nCreate 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.
\nCustomer 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.
\nMap server fields to plugin options:
\n\n\n\nServer field \nPlugin option \n \n\n\n\npaymentIntent \npaymentIntentClientSecret \n \n\nsetupIntent \nsetupIntentClientSecret \n \n\nephemeralKey \ncustomerEphemeralKeySecret \n \n\ncustomer \ncustomerId \n \n\n
\n Response shapes
\nPaymentIntent 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
\nCompleted 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.
\nTreat 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.
\nThe 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**
+ `