From 7434acca0598542e45bc209cbe942a6f383ae3d3 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 00:19:27 +0000 Subject: [PATCH] fix(docs): resolve relative links on nested documentation pages against the page directory Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../eslint-plugin-rules.en.generated.ts | 24 +++++++------- .../eslint-plugin-rules.ja.generated.ts | 20 ++++++------ scripts/generate-docs.ts | 5 +++ scripts/package-markdown.test.ts | 23 +++++++++++++ scripts/package-markdown.ts | 32 ++++++++++++++++--- 5 files changed, 78 insertions(+), 26 deletions(-) diff --git a/projects/docs/src/app/generated/projects/eslint-plugin-rules.en.generated.ts b/projects/docs/src/app/generated/projects/eslint-plugin-rules.en.generated.ts index c892755..e9a8349 100644 --- a/projects/docs/src/app/generated/projects/eslint-plugin-rules.en.generated.ts +++ b/projects/docs/src/app/generated/projects/eslint-plugin-rules.en.generated.ts @@ -366,7 +366,7 @@ export const PROJECT = { "file": "rules/deny-element.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/deny-element", - "html": "
\n

This plugin disallows the use of certain HTML tags.

\n\n
\n

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

\n

Rule Details

\n

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

\n\n

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

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

\n

Examples

\n

Incorrect

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

Correct

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

When to enable

\n

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

\n

See also

\n\n

Implementation

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

This plugin disallows the use of certain HTML tags.

\n\n
\n

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

\n

Rule Details

\n

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

\n\n

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

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

\n

Examples

\n

Incorrect

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

Correct

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

When to enable

\n

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

\n

See also

\n\n

Implementation

\n\n", "headings": [ { "id": "rule-details", @@ -425,7 +425,7 @@ export const PROJECT = { "file": "rules/deny-overlay-create.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/deny-overlay-create", - "html": "
\n

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

\n\n
\n

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

\n

Rule Details

\n

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

\n\n

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

\n

Options

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

deny

\n\n

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

\n

Examples

\n

Incorrect

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

Correct

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

When to enable

\n

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

\n

See also

\n\n

Implementation

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

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

\n\n
\n

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

\n

Rule Details

\n

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

\n\n

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

\n

Options

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

deny

\n\n

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

\n

Examples

\n

Incorrect

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

Correct

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

When to enable

\n

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

\n

See also

\n\n

Implementation

\n\n", "headings": [ { "id": "rule-details", @@ -484,7 +484,7 @@ export const PROJECT = { "file": "rules/deny-soft-private-modifier.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/deny-soft-private-modifier", - "html": "
\n

This plugin disallows the use of soft private modifier.

\n\n
\n

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

\n

Rule Details

\n

This rule checks classes for the following patterns:

\n\n

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

\n

The rule auto-fixes by:

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

Examples

\n

Incorrect

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

Correct

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

Options

\n

This rule has no options.

\n

When to enable

\n

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

\n

See also

\n\n

Implementation

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

This plugin disallows the use of soft private modifier.

\n\n
\n

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

\n

Rule Details

\n

This rule checks classes for the following patterns:

\n\n

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

\n

The rule auto-fixes by:

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

Examples

\n

Incorrect

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

Correct

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

Options

\n

This rule has no options.

\n

When to enable

\n

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

\n

See also

\n\n

Implementation

\n\n", "headings": [ { "id": "rule-details", @@ -676,7 +676,7 @@ export const PROJECT = { "file": "rules/no-component-method-except-lifecycle.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/no-component-method-except-lifecycle", - "html": "
\n

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

\n\n
\n

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

\n

Rule Details

\n

The rule checks methods inside @Component decorated classes:

\n\n

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

\n

Supported lifecycle interfaces

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

Examples

\n

Incorrect

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

Correct

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

Options

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

additionalAllowedMethods

\n\n

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

\n

When to enable

\n

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

\n

See also

\n\n

Implementation

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

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

\n\n
\n

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

\n

Rule Details

\n

The rule checks methods inside @Component decorated classes:

\n\n

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

\n

Supported lifecycle interfaces

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

Examples

\n

Incorrect

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

Correct

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

Options

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

additionalAllowedMethods

\n\n

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

\n

When to enable

\n

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

\n

See also

\n\n

Implementation

\n\n", "headings": [ { "id": "rule-details", @@ -740,7 +740,7 @@ export const PROJECT = { "file": "rules/no-component-writable-signal.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/no-component-writable-signal", - "html": "
\n

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

\n
\n

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

\n

Rule Details

\n

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

\n\n

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

\n

Examples

\n

Incorrect

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

Correct

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

Options

\n

This rule has no options.

\n

When to enable

\n

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

\n

See also

\n\n

Implementation

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

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

\n
\n

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

\n

Rule Details

\n

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

\n\n

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

\n

Examples

\n

Incorrect

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

Correct

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

Options

\n

This rule has no options.

\n

When to enable

\n

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

\n

See also

\n\n

Implementation

\n\n", "headings": [ { "id": "rule-details", @@ -834,7 +834,7 @@ export const PROJECT = { "file": "rules/no-reactive-forms.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/no-reactive-forms", - "html": "
\n

Disallow Angular Reactive Forms in favor of Signal Forms.

\n
\n

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

\n

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

\n

Rule Details

\n

This rule reports three patterns:

\n
    \n
  1. \n

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

    \n

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

    \n
  2. \n
  3. \n

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

    \n
  4. \n
  5. \n

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

    \n
  6. \n
\n

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

\n

Examples

\n

Incorrect

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

Correct

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

Options

\n

This rule has no options.

\n

When to enable

\n

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

\n

See also

\n\n

Implementation

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

Disallow Angular Reactive Forms in favor of Signal Forms.

\n
\n

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

\n

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

\n

Rule Details

\n

This rule reports three patterns:

\n
    \n
  1. \n

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

    \n

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

    \n
  2. \n
  3. \n

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

    \n
  4. \n
  5. \n

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

    \n
  6. \n
\n

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

\n

Examples

\n

Incorrect

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

Correct

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

Options

\n

This rule has no options.

\n

When to enable

\n

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

\n

See also

\n\n

Implementation

\n\n", "headings": [ { "id": "rule-details", @@ -888,7 +888,7 @@ export const PROJECT = { "file": "rules/no-template-driven-forms.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/no-template-driven-forms", - "html": "
\n

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

\n
\n

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

\n

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

\n

Rule Details

\n

The rule runs against Angular templates and checks three patterns:

\n
    \n
  1. \n

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

    \n
  2. \n
  3. \n

    ngModelGroup attribute
    \nReports any ngModelGroup attribute on any element.

    \n
  4. \n
  5. \n

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

    \n
  6. \n
\n

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

\n

Examples

\n

Incorrect

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

Correct

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

Options

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

allowedElements

\n\n

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

\n

When to enable

\n

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

\n

See also

\n\n

Implementation

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

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

\n
\n

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

\n

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

\n

Rule Details

\n

The rule runs against Angular templates and checks three patterns:

\n
    \n
  1. \n

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

    \n
  2. \n
  3. \n

    ngModelGroup attribute
    \nReports any ngModelGroup attribute on any element.

    \n
  4. \n
  5. \n

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

    \n
  6. \n
\n

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

\n

Examples

\n

Incorrect

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

Correct

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

Options

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

allowedElements

\n\n

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

\n

When to enable

\n

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

\n

See also

\n\n

Implementation

\n\n", "headings": [ { "id": "rule-details", @@ -947,7 +947,7 @@ export const PROJECT = { "file": "rules/prefer-disable-handler.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/prefer-disable-handler", - "html": "
\n

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

\n\n
\n

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

\n

Rule Details

\n

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

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

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

\n

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

\n

By default, the rule targets:

\n\n

It ignores .spec.html files.

\n

Options

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

method

\n\n

The wrapper method name expected in the handler expression.

\n

eventParam

\n\n

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

\n

targets

\n\n

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

\n

allowEventMethods

\n\n

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

\n

Examples

\n

Incorrect

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

Correct

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

Custom configuration

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

When to enable

\n

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

\n

See also

\n\n

Implementation

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

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

\n\n
\n

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

\n

Rule Details

\n

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

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

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

\n

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

\n

By default, the rule targets:

\n\n

It ignores .spec.html files.

\n

Options

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

method

\n\n

The wrapper method name expected in the handler expression.

\n

eventParam

\n\n

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

\n

targets

\n\n

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

\n

allowEventMethods

\n\n

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

\n

Examples

\n

Incorrect

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

Correct

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

Custom configuration

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

When to enable

\n

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

\n

See also

\n\n

Implementation

\n\n", "headings": [ { "id": "rule-details", @@ -1075,7 +1075,7 @@ export const PROJECT = { "file": "rules/prefer-modal-launcher.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/prefer-modal-launcher", - "html": "
\n

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

\n\n
\n

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

\n

Rule Details

\n

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

\n

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

\n\n

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

The present method names to restrict.

\n

launcherNamePattern

\n\n

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

\n

Examples

\n

Incorrect

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

Correct

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

Custom configuration

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

When to enable

\n

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

\n

See also

\n\n

Implementation

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

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

\n\n
\n

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

\n

Rule Details

\n

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

\n

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

\n\n

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

The present method names to restrict.

\n

launcherNamePattern

\n\n

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

\n

Examples

\n

Incorrect

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

Correct

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

Custom configuration

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

When to enable

\n

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

\n

See also

\n\n

Implementation

\n\n", "headings": [ { "id": "rule-details", @@ -1268,7 +1268,7 @@ export const PROJECT = { "file": "rules/require-viewmodel.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/require-viewmodel", - "html": "
\n

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

\n\n
\n

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

\n

Rule Details

\n

The rule performs three checks:

\n

1. Component must own a ViewModel

\n

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

\n

2. ViewModel must extend ViewModelStore<ComponentType>

\n

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

\n\n

3. ViewModel must not contain View APIs

\n

The ViewModel class must not call the following APIs:

\n

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

\n

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

\n

Examples

\n

Incorrect

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

Correct

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

Options

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

viewModelClassName

\n\n

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

\n

viewModelStoreClassName

\n\n

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

\n

bannedApis

\n\n

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

\n

When to enable

\n

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

\n

See also

\n\n

Implementation

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

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

\n\n
\n

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

\n

Rule Details

\n

The rule performs three checks:

\n

1. Component must own a ViewModel

\n

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

\n

2. ViewModel must extend ViewModelStore<ComponentType>

\n

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

\n\n

3. ViewModel must not contain View APIs

\n

The ViewModel class must not call the following APIs:

\n

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

\n

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

\n

Examples

\n

Incorrect

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

Correct

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

Options

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

viewModelClassName

\n\n

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

\n

viewModelStoreClassName

\n\n

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

\n

bannedApis

\n\n

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

\n

When to enable

\n

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

\n

See also

\n\n

Implementation

\n\n", "headings": [ { "id": "rule-details", @@ -1431,7 +1431,7 @@ export const PROJECT = { "file": "rules/signal-use-as-signal-template.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/signal-use-as-signal-template", - "html": "
\n

Require () when accessing Angular Signals in templates

\n\n
\n

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

\n

Rule Details

\n

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

\n\n

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

\n

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

\n\n

The rule supports both template and templateUrl components.

\n

Examples

\n

Incorrect

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

Correct

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

Passing a Signal reference to a child

\n

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

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

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

\n

Options

\n

This rule has no options.

\n

When to enable

\n

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

\n

See also

\n\n

Implementation

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

Require () when accessing Angular Signals in templates

\n\n
\n

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

\n

Rule Details

\n

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

\n\n

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

\n

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

\n\n

The rule supports both template and templateUrl components.

\n

Examples

\n

Incorrect

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

Correct

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

Passing a Signal reference to a child

\n

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

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

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

\n

Options

\n

This rule has no options.

\n

When to enable

\n

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

\n

See also

\n\n

Implementation

\n\n", "headings": [ { "id": "rule-details", @@ -1490,7 +1490,7 @@ export const PROJECT = { "file": "rules/signal-use-as-signal.md", "section": "Rules", "path": "/projects/eslint-plugin-rules/docs/rules/signal-use-as-signal", - "html": "
\n

This plugin check to valid signal use as signal.

\n\n
\n

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

\n

Rule Details

\n

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

\n\n

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

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

Examples

\n

Incorrect

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

Correct

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

Auto-fix

\n

The rule provides auto-fix for the patterns above:

\n\n

Options

\n

This rule has no options.

\n

When to enable

\n

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

\n

See also

\n\n

Implementation

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

This plugin check to valid signal use as signal.

\n\n
\n

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

\n

Rule Details

\n

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

\n\n

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

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

Examples

\n

Incorrect

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

Correct

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

Auto-fix

\n

The rule provides auto-fix for the patterns above:

\n\n

Options

\n

This rule has no options.

\n

When to enable

\n

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

\n

See also

\n\n

Implementation

\n\n", "headings": [ { "id": "rule-details", diff --git a/projects/docs/src/app/generated/projects/eslint-plugin-rules.ja.generated.ts b/projects/docs/src/app/generated/projects/eslint-plugin-rules.ja.generated.ts index 4b3532d..a7520d7 100644 --- a/projects/docs/src/app/generated/projects/eslint-plugin-rules.ja.generated.ts +++ b/projects/docs/src/app/generated/projects/eslint-plugin-rules.ja.generated.ts @@ -366,7 +366,7 @@ export const PROJECT = { "file": "rules/deny-element.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/deny-element", - "html": "\n
\n

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

\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

オプション

\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

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

\n

\n

誤り

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

正しい

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

有効にする場面

\n

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

\n

関連項目

\n\n

実装

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

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

\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

オプション

\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

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

\n

\n

誤り

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

正しい

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

有効にする場面

\n

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

\n

関連項目

\n\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", @@ -425,7 +425,7 @@ export const PROJECT = { "file": "rules/deny-overlay-create.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/deny-overlay-create", - "html": "\n
\n

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

\n\n
\n

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

\n

ルール詳細

\n

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

\n\n

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

\n

オプション

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

deny

\n\n

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

\n

\n

誤り

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

正しい

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

有効にする場面

\n

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

\n

関連項目

\n\n

実装

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

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

\n\n
\n

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

\n

ルール詳細

\n

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

\n\n

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

\n

オプション

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

deny

\n\n

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

\n

\n

誤り

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

正しい

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

有効にする場面

\n

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

\n

関連項目

\n\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", @@ -675,7 +675,7 @@ 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

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

\n

ルール詳細

\n

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

\n\n

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

\n

\n

誤り

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

正しい

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

オプション

\n

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

\n

有効にする場面

\n

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

\n

関連項目

\n\n

実装

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

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

\n
\n

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

\n

ルール詳細

\n

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

\n\n

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

\n

\n

誤り

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

正しい

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

オプション

\n

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

\n

有効にする場面

\n

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

\n

関連項目

\n\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", @@ -754,7 +754,7 @@ export const PROJECT = { "file": "rules/no-reactive-forms.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/no-reactive-forms", - "html": "\n
\n

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

\n
\n

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

\n

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

\n

ルール詳細

\n

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

\n
    \n
  1. \n

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

    \n

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

    \n
  2. \n
  3. \n

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

    \n
  4. \n
  5. \n

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

    \n
  6. \n
\n

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

\n

\n

誤り

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

正しい

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

オプション

\n

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

\n

有効にする場面

\n

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

\n

関連項目

\n\n

実装

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

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

\n
\n

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

\n

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

\n

ルール詳細

\n

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

\n
    \n
  1. \n

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

    \n

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

    \n
  2. \n
  3. \n

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

    \n
  4. \n
  5. \n

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

    \n
  6. \n
\n

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

\n

\n

誤り

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

正しい

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

オプション

\n

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

\n

有効にする場面

\n

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

\n

関連項目

\n\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", @@ -808,7 +808,7 @@ export const PROJECT = { "file": "rules/no-template-driven-forms.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/no-template-driven-forms", - "html": "\n
\n

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

\n
\n

このルールはAngularテンプレート内のtemplate-driven formsを制限します。ngFormngModelGroup はテンプレート内に可変フォーム状態を保持するため、常に拒否されます。ngModel も、Signal Formsに適さないIonic Viewバインディング向けに明示的に許可された要素でない限り拒否されます。

\n

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

\n

ルール詳細

\n

このルールはAngularテンプレートに対して次の3パターンを検査します。

\n
    \n
  1. \n

    allowedElements に含まれない要素上の ngModel
    \n許可リストにないタグの ngModel[(ngModel)][ngModel] を報告します。単独の (ngModelChange) outputは検査しません。

    \n
  2. \n
  3. \n

    ngModelGroup 属性
    \nすべての要素上の ngModelGroup 属性を報告します。

    \n
  4. \n
  5. \n

    ngForm referenceまたはdirective
    \n<form #form=\"ngForm\"><div ngForm> を報告します。

    \n
  6. \n
\n

型情報は使用せず、parse済みのtemplate ASTだけを検査します。

\n

\n

誤り

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

正しい

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

オプション

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

allowedElements

\n\n

ngModel の使用を許可する要素のタグ名です。ion-searchbarion-toggle のように、View上の便宜として ngModel で値を公開するIonicコンポーネントを想定しています。要素が許可されていても、ngModelGroupngForm は報告されます。

\n

有効にする場合

\n

Angular Signal Formsへ移行しながら、特定のIonic Viewコンポーネントに限定して ngModel バインディングが必要なプロジェクトで有効にしてください。Reactive Formsを全面的に採用し、Signal Formsを導入する予定がない場合にのみ無効にします。

\n

関連項目

\n\n

実装

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

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

\n
\n

このルールはAngularテンプレート内のtemplate-driven formsを制限します。ngFormngModelGroup はテンプレート内に可変フォーム状態を保持するため、常に拒否されます。ngModel も、Signal Formsに適さないIonic Viewバインディング向けに明示的に許可された要素でない限り拒否されます。

\n

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

\n

ルール詳細

\n

このルールはAngularテンプレートに対して次の3パターンを検査します。

\n
    \n
  1. \n

    allowedElements に含まれない要素上の ngModel
    \n許可リストにないタグの ngModel[(ngModel)][ngModel] を報告します。単独の (ngModelChange) outputは検査しません。

    \n
  2. \n
  3. \n

    ngModelGroup 属性
    \nすべての要素上の ngModelGroup 属性を報告します。

    \n
  4. \n
  5. \n

    ngForm referenceまたはdirective
    \n<form #form=\"ngForm\"><div ngForm> を報告します。

    \n
  6. \n
\n

型情報は使用せず、parse済みのtemplate ASTだけを検査します。

\n

\n

誤り

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

正しい

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

オプション

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

allowedElements

\n\n

ngModel の使用を許可する要素のタグ名です。ion-searchbarion-toggle のように、View上の便宜として ngModel で値を公開するIonicコンポーネントを想定しています。要素が許可されていても、ngModelGroupngForm は報告されます。

\n

有効にする場合

\n

Angular Signal Formsへ移行しながら、特定のIonic Viewコンポーネントに限定して ngModel バインディングが必要なプロジェクトで有効にしてください。Reactive Formsを全面的に採用し、Signal Formsを導入する予定がない場合にのみ無効にします。

\n

関連項目

\n\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", @@ -867,7 +867,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
\n

非同期処理を開始するbuttonをユーザーがtapしたら、処理がsettleするまでcontrolを無効にする必要があります。そうしなければ、2回目のtapで同じactionが再実行される可能性があります。このルールは、設定した (event) bindingにwrapper呼び出し構文を強制します。UIの無効化とwork値の適切な処理はwrapper実装の責務です。

\n

ルール詳細

\n

Angularテンプレートを検査します。設定対象に一致する各 BoundEvent のhandler expressionは、2つ以上の引数を持つwrapper method呼び出しでなければなりません。

\n
    \n
  1. event parameter(デフォルトは $event)。
  2. \n
  3. wrapperへ渡すwork expression。
  4. \n
\n

たとえば (click)=\"vm.disableHandler($event, vm.save())\" は有効です。(click)=\"vm.save()\" は報告されます。第2引数の型やPromiseを返すかどうかは検査しません。

\n

$event.stopPropagation()$event.preventDefault() のようなevent methodの単独呼び出しも許可します(allowEventMethods で設定可能)。

\n

デフォルトの対象は次のとおりです。

\n\n

.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

handler expressionに要求するwrapper method名です。

\n

eventParam

\n\n

wrapper methodの第1引数として渡す必要がある値です。

\n

targets

\n\n

各targetはwrapperを要求するeventと要素を指定します。elements は任意で、省略するとそのeventを持つすべての要素に適用されます。

\n

allowEventMethods

\n\n

wrapperなしで許可するevent methodです。たとえば (click)=\"$event.stopPropagation()\" は有効です。

\n

\n

誤り

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

正しい

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

カスタム設定

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

有効にする場合

\n

API呼び出し、navigation、modal表示などの非同期処理をユーザー操作から開始するIonic/Angularプロジェクトで有効にしてください。@rdlabo/rules/prefer-modal-launcher および @rdlabo/rules/deny-element と組み合わせることで、overlay logicを一元化できます。

\n

関連項目

\n\n

実装

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

非同期処理中の二重タップを防ぐため、設定した要素とイベントのバインディングにwrapper method(デフォルト: disableHandler($event, work))を要求する

\n\n
\n

非同期処理を開始するbuttonをユーザーがtapしたら、処理がsettleするまでcontrolを無効にする必要があります。そうしなければ、2回目のtapで同じactionが再実行される可能性があります。このルールは、設定した (event) bindingにwrapper呼び出し構文を強制します。UIの無効化とwork値の適切な処理はwrapper実装の責務です。

\n

ルール詳細

\n

Angularテンプレートを検査します。設定対象に一致する各 BoundEvent のhandler expressionは、2つ以上の引数を持つwrapper method呼び出しでなければなりません。

\n
    \n
  1. event parameter(デフォルトは $event)。
  2. \n
  3. wrapperへ渡すwork expression。
  4. \n
\n

たとえば (click)=\"vm.disableHandler($event, vm.save())\" は有効です。(click)=\"vm.save()\" は報告されます。第2引数の型やPromiseを返すかどうかは検査しません。

\n

$event.stopPropagation()$event.preventDefault() のようなevent methodの単独呼び出しも許可します(allowEventMethods で設定可能)。

\n

デフォルトの対象は次のとおりです。

\n\n

.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

handler expressionに要求するwrapper method名です。

\n

eventParam

\n\n

wrapper methodの第1引数として渡す必要がある値です。

\n

targets

\n\n

各targetはwrapperを要求するeventと要素を指定します。elements は任意で、省略するとそのeventを持つすべての要素に適用されます。

\n

allowEventMethods

\n\n

wrapperなしで許可するevent methodです。たとえば (click)=\"$event.stopPropagation()\" は有効です。

\n

\n

誤り

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

正しい

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

カスタム設定

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

有効にする場合

\n

API呼び出し、navigation、modal表示などの非同期処理をユーザー操作から開始するIonic/Angularプロジェクトで有効にしてください。@rdlabo/rules/prefer-modal-launcher および @rdlabo/rules/deny-element と組み合わせることで、overlay logicを一元化できます。

\n

関連項目

\n\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", @@ -995,7 +995,7 @@ export const PROJECT = { "file": "rules/prefer-modal-launcher.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/prefer-modal-launcher", - "html": "\n
\n

presentModal 呼び出しを launch* launcher関数内に置くことを要求する。

\n\n
\n

modalとsheetは、対象pageからexportされた専用launcher関数を介して表示してください。これにより、呼び出し側をmodal構築の詳細から分離し、application全体でmodal APIを統一できます。このルールは、presentModal(または設定した他のpresent method)がlauncher patternに一致する名前の関数内でのみ呼び出されることを保証します。

\n

ルール詳細

\n

presentModalhelper.presentModal(...)overlay.presentSheet(...) などの呼び出しについて CallExpression nodeを検査します。launcher関数内にない呼び出しは報告されます。

\n

launcher関数とは、設定した正規表現(デフォルトは ^launch)に名前が一致する関数です。次の形式を検査します。

\n\n

オプション

\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

制限対象とするpresent method名です。

\n

launcherNamePattern

\n\n

正規表現を表す文字列です。present method呼び出しは、このpatternに名前が一致する関数内になければなりません。

\n

\n

誤り

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

正しい

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

カスタム設定

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

有効にする場合

\n

modal、sheet、その他のoverlayにlauncher patternを採用するIonic/Angularプロジェクトで有効にしてください。@rdlabo/rules/deny-element および @rdlabo/rules/prefer-disable-handler と組み合わせて使用します。

\n

関連項目

\n\n

実装

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

presentModal 呼び出しを launch* launcher関数内に置くことを要求する。

\n\n
\n

modalとsheetは、対象pageからexportされた専用launcher関数を介して表示してください。これにより、呼び出し側をmodal構築の詳細から分離し、application全体でmodal APIを統一できます。このルールは、presentModal(または設定した他のpresent method)がlauncher patternに一致する名前の関数内でのみ呼び出されることを保証します。

\n

ルール詳細

\n

presentModalhelper.presentModal(...)overlay.presentSheet(...) などの呼び出しについて CallExpression nodeを検査します。launcher関数内にない呼び出しは報告されます。

\n

launcher関数とは、設定した正規表現(デフォルトは ^launch)に名前が一致する関数です。次の形式を検査します。

\n\n

オプション

\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

制限対象とするpresent method名です。

\n

launcherNamePattern

\n\n

正規表現を表す文字列です。present method呼び出しは、このpatternに名前が一致する関数内になければなりません。

\n

\n

誤り

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

正しい

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

カスタム設定

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

有効にする場合

\n

modal、sheet、その他のoverlayにlauncher patternを採用するIonic/Angularプロジェクトで有効にしてください。@rdlabo/rules/deny-element および @rdlabo/rules/prefer-disable-handler と組み合わせて使用します。

\n

関連項目

\n\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", @@ -1183,7 +1183,7 @@ export const PROJECT = { "file": "rules/require-viewmodel.md", "section": "ルール", "path": "/projects/eslint-plugin-rules/docs/rules/require-viewmodel", - "html": "\n
\n

Componentの new ViewModel(this)ViewModelStore<ComponentType, Keys> 継承を強制し、View APIをViewModelから排除する。

\n\n
\n

ViewModel architecture patternを強制します。Angular Componentは new ViewModel(this) で初期化したViewModelを所有しなければなりません。少なくとも1つの一致するpropertyを要求しますが、追加のViewModel instanceは拒否しません。ViewModelは ViewModelStore<ComponentType> を継承し、host を再宣言したり、viewChildeffectcomputedafterNextRender などのView固有APIを含めたりしないでください。

\n

ルール詳細

\n

次の3つを検査します。

\n

1. ComponentはViewModelを所有する

\n

@Component classには new ViewModel(this) で初期化したpropertyが必要です。constructor呼び出しの第1引数は this でなければなりません。

\n

2. ViewModelは ViewModelStore<ComponentType> を継承する

\n

ViewModel(または設定した viewModelClassName)というclassは、ViewModelStore<...>、名前が ViewModel で終わるbase、または ModelSearch を継承しなければなりません。最初のgeneric引数はhost Component型でなければなりません。中間classのgeneric defaultも解決します。

\n\n

3. ViewModelにView APIを含めない

\n

ViewModel classでは次のAPIを呼び出せません。

\n

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

\n

この一覧は bannedApis optionで変更できます。viewChild() のような直接呼び出しと、viewChild.required() のような .required() variantを認識します。namespace prefix付き呼び出しは解決しません。

\n

\n

誤り

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

正しい

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

オプション

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

viewModelClassName

\n\n

Component内で検索するclass名です。PageState など別の命名規則を使うプロジェクトで指定します。

\n

viewModelStoreClassName

\n\n

ViewModelが継承すべきbase class名、または名前が ViewModel で終わる中間base class名です。

\n

bannedApis

\n\n

ViewModel内で許可しないAPIです。直接呼び出しと .required(...) の使用を検出します。namespace prefix付き呼び出しは解決しません。

\n

有効にする場合

\n

@rdlabo/ionic-angular-kit または同様のarchitectureでViewModel patternを採用するプロジェクトで有効にしてください。@rdlabo/rules/no-component-writable-signal と組み合わせると、Component stateをread-only、ViewModel stateをwritableに保てます。

\n

関連項目

\n\n

実装

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

Componentの new ViewModel(this)ViewModelStore<ComponentType, Keys> 継承を強制し、View APIをViewModelから排除する。

\n\n
\n

ViewModel architecture patternを強制します。Angular Componentは new ViewModel(this) で初期化したViewModelを所有しなければなりません。少なくとも1つの一致するpropertyを要求しますが、追加のViewModel instanceは拒否しません。ViewModelは ViewModelStore<ComponentType> を継承し、host を再宣言したり、viewChildeffectcomputedafterNextRender などのView固有APIを含めたりしないでください。

\n

ルール詳細

\n

次の3つを検査します。

\n

1. ComponentはViewModelを所有する

\n

@Component classには new ViewModel(this) で初期化したpropertyが必要です。constructor呼び出しの第1引数は this でなければなりません。

\n

2. ViewModelは ViewModelStore<ComponentType> を継承する

\n

ViewModel(または設定した viewModelClassName)というclassは、ViewModelStore<...>、名前が ViewModel で終わるbase、または ModelSearch を継承しなければなりません。最初のgeneric引数はhost Component型でなければなりません。中間classのgeneric defaultも解決します。

\n\n

3. ViewModelにView APIを含めない

\n

ViewModel classでは次のAPIを呼び出せません。

\n

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

\n

この一覧は bannedApis optionで変更できます。viewChild() のような直接呼び出しと、viewChild.required() のような .required() variantを認識します。namespace prefix付き呼び出しは解決しません。

\n

\n

誤り

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

正しい

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

オプション

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

viewModelClassName

\n\n

Component内で検索するclass名です。PageState など別の命名規則を使うプロジェクトで指定します。

\n

viewModelStoreClassName

\n\n

ViewModelが継承すべきbase class名、または名前が ViewModel で終わる中間base class名です。

\n

bannedApis

\n\n

ViewModel内で許可しないAPIです。直接呼び出しと .required(...) の使用を検出します。namespace prefix付き呼び出しは解決しません。

\n

有効にする場合

\n

@rdlabo/ionic-angular-kit または同様のarchitectureでViewModel patternを採用するプロジェクトで有効にしてください。@rdlabo/rules/no-component-writable-signal と組み合わせると、Component stateをread-only、ViewModel stateをwritableに保てます。

\n

関連項目

\n\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", @@ -1346,7 +1346,7 @@ 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
\n

Angular Signalは関数です。テンプレートで現在値を読み取るには、Signalを () 付きで呼び出す必要があります。RxJSの BehaviorSubjectmodel() inputから移行するとき、括弧の付け忘れはよくあるミスです。このルールはAngularテンプレート内のSignal識別子を検出し、{{ count }}[hidden]=\"count\" のような裸の読み取りを報告します。

\n

ルール詳細

\n

@Component のAngularテンプレートを解析し、次からSignal識別子を収集します。

\n\n

検出は名前に基づき、import元は解決しません。alias付きfactory importは認識されず、逆に同名の無関係なローカル関数がSignal factoryとして扱われる場合があります。toSignal は通常 @angular/core/rxjs-interop からimportされますが、このルールはmoduleではなく名前で認識します。

\n

続いて、テンプレート内でSignalが () なしで読み取られる箇所を報告します。対象には次が含まれます。

\n\n

templatetemplateUrl の両方のcomponentに対応します。

\n

\n

誤り

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

正しい

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

Signal参照を子componentへ渡す

\n

子componentが値ではなくSignal objectを期待する場合は、() なしで参照を渡せます。

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

この場合を認識し、bound attributeとして渡された裸のSignalは報告しません。

\n

オプション

\n

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

\n

有効にする場合

\n

Signalを使用するすべてのAngularプロジェクトで有効にしてください。Observable ベースのコードから移行するときや、テンプレート内で呼び出す必要のあるSignal風objectを返す model()input() を導入するときに特に有効です。

\n

関連項目

\n\n

実装

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

テンプレートでAngular Signalにアクセスするとき () を要求する

\n\n
\n

Angular Signalは関数です。テンプレートで現在値を読み取るには、Signalを () 付きで呼び出す必要があります。RxJSの BehaviorSubjectmodel() inputから移行するとき、括弧の付け忘れはよくあるミスです。このルールはAngularテンプレート内のSignal識別子を検出し、{{ count }}[hidden]=\"count\" のような裸の読み取りを報告します。

\n

ルール詳細

\n

@Component のAngularテンプレートを解析し、次からSignal識別子を収集します。

\n\n

検出は名前に基づき、import元は解決しません。alias付きfactory importは認識されず、逆に同名の無関係なローカル関数がSignal factoryとして扱われる場合があります。toSignal は通常 @angular/core/rxjs-interop からimportされますが、このルールはmoduleではなく名前で認識します。

\n

続いて、テンプレート内でSignalが () なしで読み取られる箇所を報告します。対象には次が含まれます。

\n\n

templatetemplateUrl の両方のcomponentに対応します。

\n

\n

誤り

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

正しい

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

Signal参照を子componentへ渡す

\n

子componentが値ではなくSignal objectを期待する場合は、() なしで参照を渡せます。

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

この場合を認識し、bound attributeとして渡された裸のSignalは報告しません。

\n

オプション

\n

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

\n

有効にする場合

\n

Signalを使用するすべてのAngularプロジェクトで有効にしてください。Observable ベースのコードから移行するときや、テンプレート内で呼び出す必要のあるSignal風objectを返す model()input() を導入するときに特に有効です。

\n

関連項目

\n\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", @@ -1405,7 +1405,7 @@ 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
\n

Angular Signalはgetter関数です。読み取りには () が必要で、書き込みには .set() または .update() を使う必要があります。このルールは、Signal変数を通常の値のように扱うコードを検出し、一般的な誤りの多くを自動修正できます。

\n

ルール詳細

\n

Signal factory(signalmodelinputlinkedSignaltoSignalasReadonly)で初期化されたclass propertyを追跡し、次のような誤用を報告します。

\n\n

Signal参照が期待されるcontextと、値が期待されるcontextを区別します。たとえば、Signal objectをpropsとして渡すことは許可されます。

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

\n

誤り

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

正しい

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

自動修正

\n

次のパターンを自動修正できます。

\n\n

オプション

\n

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

\n

有効にする場合

\n

Signalを使用するすべてのAngularプロジェクトで有効にしてください。テンプレート内のSignal使用を検査する @rdlabo/rules/signal-use-as-signal-template と相互補完します。

\n

関連項目

\n\n

実装

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

SignalがSignalとして正しく使われているか検査する。

\n\n
\n

Angular Signalはgetter関数です。読み取りには () が必要で、書き込みには .set() または .update() を使う必要があります。このルールは、Signal変数を通常の値のように扱うコードを検出し、一般的な誤りの多くを自動修正できます。

\n

ルール詳細

\n

Signal factory(signalmodelinputlinkedSignaltoSignalasReadonly)で初期化されたclass propertyを追跡し、次のような誤用を報告します。

\n\n

Signal参照が期待されるcontextと、値が期待されるcontextを区別します。たとえば、Signal objectをpropsとして渡すことは許可されます。

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

\n

誤り

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

正しい

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

自動修正

\n

次のパターンを自動修正できます。

\n\n

オプション

\n

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

\n

有効にする場合

\n

Signalを使用するすべてのAngularプロジェクトで有効にしてください。テンプレート内のSignal使用を検査する @rdlabo/rules/signal-use-as-signal-template と相互補完します。

\n

関連項目

\n\n

実装

\n\n", "headings": [ { "id": "%E3%83%AB%E3%83%BC%E3%83%AB%E8%A9%B3%E7%B4%B0", diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index 47c3675..522ba1e 100644 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -29,6 +29,7 @@ import { extractRdlaboDocsPick, normalizePackageMarkdown, rewritePackageDocLinks, + rewriteRelativeDocLinks, stripLeadingH1, stripRdlaboDocsOmit, } from './package-markdown'; @@ -375,6 +376,9 @@ async function generateProject( const parsed = fm(resolved.content); const isPackageLanding = resolved.fromPackage && PACKAGE_LANDING_FILES.has(file); let preparedBody = parsed.body || resolved.content; + if (!resolved.fromPackage) { + preparedBody = rewriteRelativeDocLinks(preparedBody, file); + } if (!resolved.fromPackage && file === 'readme.md') { const extracted = extractRdlaboDocsPick(preparedBody); preparedBody = extracted.markdown; @@ -405,6 +409,7 @@ async function generateProject( stripLeadingH1(stripRdlaboDocsOmit(parsed.body || resolved.content)), apiAnchors, packageLandingSlug, + file, ), ); } diff --git a/scripts/package-markdown.test.ts b/scripts/package-markdown.test.ts index ea6ed43..d61f231 100644 --- a/scripts/package-markdown.test.ts +++ b/scripts/package-markdown.test.ts @@ -8,6 +8,7 @@ import { extractRdlaboDocsPick, normalizePackageMarkdown, rewritePackageDocLinks, + rewriteRelativeDocLinks, stripLeadingH1, stripRdlaboDocsOmit, } from './package-markdown'; @@ -354,3 +355,25 @@ test('rewrites nested package-relative guide links', () => { 'See [Event Listeners](/docs/learn/event-listeners) and [the same page](/docs/learn/event-listeners).', ); }); + +test('resolves sibling links from nested package pages against the page directory', () => { + assert.equal( + rewritePackageDocLinks( + 'See [deny-element](./deny-element.md#options), [setup](../README.md#installation), and [guide](../docs/guide.md).', + new Map(), + 'readme', + 'rules/prefer-modal-launcher.md', + ), + 'See [deny-element](/docs/rules/deny-element#options), [setup](/docs/readme#installation), and [guide](/docs/guide).', + ); +}); + +test('rewrites relative links on locally hosted nested pages', () => { + assert.equal( + rewriteRelativeDocLinks( + 'See [deny-element](./deny-element.md), [root](/docs/api), and [external](https://example.com/a.md).', + 'rules/prefer-modal-launcher.md', + ), + 'See [deny-element](/docs/rules/deny-element), [root](/docs/api), and [external](https://example.com/a.md).', + ); +}); diff --git a/scripts/package-markdown.ts b/scripts/package-markdown.ts index b4ee374..f79ceb0 100644 --- a/scripts/package-markdown.ts +++ b/scripts/package-markdown.ts @@ -1,3 +1,4 @@ +import { posix } from 'node:path'; import { splitDocgenReadme } from './docgen-readme'; const LANDING_START = /^## Overview[ \t]*$/m; @@ -157,10 +158,35 @@ export function normalizePackageMarkdown(markdown: string): string { .replaceAll(`${LEGACY_GITHUB_OWNER}/`, 'rdlabo-dev/'); } +const RELATIVE_DOC_FILE_PATTERN = + /^(?:\.\.\/)*(?:\.\/)?(?:docs\/)?((?:[a-z0-9-]+\/)*[a-z0-9-]+)\.md$/i; + +/** Resolves a relative `*.md` link against the containing page's directory into a page slug. */ +export function resolveRelativeDocSlug(path: string, pageFile?: string): string | undefined { + if (!RELATIVE_DOC_FILE_PATTERN.test(path)) return undefined; + const pageDirectory = pageFile ? posix.dirname(pageFile) : '.'; + const resolved = posix.normalize(posix.join(pageDirectory, path)); + return resolved + .replace(/^(?:\.\.\/)+/, '') + .replace(/^docs\//, '') + .replace(/\.md$/i, ''); +} + +export function rewriteRelativeDocLinks(markdown: string, pageFile: string): string { + return markdown.replace(/\]\((?!https?:|mailto:)([^)]+)\)/g, (match, target: string) => { + const hashIndex = target.indexOf('#'); + const path = hashIndex < 0 ? target : target.slice(0, hashIndex); + const hash = hashIndex < 0 ? '' : target.slice(hashIndex); + const slug = resolveRelativeDocSlug(path, pageFile); + return slug ? `](/docs/${slug}${hash})` : match; + }); +} + export function rewritePackageDocLinks( markdown: string, apiAnchors: Map, landingSlug = 'readme', + pageFile?: string, ): string { return markdown.replace(/\]\((?!https?:|mailto:)([^)]+)\)/g, (match, target: string) => { const hashIndex = target.indexOf('#'); @@ -180,10 +206,8 @@ export function rewritePackageDocLinks( return `](/docs/${landingSlug}${hash})`; } - const docFile = path.match( - /^(?:\.\.\/)?(?:\.\/)?(?:docs\/)?((?:[a-z0-9-]+\/)*[a-z0-9-]+)\.md$/i, - ); - if (docFile) return `](/docs/${docFile[1]}${hash})`; + const slug = resolveRelativeDocSlug(path, pageFile); + if (slug) return `](/docs/${slug}${hash})`; return match; }); }