-
Notifications
You must be signed in to change notification settings - Fork 126
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
Headless virtual list #702
Conversation
🦋 Changeset detectedLatest commit: 0a6d281 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
There was a problem hiding this 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
rowHeight, | ||
overscanCount, | ||
}: { | ||
rootElement: Accessor<Element>; |
There was a problem hiding this comment.
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 anywayonScroll: (el: Element) => void
and we read the scrollTop from the element and the user adds the listeneronScroll: (offset: number) => void
and the user reads the scrollTop from the element and adds the listener
There was a problem hiding this comment.
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
}, | ||
containerHeight: () => items.length * rowHeight, | ||
viewerTop: () => getFirstIdx() * rowHeight, | ||
visibleItems: () => items.slice(getFirstIdx(), getLastIdx()), |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why's that desirable?
There was a problem hiding this comment.
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(...)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 thenmyVirtual.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
There was a problem hiding this comment.
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.
74549d1
to
5a835c4
Compare
@thetarnav thank you for your review and especially all the api design suggestions. It's ready for rereview. |
bc8ed0a
to
e128a76
Compare
@thetarnav any word on what API to go with for the final version? |
e128a76
to
1ae3ec6
Compare
@thetarnav thank you, updated |
@thetarnav I had to silence/change some things for TS, but I've made the rest of the changes. |
4af3994
to
bc00ee3
Compare
bc00ee3
to
0a6d281
Compare
@thetarnav thanks for all your help and patience through the review process |
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.