Skip to content

feat(kit): KitLoadingController + inferred presentModal typing - #9

Merged
rdlabo merged 4 commits into
mainfrom
feat/kit-loading-and-modal-input-typing
Jul 2, 2026
Merged

feat(kit): KitLoadingController + inferred presentModal typing#9
rdlabo merged 4 commits into
mainfrom
feat/kit-loading-and-modal-input-typing

Conversation

@rdlabo

@rdlabo rdlabo commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Two additions to the kit overlay module, both fully covered by tests.

1. KitLoadingController (new)

Reference-counted wrapper around Ionic's LoadingController that keeps at most one loading indicator on screen across concurrent async work.

  • presentLoading presents on the 0 → 1 transition; dismissLoading dismisses on the N → 0 transition.
  • All operations are serialized through an internal promise chain, so a dismiss arriving mid-presentation runs after present() settles and tears the element down instead of orphaning it.
  • A failed create/present rolls back its reference, so the counter can't stay elevated and wedge a later cycle into a stuck spinner.

Pair every presentLoading() with exactly one dismissLoading() (a try/finally is the safest shape).

2. presentModal — props and return type inferred from the component

Props are inferred from the component's input() fields, making input() the single source of truth (no hand-written declaration to drift out of sync):

  • input.required<T>()required prop; the compiler rejects a call that omits it.
  • default-less input<T>()optional prop.
  • Angular's types can't distinguish input.required<T>() from a defaulted input<T>(default), so a defaulted input is (safely) treated as required. Declare inputs you want to omit at the call site as default-less input<T>().
  • Components with no signal inputs fall back to loose, untyped props (plain classes / @Input()-decorator components).

The return type is inferred from an optional declare static modalReturn phantom type (zero runtime cost). A component without modalReturn resolves to void — reading the result does not compile — so modals that resolve with data are required to declare their shape.

export class EditPage {
  declare static modalReturn: { saved: boolean };
  readonly id = input.required<number>();   // required prop
  readonly note = input<string>();          // optional prop
}

// id required, note optional; result typed `{ saved: boolean } | undefined`
const result = await overlay.presentModal(EditPage, { id: 1 });

Breaking change

The single-type-argument form presentModal<ReturnType>(...) no longer sets the return type. Return data is now inferred from a static modalReturn; declare it on modals that resolve with data.

Test plan

  • ng test kit --watch=false → 139 passed (includes @ts-expect-error type-level assertions for required-prop enforcement and the void return contract).
  • ng build kit → succeeds.

Open in Devin Review

rdlabo added 2 commits July 2, 2026 13:19
Wraps Ionic's LoadingController so at most one loading indicator is on
screen across concurrent async work: presentLoading increments a counter
and presents on the 0 → 1 transition, dismissLoading decrements and
dismisses on the N → 0 transition. All operations are serialized through
an internal promise chain, so a dismiss that arrives mid-presentation runs
after present() settles and tears the element down instead of orphaning it.

A failed create/present rolls back its reference so the counter cannot
stay elevated and wedge a later cycle into a stuck spinner.
presentModal now infers componentProps from the component's input() fields
(the single source of truth, so props can never drift from a hand-written
declaration): required inputs become required props and the compiler rejects
a call that omits them; default-less input<T>() fields are optional. Because
Angular's types cannot distinguish input.required<T>() from a defaulted
input<T>(default), a defaulted input is treated as required. Components with
no signal inputs fall back to loose, untyped props.

The return type is inferred from an optional `declare static modalReturn`
phantom type; a component without one resolves to `void`, so the modal is
treated as returning no dismiss data and reading the result does not compile.

BREAKING CHANGE: the single-type-argument form presentModal<ReturnType>(...)
no longer sets the return type. Return data is inferred from a static
modalReturn on the component; declare it on modals that resolve with data.
@netlify

netlify Bot commented Jul 2, 2026

Copy link
Copy Markdown

Deploy Preview for rdlabo-ionic-angular-library ready!

Name Link
🔨 Latest commit 1c4d8ec
🔍 Latest deploy log https://app.netlify.com/projects/rdlabo-ionic-angular-library/deploys/6a45e979a4b78700086199ef
😎 Deploy Preview https://deploy-preview-9--rdlabo-ionic-angular-library.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Co-Authored-By: rdlabo <sakakibara@rdlabo.jp>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Open in Devin Review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 クラスレベルJSDoc例が旧APIスタイルのまま残存

クラスレベルのJSDoc例(kit-overlay.controller.ts:170)は presentModal<EditResult>(EditPage, { id: 1 }) という旧ジェネリック構文を使用しています。新しいオーバーロードでは型パラメータ C はコンポーネント型として解釈されるため、この例は型エラーになります。メソッドレベルの例は更新済みですが、クラスレベルの例とREADME(README.md:130, README.md:152, README.md:187)は旧APIのままです。diff の変更範囲外のため bug として報告していませんが、ドキュメントの一貫性のため更新が推奨されます。

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1c4d8ec — updated the class-level JSDoc example and all README examples (projects/kit/README.md lines 130, 152, 187) to use the new inferred-typing API.

* ```
*/
async presentModal<O = unknown>(
presentModal<C extends ModalOptions['component']>(component: C, ...args: ModalPresentArgs<C>): Promise<ModalReturnOf<C> | undefined>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 presentModalの破壊的API変更 — 既存の明示的ジェネリック呼び出しが非互換に

旧シグネチャ presentModal<O = unknown>(...) では呼び出し元が presentModal<ReturnType>(...) のように戻り値の型を明示指定できましたが、新オーバーロード presentModal<C extends ModalOptions['component']>(...) ではジェネリックパラメータがコンポーネント型になり、戻り値型の明示指定ができなくなります。既存の利用箇所は declare static modalReturn パターンへの移行が必要です。これは意図的な破壊的変更ですが、ライブラリの利用者に影響があるためマイグレーションガイドやCHANGELOGでの明記が望ましいです。

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an intentional breaking change documented in the PR description. The migration path is clear:

  1. Remove explicit type arg <ReturnType>
  2. Add declare static modalReturn: T to modals that return data
  3. For components without signal input() fields, the API falls back to loose untyped props — so migration can be incremental.

The CHANGELOG/docs aspect has been addressed in 1c4d8ec (updated README and JSDoc examples to the new API).

Co-Authored-By: rdlabo <sakakibara@rdlabo.jp>
@rdlabo
rdlabo merged commit 9747baa into main Jul 2, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant