Customizing the UI for an array of objects in a Slack thread.
6 replies
Last updated: May 20, 2024
T
Hey everyone!
I have just a simple array of objects the user can input in this field. I would like to customise the UI so that instead of the basic stacked list i would get something like a popover window with more descriptive cards for each possible item.
I really don't know where to start and I the documentation is not helping at all.
I have just a simple array of objects the user can input in this field. I would like to customise the UI so that instead of the basic stacked list i would get something like a popover window with more descriptive cards for each possible item.
I really don't know where to start and I the documentation is not helping at all.
May 19, 2024, 6:30 PM
T
Resolved this problem. There was a nice useful snippet on some forum i was able to adopt.
May 19, 2024, 6:47 PM
H
You should post your solution! I’m sure it would help others
May 19, 2024, 6:54 PM
T
Gladly!
May 19, 2024, 7:27 PM
T
This is the custom component that will be attached to the array field. I didnt have time to style it much, but basically we are passing in all props and they are available in here for displaying anything. I also noticed it can be used to prepopulate fields is that is interesting.
A simple example of a usecase bellow:
It will look something like this:
import { ArrayOfObjectsInputProps, BooleanSchemaType, FileSchemaType, NumberSchemaType, ObjectSchemaType, ReferenceSchemaType, StringSchemaType } from "sanity"; import { Grid, Stack, Button, Dialog, Box, Card, Heading } from "@sanity/ui"; import { useCallback, useState } from "react"; import { AddIcon } from "@sanity/icons"; import { randomKey } from "@sanity/util/content"; import React from "react"; type Schema = BooleanSchemaType | FileSchemaType | NumberSchemaType | ObjectSchemaType | StringSchemaType | ReferenceSchemaType; const PageBuilderInput = (props: ArrayOfObjectsInputProps) => { const { onInsert } = props; const [open, setOpen] = useState(false); const onClose = useCallback(() => setOpen(false), []); const onOpen = useCallback(() => setOpen(true), []); const onSelectItem = useCallback((schema: Schema) => { const key = randomKey(12); onInsert({ items: [ { _type: schema.name, _key: key, } as any, ], position: "after", referenceItem: -1, open: true, }); onClose(); }, [onInsert, onClose]); return ( <> <Stack space={3}> {props.renderDefault({ ...props, arrayFunctions: () => { return ( <Button onClick={onOpen} icon={AddIcon} mode="ghost" text="Add item" /> ); }, })} </Stack> {open && ( <Dialog header="Select a section" id="dialog-example" width={4} onClose={onClose} zOffset={1000} > <Box padding={1}> <Grid autoCols={'auto'} columns={[1, 2, 2, 3, 4]} autoFlow={'row dense'} gap={[3]} padding={4}> {props.schemaType.of.map((schema, index) => { return ( <PreviewCard key={index} schema={schema} onClick={() => onSelectItem(schema)} /> ); })} </Grid> </Box> </Dialog> )} </> ); }; type PreviewProps = { onClick: React.MouseEventHandler<HTMLDivElement> | undefined, schema: Schema } function PreviewCard(props: PreviewProps) { const { onClick, schema } = props; const Icon = schema.icon || (() => <span />); // Default to empty span if no icon return ( <Card role="button" padding={2} onClick={onClick} style={{ cursor: "pointer", textAlign: "center", borderRadius: "10px", border: "1px solid #f0f0f0", backgroundColor: "#fff", boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)" }} > <Stack padding={2} space={[3]}> <div style={{ justifyContent: "center", alignItems: "center" }}> <div style={{ fontSize: "2rem", color: "#333" }}> <Icon /> </div> <Heading as="h5" size={1} style={{ marginTop: "1rem", color: "#333" }}> {schema.title} </Heading> <p style={{ color: "#777", fontSize: "0.875rem", marginTop: "0.5rem" }}> {schema.description} </p> </div> </Stack> </Card> ); } export default PageBuilderInput;
defineField({ name: 'content', title: 'Content', description: 'Content of the page. Build your page by adding blocks here.', validation: (Rule) => Rule.required().error('A page without content is not really a page...'), type: 'array', group: 'page', of: [ defineArrayMember({ type: "heroBlock", icon: DropIcon, description: 'A hero section with a title, subtitle, image and buttons', }), defineArrayMember({ type: "textBlock", icon: SunIcon, description: 'A hero section with a title, subtitle, image and buttons', }), defineArrayMember({ type: "ctaBlock", icon: SunIcon, description: 'A hero section with a title, subtitle, image and buttons', }), defineArrayMember({ type: "imageCarousel", icon: SunIcon, description: 'A hero section with a title, subtitle, image and buttons', }), defineArrayMember({ type: 'heroSection', icon: SunIcon, description: 'A hero section with a title, subtitle, image and buttons', }), ], components: { input: PageBuilderInput, } }),
May 19, 2024, 7:28 PM
T
I've created a pretty nice custom array input component borrowing snippets from here and there.
Updating here for those that are interested.
I had more time to play around with the custom input component. I added some improved styling and instead of icons I eventually decided it would be better to showcase an example image of the component to improve the user experience. I also added a search feature which is great after the number of possible content blocks grows. This new component is also adjusted to better respond to different screen sizes.
In this version the images come form a public/images folder setup at the root of the project and are fetched using the schema name. so as long as the naming for the files and the naming for the components is identical it works like a charm.
This is something i've been meaning to do for a long time and i'm sure others will also have some use for this component.
Updating here for those that are interested.
user T
I had more time to play around with the custom input component. I added some improved styling and instead of icons I eventually decided it would be better to showcase an example image of the component to improve the user experience. I also added a search feature which is great after the number of possible content blocks grows. This new component is also adjusted to better respond to different screen sizes.
import { ArrayOfObjectsInputProps, BooleanSchemaType, FileSchemaType, NumberSchemaType, ObjectSchemaType, ReferenceSchemaType, StringSchemaType } from "sanity"; import { Grid, Stack, Button, Dialog, Box, Card, Heading, Flex, Text, TextInput } from "@sanity/ui"; import { useCallback, useState } from "react"; import { AddIcon, SearchIcon } from "@sanity/icons"; import { randomKey } from "@sanity/util/content"; import React from "react"; type Schema = BooleanSchemaType | FileSchemaType | NumberSchemaType | ObjectSchemaType | StringSchemaType | ReferenceSchemaType; const PageBuilderInput = (props: ArrayOfObjectsInputProps) => { const { onInsert } = props; const [open, setOpen] = useState(false); const [searchValue, setSearchValue] = useState(''); const onClose = useCallback(() => setOpen(false), []); const onOpen = useCallback(() => setOpen(true), []); const onSelectItem = useCallback((schema: Schema) => { const key = randomKey(12); onInsert({ items: [ { _type: schema.name, _key: key, } as any, ], position: "after", referenceItem: -1, open: true, }); onClose(); }, [onInsert, onClose]); const filteredSchemas = props.schemaType.of.filter((schema) => { const searchValueLower = searchValue.toLowerCase(); return ( schema.title?.toLowerCase().includes(searchValueLower) || schema.description?.toLowerCase().includes(searchValueLower) ); }); return ( <> <Stack space={3}> {props.renderDefault({ ...props, arrayFunctions: () => { return ( <Button onClick={onOpen} icon={AddIcon} mode="ghost" text="Add item" /> ); }, })} </Stack> {open && ( <Dialog header="Select a section" id="dialog-example" width={800} onClose={onClose} zOffset={1000} > <Box padding={4} style={{ borderBottom: '1px solid var(--card-border-color)' }}> <TextInput fontSize={[2]} onChange={(event) => setSearchValue(event.currentTarget.value)} padding={[3, 3, 4]} radius={2} placeholder="Search" value={searchValue} autoFocus={true} icon={SearchIcon} /> </Box> <Grid columns={[1, 1, 1, 2, 3]} gap={4} padding={4}> {filteredSchemas.map((schema, index) => ( <PreviewCard key={index} schema={schema} onClick={() => onSelectItem(schema)} /> ))} </Grid> </Dialog> )} </> ); }; type PreviewProps = { onClick: React.MouseEventHandler<HTMLDivElement> | undefined, schema: Schema } function PreviewCard(props: PreviewProps) { const { onClick, schema } = props; const [isHovered, setIsHovered] = useState(false); return ( <Card padding={[3, 3, 4]} radius={2} shadow={isHovered ? 2 : 1} onClick={onClick} style={{ cursor: 'pointer' }} tone="default" onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > <Flex gap={4} direction="column"> <Card radius={2} shadow={1} style={{ position: 'relative', aspectRatio: '2 / 1', backgroundColor: 'var(--card-skeleton-color-from)', backgroundImage: `url(/images/${schema.name}.png)`, backgroundSize: '100% auto', backgroundPosition: 'center center', backgroundRepeat: 'no-repeat', }} /> <Flex gap={3} direction="column"> <Text size={[2]} weight="medium"> {schema.title} </Text> <Text size={1} muted> {schema.description ? schema.description : 'No description'} </Text> </Flex> </Flex> </Card> ); } export default PageBuilderInput;
defineField({ name: 'content', title: 'Content', description: 'Content of the page. Build your page by adding blocks here.', validation: (Rule) => Rule.required().error('A page without content is not really a page...'), type: 'array', group: 'page', of: [ defineArrayMember({ type: "textBlock", description: 'Rich text content. Ideal for adding paragraphs of text.', }), defineArrayMember({ type: "imageCarousel", description: 'Content: a list of images. Ideal for showcasing a collection of images.', }), defineArrayMember({ type: 'heroSection', description: 'Content:title, description, image and buttons. Ideal for presenting general information about a single entity.', }), ], components: { input: PageBuilderInput, } }),
May 20, 2024, 6:28 AM
Sanity– build remarkable experiences at scale
Sanity is a modern headless CMS that treats content as data to power your digital business. Free to get started, and pay-as-you-go on all plans.