Adding client-side interaction to Astro
2022-09-20
So, after getting this site up and running on Astro I wanted to explore a bit more and see where the limits of the framework are. Obviously Astro performs very well as a static site generator and produces minimal or zero JavaScript bundle sizes even though much of the site is built with React components. But what if we wanted to force in a little bit of client-side interaction?
My idea to introduce this was with a form that filters projects I’ve worked on based on a search phrase. I wanted to this as simple as possible, with no external libs if possible, once again to limit the payload. There’s no validation needed, no complicated form state to handle - just an input field and some buttons. The first version looked something like this:
export const Search = () => {
return (
<form>
<input id="search" type="search" name="search"></input>
<Button type="submit">Filter</Button>
</form>
);
};
The funny this about this is that when submitted it will GET
itself to the same URL as it’s on, but with
?search=<your query>
which works very well for server-rendering the same page but with filtering on projects. Oh, how we’re back at the PHP pages of old :heart-eyes: (was that good, though :thinking:?).
So, here I opted to add an external lib to get the highlighting working: react-highlighter.
So, all of the above works without any client-side code really, it works with JavaScript disabled in Chrome.
It works by on the index.astro
page reading the query parameters:
const search = Astro.url.searchParams.get("search") || "";
Then passing that to the Resume
component:
<Resume search={search} />
export const Resume = ({ search = "" }: Props) => {
/* ... */
return (
<>
// ...
<Search initialValue={search} />
// ...
</>
);
};
and finally back to a modified Search
component that sets this as defaultValue
:
export const Search = ({ initialValue = "" }: SearchProps) => {
return (
<form>
<input
id="search"
type="search"
name="search"
defaultValue={initialValue || ""}
></input>
<Button type="submit">Filter</Button>
</form>
);
};
However, the defaultValue
introduced a problem. I wanted to have a Clear
button as well, and an
<input type="reset" value="Clear" />
element would simply reset the value to whatever was previously filtered on, since I set that value to defaultValue
. Perhaps there are other solutions to this, but what I ended up doing was creating a custom button click handler that empties the form value before submitting it. Obviously this doesn’t work without JavaScript, so here we are already moving away from a server-side-only working web page…
export const Search = ({ initialValue = "" }: SearchProps) => {
c;
const formRef = useRef<HTMLFormElement>(null);
const searchRef = useRef<HTMLInputElement>(null);
const onHandleReset = () => {
if (!searchRef || !searchRef.current) return;
if (!formRef || !formRef.current) return;
searchRef.current.setAttribute("value", "");
formRef.current.submit();
};
return (
<form ref={formRef}>
<input
type="search"
name="search"
defaultValue={initialValue || ""}
ref={searchRef}
></input>
<Button type="submit">Filter</Button>
<Button onClick={onHandleReset}>Clear</Button>
</form>
);
};
So, now we have a way of filtering content on the page. To get the interaction working client-side you need to tell Astro to load JS code:
<Resume search={search} client:visible />
What still doesn’t work is that since the page is reloaded every time the form is submitted, the user is scrolled to the top of the page which is a bit annoying… Ah, the UX we have adjusted ourselves to with JS is seldom thought of, but becomes quite obvious once you remove it.
To be continued!