Skip to content
This repository has been archived by the owner on Dec 30, 2022. It is now read-only.

feat(search-client): Add support for Custom Search Clients #432

Merged
merged 7 commits into from
Apr 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/components/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Provide search query parameters:
| cache | Boolean | `true` | Whether to cache results or not. See [the documentation](https://www.algolia.com/doc/tutorials/getting-started/quick-start-with-the-api-client/javascript/#cache) |
| auto-search | Boolean | `true` | Whether to initiate a query to Algolia when this component is mounted |
| stalledSearchDelay | number | `200` | Time before the search is considered unresponsive. Used to display a loading indicator. |
| search-client | Object | `` | The search client to plug to InstantSearch |

## Slots

Expand Down
36 changes: 36 additions & 0 deletions docs/src/getting-started/search-store.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,42 @@ const searchStore = createFromAlgoliaClient(client);

Note that there is no reason to provide your own client if you are not reusing it elsewhere.

### Create a search store from a custom search client

If you want to use the [official JavaScript API Client](https://github.com/algolia/algoliasearch-client-javascript) on your backend, or even a custom search client, you can create an object implementing the `search()` method (and `searchForFacetValues()` if needed).

The search client can be passed to the [`search-client`](../components/index.html#props) prop of the [`Index`](../components/index.html):

```html
<template>
<ais-index
index-name="your_indexName"
:search-client="searchClient"
>
<!-- Add your InstantSearch components here. -->
</ais-index>
</template>

<script>
export default {
data() {
return {
searchClient: {
search(requests) {
// perform the requests
return response;
},
searchForFacetValues(requests) {
// perform the requests
return response;
},
},
},
},
};
</script>
```

### Create a search store from an Algolia helper instance

The [Algolia helper](https://github.com/algolia/algoliasearch-helper-js) is a JavaScript library that is built on top of the Algolia API client. Its goal is to enable a simple API to achieve advanced queries while also providing utility methods and behavior like keeping track of the last result.
Expand Down
40 changes: 38 additions & 2 deletions src/components/Index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
</template>

<script>
import { createFromAlgoliaCredentials } from '../store';
import {
createFromAlgoliaCredentials,
createFromAlgoliaClient,
} from '../store';
import algoliaComponent from '../component';

export default {
Expand All @@ -17,6 +20,9 @@ export default {
return this._searchStore;
},
},
searchClient: {
type: Object,
},
apiKey: {
type: String,
default() {
Expand Down Expand Up @@ -73,7 +79,37 @@ export default {
};
},
provide() {
if (!this.searchStore) {
if (this.searchClient) {
if (!this.indexName) {
throw new Error(
'vue-instantsearch: `indexName` is required with `searchClient`'
);
}

if (this.searchStore) {
throw new Error('`searchStore` cannot be used with `searchClient`');
}

if (this.appId) {
throw new Error(
'vue-instantsearch: `appId` cannot be used with `searchClient`'
);
}

if (this.apiKey) {
throw new Error(
'vue-instantsearch: `apiKey` cannot be used with `searchClient`'
);
}

if (typeof this.searchClient.search !== 'function') {
throw new Error(
'vue-instantsearch: `searchClient` must implement a method `search(requests)`'
);
}

this._localSearchStore = createFromAlgoliaClient(this.searchClient);
} else if (!this.searchStore) {
this._localSearchStore = createFromAlgoliaCredentials(
this.appId,
this.apiKey,
Expand Down
11 changes: 11 additions & 0 deletions src/components/__tests__/__snapshots__/index.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Search Client API collision and apiKey 1`] = `"vue-instantsearch: \`apiKey\` cannot be used with \`searchClient\`"`;

exports[`Search Client API collision and appId 1`] = `"vue-instantsearch: \`appId\` cannot be used with \`searchClient\`"`;

exports[`Search Client API collision and searchStore 1`] = `"\`searchStore\` cannot be used with \`searchClient\`"`;

exports[`Search Client API collision without indexName 1`] = `"vue-instantsearch: \`indexName\` is required with \`searchClient\`"`;

exports[`Search Client Properties throws if no \`search()\` method 1`] = `"vue-instantsearch: \`searchClient\` must implement a method \`search(requests)\`"`;
120 changes: 120 additions & 0 deletions src/components/__tests__/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import Vue from 'vue';
import Index from '../Index.vue';

describe('Search Client', () => {
describe('Properties', () => {
it('throws if no `search()` method', () => {
expect(() => {
const Component = Vue.extend(Index);

const vm = new Component({
propsData: {
indexName: 'indexName',
searchClient: {},
},
});

vm.$mount();
}).toThrowErrorMatchingSnapshot();
});
});

describe('API collision', () => {
test('and indexName', () => {
expect(() => {
const Component = Vue.extend(Index);

const vm = new Component({
propsData: {
indexName: 'indexName',
searchClient: {
search() {
return Promise.resolve({ results: [{ hits: [] }] });
},
},
},
});

vm.$mount();
}).not.toThrow();
});

test('without indexName', () => {
expect(() => {
const Component = Vue.extend(Index);

const vm = new Component({
propsData: {
searchClient: {
search() {
return Promise.resolve({ results: [{ hits: [] }] });
},
},
},
});

vm.$mount();
}).toThrowErrorMatchingSnapshot();
});

test('and appId', () => {
expect(() => {
const Component = Vue.extend(Index);

const vm = new Component({
propsData: {
indexName: 'indexName',
appId: 'appId',
searchClient: {
search() {
return Promise.resolve({ results: [{ hits: [] }] });
},
},
},
});

vm.$mount();
}).toThrowErrorMatchingSnapshot();
});

test('and apiKey', () => {
expect(() => {
const Component = Vue.extend(Index);

const vm = new Component({
propsData: {
indexName: 'indexName',
apiKey: 'apiKey',
searchClient: {
search() {
return Promise.resolve({ results: [{ hits: [] }] });
},
},
},
});

vm.$mount();
}).toThrowErrorMatchingSnapshot();
});

test('and searchStore', () => {
expect(() => {
const Component = Vue.extend(Index);

const vm = new Component({
propsData: {
indexName: 'indexName',
searchStore: {},
searchClient: {
search() {
return Promise.resolve({ results: [{ hits: [] }] });
},
},
},
});

vm.$mount();
}).toThrowErrorMatchingSnapshot();
});
});
});
6 changes: 5 additions & 1 deletion src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,11 @@ export class Store {
this._helper.on('result', onHelperResult.bind(this));
this._helper.on('search', onHelperSearch.bind(this));

this._helper.getClient().addAlgoliaAgent(`vue-instantsearch ${version}`);
const client = this._helper.getClient();

if (typeof client.addAlgoliaAgent === 'function') {
client.addAlgoliaAgent(`vue-instantsearch ${version}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could add this conditionally in the helper so we can call it without checking here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm OK with this change. It's yet another helper upgrade though haha. WDYT @bobylito?

}

this._stalledSearchTimer = null;
this.isSearchStalled = true;
Expand Down