Thursday, 28 March, 2019 UTC


Summary

This post was made collaboratively with Bram Adams.
In the previous post we announced the release of our 2.0 library for Vue Instant Search. That post introduced us to the new features included in the release. This time we’d like to take a deeper look into how we updated the library, and why we made the decisions we did.
When your legs don’t work like they used to
We’ve learned a lot since our first Vue InstantSearch release back in 2017. The library began to fall behind our more up-to-date InstantSearch.js library, and we were able to take many of the lessons that we learned and apply them to Vue.
The original version of Vue InstantSearch started as a small hack by Raymond. We were very excited about the trajectory of Vue and wanted to be able to release a library that would make it easier for Algolia users to be able to integrate it into their frameworks.
The first iteration of our Vue library had a few problems. The main issue was that we built it in the dark! Well, I mean, not really, our computers were backlit. It was dark in the sense that we built the library without much community input.
The other major issue is that a lot of our components lacked customizability, requiring people to often copy our components completely to be able to edit them sufficiently.
By acknowledging our weaknesses, we were able to build a much better library. We hope you’ll like it too. If you have used the previous version of Vue InstantSearch, and are wondering which new features were added, check out the previous post.
Miro, Miro on the wall, who’s the most abstract of them all? Joan Miro – Portrait IV (1938)
When it comes to abstractions, Vue InstantSearch now has you covered. We wanted to extend the customizability of components, to let the end user (that’s you!) decide how their Algolia experience should look and feel.
In the following paragraphs, we will discuss how to customize InstantSearch components. Each layer is more customizable than the last, allowing you to decide if you’d like an easy-to-integrate already-built component, or have complete control over the look, feel, and functionality.
Imagine the example of a search box. This might seem like a simple component, but it’s actually one of our more complex widgets, and supports all these different levels of customization.
The default ais-search-box component

Text as a prop

One of the easiest to modify parts of a search box is the placeholder. This is the text that will be visible in the input element itself. We specifically chose to forward this prop to the inner input, since we wrap the input in a form (see more on why we architect our search box like this in the detailed blog post about SearchBox).
When a prop gets forwarded to an inner component, it always stays a prop on your outer component, either with the same name, or with a consistent prefix indicating which of the inner elements this gets applied to.
<ais-searchbox
  placeholder="Search for flavors: e.g. vanilla, chocolate…"
/>

Child components as slots

Within the search box, we also have a button with an icon that lets a user clear the query. At this point, not only would a string be valid, so would any Vue component. Since it doesn’t need access to any of the states depending on its render, it’s a regular slot.
You can read more about Vue slots here if you’re unfamiliar.
<ais-searchbox>
  <template slot="resetIcon">✕</template>
</ais-searchbox>

Rewriting the full DOM as scoped slot

As a final touch, we also add a scoped slot just inside the root of the component. To that slot we provide the information we retrieve from the widget’s data layer (connector). Anything within this template now has access to the currentRefinement, refine, and everything else needed to render that component.
Thanks to the slot-scope we now can easily rewrite the look of a widget without ever having to leave the template.
<ais-searchbox>
  <template
    slot="default"
    slot-scope="{ currentRefinement, refine }"
  >
    <input
      type="search"
      :value="currentRefinement"
      @input="refine($event.target.value)"
    />
  </template>
</ais-searchbox>

Writing an all-new Vue component

Occasionally, the level of control slot-scope provides still isn’t enough for what you’re trying to build. This can be the case for two different reasons: needing access to the scope provided by the underlying business logic (connector) in component life cycles, or writing a custom connector yourself. We made this process simpler by abstracting away the logic to create a “widget” from a connector and registering it to its root component with the createWidgetMixin mixin.
Let’s say that you would like to render your own search box (instead of ais-searchbox). We can use the connectSearchBox to fetch the data and createWidgetMixin to directly insert the Algolia data into our template. From here, the sky is the limit. You can make your component look anyway you want! The reason we can do this is that the data is populated to the state key during the created() hook. This allows you to be in full control over both the render logic and the transforming business logic.
<template>
  <form
    v-if="state"
    action=""
    @submit="onFormSubmit"
    @reset="onFormReset"
  >
    <input
      :value="state.currentRefinement"
      @input="onInput"
    />
    <button type="reset">reset</button>
  </form>
</template>

<script>
import { connectSearchBox } from 'instantsearch.js/es/connectors';
import { createWidgetMixin } from 'vue-instantsearch';

export default {
  mixins: [
    createWidgetMixin({ connector: connectSearchBox }),
  ],
  methods: {
    onInput(event) {
      this.state.refine(event.target.value);
    },
    onFormSubmit() {
      const input = this.$el.querySelector('input[type=search]');
      input.blur();
    },
    onFormReset() {
      this.state.refine('');
    },
  },
};
</script>
In this component there isn’t much use case for going for a completely custom widget. However, for wrapping more advanced third-party components, or for modifying the way the connector works, this is a very useful API.
But let’s take a look at why we might want to split the logic in the first place.
Is it irrational to split logic?
With Vue InstantSearch 2 we chose to split the concerns between rendering components and connectors responsible for business logic. This is useful when you are creating your own custom components and want to use data from your Algolia results. By separating business logic from the components themselves, a component which is conceptually the same as another existing one doesn’t need to have their business logic rewritten from scratch.
An abstraction should be able to fit any use case with the same conceptual idea
Splitting logic requires finding a balance. You can’t separate it too much, because it becomes tedious to build all the boilerplate required for different flavors and frameworks. However, you can’t have it too view-specific either, because then you lose the flexibility to add new components in the future. A rule of thumb we used here is that any connector should be able to fit into all rendered components with the same conceptual idea.
An example here is a menu (a list with only one selection possible) which is showing as a list, or a menu which is showing as a dropdown. If in a business logic abstraction you put code dealing with the selection logic, it would overstep its bounds and you would be more likely to make an abstraction for each case.
The choice at that point is deciding whether the logic is easy enough to be rewritten in the view layer (using things like event handlers), and thus making only a single business logic abstraction. On the other hand, sometimes an idea turns out to be two completely different abstractions.
Two completely different abstractions in this context would be Hits (i.e. results) and InfiniteHits. Both of these read from the results of the query to display the ones that currently apply, but the infinite hits abstraction would contain things like keeping the previous hits in scope so that a concatenation of all visible hits can be done.
Consistency Between Business Logic Abstractions
Algolia prides itself in having the best developer experience possible. To achieve this goal, we focused on building a consistent foundation between our different JavaScript flavors. These connectors work whether you’re in Vue, React, or even vanilla JavaScript. If your business case is outside of what our widgets can handle, we encourage you to check them out!
The move to a consistent abstraction here has proven very useful in making libraries like Vue InstantSearch. We only need to account for one type of API (using a connector) in a Vue component to make it aware of the business logic (reading from state, as well as changing refinement state) consistently.
If we had chosen to split this up in functions, so that each would have its own API, it would have made those functions individually simpler—but harder to work together. The goal here is to make the view layers as simple as possible, to make adding new framework flavors easier.
Conclusion
We hope you enjoyed this look into the internals of our new Vue library, and we hope you enjoy the library as much as we do.
The post Deep Diving into Vue InstantSearch Version 2 appeared first on Algolia Blog.