Toggle Open/Collapse On Click

Looking into displaying posts in a child route of a category archive parent route, the first thing that comes to mind is that the archive’s posts listing on the parent route needs to expand/collapse on click.

Maybe this can be done without using client side javascript. Looking at the Remix docs provides a clue:

I think what they’re doing is displaying the parent route’s items inside a details element. (On that site the element is being hidden on large screens and the items are being displayed in a sidebar that’s hidden on small screens.)

Something like this:

// app/routes/blog.category.$categorySlug.tsx

...
export default function CategorySlug() {
  const { pageInfo, postConnectionEdges, categoryName, categorySlug } =
    useLoaderData<typeof loader>();

  return (
    <div>
      <div className="h-full w-full bg-slate-200">
        <details>
          <summary className="text-xl px-6 py-3 font-serif">
            {categoryName}
          </summary>
          <div className="px-6">
            <nav>
              <ul>
                <li>
                  {postConnectionEdges.map((edge: PostConnectionEdge) => (
                    <PostExcerptCard
                      title={edge.node?.title}
                      date={edge.node?.date}
                      featuredImage={null}
                      excerpt={edge.node?.excerpt}
                      authorName={edge.node?.author?.node?.name}
                      slug={edge.node?.slug}
                      databaseId={edge.node.databaseId}
                      basePath={`/blog/category/${categorySlug}`}
                      key={edge.node.databaseId}
                    />
                  ))}
                </li>
              </ul>
            </nav>
          </div>
        </details>
      </div>
      <div className="px-6 mx-auto">
        <Paginator pageInfo={pageInfo} />
      </div>

      <div className="md:col-span-2">
        <Outlet />
      </div>
    </div>
  );
}

After making some adjustments to the PostExcerptCard component, that gives me this:

Which leads to:

  • Can a post from the child route be automatically loaded when the parent route is visited?
  • Can the details element be made to automatically close when a post is selected from the list?
  • Can the details element be made to “stick” at the top of the page?

Stick the details element below the header

Maybe the element should be in the header, but this works:

// app/routes/blog.category.$categorySlug.tsx

...
export default function CategorySlug() {
  const { pageInfo, postConnectionEdges, categoryName, categorySlug } =
    useLoaderData<typeof loader>();

  return (
    <div>
      <div className="sticky top-16">
        <details className="flex h-full flex-col">
          <summary className="text-xl px-6 py-3 font-serif bg-slate-200">
            {categoryName}
          </summary>
...

Close details when an item is selected

Default behaviour: when a details element’s summary element is clicked, an open attribute is added to the details element. When the summary element is clicked again, the open attribute is removed from the details element.

There doesn’t seem to be a way of overriding the default behaviour (CanAm spelling on this site :)) without using client side Javascript.

This works:

// app/routes/blog.category.$categorySlug.tsx

...
import { useLocation } from "@remix-run/react";
import { useEffect } from "react";
...

export default function CategorySlug() {
  const { pageInfo, postConnectionEdges, categoryName, categorySlug } =
    useLoaderData<typeof loader>();
  const location = useLocation();
  useEffect(() => {
    const detailsElement = document.getElementById("postDetails");
    if (detailsElement) {
      detailsElement.removeAttribute("open");
    }
  }, [location]);
  return (
    <div>
      <div className="sticky top-16">
        <details className="flex h-full flex-col" id="postDetails">
          <summary className="text-xl px-6 py-3 font-serif bg-slate-200">
            {categoryName}
          </summary>
...

useEffect: I’m seeing definitions like “It allows you to perform side effects in function components. Side effects are operations that can affect other components or cannot be done during rendering, such as directly manipulating the DOM, fetching data, subscribing to some external stream…”

For now, can I just understand it as Javascript that’s delivered to the client? When the value of location changes, React updates the DOM and runs the useEffect function (removes the open attribute from the details element.) I guess the important part is to understand when the code is run. I’m guessing a bit here, but when React “hydrates” the application, the code will be executed to attach an event handler that listes for changes to location (or location.path if I want to ignore changes to URL params.)

Load a default post

I’m not sure what the appropriate behaviour would be here:

Could it load the category’s first post by default?

Maybe it could, but that seems to defeat the purpose of an archive route. Could it be open when the route is visited directly?

… a day later. That was interesting. The big takeaway was learning how to create expandable/collapsible navigation with a details element. It can be made to work without client side Javascript. But as far as I can tell, to get the navigation menu to collapse after an item is selected requires a bit of client side Javascript:

const location = useLocation();
useEffect(() => {
  const detailsElement = document.getElementById("postDetails");
  if (detailsElement) {
    detailsElement.removeAttribute("open");
  }
}, [location.path]); 

Another takeaway from this bit of exploration is that I don’t want to load category posts in a child route of blog.category.$categorySlug.tsx. Instead of doing that, I’ll convert the route to an index route (blog.category.$categorySlug._index.tsx). To provide context to users about where they are on the site, I’ll add a “breadcrumbs” type of navigation item to the top of non-index pages. I might use the built in Remix approach (https://remix.run/docs/en/main/guides/breadcrumbs), but I’ve got something fancier in mind.