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

Headless virtual list #702

Merged

Conversation

SpencerWhitehead7
Copy link
Contributor

As previously suggested, this is an attempt at rewriting the virtual list comp to be a headless utility function and adds tests and docs. It still exports the VirtualList comp for convenience, which uses the utility internally. The utility and comp are tested separately.

I think a lot of the TS in here is gross and people who know solid better than me might have better ideas for how to handle the ref. However, it does work and most of the ugliness is hidden from consumers. I don't really like the API, in terms of what you need to pass in and what comes back out and how you have to use it with the JSX, but I couldn't find anything to remove/add/simplify from/to/in it.

Copy link

changeset-bot bot commented Sep 30, 2024

🦋 Changeset detected

Latest commit: 0a6d281

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@solid-primitives/virtual Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@SpencerWhitehead7
Copy link
Contributor Author

@thetarnav #667 (comment)

Copy link
Member

@thetarnav thetarnav left a comment

Choose a reason for hiding this comment

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

I like this direction. There has already been an issue about not being able to control some part of rendering: #698 so this will be very helpful, even if a lot of people will end up using the component for convenience.

I don't really like the API

That may be because the boilerplate of inputs and outputs and passing everything makes more code than the calculation itself.

A non-reactive function like this would work as well:

export function getVirtualList(offset, items, rowHeight, overscanCount) {

  let firstIdx = Math.max(0, Math.floor(offset / rowHeight) - overscanCount)
  let lastIdx = Math.min(
    items.length,
    Math.floor(offset / rowHeight) + Math.ceil(rootHeight / rowHeight) + overscanCount,
  )

  return {
    firstIdx,
    lastIdx,
    containerHeight: items.length * rowHeight,
    viewerTop: firstIdx * rowHeight,
    visibleItems: items.slice(firstIdx, lastIdx),
  }
}

packages/virtual/src/index.tsx Outdated Show resolved Hide resolved
packages/virtual/src/index.tsx Outdated Show resolved Hide resolved
packages/virtual/src/index.tsx Outdated Show resolved Hide resolved
rowHeight,
overscanCount,
}: {
rootElement: Accessor<Element>;
Copy link
Member

Choose a reason for hiding this comment

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

Instead of having both rootElement: Accessor<Element> and onScroll: VoidFunction, I think we should stick to one of those:

  • rootElement: Accessor<Element | undefined | null | false> and we add an event listener in an effect ourselves, no need for handling scroll by the user.
  • offset: Accessor<number> and the user instead of creating a signal for the element, created one for the scrollTop value and manages that - it's the same amount of code for the user anyway
  • onScroll: (el: Element) => void and we read the scrollTop from the element and the user adds the listener
  • onScroll: (offset: number) => void and the user reads the scrollTop from the element and adds the listener

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I ended up with not passing in the root element and using onScroll(event: ScrollEvent) => void and having the utility get the scrollTop from event.target. The typescript for it is relatively straightforwards and it's the easiest for the end user.

packages/virtual/src/index.tsx Outdated Show resolved Hide resolved
},
containerHeight: () => items.length * rowHeight,
viewerTop: () => getFirstIdx() * rowHeight,
visibleItems: () => items.slice(getFirstIdx(), getLastIdx()),
Copy link
Member

Choose a reason for hiding this comment

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

getFirstIdx and getLastIdx could be returned as well

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why's that desirable?

Choose a reason for hiding this comment

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

Personal opinion: A single object returned would make it cleaner. const myVirtual = createVirtualList(...) and then myVirtual.onScroll can be assigned to elements as necessary. Renaming it is still possible during destructuring, like: const {onScroll: myOnScroll} = createVirtualList(...)

Choose a reason for hiding this comment

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

getFirstIdx could be used to implement a ledger effect.

Copy link
Member

Choose a reason for hiding this comment

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

Personal opinion: A single object returned would make it cleaner. const myVirtual = createVirtualList(...) and then myVirtual.onScroll can be assigned to elements as necessary. Renaming it is still possible during destructuring, like: const {onScroll: myOnScroll} = createVirtualList(...)

yeah i'm on the fence about this one as well

the object could also be made of getters instead of accessors
virtual.visibleItems vs virtual.visibleItems()
since I doubt anyone will try to destructure it further

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the [values, setter] thing is nice and symmetrical with the createSignal API, but maybe it's a little too cute. I'm fine with implementing whatever API you settle on out of the options raised in this PR.

packages/virtual/README.md Outdated Show resolved Hide resolved
packages/virtual/README.md Show resolved Hide resolved
@SpencerWhitehead7
Copy link
Contributor Author

@thetarnav thank you for your review and especially all the api design suggestions. It's ready for rereview.

@SpencerWhitehead7
Copy link
Contributor Author

@thetarnav any word on what API to go with for the final version?

packages/virtual/src/index.tsx Outdated Show resolved Hide resolved
packages/virtual/src/index.tsx Outdated Show resolved Hide resolved
packages/virtual/src/index.tsx Outdated Show resolved Hide resolved
@SpencerWhitehead7
Copy link
Contributor Author

@thetarnav thank you, updated

@SpencerWhitehead7
Copy link
Contributor Author

@thetarnav I had to silence/change some things for TS, but I've made the rest of the changes.

@SpencerWhitehead7 SpencerWhitehead7 force-pushed the headless-virtual-list branch 2 times, most recently from 4af3994 to bc00ee3 Compare October 29, 2024 13:44
@thetarnav thetarnav merged commit d4763fe into solidjs-community:main Oct 29, 2024
3 checks passed
@github-actions github-actions bot mentioned this pull request Oct 29, 2024
@SpencerWhitehead7
Copy link
Contributor Author

@thetarnav thanks for all your help and patience through the review process

@SpencerWhitehead7 SpencerWhitehead7 deleted the headless-virtual-list branch November 2, 2024 02:13
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.

3 participants