改善
Kaizen  · Today I Learned by Ville Säävuori

Weeknotes 2022/4 - Web Components And Search

Native Web Components are something I’ve wanted to learn better for a long time. I decided to try to build a simple search component for this blog and packaging it as a Web Component seemed a good idea. Turns out, in practise, that solution is not at all optimal if the component needs any non-trivial markup as it’s hard to change that markup and styling it is super hard.

The new Vue 3 docs have an excellent Vue and Web Components page that describes pretty much everything one needs to know on the topic from Vue point of view. Vue 3 has a nice defineCustomElement function that allows you to build native custom elements from Vue components. Still, the resulting component is still pretty limited comparing to native Vue components.

The Rise of the Headless

After following the story of Tailwind CSS and the Headless UI library, I’m completely sold to the idea of headless components (that is, components w/o any markup and/or 100% customizable markup).

Roughly 90% of the fiction of planting in any UI component is related to trying to match it’s styles to the projects’. Even if the styling options are really good, you often need to do plenty of work trying to wrangle the markup in some way. But if the component comes with zero markup, all you need to do is write whatever you need and everythin Just Works — this is much better way to work with UI components. (It also leaves your designers hands fully free to come up with whatever they imagine instead of being limited to whatever someone else has written as a generic thing.)

The more I’ve been using headless components, the more I’m sold to the idea. So ended up ditching the idea of a native Web Components but instead package the search as a fully headless Vue Component.

New Headless Vue Search Component

A search component is an interesting problem in itself as it hides a lot of complexity one might not think about at first sight. Firstly, search is usually composed of multiple elements, often also split in different prts of the page. Secondly, you might want to have several instances of the component on a page (For example a simple one in every header plus a more complex one on a search page, for example). Lastly, the way you want to display your search results varies a lot by your content. If you can’t control the marku, you need to compromise. This is a perfect situation for a headless component.

My first iteration of a self-contained search component looked like this:

<div id="searchapp">
  <div class="jsonsearch">
    <label for="jsonsearchinput">Search</label
    ><input name="jsonsearchinput" class="jsonsearchinput" autocomplete="off" placeholder="Search" type="text" />
    <!-- Shown only if results.length > 0 -->
    <div class="searchresults">
      <h3>N results</h3>
      <ol>
        <div class="result">
          <div class="title">
            <a>Result title</a>
          </div>
          <!-- Shown only if showTags === true -->
          <div class="tags">
            <span><a rel="tag" class="tag">tag</a>, </span>
            ><span><a rel="tag" class="tag">last tag</a></span>
          </div>
        </div>
      </ol>
    </div>
  </div>
</div>

While there’s not much markup there it’s already hugely opinionated. What if you don’t want to have a label in english? What if you’re using tables and want your search to use tables also? What if you want to bundle some other content instead of tags for each search result row?

This is all super easy to fix with headless design. Here’s the same component using Vue slots and default subcomponents:

<JsonSearch :show-tags="true" v-slot="{ results }">
  <SearchInput />
  <SearchResults>
    <ResultTitle />
    <div v-for="res in results" :key="res.refIndex">
      <ResultListItem v-slot="{ result }" :result="res.item"></ResultListItem>
    </div>
  </SearchResults>
</JsonSearch>

You can swap out literally everything, and put your own markup inside any of the subcomponent or swap them all out and write everything by yourself while still using the free plumbing the component gives you (for example searchTerm, results array, etc.)

I didn’t have time to put much effort in documenting everything cleaning it up but I did publish the final component as open source and I wrote simple instructions on how to set it up with Hugo (or any other static site builder) in just minutes.

I have a feeling I’ll come back and take another stab at writing a simple Web Component version with a markup that is not too hard to style. That way you could just add one JS import to your template and start using the component without any JS tooling at all.

Check out the end result of the finished search component from the homepage!