Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(angular-virtual): add Angular adapter #726

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

garrettld
Copy link

@garrettld garrettld commented May 10, 2024

Adds an Angular adapter:

  • Supports Angular >=17
  • Signal-based API
  • Packaged with ng-packagr
  • Examples
  • Works with required input signals
  • Works with OnPush change detection

StackBlitz playground: https://stackblitz.com/edit/stackblitz-starters-sd2s4h?file=src%2Frow-virtualizer-fixed.component.ts

Adds an Angular adapter:

- [x] Supports Angular >=17
- [x] Signal-based API
- [x] Packaged with ng-packagr
- [x] Examples
- [x] Works with required input signals
- [x] Works with OnPush change detection
@KevinVandy
Copy link
Member

Nice, maybe we'll ship 2 new TanStack Angular adapters this week. The Table one should ship in the next few days.

@garrettld
Copy link
Author

garrettld commented May 10, 2024

Hi! At this point I'm mostly just looking for some intent to move forward by the project maintainers as well as feedback from the community. I don't necessarily expect this to be merged as-is, though it is fully functional.

This began as a weeknight project inspired by a tweet and before long I had implemented all the examples and fell in love with it. The API and its implementation are heavily inspired by Angular Query and the upcoming Angular Table.

There are some specific areas of the API that I would love community feedback on. For example, the adapter replaces the getScrollElement function on the tanstack/virtual-core Virtualizer with a simple scrollElement property and automatically unwraps ElementRefs for you, which simplifies things a bit, especially when using signal queries:

scrollElement = viewChild<ElementRef<HTMLDivElement>>('scrollElement')

// current implementation
virtualizer = injectVirtualizer(() => ({ scrollElement: this.scrollElement() })

// theoretical implementation that matches the core API
virtualizer = injectVirtualizer(() => ({ getScrollElement: () => this.scrollElement()?.nativeElement })

This is currently the only property from the core Virtualizer API that has been renamed or changed outside of turning things into signals, but there are other parts of the API that could potentially be more signal-ified. For one example, rangeExtractor can create friction when the callback's result depends on one or more signals.

headerIsSticky = input.required<boolean>()
virtualizer = injectVirtualizer(() => ({
  rangeExtractor: (range: { min: number, max: number }) =>
    // oops! this won't be re-ran when `this.headerIsSticky()` changes!
    this.headerIsSticky() && range.min > 0
      ? [0, ...defaultRangeExtractor(range)]
      : defaultRangeExtractor(range)
})

// you have to do this instead:
headerIsSticky = input.required<boolean>()
virtualizer = injectVirtualizer(() => ({
  // rangeExtractor is recreated whenever this.headerIsSticky() changes
  rangeExtractor: this.headerIsSticky()
    ? (range: { min: number, max: number }) => range.min > 0
      ? [0, ...defaultRangeExtractor(range)]
      : defaultRangeExtractor(range)
    : defaultRangeExtractor
})

This exact issue (callbacks that close over signals are not reactive) is present in Angular Query as well, and it's not necessarily a dealbreaker, but I can imagine an API where the options object is static (i.e., it's not recreated), but instead each property on it is (optionally?) a signal.

For the TS wizards, think something like this (simplified):

type AngularVirtualizerOptions = { [K in keyof VirtualizerOptions]: Signal<VirtualizerOptions[K]> }

for example:

virtualizer: injectVirtualizer({ // no callback, just a {}
  // pass signals directly for everything
  data: this.data,
  count: computed(() => this.data().length),
  size: computed(() => this.big() ? ((index) => 250) : ((index) =>  100)),
  // functions accept signal parameters and expect signal return values
  rangeExtractor: (range: Signal<{ min: number; max: number }>) => computed(() =>
    // because `rangeExtractor` must return signal, you don't have to remember to hoist signal invocations out of the callback
    this.headerIsSticky() && range().min > 0
      ? [0, ...defaultRangeExtractor(range)]
      : defaultRangeExtractor(range))
})

The adapter could easily convert this into the shape that the core Virtualizer needs.

Are these terrible ideas? Awesome improvements? If you have thoughts on any of these examples or anything else, please share them!

Add devcontainer.json to all Angular examples so they run in a container.

Updates the Table example to use TanStack Angular Table
@garrettld
Copy link
Author

The table example has been updated to use the TanStack Angular Table 🚀

Copy link

nx-cloud bot commented May 14, 2024

☁️ Nx Cloud Report

CI is running/has finished running commands for commit aa61434. As they complete they will appear below. Click to see the status, the terminal output, and the build insights.

📂 See all runs for this CI Pipeline Execution


🟥 Failed Commands
nx affected --targets=test:format,test:sherif,test:knip,test:lib,test:types,test:build,build

Sent with 💌 from NxCloud.

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.

None yet

2 participants