Skip to content

feat(observable): return this from on, once and off - #11617

Open
xh123637 wants to merge 1 commit into
NativeScript:mainfrom
xh123637:fix/6828-observable-return-this
Open

xh123637 wants to merge 1 commit into
NativeScript:mainfrom
xh123637:fix/6828-observable-return-this

Conversation

@xh123637

@xh123637 xh123637 commented Nov 5, 2026

Copy link
Copy Markdown

Fixes #6828

Observable.on, once and off returned void, so listeners could not be chained the way Node's EventEmitter allows. They now return the Observable instance (this), so calls like observable.on('tap', handler).once('load', other).off('tap', handler) work.

The same change applies to addEventListener and removeEventListener, including the overrides in ViewBase, ScrollViewBase, MediaQueryList and Span, and the typed on/once/off overloads declared across the core package (including the handwritten .d.ts files) were updated to return this as well, so the public typings match the runtime behavior.

What is the current behavior?

on, once and off return nothing, so chaining listener registrations is not possible.

What is the new behavior?

All of them return the instance they were called on. Existing code that ignores the return value is unaffected.

Tests

  • New returning this for chaining tests in packages/core/data/observable/index.spec.ts: on/once/off/addEventListener/removeEventListener return the observable, and a chained on().on().once().off() sequence still notifies the remaining listeners. The 5 new tests fail on the clean tree and pass with this change.
  • Full @nativescript/core unit suite: 56 files, 648 passed, 1 skipped.
  • tsc --noEmit on the lib config is clean (the spec config has 25 pre-existing errors, unchanged by this change), ESLint on the core package passes, and Prettier is clean on the changed files (3 pre-existing Prettier warnings in unrelated .d.ts files remain untouched).

Observable.on, once and off returned void, so listeners could not be
chained. They now return the Observable instance, matching the Node
EventEmitter API. addEventListener and removeEventListener return this
as well, and the typed on/once/off overloads across the core package
were updated to match.

Fixes NativeScript#6828
@nx-cloud

nx-cloud Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 309fbaa

Command Status Duration Result
nx test apps-automated -c=android ✅ Succeeded 3m 21s View ↗
nx run-many --target=test --configuration=ci --... ✅ Succeeded 7s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-11-05 22:10:14 UTC

@pkg-pr-new

pkg-pr-new Bot commented Nov 5, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@nativescript/core@11617
npm i https://pkg.pr.new/@nativescript/vite@11617
npm i https://pkg.pr.new/@nativescript/webpack@11617

commit: 309fbaa

@NathanWalker

Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Explain Change →

Walkthrough

Event-listener APIs across core observables, windows, and UI components now return their instance, with Observable tests covering chaining. The Android fullscreen configuration also reapplies edge-to-edge settings when night mode changes.

Changes: Event Listener Return Contracts

Layer / File(s) Summary
Observable listener methods and tests
packages/core/data/observable/*, packages/core/data/observable-array/index.ts, packages/core/data/virtual-array/index.ts, packages/core/media-query-list/index.ts
Observable listener methods now return the instance on registration and removal paths. ObservableArray, VirtualArray, and MediaQueryList listener APIs declare instance return types. Tests check return values and verify chaining with regular and once-listeners.
Native window event APIs
packages/core/native-window/*
WindowBase and NativeWindow event overloads return this. Their on implementations return the superclass result.
UI listener implementations
packages/core/ui/core/view/*, packages/core/ui/scroll-view/scroll-view-common.ts, packages/core/ui/text-base/span.ts
View, ScrollView, and Span listener methods return their instance after handling listeners. DialogFragmentImpl tracks fullscreen night mode and reapplies edge-to-edge configuration when that mode changes.
UI event overload contracts
packages/core/ui/action-bar/index.d.ts, packages/core/ui/button/index.d.ts, packages/core/ui/frame/index.d.ts, packages/core/ui/image-cache/*, packages/core/ui/list-view/*, packages/core/ui/page/*, packages/core/ui/placeholder/*, packages/core/ui/scroll-view/index.d.ts, packages/core/ui/search-bar/index.d.ts, packages/core/ui/segmented-bar/*, packages/core/ui/tab-view/*, packages/core/ui/web-view/*
Event overloads for UI components and Cache now return this. Button, SearchBar, and WebView native property declarations also move semicolons before inline comments; their types remain unchanged.

Severity of issue fixed: Low

Merge Risk: Low · up to 309fb

Most listener APIs now support chaining, but SharedTransition listener chains still fail and global matchMedia chains remain unavailable to TypeScript callers. These are bounded API gaps; address them and the Span declaration requirement before considering the feature complete.

Pre-merge checks: Passed 4 · Inconclusive 1

Failed Failed checks: 1 inconclusive

Check name Status Explanation Resolution
Out of Scope Changes check Inconclusive [#6828] The listener, type, and test changes support chaining. The summary also describes a fullscreen modal night-mode change in packages/core/ui/core/view/index.android.ts, which is unrelated to c… Provide the merge-base version or whole-PR diff for packages/core/ui/core/view/index.android.ts to establish whether the night-mode code is part of this PR.
Passed Passed checks: 4 passed

Check name Status Explanation
Title check Passed The title identifies the main change: chainable Observable listener methods.
Description check Passed The description explains the chainable listener changes and related tests.
Linked Issues check Passed [#6828] Observable.on, once, and off now return this. The listener overrides and public overloads support the same contract. The new tests check return values and chained listener behavior.
Docstring Coverage Passed No eligible functions found in the changed files. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat…
Full details: Docstring Coverage

Explanation

No eligible functions found in the changed files. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 0 functions across 33 files.

Full details: Out of Scope Changes check

Explanation

[#6828] The listener, type, and test changes support chaining. The summary also describes a fullscreen modal night-mode change in packages/core/ui/core/view/index.android.ts, which is unrelated to chaining. The head contains that code, but the summary reports only six added lines in the file. The base blob and file diff were unavailable, so the evidence does not establish whether the PR introduced the night-mode code.

Resolution

Provide the merge-base version or whole-PR diff for packages/core/ui/core/view/index.android.ts to establish whether the night-mode code is part of this PR.

Finishing Touches

Generate unit tests (beta)

  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.

Tip

🎯 Customize your risk assessments

Guide how CodeRabbit assesses merge risk and security architecture risk. Highlight sensitive changes and explain your security boundaries using Merge Risk Instructions and Security Risk Instructions in your review settings. Security architecture risk assessments require the Advanced plan or above.

✅ Automatic approvals, with your rules

Let CodeRabbit approve pull requests once required threads are resolved, pre-merge checks pass, and your risk limits are met. Enable Automatic approvals in review settings, with overrides for teams or file paths.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Note

Follow-up findings are listed below so you can review them separately from inline comments.

Follow-ups (2)

Minor (2)

Minor · Annotate every View listener overload with this. · index.d.ts:859

packages/core/ui/core/view/index.d.ts:859
Minor · Functional Correctness · Quick win

Annotate every View listener overload with this.

View.on('loaded', ...) and View.off(...) currently resolve to declarations with implicit any returns. Chained calls can therefore bypass View type checking. Add : this to the generic and event-specific on overloads and to off.

Minor · Update the Span declaration. · span.ts:116

packages/core/ui/text-base/span.ts:116
Minor · Maintainability & Code Quality · Low value

Update the Span declaration.

Span inherits the updated signatures, but span.d.ts is unchanged. Declare the updated listener signatures there.

As per coding guidelines, “update the neighboring .d.ts whenever a public API changes.”

Nitpick comments: 2

packages/core/ui/core/view/view-common.ts: 1

412-412: Trivial · Maintainability & Code Quality · Quick win

Add View gesture chaining tests.

The new tests cover only Observable. They do not execute ViewCommon.addEventListener or ViewCommon.removeEventListener through the gesture branch. A regression to return; would therefore pass the current tests while breaking direct gesture calls such as view.addEventListener('tap', handler).removeEventListener('tap', handler).

packages/core/data/observable/index.spec.ts: 1

166-170: Trivial · Functional Correctness · Quick win

Cover idempotent listener operations.

Add return-value assertions for duplicate registration and removal of an absent event. These paths are not covered by the new tests, so a bare early return could regress the fluent contract unnoticed.

Suggested test additions

 		it('addEventListener and removeEventListener return the observable', () => {
 			const observable = new Observable();
 			const handler = () => {};
 			expect(observable.addEventListener('test', handler)).toBe(observable);
+			expect(observable.addEventListener('test', handler)).toBe(observable);
 			expect(observable.removeEventListener('test', handler)).toBe(observable);
+			expect(observable.removeEventListener('missing', handler)).toBe(observable);
 		});
Review info

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 34ee8577-b621-4208-b62c-f8f006ca5745
Commits

Reviewing files that changed from the base of the PR and between 07e84ec and 309fbaa.

Files selected for processing: 33

  • packages/core/data/observable-array/index.ts
  • packages/core/data/observable/index.spec.ts
  • packages/core/data/observable/index.ts
  • packages/core/data/virtual-array/index.ts
  • packages/core/media-query-list/index.ts
  • packages/core/native-window/index.d.ts
  • packages/core/native-window/native-window-common.ts
  • packages/core/native-window/window-base.ts
  • packages/core/ui/action-bar/index.d.ts
  • packages/core/ui/button/index.d.ts
  • packages/core/ui/core/view/index.android.ts
  • packages/core/ui/core/view/index.d.ts
  • packages/core/ui/core/view/view-common.ts
  • packages/core/ui/frame/index.d.ts
  • packages/core/ui/image-cache/image-cache-common.ts
  • packages/core/ui/image-cache/index.d.ts
  • packages/core/ui/list-view/index.d.ts
  • packages/core/ui/list-view/list-view-common.ts
  • packages/core/ui/page/index.d.ts
  • packages/core/ui/page/page-common.ts
  • packages/core/ui/placeholder/index.android.ts
  • packages/core/ui/placeholder/index.d.ts
  • packages/core/ui/placeholder/index.ts
  • packages/core/ui/scroll-view/index.d.ts
  • packages/core/ui/scroll-view/scroll-view-common.ts
  • packages/core/ui/search-bar/index.d.ts
  • packages/core/ui/segmented-bar/index.d.ts
  • packages/core/ui/segmented-bar/segmented-bar-common.ts
  • packages/core/ui/tab-view/index.d.ts
  • packages/core/ui/tab-view/tab-view-common.ts
  • packages/core/ui/text-base/span.ts
  • packages/core/ui/web-view/index.d.ts
  • packages/core/ui/web-view/web-view-common.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Comment on lines +189 to +192
public on(eventName: string, callback: (data: EventData) => void, thisArg?: any): this {
this.addEventListener(eventName, callback, thisArg);

return this;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor · Functional Correctness · Quick win

Supported by static analysis

Script executed:

rg -n --glob '*.ts' --glob '*.js' '(on|once|off|addEventListener|removeEventListener)\s*\(' packages/core | head -n 220

Repository: NativeScript/NativeScript

Length of output: 25903


Script executed:

git diff --stat 07e84ec588e1103bda0f58bbb37594bf48ccba57 309fbaa167e2b3e49391e7652039118cb30db01a
nl -ba packages/core/data/observable/index.ts | sed -n '125,310p'
rg -n --glob '*.{ts,js}' '(^|[[:space:]])(public |protected |private |override |static |async )*(on|once|off|addEventListener|removeEventListener)[[:space:]]*\(' packages --glob '!*.spec.ts' --glob '!*.test.ts'

Repository: NativeScript/NativeScript

Length of output: 41344


Script executed:

for f in \
packages/core/data/observable-array/index.ts \
packages/core/data/virtual-array/index.ts \
packages/core/media-query-list/index.ts \
packages/core/native-window/window-base.ts \
packages/core/native-window/native-window-common.ts \
packages/core/ui/core/view/view-common.ts \
packages/core/ui/core/view/index.android.ts \
packages/core/ui/scroll-view/scroll-view-common.ts \
packages/core/ui/text-base/span.ts \
packages/core/ui/shared-transition/shared-transition.ts \
packages/core/ui/transition/shared-transition.ts \
packages/core/ui/image-cache/image-cache-common.ts \
packages/core/ui/page/page-common.ts \
packages/core/ui/web-view/web-view-common.ts \
packages/core/ui/list-view/list-view-common.ts \
packages/core/ui/tab-view/tab-view-common.ts \
packages/core/ui/segmented-bar/segmented-bar-common.ts \
packages/core/ui/placeholder/index.ts \
packages/core/ui/placeholder/index.android.ts; do
  if test -f "$f"; then
    echo "===== $f ====="
    rg -n -C 8 '(^|[[:space:]])(public |protected |private |override |static |async )*(on|once|off|addEventListener|removeEventListener)[[:space:]]*\(' "$f" || true
  fi
done
echo "===== class inheritance ====="
rg -n --glob '*.ts' 'class [A-Za-z0-9_$]+ extends (Observable|ViewBase|ScrollViewBase|NativeWindow|WindowBase|MediaQueryListImpl|Span)' packages/core || true
echo "===== shared transition ====="
nl -ba packages/core/ui/transition/shared-transition.ts | sed -n '170,220p'

Repository: NativeScript/NativeScript

Length of output: 29876


Script executed:

for f in \
packages/core/data/observable-array/index.ts \
packages/core/data/virtual-array/index.ts \
packages/core/media-query-list/index.ts \
packages/core/native-window/window-base.ts \
packages/core/native-window/native-window-common.ts \
packages/core/ui/core/view/view-common.ts \
packages/core/ui/core/view/index.android.ts \
packages/core/ui/scroll-view/scroll-view-common.ts \
packages/core/ui/text-base/span.ts \
packages/core/ui/transition/shared-transition.ts \
packages/core/ui/image-cache/image-cache-common.ts \
packages/core/ui/page/page-common.ts \
packages/core/ui/web-view/web-view-common.ts \
packages/core/ui/list-view/list-view-common.ts \
packages/core/ui/tab-view/tab-view-common.ts \
packages/core/ui/segmented-bar/segmented-bar-common.ts \
packages/core/ui/placeholder/index.ts \
packages/core/ui/placeholder/index.android.ts; do
  if test -f "$f"; then
    echo "===== $f ====="
    rg -n -C 8 '(^|[[:space:]])(public |protected |private |override |static |async )*(on|once|off|addEventListener|removeEventListener)[[:space:]]*\(' "$f" || true
  fi
done
echo "===== class inheritance ====="
rg -n --glob '*.ts' 'class [A-Za-z0-9_$]+ extends (Observable|ViewBase|ScrollViewBase|NativeWindow|WindowBase|MediaQueryListImpl|Span)' packages/core || true
echo "===== shared transition ====="
nl -ba packages/core/ui/transition/shared-transition.ts | sed -n '170,220p'

Repository: NativeScript/NativeScript

Length of output: 29876


Script executed:

rg -n -C 5 -e 'sharedTransitionEvents' -e 'SharedTransitionObservable' packages/core/ui/transition packages/core
rg -n -C 3 -e 'SharedTransition.events' -e 'SharedTransition.get' packages/core apps --glob '*.{ts,js,xml}' || test "$?" -eq 1
nl -ba packages/core/ui/transition/shared-transition.ts | sed -n '215,360p'
nl -ba packages/core/ui/core/view/view-common.ts | sed -n '388,455p'
nl -ba packages/core/ui/core/view/index.android.ts | sed -n '638,680p'
nl -ba packages/core/media-query-list/index.ts | sed -n '110,158p'
nl -ba packages/core/ui/scroll-view/scroll-view-common.ts | sed -n '33,68p'
git diff 07e84ec588e1103bda0f58bbb37594bf48ccba57 309fbaa167e2b3e49391e7652039118cb30db01a -- packages/core/ui/transition/shared-transition.ts

Repository: NativeScript/NativeScript

Length of output: 44157


Return the receiver from SharedTransitionObservable.on.

SharedTransition.events().on(...) returns undefined because the override discards the superclass result. A JavaScript chain then throws, and TypeScript callers cannot chain through on. Return super.on(eventNames, <any>callback, thisArg) from the override.


// @ts-ignore
public addEventListener(eventName: string, callback: (data: EventData) => void, thisArg?: any, once?: boolean): void {
public addEventListener(eventName: string, callback: (data: EventData) => void, thisArg?: any, once?: boolean): this {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor · Functional Correctness · Quick win

Supported by static analysis

Script executed:

sed -n '1,75p' packages/core/media-query-list/index.ts; sed -n '200,360p' packages/core/media-query-list/index.ts; rg -n --glob '*.ts' 'interface MediaQueryList|declare.*MediaQueryList|function matchMedia|matchMedia\s*[:=]' packages/core | head -n 70

Repository: NativeScript/NativeScript

Length of output: 3003


Script executed:

git diff 07e84ec588e1103bda0f58bbb37594bf48ccba57 309fbaa167e2b3e49391e7652039118cb30db01a -- packages/core/media-query-list/index.ts packages/core/native-window/index.d.ts packages/core/native-window/window-base.ts packages/core/native-window/native-window-common.ts
nl -ba packages/core/media-query-list/index.ts | sed -n '45,215p'
rg -n -F -e 'matchMedia' -e 'MediaQueryList' -- packages/core/native-window packages/core/globals packages/core/media-query-list packages/core/package.json || test "$?" -eq 1
rg --files packages/core/media-query-list

Repository: NativeScript/NativeScript

Length of output: 41775


Script executed:

nl -ba packages/core/globals/index.ts | sed -n '300,340p'
rg -n -F -e 'interface MediaQueryList' -e 'declare var matchMedia' -e 'matchMedia(' -e 'MediaQueryList' --glob '*.ts' --glob '*.d.ts' --glob 'tsconfig*.json' .
rg -n -F -e '"dom"' -e "'dom'" -e 'lib' --glob 'tsconfig*.json' --glob '*.json' packages/core | head -n 100
find packages/core -maxdepth 3 -type f \( -name '*.d.ts' -o -name 'tsconfig*.json' \) -print

Repository: NativeScript/NativeScript

Length of output: 13555


Script executed:

nl -ba packages/core/tsconfig.lib.json
nl -ba tsconfig.base.json | sed -n '1,140p'
nl -ba apps/ui/tsconfig.json
nl -ba apps/ui/src/media-queries/main-page.ts | sed -n '1,48p'
nl -ba packages/core/package.json | sed -n '1,100p'
nl -ba packages/core/globals/index.ts | sed -n '1,60p;275,330p'
rg -n '"typescript"|typescript@|typescript:' package.json pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null | head -n 50
command -v tsc
TSC="$(command -v tsc)"; if [ -n "$TSC" ]; then ROOT="$(dirname "$(dirname "$(readlink -f "$TSC")")")"; echo "typescript root: $ROOT"; rg -n '^(interface MediaQueryList extends|declare function matchMedia|    addEventListener<K extends keyof MediaQueryListEventMap>|    removeEventListener<K extends keyof MediaQueryListEventMap>)' "$ROOT/lib/lib.dom.d.ts"; fi

Repository: NativeScript/NativeScript

Length of output: 12930


Web query:

TypeScript 5.9.3 lib.dom.d.ts "declare function matchMedia" "interface MediaQueryList" addEventListener void

Result:

Sources consulted:
- https://raw.githubusercontent.com/microsoft/TypeScript/v5.9.3/lib/lib.dom.d.ts
- https://raw.githubusercontent.com/microsoft/TypeScript/v5.9.3/src/lib/dom.generated.d.ts
- Releases · microsoft/TypeScript · GitHub — https://github.com/microsoft/TypeScript/releases
- …and 26 more

Script executed:

readlink -f /usr/local/bin/tsc
ls -l /usr/local/bin/tsc /usr/local/lib/node_modules/typescript 2>&1
nl -ba packages/core/tsconfig.json
nl -ba package.json | sed -n '100,115p'

Repository: NativeScript/NativeScript

Length of output: 1702


Script executed:

TSROOT=/usr/local/lib/node_modules/typescript
rg -n -F -e 'interface MediaQueryList' -e 'declare function matchMedia' -e 'addEventListener<K extends keyof MediaQueryListEventMap>' -e 'removeEventListener<K extends keyof MediaQueryListEventMap>' "$TSROOT/lib/lib.dom.d.ts"
rg -n -F -e 'matchMedia' -e 'MediaQueryList' packages/core/index.ts packages/core/index.d.ts packages/core/global-types.d.ts packages/core/references.d.ts packages/core/typings packages/core 2>/dev/null | head -n 120
nl -ba packages/core/index.ts | sed -n '1,180p'
nl -ba packages/core/index.d.ts | sed -n '1,180p'
nl -ba packages/core/global-types.d.ts | sed -n '1,160p'

Repository: NativeScript/NativeScript

Length of output: 21053


Web query:

site:raw.githubusercontent.com/microsoft/TypeScript/v5.9.3/lib/lib.dom.d.ts "interface MediaQueryList" "addEventListener" "removeEventListener"

Result:

Sources consulted:
- https://raw.githubusercontent.com/microsoft/TypeScript/v5.9.3/lib/lib.dom.d.ts

Expose fluent listener types for the global matchMedia API.

tsconfig.base.json includes the DOM declarations, where MediaQueryList.addEventListener and removeEventListener return void. NativeScript does not augment these declarations, so chaining global matchMedia(...).addEventListener(...) still fails type-checking despite the runtime implementation returning this.

This branch has not been deployed

No deployments
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.

Observable 'on' and 'off' should return 'this'

2 participants