Skip to content

Instantly share code, notes, and snippets.

@MrJackdaw
Last active February 10, 2025 22:54
Show Gist options
  • Save MrJackdaw/9c3629386c612fd522e71d973e3e80b0 to your computer and use it in GitHub Desktop.
Save MrJackdaw/9c3629386c612fd522e71d973e3e80b0 to your computer and use it in GitHub Desktop.
ReactJS ListView Component (TSX, CSS, and example usage)
: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;
}
}
/**
* 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>
);
}
/** 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