As my readers know, I've been falling in love with Alpine.js lately and am always on the hunt for more ways to practice using the framework. I thought I'd share an example of how you could use it with Algolia's JavaScript client. I use that on my search page here with Vue.js, so it wasn't a terribly difficult thing to rebuild a similar interface in Alpine.js. Here's how I did it.
The Layout
For the layout, I went with a simple search interface and results that displayed the title, date, and a snippet for each result. Here's that HTML with Alpine.js directives throughout:
<div x-data="app" x-cloak>
<input type="search" x-model="term">
<button @click="search" :disabled="!searchReady">Search</button>
<div x-show="noResults">
<p>
Sorry, but there were no results.
</p>
</div>
<div x-show="results">
<h2>Results</h2>
<p>
There were <span x-text="totalHits"></span> total matches. Returning the first <span x-text="resultsPerPage"></span> results:
</p>
<template x-for="result in results">
<div>
<p>
<a :href="result.url"><span x-text="result.title"></span></a> (posted <span x-text="result.date"></span>)
</p>
<p class="snippet" x-html="result.snippet"></p>
</div>
</template>
</div>
</div>
From the top, the first two elements are my search field, using x-model
, and a button that will initiate the search. I've got it disabled based on a value searchReady
that you will see soon.
The next block handles cases where no results were found.
And then I have a block that shows up when results are ready. I render the total number of hits as well as how many I'm showing. (I could do paging here, and if folks want to see that, just ask.) I then loop over my results making use of the url, title, date, and snippet values.
The JavaScript
Now let's look at the JavaScript code:
const appId = 'WFABFE7Z9Q';
const apiKey = 'd1c88c3f98648a69f11cdd9d5a87de08';
const indexName = 'raymondcamden';
document.addEventListener('alpine:init', () => {
Alpine.data('app', () => ({
init() {
let client = algoliasearch(appId, apiKey);
this.index = client.initIndex(indexName);
this.searchReady = true;
},
index:null,
term:'',
searchReady:false,
noResults:false,
results:null,
totalHits:null,
resultsPerPage:null,
async search() {
if(this.term === '') return;
this.noResults = false;
console.log(`search for ${this.term}`);
// let rawResults = await this.index.search(this.term);
let rawResults = await this.index.search(this.term, {
attributesToSnippet: ['content']
});
if(rawResults.nbHits === 0) {
this.noResults = true;
return;
}
this.totalHits = rawResults.nbHits;
this.resultsPerPage = rawResults.hitsPerPage;
this.results = rawResults.hits.map(h => {
h.snippet = h._snippetResult.content.value;
h.date = new Intl.DateTimeFormat('en-us').format(new Date(h.date));
return h;
});
}
}))
});
On top, I've got three constants related to Algolia. An application ID, my API token which only has read access, and the name of my index. When Alpine initializes the application, I create an instance of the Algolia index wrapper, and then set my searchReady
boolean to true. This will enable that button in HTML.
For search, I do a quick validation of the value, and then just pass it to Algolia. The commented-out line shows how simple this can be if you want the defaults, but I wanted Algolia to create a snippet on the content field so the search results would be a bit nicer.
Finally, I do a bit of work on the results. If none were found, I set the value so that it will get flagged in the HTML. If we have results, I copy over the total hits and per page values. I then map the results to make things a bit easier in the HTML. Specifically, I copy over the snippet to an easier-to-use key, and then I use the Intl
object to make the dates a bit nicer.
Here's an example of how it looks, and please note that it's me doing my best at design. Don't blame Alpine or Algolia. ;)
If you want to give this a try yourself, play with the CodePen below, and as always, let me know if you've got any questions!
See the Pen Algolia + Alpine example by Raymond Camden (@cfjedimaster) on CodePen.