Last active
February 10, 2025 22:54
-
-
Save MrJackdaw/9c3629386c612fd522e71d973e3e80b0 to your computer and use it in GitHub Desktop.
ReactJS ListView Component (TSX, CSS, and example usage)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
:root { | |
--sm: 0.4rem; | |
--md: 0.6rem; | |
--lg: 1.2rem; | |
} | |
/* */ | |
.list-view { | |
display: grid; | |
padding: 0; | |
&:not(.list-view--grid) { | |
list-style: none; | |
/* margin: 0; */ | |
} | |
} | |
.list-view.list-view--grid { | |
align-items: flex-start; | |
column-gap: 1.75rem; | |
row-gap: 4.2rem; | |
display: grid; | |
grid-auto-rows: minmax(112px, auto); | |
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); | |
padding: 1rem 0; | |
margin: 0 auto; | |
max-width: 100%; | |
width: 100%; | |
} | |
.list-view.list-view--masonry { | |
column-count: 2; | |
column-gap: var(--md); | |
display: block; | |
margin: 0 auto; | |
max-width: 100%; | |
padding: 1rem 0; | |
width: 100%; | |
> * { | |
display: inline-block; | |
break-inside: avoid; | |
margin-bottom: 1rem; | |
width: 100%; | |
} | |
} | |
.list-view .list-view__scroll-container { | |
-ms-overflow-style: none; | |
flex-shrink: 0; | |
scrollbar-width: none; | |
text-align: left; | |
&::-webkit-scrollbar { | |
display: none; | |
} | |
&:last-of-type { | |
border: 0; | |
} | |
&.rounded { | |
&:last-child, | |
&:first-child { | |
border-radius: var(--sm); | |
} | |
&:last-child { | |
border-top-left-radius: 0; | |
border-top-right-radius: 0; | |
} | |
&:first-child { | |
border-bottom-left-radius: 0; | |
border-bottom-right-radius: 0; | |
} | |
} | |
} | |
/* */ | |
@media screen and (max-width: 1250px) { | |
.list-view.list-view--grid { | |
column-gap: 0.6rem; | |
} | |
} | |
/* */ | |
@media screen and (max-width: 1400px) { | |
.list-view.list-view--grid { | |
column-gap: 0.6rem; | |
padding-left: 0.6rem; | |
padding-right: 0.6rem; | |
row-gap: var(--md); | |
} | |
} | |
@media screen and (max-width: 1300px) { | |
.list-view.list-view--grid { | |
row-gap: var(--sm); | |
column-gap: var(--sm); | |
} | |
} | |
/* */ | |
@media screen and (max-width: 990px) { | |
.list-view.list-view--masonry { | |
padding-left: 0.6rem; | |
padding-right: 0.6rem; | |
column-gap: 0.6rem; | |
} | |
} | |
/* */ | |
@media screen and (max-width: 768px) { | |
} | |
/* */ | |
@media screen and (max-width: 600px) { | |
.list-view.list-view--grid { | |
> * { | |
margin-bottom: var(--sm); | |
} | |
} | |
} | |
/* */ | |
@media screen and (max-width: 475px) { | |
.list-view--header { | |
padding-left: 1rem; | |
padding-right: 1rem; | |
} | |
.list-view.list-view--grid { | |
row-gap: 1.6rem; | |
} | |
.list-view.list-view--masonry { | |
column-count: 1; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* A ReactJS Typescript component for generating lists of any sort. Can be used with | |
* your favorite css framework (e.g. TailwindCSS). The component provides type-inferencing | |
* for your list data for code-editor hints: you don't need to apply a `key` directive | |
* for each child element. | |
* | |
* The component can be used as-is in any react framework. You can also modify the file | |
* and/or its CSS to suit your needs, or override them with classes from another CSS framework. | |
* You may be able to reuse it with minimal modification in a non-ReactJS JSX framework. | |
*/ | |
import { ComponentPropsWithRef, Fragment, JSX, ReactNode } from "react"; | |
import "./ListView.css"; | |
export type ListViewProps<T> = | |
| Omit<ComponentPropsWithRef<"div">, "placeholder"> & { | |
/** Data that will be converted into ui elements */ | |
data: T[]; | |
/** Grid list when true */ | |
grid?: boolean; | |
/** Optional text or react element to show when empty */ | |
placeholder?: ReactNode; | |
/** Dummy first item (e.g. "add new item" button) */ | |
listHeader?: ReactNode; | |
/** Dummy last item (e.g. "add new item" button) */ | |
listFooter?: ReactNode; | |
/** Function that returns the element for each list item */ | |
itemText: (d: T, i: number) => ReactNode; | |
/** Unique identifier for list-items. Defaults to list-item index */ | |
itemKey?: (d: T, i: number) => string | number; | |
/** Max height for fixed-height list */ | |
maxHeight?: string; | |
}; | |
/** @Component Generalized `ListView` renders a vertical/horizontal list (or grid) of items */ | |
export default function ListView<T>(props: ListViewProps<T>): JSX.Element { | |
const { | |
data, | |
itemText, | |
itemKey = (_d: T, i: number) => i, | |
grid = false, | |
placeholder, | |
listHeader, | |
listFooter, | |
className = "", | |
maxHeight, | |
...rest | |
} = props; | |
let classlist = `list-view`.trim(); | |
if (grid) { | |
classlist = `${classlist} list-view--grid`.trim(); | |
if (data.length >= 3) classlist = `${classlist} list-view--center`.trim(); | |
} | |
classlist = `${classlist} ${className || ""}`; | |
const contents = data.map((item, i: number) => ( | |
<Fragment key={itemKey(item, i)}>{itemText(item, i)}</Fragment> | |
)); | |
return ( | |
<div className={classlist} role="list" {...rest}> | |
{/* Dummy first item (e.g. "add new item" button) */} | |
{listHeader} | |
{/* Placeholder for empty list (if supplied) */} | |
{!data.length && placeholder && placeholder} | |
{maxHeight ? ( | |
// Fixed-size list (enables list-item scrolling beyond certain height) | |
<div | |
className="list-view__scroll-container" | |
role="listitem" | |
style={{ maxHeight, overflowY: "auto" }} | |
> | |
{contents} | |
</div> | |
) : ( | |
// Variable-size list | |
data.length > 0 && contents | |
)} | |
{/* Dummy last item (e.g. "add new item" button) */} | |
{listFooter} | |
</div> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** The following is the bare-minimum usage example */ | |
// For type-inferencing, make sure all list data is the same. Otherwise, | |
// you will have to do gymnastics with the item index to determine what | |
// to render (see below) | |
const listData = [ | |
{ title: "Item 1", text: "First Item" }, | |
{ title: "Item 2", text: "Second Item" }, | |
{ title: "Item 3", text: "Third Item" }, | |
] | |
return ( | |
<ListView | |
data={listData} // data for the list | |
itemText={ | |
(data, i) => { | |
// NOTE: You can return any sort of element (or fragment) from this function. | |
// Editor knows that data's type is { title: string; text: string } | |
// Item index `i` is optional, but shown here so you know it exists. | |
// If you're not using typescript and have a mixed data array, you | |
// can use it to return unique items (e.g. if (i === 3) return ... ) | |
return ( | |
<> | |
<h3>{data.title}</h3> | |
<p>{data.text}</p> | |
</> | |
) | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment