
Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag store {
name: "read",
title: "read",
type: "number",
description: "put number in minutes for reading time👇",
validation: (Rule) => Rule.required(),
},import RenderReadingTime from "./RenderReadingTime";
export default {
name: "page",
title: "Pages",
type: "document",
fields: [
{
title: "Content",
name: "content",
type: "array",
of: [{ type: "block" }],
},
{
name: "reading",
title: "Reading Time",
type: "string",
inputComponent: RenderReadingTime,
},
],
};import React from "react";
import { Card, Stack, Inline, Badge } from "@sanity/ui";
import { FormField } from "@sanity/base/components";
import { withDocument } from "part:@sanity/form-builder";
import { useReadingTime } from "react-hook-reading-time";
export const RenderReadingTime = React.forwardRef((props, ref) => {
const { type, document } = props;
const filteredBlock = document.content
.filter((x) => x._type === "block")
.map((children) => children.children.map((texts) => texts.text));
const readingTime = useReadingTime(filteredBlock.join(" "));
return (
<FormField description={type.description} title={type.title}>
<Card padding={[3, 3, 3, 3]} shadow={1} radius={2} ref={ref}>
<Stack space={[3, 3, 3]}>
<Inline space={2}>
<Badge tone={"primary"}>{readingTime.text}</Badge>
<Badge>{`${readingTime.words} words`}</Badge>
</Inline>
</Stack>
</Card>
</FormField>
);
});
export default withDocument(RenderReadingTime);export const articleQuery = groq`*[_type == "article" && slug.current == $slug][0] {
...,
reading,
body[] {
...,
_type == "imageWithAltText" => {
asset -> {
...,
url,
},
},
_type == "video" => {
asset -> {
...,
videoPlatform,
href,
},
},
markDefs[] {
...,
_type == "internalLink" => {
"path": reference->slug.current
}
},
},
'comments': *[
_type == "comment" &&
article._ref == ^._id &&
approved == true
],
user -> {
name,
featuredImage,
},
"slug": slug.current,
}`;reading->,?
export const articleQuery = groq`*[_type == "article" && slug.current == $slug][0] {
...,
reading->,
body[] {
...,
_type == "imageWithAltText" => {
asset -> {
...,
url,
},
},
_type == "video" => {
asset -> {
...,
videoPlatform,
href,
},
},
markDefs[] {
...,
_type == "internalLink" => {
"path": reference->slug.current
}
},
},
'comments': *[
_type == "comment" &&
article._ref == ^._id &&
approved == true
],
user -> {
name,
featuredImage,
},
"slug": slug.current,
}`;PatchEventfrom
"@sanity/form-builder/PatchEvent"into the RenderReadingTime component. I will try and update the component a bit later today!
import React from "react";
import { Card, Stack, Inline, Badge, TextInput } from "@sanity/ui";
import { FormField } from "@sanity/base/components";
import { withDocument } from "part:@sanity/form-builder";
import { useReadingTime } from "react-hook-reading-time";
import { useId } from "@reach/auto-id"; // hook to generate unique IDs
import PatchEvent, { set, unset } from "@sanity/form-builder/PatchEvent";
export const RenderReadingTime = React.forwardRef((props, ref) => {
const {
type,
document,
value,
readOnly,
onChange,
compareValue,
markers,
presence,
} = props;
const filteredBlock = document.body
.filter((x) => x._type === "block")
.map((children) => children.children.map((texts) => texts.text));
const readingTime = useReadingTime(filteredBlock.join(" "));
// Creates a unique ID for our input
const inputId = useId();
// Creates a change handler for patching data
const handleChange = React.useCallback(
// useCallback will help with performance
(event) => {
const inputValue = event.currentTarget.value; // get current value
// if the value exists, set the data, if not, unset the data
onChange(PatchEvent.from(inputValue ? set(inputValue) : unset()));
},
[]
);
return (
<FormField
description={type.description}
title={type.title}
compareValue={compareValue}
inputId={inputId}
__unstable_markers={markers}
__unstable_presence={presence}
>
<Card padding={[3, 3, 3, 3]} shadow={1} radius={2} ref={ref}>
<Stack space={[3, 3, 3]}>
<Inline space={2}>
<Badge tone="primary">{readingTime.text}</Badge>
<Badge>{`${readingTime.words} words`}</Badge>
<TextInput
id={inputId}
onChange={handleChange}
value={readingTime.minutes || ""}
readOnly={readOnly}
placeholder={readingTime.minutes}
ref={ref}
/>
</Inline>
</Stack>
</Card>
</FormField>
);
});
export default withDocument(RenderReadingTime);sanityClientrather than
PatchEventas I believe PatchEvent requires an onChange event to trigger, and as the component doesn’t have a direct user input, this isn’t possible (correct me if I’m wrong).
stringand
blocktypes, and requires the options of
fieldName(which field that will be patched, which is probably is the same field) & `target`:
{
title: "Content",
name: "content",
type: "array",
of: [{ type: "block" }],
},
{
name: "reading",
title: "Reading Time",
type: "string",
inputComponent: RenderReadingTime,
options: { fieldName: "reading", target: "content" }
},import React, { useState, useMemo, useEffect } from "react";
import { Card, Stack, Inline, Badge } from "@sanity/ui";
import { FormField } from "@sanity/base/components";
import { withDocument } from "part:@sanity/form-builder";
import { useReadingTime } from "react-hook-reading-time";
import sanityClient from "part:@sanity/base/client";
const client = sanityClient.withConfig({ apiVersion: "2022-02-15" });
export const RenderReadingTime = React.forwardRef((props, ref) => {
const { type, document } = props;
const [time, setTime] = useState(null);
const valid = useMemo(() => {
const validate =
(type.options?.target !== undefined &&
document[type.options?.target] !== undefined &&
document[type.options?.fieldName] !== undefined &&
Array.isArray(document[type.options?.target])) ||
Object.prototype.toString.call(document[type.options?.target]) === "[object String]";
const targetType = validate
? Array.isArray(document[type.options?.target])
? "block"
: "string"
: false;
return targetType;
}, []);
useEffect(() => {
client
.patch(document._id)
.set({ reading: time?.words && valid !== false ? time.text : "" })
.commit()
.catch((err) => console.error("The update to 'reading' failed: ", err.message));
}, [time]);
return (
<FormField description={type.description} title={type.title}>
<Card padding={[3, 3, 3, 3]} shadow={1} radius={2} ref={ref}>
<Stack space={[3, 3, 3]}>
{valid !== false ? (
<RenderReadingTimePatch
document={document}
type={type}
valid={valid}
time={time}
setTime={setTime}
/>
) : (
<Inline space={2}>
<Badge tone={"critical"}>Invalid or missing target/fieldName</Badge>
</Inline>
)}
</Stack>
</Card>
</FormField>
);
});
const RenderReadingTimePatch = ({ document, type, valid, time, setTime }) => {
const debouncedInput = useDebounce(document[type.options.target], 1000);
useEffect(() => {
if (valid === "block") {
const filteredBlock = document[type.options.target]
.filter((x) => x._type === "block")
.map((children) => children.children.map((texts) => texts.text));
setTime(useReadingTime(filteredBlock.join(" ")));
} else {
setTime(useReadingTime(useReadingTime(document[type.options.target])));
}
}, [debouncedInput]);
return (
<div>
{time && (
<Inline space={2}>
<Badge tone={"primary"}>{time.text}</Badge>
<Badge>{`${time.words} words`}</Badge>
</Inline>
)}
</div>
);
};
const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
};
export default withDocument(RenderReadingTime);time.textto
time.minutes.toString()
useEffect(() => {
client
.patch(document._id)
.set({ reading: time?.words && valid !== false ? time.text : "" }) //CHANGE HERE
.commit()
.catch((err) => console.error("The update to 'reading' failed: ", err.message));
}, [time]);.set({ reading: time?.words && valid !== false ? time.text : "" }).set({ reading: time?.words && valid !== false ? time.minutes.toString() : "" })Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.
Content operations
Content backend


The only platform powering content operations
By Industry


Tecovas strengthens their customer connections
Build and Share

Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag store