├── .babelrc ├── .circleci └── config.yml ├── .codesandbox └── ci.json ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug.md │ └── feature.md └── vue-instantsearch-readme.png ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .storybook ├── addons.js ├── config.js └── styles.css ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __mocks__ └── instantsearch.js │ └── es.js ├── examples ├── build.sh ├── default-theme │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── src │ │ ├── App.css │ │ ├── App.vue │ │ └── main.js │ ├── vue.config.js │ └── yarn.lock ├── e-commerce │ ├── .editorconfig │ ├── .eslintrc.js │ ├── .gitignore │ ├── .prettierrc │ ├── README.md │ ├── babel.config.js │ ├── package.json │ ├── public │ │ ├── favicon.png │ │ ├── index.html │ │ └── manifest.webmanifest │ ├── src │ │ ├── App.css │ │ ├── App.mobile.css │ │ ├── App.vue │ │ ├── Theme.css │ │ ├── images │ │ │ ├── cover-mobile.jpg │ │ │ └── cover.jpg │ │ ├── main.js │ │ ├── routing.js │ │ ├── utils.js │ │ └── widgets │ │ │ ├── ClearRefinements.vue │ │ │ ├── NoResults.vue │ │ │ └── PriceSlider.css │ ├── vue.config.js │ └── yarn.lock ├── media │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── src │ │ ├── App.css │ │ ├── App.vue │ │ └── main.js │ ├── vue.config.js │ └── yarn.lock ├── nuxt │ ├── README.md │ ├── layouts │ │ ├── README.md │ │ └── default.vue │ ├── nuxt.config.js │ ├── package.json │ ├── pages │ │ ├── README.md │ │ ├── index.vue │ │ └── search.vue │ ├── static │ │ ├── README.md │ │ └── favicon.ico │ └── yarn.lock └── ssr │ ├── .browserslistrc │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── not.eslintrc.js │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── favicon.ico │ └── index.html │ ├── src │ ├── App.vue │ ├── entry-client.js │ ├── entry-server.js │ ├── main.js │ ├── router.js │ └── views │ │ ├── About.vue │ │ ├── Home.vue │ │ └── Search.vue │ ├── vue.config.js │ └── yarn.lock ├── jest.setup.js ├── netlify.toml ├── package.json ├── rollup.config.js ├── scripts ├── __mocks__ │ └── fs.js ├── __tests__ │ └── babel-plugin-extension-resolver.test.js ├── babel-plugin-extension-resolver.js ├── clean-node-modules.sh └── test-vue3.sh ├── ship.config.js ├── src ├── __tests__ │ └── index.js ├── components │ ├── Autocomplete.vue │ ├── Breadcrumb.vue │ ├── ClearRefinements.vue │ ├── Configure.js │ ├── ConfigureRelatedItems.js │ ├── CurrentRefinements.vue │ ├── DynamicWidgets.js │ ├── ExperimentalDynamicWidgets.js │ ├── HierarchicalMenu.vue │ ├── HierarchicalMenuList.vue │ ├── Highlight.vue │ ├── Highlighter.vue │ ├── Hits.vue │ ├── HitsPerPage.vue │ ├── Index.js │ ├── InfiniteHits.vue │ ├── InstantSearch.js │ ├── InstantSearchSsr.js │ ├── Menu.vue │ ├── MenuSelect.vue │ ├── NumericMenu.vue │ ├── Pagination.vue │ ├── Panel.vue │ ├── PoweredBy.vue │ ├── QueryRuleContext.js │ ├── QueryRuleCustomData.vue │ ├── RangeInput.vue │ ├── RatingMenu.vue │ ├── RefinementList.vue │ ├── RelevantSort.vue │ ├── SearchBox.vue │ ├── SearchInput.vue │ ├── Snippet.vue │ ├── SortBy.vue │ ├── StateResults.vue │ ├── Stats.vue │ ├── ToggleRefinement.vue │ ├── VoiceSearch.vue │ ├── __Template.vue │ └── __tests__ │ │ ├── Autocomplete.js │ │ ├── Breadcrumb.js │ │ ├── ClearRefinements.js │ │ ├── Configure.js │ │ ├── ConfigureRelatedItems.js │ │ ├── CurrentRefinements.js │ │ ├── DynamicWidgets.js │ │ ├── ExperimentalDynamicWidgets.js │ │ ├── HierarchicalMenu.js │ │ ├── Highlight.js │ │ ├── Hits.js │ │ ├── HitsPerPage.js │ │ ├── Index-integration.js │ │ ├── Index.js │ │ ├── InfiniteHits.js │ │ ├── InstantSearch-integration.js │ │ ├── InstantSearch.js │ │ ├── InstantSearchSsr.js │ │ ├── Menu.js │ │ ├── MenuSelect.js │ │ ├── NumericMenu.js │ │ ├── Pagination.js │ │ ├── Panel.js │ │ ├── PoweredBy.js │ │ ├── QueryRuleContext.js │ │ ├── QueryRuleCustomData.js │ │ ├── RangeInput.js │ │ ├── RatingMenu.js │ │ ├── RefinementList.js │ │ ├── RelevantSort.js │ │ ├── SearchBox.js │ │ ├── Snippet.js │ │ ├── SortBy.js │ │ ├── StateResults.js │ │ ├── Stats.js │ │ ├── ToggleRefinement.js │ │ ├── VoiceSearch.js │ │ ├── __Template.js │ │ └── __snapshots__ │ │ ├── Autocomplete.js.snap │ │ ├── Breadcrumb.js.snap │ │ ├── ClearRefinements.js.snap │ │ ├── Configure.js.snap │ │ ├── CurrentRefinements.js.snap │ │ ├── HierarchicalMenu.js.snap │ │ ├── Highlight.js.snap │ │ ├── Hits.js.snap │ │ ├── HitsPerPage.js.snap │ │ ├── InfiniteHits.js.snap │ │ ├── InstantSearch.js.snap │ │ ├── Menu.js.snap │ │ ├── MenuSelect.js.snap │ │ ├── NumericMenu.js.snap │ │ ├── Pagination.js.snap │ │ ├── Panel.js.snap │ │ ├── PoweredBy.js.snap │ │ ├── RangeInput.js.snap │ │ ├── RatingMenu.js.snap │ │ ├── RefinementList.js.snap │ │ ├── SearchBox.js.snap │ │ ├── Snippet.js.snap │ │ ├── SortBy.js.snap │ │ ├── StateResults.js.snap │ │ ├── ToggleRefinement.js.snap │ │ ├── VoiceSearch.js.snap │ │ └── __Template.js.snap ├── connectors │ ├── connectStateResults.js │ └── connectStateResults.test.js ├── instantsearch.js ├── instantsearch.umd.js ├── mixins │ ├── __mocks__ │ │ ├── panel.js │ │ └── widget.js │ ├── __tests__ │ │ ├── panel.test.js │ │ ├── suit.test.js │ │ └── widget.test.js │ ├── panel.js │ ├── suit.js │ └── widget.js ├── plugin.js ├── util │ ├── __tests__ │ │ ├── createServerRootMixin.test.js │ │ ├── parseAlgoliaHit.test.js │ │ ├── suit.test.js │ │ ├── unescape.test.js │ │ └── warn.test.js │ ├── createInstantSearchComponent.js │ ├── createServerRootMixin.js │ ├── parseAlgoliaHit.js │ ├── polyfills.js │ ├── suit.js │ ├── testutils │ │ ├── client.js │ │ └── helper.js │ ├── unescape.js │ ├── vue-compat │ │ ├── index-vue2.js │ │ ├── index-vue3.js │ │ └── index.js │ └── warn.js └── widgets.js ├── stories ├── Autocomplete.stories.js ├── Breadcrumb.stories.js ├── ClearRefinements.stories.js ├── Configure.stories.js ├── ConfigureRelatedItems.stories.js ├── CurrentRefinements.stories.js ├── DynamicWidgets.stories.js ├── HierarchicalMenu.stories.js ├── Highlight.stories.js ├── Hits.stories.js ├── HitsPerPage.stories.js ├── Index.stories.js ├── InfiniteHits.stories.js ├── InstantSearch.stories.js ├── MemoryRouter.js ├── Menu.stories.js ├── MenuSelect.stories.js ├── NumericMenu.stories.js ├── Pagination.stories.js ├── Panel.stories.js ├── PoweredBy.stories.js ├── QueryRuleContext.stories.js ├── QueryRuleCustomData.stories.js ├── RangeInput.stories.js ├── RatingMenu.stories.js ├── RefinementList.stories.js ├── RelevantSort.stories.js ├── SearchBox.stories.js ├── Snippet.stories.js ├── SortBy.stories.js ├── StateResults.stories.js ├── Stats.stories.js ├── ToggleRefinement.stories.js ├── VoiceSearch.stories.js ├── __Template.stories.js └── utils.js ├── test ├── modules │ ├── vue2 │ │ ├── package-is-cjs-module.cjs │ │ └── package-is-es-module.mjs │ └── vue3 │ │ ├── package-is-cjs-module.cjs │ │ └── package-is-es-module.mjs └── utils │ └── index.js ├── wdio.local.conf.js ├── wdio.saucelabs.conf.js ├── website └── _redirects └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: ['es2015'] 3 | } 4 | -------------------------------------------------------------------------------- /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "sandboxes": ["/examples/e-commerce"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | _book/ 2 | docs/ 3 | coverage/ 4 | dist/ 5 | vue2/ 6 | vue3/ 7 | examples/*/node_modules/* 8 | scripts/es-index-template.js 9 | website/examples/* 10 | website/stories/* 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['algolia/jest', 'algolia/vue'], 3 | rules: { 4 | 'no-warning-comments': 'warn', // we have many Todo:, this will remind us to deal with them 5 | 'no-use-before-define': 'off', 6 | 'vue/attribute-hyphenation': [ 7 | 'error', 8 | 'always', 9 | { 10 | ignore: ['createURL'], 11 | }, 12 | ], 13 | camelcase: [ 14 | 'error', 15 | { 16 | allow: ['^\\$_ais', '^EXPERIMENTAL_', 'instant_search'], 17 | }, 18 | ], 19 | }, 20 | overrides: { 21 | files: ['src/components/__tests__/*.js'], 22 | rules: { 23 | 'import/named': 'off', // we import __setState and use it 24 | }, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | docs/* linguist-documentation 2 | build/* linguist-documentation 3 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First of all, thanks for contributing! You can check out the issues tagged with "difficulty: easy ❄️" for a start. 4 | 5 | ## Get ready for contributions 6 | 7 | You'll first need to install the dependencies: 8 | 9 | ```sh 10 | yarn install 11 | ``` 12 | 13 | Then we recommend that you run: 14 | 15 | ```sh 16 | yarn test:watch 17 | ``` 18 | 19 | This will watch the files for changes and build the CommonJS bundle that is required by the tests. 20 | It will the run the test on that newly generated build. 21 | 22 | ## Commit format 23 | 24 | The project uses [conventional commit format](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md) to automate the updates of the CHANGELOG.md. 25 | 26 | ## Releasing the library 27 | 28 | To release the library, the first step is to create a "release PR" by running: 29 | 30 | ```bash 31 | yarn release 32 | ``` 33 | 34 | For that script to work, you need to provide `GITHUB_TOKEN` environment variable. You can either prepend it or put it in `.env` file. 35 | 36 | ```bash 37 | GITHUB_TOKEN=xyz yarn release 38 | 39 | or 40 | 41 | echo "GITHUB_TOKEN=xyz" >> .env 42 | yarn release 43 | ``` 44 | 45 | You can create a token at [GitHub](https://github.com/settings/tokens/new) with `Full control of private repositories` scope. 46 | 47 | This will ask you the new version of the library, and update all the required files accordingly. 48 | At the end of the process, the release branch is pushed to GitHub and a Pull Request is automatically created. 49 | 50 | Once the changes are approved you can merge it there. Then CircleCI will be triggered and it will run 51 | 52 | ```bash 53 | yarn shipjs trigger 54 | ``` 55 | 56 | This will: 57 | 58 | - publish the new version on NPM 59 | - tag and push the tag to GitHub 60 | 61 | ## Documentation 62 | 63 | You can either directly click on "EDIT ON GITHUB" links on the live documentation: https://community.algolia.com/vue-instantsearch/. 64 | 65 | Or you can run the documentation locally: 66 | 67 | ```sh 68 | $ npm run docs:watch 69 | ``` 70 | 71 | ### Deploying documentation 72 | 73 | The documentation is automatically deployed on temporary URLs by [netlify](https://www.netlify.com/) on pull requests. 74 | 75 | To release the documentation to the community website, you can run: 76 | 77 | ```bash 78 | yarn run docs:deploy 79 | ``` 80 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve Vue InstantSearch 4 | --- 5 | 6 | **Bug 🐞** 7 | 8 | ### What is the current behavior? 9 | 10 | ### Make a sandbox with the current behavior 11 | 12 | Template: https://codesandbox.io/s/github/algolia/create-instantsearch-app/tree/templates/vue-instantsearch 13 | 14 | ### What is the expected behavior? 15 | 16 | ### Does this happen only in specific situations? 17 | 18 | ### What is the proposed solution? 19 | 20 | ### What is the version you are using? 21 | 22 | > Always try the latest one before opening an issue. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Create a report to help us improve Vue InstantSearch 4 | --- 5 | 6 | **Feature ⚡️** 7 | 8 | ### What is your use case for such a feature? 9 | 10 | ### What is your proposal 11 | 12 | 17 | 18 | ### What is the version you are using? 19 | 20 | > Always try the latest one before opening an issue. 21 | -------------------------------------------------------------------------------- /.github/vue-instantsearch-readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/vue-instantsearch/f9dd4b0b33d47ad0d34292f8d826b6b5882e930d/.github/vue-instantsearch-readme.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # package manager 2 | node_modules/ 3 | yarn-error.log 4 | 5 | # OS-specific files 6 | .DS_store 7 | 8 | # cache 9 | .nuxt 10 | 11 | # secret 12 | .env 13 | 14 | # Generated files 15 | /website/stories 16 | /website/examples/* 17 | !/website/examples/index.html 18 | coverage 19 | 20 | # published files 21 | /vue2/ 22 | /vue3/ 23 | 24 | # Test output 25 | /junit 26 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.16.3 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | coverage 3 | docs 4 | dist 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-knobs/register'; 2 | import '@storybook/addon-options/register'; 3 | import '@storybook/addon-actions/register'; 4 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/vue'; 2 | import { setOptions } from '@storybook/addon-options'; 3 | 4 | setOptions({ 5 | name: 'vue-instantsearch', 6 | url: 'https://community.algolia.com/vue-instantsearch/', 7 | goFullScreen: false, 8 | showStoriesPanel: true, 9 | showAddonPanel: true, 10 | showSearchBox: false, 11 | addonPanelInRight: true, 12 | sidebarAnimations: false, 13 | }); 14 | 15 | import 'instantsearch.css/themes/algolia-min.css'; 16 | import './styles.css'; 17 | 18 | import Vue from 'vue'; 19 | import InstantSearch from '../src/instantsearch'; 20 | 21 | Vue.config.productionTip = false; 22 | Vue.use(InstantSearch); 23 | 24 | const req = require.context('../stories', true, /\.stories\.js$/); 25 | 26 | function loadStories() { 27 | req.keys().forEach(filename => req(filename)); 28 | } 29 | 30 | configure(loadStories, module); 31 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": false, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Algolia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue InstantSearch has a new home 👋 2 | 3 | This project has moved and is now part of the [InstantSearch monorepo](https://github.com/algolia/instantsearch)! **The library remains unchanged and is still available on npm and CDNs like jsDelivr.** 4 | 5 | You can [browse the code](https://github.com/algolia/instantsearch/tree/master/packages), find [existing issues](https://github.com/algolia/instantsearch/issues?q=is%3Aissue+is%3Aopen+label%3A%22Library%3A+Vue+InstantSearch%22) and follow [new releases](https://github.com/algolia/instantsearch/releases) over there. 6 | -------------------------------------------------------------------------------- /__mocks__/instantsearch.js/es.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-commonjs */ 2 | const isPlainObject = require('lodash/isPlainObject'); 3 | 4 | class RoutingManager { 5 | constructor(routing) { 6 | this._routing = routing; 7 | } 8 | } 9 | 10 | class Helper { 11 | constructor() { 12 | this.search = jest.fn(); 13 | this.setClient = jest.fn(() => this); 14 | this.setIndex = jest.fn(() => this); 15 | } 16 | } 17 | 18 | const fakeInstantSearch = jest.fn( 19 | ({ 20 | indexName, 21 | searchClient, 22 | routing, 23 | stalledSearchDelay, 24 | searchFunction, 25 | }) => { 26 | if (!searchClient && !isPlainObject(searchClient)) { 27 | throw new Error('need searchClient to be a plain object'); 28 | } 29 | if (!indexName) { 30 | throw new Error('need indexName to be a string'); 31 | } 32 | 33 | const instantsearchInstance = { 34 | _stalledSearchDelay: stalledSearchDelay || 200, 35 | _searchFunction: searchFunction, 36 | routing: new RoutingManager(routing), 37 | helper: new Helper(), 38 | client: searchClient, 39 | start: jest.fn(() => { 40 | instantsearchInstance.started = true; 41 | }), 42 | dispose: jest.fn(() => { 43 | instantsearchInstance.started = false; 44 | }), 45 | mainIndex: { 46 | $$type: 'ais.index', 47 | _widgets: [], 48 | addWidgets(widgets) { 49 | this._widgets.push(...widgets); 50 | }, 51 | getWidgets() { 52 | return this._widgets; 53 | }, 54 | }, 55 | addWidgets(widgets) { 56 | instantsearchInstance.mainIndex.addWidgets(widgets); 57 | }, 58 | removeWidgets(widgets) { 59 | widgets.forEach(widget => { 60 | const i = instantsearchInstance.mainIndex._widgets.findIndex(widget); 61 | if (i === -1) { 62 | return; 63 | } 64 | instantsearchInstance.mainIndex._widgets.splice(i, 1); 65 | }); 66 | }, 67 | }; 68 | 69 | return instantsearchInstance; 70 | } 71 | ); 72 | 73 | module.exports = fakeInstantSearch; 74 | -------------------------------------------------------------------------------- /examples/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # this builds examples, works from any directory (yarn build:examples) 3 | # This script can be removed once we figure out why CodeSandbox doesn't build the examples 4 | # try: https://codesandbox.io/s/github/algolia/vue-instantsearch/tree/feat/connectors/examples/e-commerce 5 | set -e 6 | 7 | # go into directory of script 8 | cd $(dirname `which $0`) 9 | 10 | 11 | function build_example { 12 | dir=$1 13 | if [ -d "$dir" ]; then 14 | name=$(basename "$dir") 15 | echo "building example: $name" 16 | cd $name 17 | if [[ "$name" != "nuxt" && "$name" != "ssr" ]]; then 18 | yarn 19 | yarn build 20 | mkdir -p ../../website/examples/$name 21 | cp -R dist/* ../../website/examples/$name 22 | else 23 | echo "build of $name skipped" 24 | fi 25 | cd .. 26 | fi 27 | } 28 | 29 | if [ $# -eq 0 ];then 30 | for dir in ./* ; do 31 | build_example $dir 32 | done 33 | else 34 | build_example $1 35 | fi 36 | 37 | echo "done building examples 🙌" 38 | -------------------------------------------------------------------------------- /examples/default-theme/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /examples/default-theme/README.md: -------------------------------------------------------------------------------- 1 | # default-theme 2 | 3 | ## Project setup 4 | 5 | ``` 6 | yarn install 7 | ``` 8 | 9 | ### Compiles and hot-reloads for development 10 | 11 | ``` 12 | yarn run serve 13 | ``` 14 | 15 | ### Compiles and minifies for production 16 | 17 | ``` 18 | yarn run build 19 | ``` 20 | 21 | ### Lints and fixes files 22 | 23 | ``` 24 | yarn run lint 25 | ``` 26 | -------------------------------------------------------------------------------- /examples/default-theme/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/app'], 3 | }; 4 | -------------------------------------------------------------------------------- /examples/default-theme/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "default-theme", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "algoliasearch": "^4.0.1", 12 | "instantsearch.css": "^7.1.1", 13 | "instantsearch.js": "^4.2.0", 14 | "vue": "^2.6.7", 15 | "vue-instantsearch": "^3.0.0-beta.0" 16 | }, 17 | "devDependencies": { 18 | "@vue/cli-plugin-babel": "^3.4.1", 19 | "@vue/cli-plugin-eslint": "^3.4.1", 20 | "@vue/cli-service": "^3.4.1", 21 | "vue-template-compiler": "^2.6.7" 22 | }, 23 | "eslintConfig": { 24 | "root": true, 25 | "env": { 26 | "node": true 27 | }, 28 | "extends": [ 29 | "plugin:vue/essential", 30 | "eslint:recommended" 31 | ], 32 | "rules": {}, 33 | "parserOptions": { 34 | "parser": "babel-eslint" 35 | } 36 | }, 37 | "postcss": { 38 | "plugins": { 39 | "autoprefixer": {} 40 | } 41 | }, 42 | "browserslist": [ 43 | "> 1%", 44 | "last 2 versions", 45 | "not ie <= 8" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /examples/default-theme/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/vue-instantsearch/f9dd4b0b33d47ad0d34292f8d826b6b5882e930d/examples/default-theme/public/favicon.ico -------------------------------------------------------------------------------- /examples/default-theme/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 |
9 |
15 |
This widget doesn't render anything without a filled in default slot.
12 |query, function to refine and results are provided.
13 |refine: Function14 |
currentRefinement: "{{ state.currentRefinement }}"15 |
indices
:{{ state.indices }}18 |
{{ item }}16 |
8 | Use this component to have a different layout based on a certain state. 9 |
10 |11 | Fill in the slot, and get access to the following things: 12 |
13 |results: {{ Object.keys(state.results) }}14 |
state: {{ Object.keys(state.state) }}15 |
status: {{ state.status }}16 |
error: {{ state.error }}17 |
This is the body of the Panel.
7 | `; 8 | 9 | it('renders correctly', () => { 10 | const wrapper = mount(Panel, { 11 | slots: { 12 | default: defaultSlot, 13 | }, 14 | }); 15 | 16 | expect(wrapper.html()).toMatchSnapshot(); 17 | }); 18 | 19 | it('renders correctly without refinement', async () => { 20 | const wrapper = mount(Panel, { 21 | slots: { 22 | default: defaultSlot, 23 | }, 24 | }); 25 | 26 | await wrapper.setData({ 27 | canRefine: false, 28 | }); 29 | 30 | expect(wrapper.html()).toMatchSnapshot(); 31 | }); 32 | 33 | it('passes data without refinement', async () => { 34 | const defaultScopedSlot = jest.fn(); 35 | const headerScopedSlot = jest.fn(); 36 | const footerScopedSlot = jest.fn(); 37 | const wrapper = mount(Panel, { 38 | scopedSlots: { 39 | default: defaultScopedSlot, 40 | header: headerScopedSlot, 41 | footer: footerScopedSlot, 42 | }, 43 | }); 44 | 45 | await wrapper.setData({ 46 | canRefine: false, 47 | }); 48 | 49 | expect(defaultScopedSlot).toHaveBeenCalledWith({ hasRefinements: false }); 50 | expect(headerScopedSlot).toHaveBeenCalledWith({ hasRefinements: false }); 51 | expect(footerScopedSlot).toHaveBeenCalledWith({ hasRefinements: false }); 52 | }); 53 | 54 | it('passes data with refinement', async () => { 55 | const defaultScopedSlot = jest.fn(); 56 | const headerScopedSlot = jest.fn(); 57 | const footerScopedSlot = jest.fn(); 58 | const wrapper = mount(Panel, { 59 | scopedSlots: { 60 | default: defaultScopedSlot, 61 | header: headerScopedSlot, 62 | footer: footerScopedSlot, 63 | }, 64 | }); 65 | 66 | await wrapper.setData({ 67 | canRefine: true, 68 | }); 69 | 70 | expect(defaultScopedSlot).toHaveBeenCalledWith({ hasRefinements: true }); 71 | expect(headerScopedSlot).toHaveBeenCalledWith({ hasRefinements: true }); 72 | expect(footerScopedSlot).toHaveBeenCalledWith({ hasRefinements: true }); 73 | }); 74 | 75 | it('renders correctly with header', () => { 76 | const wrapper = mount(Panel, { 77 | slots: { 78 | default: defaultSlot, 79 | header: `Header`, 80 | }, 81 | }); 82 | 83 | expect(wrapper.html()).toMatchSnapshot(); 84 | }); 85 | 86 | it('renders correctly with footer', () => { 87 | const wrapper = mount(Panel, { 88 | slots: { 89 | default: defaultSlot, 90 | footer: `Footer`, 91 | }, 92 | }); 93 | 94 | expect(wrapper.html()).toMatchSnapshot(); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /src/components/__tests__/PoweredBy.js: -------------------------------------------------------------------------------- 1 | import { mount } from '../../../test/utils'; 2 | import PoweredBy from '../PoweredBy.vue'; 3 | jest.mock('../../mixins/widget'); 4 | 5 | test('includes the hostname in the URL', () => { 6 | const wrapper = mount(PoweredBy); 7 | 8 | const algoliaURL = new URL(wrapper.vm.algoliaUrl); 9 | 10 | expect(algoliaURL.origin).toBe('https://www.algolia.com'); 11 | expect(algoliaURL.searchParams.get('utm_source')).toBe('vue-instantsearch'); 12 | expect(algoliaURL.searchParams.get('utm_medium')).toBe('website'); 13 | expect(algoliaURL.searchParams.get('utm_content')).toBe(location.hostname); 14 | expect(algoliaURL.searchParams.get('utm_content')).toBe('example.com'); 15 | expect(algoliaURL.searchParams.get('utm_campaign')).toBe('poweredby'); 16 | }); 17 | 18 | test('has proper HTML rendering', () => { 19 | const wrapper = mount(PoweredBy); 20 | 21 | expect(wrapper.html()).toMatchSnapshot(); 22 | }); 23 | 24 | test('has proper HTML rendering (dark)', () => { 25 | const wrapper = mount(PoweredBy, { 26 | propsData: { 27 | theme: 'dark', 28 | }, 29 | }); 30 | 31 | expect(wrapper.html()).toMatchSnapshot(); 32 | }); 33 | -------------------------------------------------------------------------------- /src/components/__tests__/QueryRuleContext.js: -------------------------------------------------------------------------------- 1 | import { mount } from '../../../test/utils'; 2 | import QueryRuleContext from '../QueryRuleContext'; 3 | import { __setState } from '../../mixins/widget'; 4 | 5 | jest.mock('../../mixins/widget'); 6 | 7 | it('is renderless', () => { 8 | __setState({ 9 | items: ["this isn't used"], 10 | }); 11 | const wrapper = mount(QueryRuleContext, { 12 | propsData: { 13 | trackedFilters: {}, 14 | }, 15 | }); 16 | expect(wrapper.text()).toMatchInlineSnapshot(`""`); 17 | }); 18 | 19 | it('accepts only trackedFilters and transformRuleContexts', () => { 20 | const trackedFilters = {}; 21 | const transformRuleContexts = jest.fn(); 22 | const wrapper = mount(QueryRuleContext, { 23 | propsData: { 24 | trackedFilters, 25 | transformRuleContexts, 26 | transformItems: "won't be transferred", 27 | }, 28 | }); 29 | 30 | expect(wrapper.vm.widgetParams).toEqual({ 31 | trackedFilters, 32 | transformRuleContexts, 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/components/__tests__/QueryRuleCustomData.js: -------------------------------------------------------------------------------- 1 | import { mount } from '../../../test/utils'; 2 | import QueryRuleCustomData from '../QueryRuleCustomData.vue'; 3 | import { __setState } from '../../mixins/widget'; 4 | 5 | jest.mock('../../mixins/widget'); 6 | 7 | it('renders in a list ofby default', () => { 8 | __setState({ 9 | items: [{ text: 'this is user data' }, { text: 'this too!' }], 10 | }); 11 | 12 | const wrapper = mount(QueryRuleCustomData); 13 | 14 | expect(wrapper.html()).toMatchInlineSnapshot(` 15 |16 |31 | `); 32 | }); 33 | 34 | it('gives the items to the main slot', () => { 35 | const items = [{ text: 'this is user data' }, { text: 'this too!' }]; 36 | __setState({ 37 | items, 38 | }); 39 | 40 | mount(QueryRuleCustomData, { 41 | scopedSlots: { 42 | default(props) { 43 | expect(props).toEqual({ 44 | items, 45 | }); 46 | }, 47 | }, 48 | }); 49 | }); 50 | 51 | it('gives individual items to the item slot', () => { 52 | const items = [{ text: 'this is user data' }, { text: 'this too!' }]; 53 | expect.assertions(items.length); 54 | __setState({ 55 | items, 56 | }); 57 | 58 | mount(QueryRuleCustomData, { 59 | scopedSlots: { 60 | item(props) { 61 | expect(props).toEqual({ 62 | item: expect.objectContaining({ text: expect.any(String) }), 63 | }); 64 | }, 65 | }, 66 | }); 67 | }); 68 | 69 | it('accepts transformItems', () => { 70 | const transformItems = jest.fn(); 71 | const wrapper = mount(QueryRuleCustomData, { 72 | propsData: { 73 | transformItems, 74 | }, 75 | }); 76 | 77 | expect(wrapper.vm.widgetParams).toEqual({ 78 | transformItems, 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/components/__tests__/RelevantSort.js: -------------------------------------------------------------------------------- 1 | import { mount } from '../../../test/utils'; 2 | import RelevantSort from '../RelevantSort.vue'; 3 | import { __setState } from '../../mixins/widget'; 4 | jest.mock('../../mixins/widget'); 5 | 6 | describe('renders correctly', () => { 7 | test('no virtual replica', () => { 8 | __setState({ 9 | isVirtualReplica: false, 10 | isRelevantSorted: false, 11 | }); 12 | const wrapper = mount(RelevantSort); 13 | expect(wrapper).toHaveEmptyHTML(); 14 | }); 15 | 16 | test('not relevant sorted', () => { 17 | __setState({ 18 | isVirtualReplica: true, 19 | isRelevantSorted: false, 20 | }); 21 | const wrapper = mount(RelevantSort); 22 | expect(wrapper.html()).toMatchInlineSnapshot(` 23 |17 |23 |18 | { 19 | "text": "this is user data" 20 | } 21 |22 |24 |30 |25 | { 26 | "text": "this too!" 27 | } 28 |29 |24 |32 | `); 33 | }); 34 | 35 | test('relevant sorted', () => { 36 | __setState({ 37 | isVirtualReplica: true, 38 | isRelevantSorted: true, 39 | }); 40 | const wrapper = mount(RelevantSort); 41 | expect(wrapper.html()).toMatchInlineSnapshot(` 42 |25 |26 | 31 |43 |51 | `); 52 | }); 53 | }); 54 | 55 | it("calls the connector's refine function with 0 and undefined", async () => { 56 | __setState({ 57 | isRelevantSorted: true, 58 | isVirtualReplica: true, 59 | refine: jest.fn(() => { 60 | wrapper.vm.state.isRelevantSorted = !wrapper.vm.state.isRelevantSorted; 61 | }), 62 | }); 63 | 64 | const wrapper = mount(RelevantSort); 65 | 66 | const button = wrapper.find('button'); 67 | 68 | await button.trigger('click'); 69 | expect(wrapper.vm.state.refine).toHaveBeenLastCalledWith(0); 70 | 71 | await button.trigger('click'); 72 | expect(wrapper.vm.state.refine).toHaveBeenLastCalledWith(undefined); 73 | 74 | await button.trigger('click'); 75 | expect(wrapper.vm.state.refine).toHaveBeenLastCalledWith(0); 76 | }); 77 | -------------------------------------------------------------------------------- /src/components/__tests__/Snippet.js: -------------------------------------------------------------------------------- 1 | import { mount } from '../../../test/utils'; 2 | import Snippet from '../Snippet.vue'; 3 | 4 | jest.unmock('instantsearch.js/es'); 5 | 6 | test('renders proper HTML', () => { 7 | const hit = { 8 | _snippetResult: { 9 | attr: { 10 | value: `content`, 11 | }, 12 | }, 13 | }; 14 | 15 | const wrapper = mount(Snippet, { 16 | propsData: { 17 | attribute: 'attr', 18 | hit, 19 | }, 20 | }); 21 | 22 | expect(wrapper.html()).toMatchSnapshot(); 23 | }); 24 | 25 | test('renders proper HTML with highlightTagName', () => { 26 | const hit = { 27 | _snippetResult: { 28 | attr: { 29 | value: `content`, 30 | }, 31 | }, 32 | }; 33 | 34 | const wrapper = mount(Snippet, { 35 | propsData: { 36 | attribute: 'attr', 37 | highlightedTagName: 'marquee', 38 | hit, 39 | }, 40 | }); 41 | 42 | expect(wrapper.html()).toMatchSnapshot(); 43 | }); 44 | 45 | test('should render an empty string in production if attribute is not snippeted', () => { 46 | process.env.NODE_ENV = 'production'; 47 | const hit = { 48 | _snippetResult: {}, 49 | }; 50 | global.console.warn = jest.fn(); 51 | 52 | const wrapper = mount(Snippet, { 53 | propsData: { 54 | attribute: 'attr', 55 | hit, 56 | }, 57 | }); 58 | 59 | expect(wrapper.html()).toMatchSnapshot(); 60 | expect(global.console.warn).not.toHaveBeenCalled(); 61 | }); 62 | 63 | test('allows usage of dot delimited path to access nested attribute', () => { 64 | const hit = { 65 | _snippetResult: { 66 | attr: { 67 | nested: { 68 | value: `nested val`, 69 | }, 70 | }, 71 | }, 72 | }; 73 | 74 | const wrapper = mount(Snippet, { 75 | propsData: { 76 | attribute: 'attr.nested', 77 | hit, 78 | }, 79 | }); 80 | 81 | expect(wrapper.html()).toMatchSnapshot(); 82 | }); 83 | -------------------------------------------------------------------------------- /src/components/__tests__/SortBy.js: -------------------------------------------------------------------------------- 1 | import { mount } from '../../../test/utils'; 2 | import { __setState } from '../../mixins/widget'; 3 | import SortBy from '../SortBy.vue'; 4 | 5 | jest.mock('../../mixins/widget'); 6 | jest.mock('../../mixins/panel'); 7 | 8 | const defaultState = { 9 | options: [ 10 | { value: 'some_index', label: 'Relevance' }, 11 | { value: 'some_index_cool', label: 'Coolness ascending' }, 12 | { value: 'some_index_quality', label: 'Quality ascending' }, 13 | ], 14 | hasNoResults: false, 15 | canRefine: true, 16 | currentRefinement: 'some_index', 17 | }; 18 | 19 | const defaultProps = { 20 | items: [ 21 | { value: 'some_index', label: 'Relevance' }, 22 | { value: 'some_index_cool', label: 'Coolness ascending' }, 23 | { value: 'some_index_quality', label: 'Quality ascending' }, 24 | ], 25 | }; 26 | 27 | it('accepts transformItems prop', () => { 28 | __setState({ ...defaultState }); 29 | 30 | const transformItems = () => {}; 31 | 32 | const wrapper = mount(SortBy, { 33 | propsData: { 34 | ...defaultProps, 35 | transformItems, 36 | }, 37 | }); 38 | 39 | expect(wrapper.vm.widgetParams.transformItems).toBe(transformItems); 40 | }); 41 | 42 | it('renders correctly', () => { 43 | __setState({ ...defaultState }); 44 | 45 | const wrapper = mount(SortBy, { 46 | propsData: { 47 | ...defaultProps, 48 | }, 49 | }); 50 | expect(wrapper.html()).toMatchSnapshot(); 51 | }); 52 | 53 | it('renders with scoped slots', () => { 54 | const defaultSlot = ` 55 | 56 | 66 | 67 | `; 68 | 69 | __setState({ 70 | ...defaultState, 71 | }); 72 | 73 | const wrapper = mount({ 74 | components: { SortBy }, 75 | data() { 76 | return { props: defaultProps }; 77 | }, 78 | template: ` 79 |44 |45 | 50 |80 | ${defaultSlot} 81 | 82 | `, 83 | }); 84 | 85 | expect(wrapper.html()).toMatchSnapshot(); 86 | }); 87 | 88 | it('calls `refine` when the selection changes with the `value`', async () => { 89 | const refine = jest.fn(); 90 | __setState({ 91 | ...defaultState, 92 | refine, 93 | }); 94 | const wrapper = mount(SortBy, { 95 | propsData: { 96 | ...defaultProps, 97 | }, 98 | }); 99 | // This is bad 👇🏽 but the only way for now to trigger changes 100 | // on a select: https://github.com/vuejs/vue-test-utils/issues/260 101 | const select = wrapper.find('select'); 102 | select.element.value = 'some_index_quality'; 103 | await select.trigger('change'); 104 | const selectedOption = wrapper.find('option[value=some_index_quality]'); 105 | 106 | expect(refine).toHaveBeenCalledTimes(1); 107 | expect(refine).toHaveBeenLastCalledWith('some_index_quality'); 108 | expect(selectedOption.element.selected).toBe(true); 109 | }); 110 | -------------------------------------------------------------------------------- /src/components/__tests__/Stats.js: -------------------------------------------------------------------------------- 1 | import { mount } from '../../../test/utils'; 2 | 3 | import Stats from '../Stats.vue'; 4 | 5 | import { __setState } from '../../mixins/widget'; 6 | jest.mock('../../mixins/widget'); 7 | 8 | it('renders correctly', () => { 9 | __setState({ 10 | hitsPerPage: 50, 11 | nbPages: 20, 12 | nbHits: 1000, 13 | page: 2, 14 | processingTimeMS: 12, 15 | query: 'ipho', 16 | instantSearchInstance: { 17 | helper: { 18 | lastResults: [], 19 | }, 20 | }, 21 | }); 22 | 23 | const wrapper = mount(Stats); 24 | expect(wrapper.html()).toMatchInlineSnapshot(` 25 |26 | 27 | 1,000 results found in 12ms 28 | 29 |30 | `); 31 | }); 32 | 33 | it('renders correctly (relevant sort)', () => { 34 | __setState({ 35 | areHitsSorted: true, 36 | hitsPerPage: 50, 37 | nbPages: 20, 38 | nbHits: 1000, 39 | nbSortedHits: 12, 40 | page: 2, 41 | processingTimeMS: 12, 42 | query: 'ipho', 43 | instantSearchInstance: { 44 | helper: { 45 | lastResults: [], 46 | }, 47 | }, 48 | }); 49 | 50 | const wrapper = mount(Stats); 51 | expect(wrapper.html()).toMatchInlineSnapshot(` 52 |53 | 54 | 12 relevant results sorted out of 1,000 found in 12ms 55 | 56 |57 | `); 58 | }); 59 | -------------------------------------------------------------------------------- /src/components/__tests__/__Template.js: -------------------------------------------------------------------------------- 1 | import { mount } from '../../../test/utils'; 2 | import Template from '../__Template.vue'; 3 | import { __setState } from '../../mixins/widget'; 4 | jest.mock('../../mixins/widget'); 5 | 6 | it('renders correctly', () => { 7 | __setState({ 8 | hits: ['yo', 'how', 'are', 'you', 'doing', '?'], 9 | }); 10 | const wrapper = mount(Template); 11 | expect(wrapper.html()).toMatchSnapshot(); 12 | }); 13 | 14 | // ☑️ add another rendering test if it's different given the propsData 15 | 16 | it('behaves correctly', async () => { 17 | __setState({ 18 | refine: jest.fn(), 19 | }); 20 | const wrapper = mount(Template); 21 | const button = wrapper.find('button'); 22 | await button.trigger('click'); 23 | expect(wrapper.vm.state.refine).toHaveBeenLastCalledWith('hi'); 24 | }); 25 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/Autocomplete.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 |5 |41 | `; 42 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/ClearRefinements.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`custom default render renders correctly 1`] = ` 4 |6 | This widget doesn't render anything without a filled in default slot. 7 |
8 |9 | query, function to refine and results are provided. 10 |
11 |12 | refine: Function 13 |14 |15 | currentRefinement: "" 16 |17 |18 |40 |19 |
24 |20 | indices 21 |
22 | : 23 |25 | [ 26 | { 27 | "index": "bla", 28 | "label": "bla bla bla ", 29 | "hits": [ 30 | { 31 | "objectID": 1, 32 | "name": "hi" 33 | } 34 | ], 35 | "results": {} 36 | } 37 | ] 38 |39 |5 |11 | `; 12 | 13 | exports[`custom default render renders correctly with an URL for the href 1`] = ` 14 |6 | 7 | Clear refinements 8 | 9 |10 |15 |21 | `; 22 | 23 | exports[`custom default render renders correctly without refinement 1`] = ` 24 |16 | 17 | Clear refinements 18 | 19 |20 |25 |31 | `; 32 | 33 | exports[`custom resetLabel render renders correctly with a custom reset label 1`] = ` 34 |26 | 27 | Clear refinements 28 | 29 |30 |35 | 42 |43 | `; 44 | 45 | exports[`default render renders correctly 1`] = ` 46 |47 | 52 |53 | `; 54 | 55 | exports[`default render renders correctly without refinements 1`] = ` 56 |57 | 63 |64 | `; 65 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/Configure.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders with scoped slots 1`] = ` 4 |5 | 6 | hitsPerPage: 5 7 | 8 |9 | `; 10 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/Highlight.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`allows usage of dot delimited path to access nested attribute 1`] = ` 4 | 5 | nested 6 | 7 | val 8 | 9 | 10 | `; 11 | 12 | exports[`renders proper HTML 1`] = ` 13 | 14 | con 15 | 16 | ten 17 | 18 | t 19 | 20 | `; 21 | 22 | exports[`renders proper HTML with highlightTagName 1`] = ` 23 | 24 | con 25 | 28 | t 29 | 30 | `; 31 | 32 | exports[`should render an empty string in production if attribute is not highlighted 1`] = ` 33 | 34 | 35 | `; 36 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/Hits.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 |5 |14 | `; 15 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/HitsPerPage.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 |6 |
13 |- 7 | objectID: one, index: 0 8 |
9 |- 10 | objectID: two, index: 1 11 |
12 |5 | 17 |18 | `; 19 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/InstantSearch.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly (empty) 1`] = ` 4 |5 |6 | `; 7 | 8 | exports[`renders correctly (with slot used) 1`] = ` 9 |10 |14 | `; 15 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/Panel.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`default render renders correctly 1`] = ` 4 |11 | Hi there, this is the main slot 12 |13 |5 |11 | `; 12 | 13 | exports[`default render renders correctly with footer 1`] = ` 14 |6 |10 |7 | This is the body of the Panel. 8 |
9 |15 |26 | `; 27 | 28 | exports[`default render renders correctly with header 1`] = ` 29 |16 |20 | 25 |17 | This is the body of the Panel. 18 |
19 |30 |41 | `; 42 | 43 | exports[`default render renders correctly without refinement 1`] = ` 44 |31 | 32 | Header 33 | 34 |35 |36 |40 |37 | This is the body of the Panel. 38 |
39 |45 |51 | `; 52 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/SearchBox.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders HTML correctly 1`] = ` 4 |46 |50 |47 | This is the body of the Panel. 48 |
49 |5 | 57 |58 | `; 59 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/Snippet.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`allows usage of dot delimited path to access nested attribute 1`] = ` 4 | 5 | nested 6 | 7 | val 8 | 9 | 10 | `; 11 | 12 | exports[`renders proper HTML 1`] = ` 13 | 14 | con 15 | 16 | ten 17 | 18 | t 19 | 20 | `; 21 | 22 | exports[`renders proper HTML with highlightTagName 1`] = ` 23 | 24 | con 25 | 28 | t 29 | 30 | `; 31 | 32 | exports[`should render an empty string in production if attribute is not snippeted 1`] = ` 33 | 34 | 35 | `; 36 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/SortBy.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 |5 | 22 |23 | `; 24 | 25 | exports[`renders with scoped slots 1`] = ` 26 |27 | 38 |39 | `; 40 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/StateResults.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders explanation if no slot is used 1`] = ` 4 |5 |30 | `; 31 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/ToggleRefinement.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`custom default render renders correctly 1`] = ` 4 | 14 | `; 15 | 16 | exports[`custom default render renders correctly with a URL for the href 1`] = ` 17 | 29 | `; 30 | 31 | exports[`custom default render renders correctly with the value selected 1`] = ` 32 | 42 | `; 43 | 44 | exports[`custom default render renders correctly without refinement 1`] = ` 45 | 55 | `; 56 | 57 | exports[`default render renders correctly 1`] = ` 58 |6 | Use this component to have a different layout based on a certain state. 7 |
8 |9 | Fill in the slot, and get access to the following things: 10 |
11 |12 | results: [ 13 | "query", 14 | "hits", 15 | "page" 16 | ] 17 |18 |19 | state: [ 20 | "query" 21 | ] 22 |23 |24 | status: idle 25 |26 |27 | error: 28 |29 |59 | 72 |73 | `; 74 | 75 | exports[`default render renders correctly without refinement (with 0) 1`] = ` 76 |77 | 90 |91 | `; 92 | 93 | exports[`default render renders correctly without refinement (with null) 1`] = ` 94 |95 | 105 |106 | `; 107 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/VoiceSearch.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Rendering renders default template correctly 1`] = ` 4 |5 | 39 |44 | `; 45 | 46 | exports[`Rendering with custom template for status 1`] = ` 47 |40 |43 |41 |
42 |48 |69 | `; 70 | -------------------------------------------------------------------------------- /src/connectors/connectStateResults.js: -------------------------------------------------------------------------------- 1 | import { _objectSpread } from '../util/polyfills'; 2 | 3 | const connectStateResults = (renderFn, unmountFn = () => {}) => ( 4 | widgetParams = {} 5 | ) => ({ 6 | init({ instantSearchInstance }) { 7 | renderFn( 8 | { 9 | state: undefined, 10 | results: undefined, 11 | instantSearchInstance, 12 | widgetParams, 13 | }, 14 | true 15 | ); 16 | }, 17 | 18 | render({ results, instantSearchInstance, state }) { 19 | const resultsCopy = _objectSpread({}, results); 20 | 21 | const stateCopy = _objectSpread({}, state); 22 | 23 | renderFn( 24 | { 25 | results: resultsCopy, 26 | state: stateCopy, 27 | instantSearchInstance, 28 | widgetParams, 29 | }, 30 | false 31 | ); 32 | }, 33 | 34 | dispose() { 35 | unmountFn(); 36 | }, 37 | }); 38 | 39 | export default connectStateResults; 40 | -------------------------------------------------------------------------------- /src/instantsearch.js: -------------------------------------------------------------------------------- 1 | export { createSuitMixin } from './mixins/suit'; 2 | export { createWidgetMixin } from './mixins/widget'; 3 | export * from './widgets'; 4 | export { plugin as default } from './plugin'; 5 | export { createServerRootMixin } from './util/createServerRootMixin'; 6 | -------------------------------------------------------------------------------- /src/instantsearch.umd.js: -------------------------------------------------------------------------------- 1 | import { plugin } from './plugin'; 2 | import { isVue2 } from './util/vue-compat'; 3 | 4 | // Automatically register Algolia Search components if Vue 2.x is available globally. 5 | if (typeof window !== 'undefined' && window.Vue && isVue2) { 6 | window.Vue.use(plugin); 7 | } 8 | 9 | export { createSuitMixin } from './mixins/suit'; 10 | export { createWidgetMixin } from './mixins/widget'; 11 | export * from './widgets'; 12 | -------------------------------------------------------------------------------- /src/mixins/__mocks__/panel.js: -------------------------------------------------------------------------------- 1 | export const createPanelProviderMixin = jest.fn(() => ({})); 2 | 3 | export const createPanelConsumerMixin = jest.fn(() => ({})); 4 | -------------------------------------------------------------------------------- /src/mixins/__mocks__/widget.js: -------------------------------------------------------------------------------- 1 | let state = {}; 2 | let widget = {}; 3 | let indexResults = null; 4 | let indexHelper = null; 5 | let instantSearchInstance = { 6 | status: 'idle', 7 | error: undefined, 8 | addListener: () => {}, 9 | removeListener: () => {}, 10 | }; 11 | 12 | // we need to have state given by `component` before it is mounted, otherwise 13 | // we can't render it in most cases (items, hits, etc. are used in the template) 14 | // so we share a (mock) global state during a whole test. 15 | // 16 | // (a mock is imported once per test file, so the state is isolated between tests) 17 | // 18 | // This allows us to import this `__setState` function and call it in the test 19 | // to give the necessary data before mounting. 20 | export function __setState(newState) { 21 | state = newState; 22 | } 23 | 24 | export function __setWidget(newWidget) { 25 | widget = newWidget; 26 | } 27 | 28 | export function __setIndexResults(newResults) { 29 | indexResults = newResults; 30 | } 31 | 32 | export function __setIndexHelper(newHelper) { 33 | indexHelper = newHelper; 34 | } 35 | 36 | export function __overrideInstantSearchInstance(newInstantSearchInstance) { 37 | instantSearchInstance = Object.assign( 38 | instantSearchInstance, 39 | newInstantSearchInstance 40 | ); 41 | } 42 | 43 | export const createWidgetMixin = jest.fn(() => ({ 44 | data() { 45 | return { 46 | state, 47 | widget, 48 | instantSearchInstance, 49 | getParentIndex: () => ({ 50 | getResults: () => indexResults, 51 | getHelper: () => indexHelper, 52 | }), 53 | }; 54 | }, 55 | })); 56 | -------------------------------------------------------------------------------- /src/mixins/__tests__/suit.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from '../../../test/utils'; 2 | import { createSuitMixin } from '../suit'; 3 | 4 | const createFakeComponent = () => ({ 5 | render: () => null, 6 | }); 7 | 8 | it('exposes the regular suit function for this widget', () => { 9 | const Test = createFakeComponent(); 10 | 11 | const wrapper = mount(Test, { 12 | mixins: [createSuitMixin({ name: 'Test' })], 13 | }); 14 | const { suit } = wrapper.vm; 15 | 16 | expect(suit()).toMatchInlineSnapshot(`"ais-Test"`); 17 | expect(suit('', 'ok')).toMatchInlineSnapshot(`"ais-Test--ok"`); 18 | expect(suit('ok')).toMatchInlineSnapshot(`"ais-Test-ok"`); 19 | expect(suit('ok', 'there')).toMatchInlineSnapshot(`"ais-Test-ok--there"`); 20 | }); 21 | 22 | it('allows overriding from the `class-names` prop', () => { 23 | const Test = { 24 | props: { 25 | classNames: { type: Object }, 26 | }, 27 | render: () => null, 28 | }; 29 | 30 | const wrapper = mount(Test, { 31 | propsData: { 32 | classNames: { 33 | 'ais-Test': 'dogs', 34 | 'ais-Test--ok': 'dogs cats', 35 | }, 36 | }, 37 | mixins: [createSuitMixin({ name: 'Test' })], 38 | }); 39 | 40 | const { suit } = wrapper.vm; 41 | 42 | expect(suit()).toMatchInlineSnapshot(`"ais-Test dogs"`); 43 | expect(suit('', 'ok')).toMatchInlineSnapshot(`"ais-Test--ok dogs cats"`); 44 | expect(suit('ok')).toMatchInlineSnapshot(`"ais-Test-ok"`); 45 | expect(suit('ok', 'there')).toMatchInlineSnapshot(`"ais-Test-ok--there"`); 46 | }); 47 | -------------------------------------------------------------------------------- /src/mixins/panel.js: -------------------------------------------------------------------------------- 1 | import { isVue3 } from '../util/vue-compat'; 2 | import mitt from 'mitt'; 3 | 4 | export const PANEL_EMITTER_NAMESPACE = 'instantSearchPanelEmitter'; 5 | export const PANEL_CHANGE_EVENT = 'PANEL_CHANGE_EVENT'; 6 | 7 | export const createPanelProviderMixin = () => ({ 8 | props: { 9 | emitter: { 10 | type: Object, 11 | required: false, 12 | default() { 13 | return mitt(); 14 | }, 15 | }, 16 | }, 17 | provide() { 18 | return { 19 | [PANEL_EMITTER_NAMESPACE]: this.emitter, 20 | }; 21 | }, 22 | data() { 23 | return { 24 | canRefine: true, 25 | }; 26 | }, 27 | created() { 28 | this.emitter.on(PANEL_CHANGE_EVENT, value => { 29 | this.updateCanRefine(value); 30 | }); 31 | }, 32 | [isVue3 ? 'beforeUnmount' : 'beforeDestroy']() { 33 | this.emitter.all.clear(); 34 | }, 35 | methods: { 36 | updateCanRefine(value) { 37 | this.canRefine = value; 38 | }, 39 | }, 40 | }); 41 | 42 | export const createPanelConsumerMixin = ({ 43 | mapStateToCanRefine = state => Boolean(state.canRefine), 44 | } = {}) => ({ 45 | inject: { 46 | emitter: { 47 | from: PANEL_EMITTER_NAMESPACE, 48 | default() { 49 | return { 50 | emit: () => {}, 51 | }; 52 | }, 53 | }, 54 | }, 55 | data() { 56 | return { 57 | state: null, 58 | hasAlreadyEmitted: false, 59 | }; 60 | }, 61 | watch: { 62 | state: { 63 | immediate: true, 64 | handler(nextState, previousState) { 65 | if (!nextState) { 66 | return; 67 | } 68 | 69 | const previousCanRefine = mapStateToCanRefine(previousState || {}); 70 | const nextCanRefine = mapStateToCanRefine(nextState); 71 | 72 | if (!this.hasAlreadyEmitted || previousCanRefine !== nextCanRefine) { 73 | this.emitter.emit(PANEL_CHANGE_EVENT, nextCanRefine); 74 | this.hasAlreadyEmitted = true; 75 | } 76 | }, 77 | }, 78 | }, 79 | }); 80 | -------------------------------------------------------------------------------- /src/mixins/suit.js: -------------------------------------------------------------------------------- 1 | import suit from '../util/suit'; 2 | 3 | export const createSuitMixin = ({ name }) => ({ 4 | props: { 5 | classNames: { 6 | type: Object, 7 | default: undefined, 8 | }, 9 | }, 10 | methods: { 11 | suit(element, modifier) { 12 | const className = suit(name, element, modifier); 13 | const userClassName = this.classNames && this.classNames[className]; 14 | if (userClassName) { 15 | return [className, userClassName].join(' '); 16 | } 17 | return className; 18 | }, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /src/mixins/widget.js: -------------------------------------------------------------------------------- 1 | import { _objectSpread } from '../util/polyfills'; 2 | import { isVue3 } from '../util/vue-compat'; 3 | import { warn } from '../util/warn'; 4 | 5 | export const createWidgetMixin = ( 6 | { connector } = {}, 7 | additionalProperties = {} 8 | ) => ({ 9 | inject: { 10 | instantSearchInstance: { 11 | from: '$_ais_instantSearchInstance', 12 | default() { 13 | const tag = this.$options._componentTag; 14 | throw new TypeError( 15 | `It looks like you forgot to wrap your Algolia search component "<${tag}>" inside of an "49 |68 |50 | status: recognizing 51 |
52 |53 | errorCode: 54 |
55 |56 | isListening: true 57 |
58 |59 | transcript: Hello 60 |
61 |62 | isSpeechFinal: false 63 |
64 |65 | isBrowserSupported: true 66 |
67 |" component.` 16 | ); 17 | }, 18 | }, 19 | getParentIndex: { 20 | from: '$_ais_getParentIndex', 21 | default() { 22 | return () => this.instantSearchInstance.mainIndex; 23 | }, 24 | }, 25 | }, 26 | data() { 27 | return { 28 | state: null, 29 | }; 30 | }, 31 | created() { 32 | if (typeof connector === 'function') { 33 | this.factory = connector(this.updateState, () => {}); 34 | this.widget = _objectSpread( 35 | this.factory(this.widgetParams), 36 | additionalProperties 37 | ); 38 | this.getParentIndex().addWidgets([this.widget]); 39 | 40 | if ( 41 | this.instantSearchInstance._initialResults && 42 | !this.instantSearchInstance.started 43 | ) { 44 | if (typeof this.instantSearchInstance.__forceRender !== 'function') { 45 | throw new Error( 46 | 'You are using server side rendering with instead of .' 47 | ); 48 | } 49 | this.instantSearchInstance.__forceRender( 50 | this.widget, 51 | this.getParentIndex() 52 | ); 53 | } 54 | } else if (connector !== true) { 55 | warn( 56 | `You are using the InstantSearch widget mixin, but didn't provide a connector. 57 | While this is technically possible, and will give you access to the Helper, 58 | it's not the recommended way of making custom components. 59 | 60 | If you want to disable this message, pass { connector: true } to the mixin. 61 | 62 | Read more on using connectors: https://alg.li/vue-custom` 63 | ); 64 | } 65 | }, 66 | [isVue3 ? 'beforeUnmount' : 'beforeDestroy']() { 67 | if (this.widget) { 68 | this.getParentIndex().removeWidgets([this.widget]); 69 | } 70 | }, 71 | watch: { 72 | widgetParams: { 73 | handler(nextWidgetParams) { 74 | this.state = null; 75 | this.getParentIndex().removeWidgets([this.widget]); 76 | this.widget = _objectSpread( 77 | this.factory(nextWidgetParams), 78 | additionalProperties 79 | ); 80 | this.getParentIndex().addWidgets([this.widget]); 81 | }, 82 | deep: true, 83 | }, 84 | }, 85 | methods: { 86 | updateState(state = {}, isFirstRender) { 87 | if (!isFirstRender) { 88 | // Avoid updating the state on first render 89 | // otherwise there will be a flash of placeholder data 90 | this.state = state; 91 | } 92 | }, 93 | }, 94 | }); 95 | -------------------------------------------------------------------------------- /src/plugin.js: -------------------------------------------------------------------------------- 1 | /* eslint import/namespace: ['error', { allowComputed: true }]*/ 2 | 3 | import * as widgets from './widgets'; 4 | 5 | export const plugin = { 6 | install(localVue) { 7 | Object.keys(widgets).forEach(widgetName => { 8 | localVue.component(widgets[widgetName].name, widgets[widgetName]); 9 | }); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/util/__tests__/suit.test.js: -------------------------------------------------------------------------------- 1 | import suit from '../suit'; 2 | 3 | it('expect to return "ais-Widget"', () => { 4 | const name = 'Widget'; 5 | 6 | const expectation = 'ais-Widget'; 7 | const actual = suit(name); 8 | 9 | expect(actual).toBe(expectation); 10 | }); 11 | 12 | it('expect to return "ais-Widget--modifier"', () => { 13 | const name = 'Widget'; 14 | const modifier = 'modifier'; 15 | 16 | const expectation = 'ais-Widget--modifier'; 17 | const actual = suit(name, '', modifier); 18 | 19 | expect(actual).toBe(expectation); 20 | }); 21 | 22 | it('expect to return "ais-Widget-element"', () => { 23 | const name = 'Widget'; 24 | const element = 'element'; 25 | 26 | const expectation = 'ais-Widget-element'; 27 | const actual = suit(name, element); 28 | 29 | expect(actual).toBe(expectation); 30 | }); 31 | 32 | it('expect to return "ais-Widget-element--modifier"', () => { 33 | const name = 'Widget'; 34 | const element = 'element'; 35 | const modifier = 'modifier'; 36 | 37 | const expectation = 'ais-Widget-element--modifier'; 38 | const actual = suit(name, element, modifier); 39 | 40 | expect(actual).toBe(expectation); 41 | }); 42 | 43 | it('expect to throw when widget is not provided', () => { 44 | expect(() => suit()).toThrow('You need to provide `widgetName` in your data'); 45 | }); 46 | -------------------------------------------------------------------------------- /src/util/__tests__/unescape.test.js: -------------------------------------------------------------------------------- 1 | import { unescape } from '../unescape'; 2 | 3 | describe('unescape', () => { 4 | it('unescapes value', () => { 5 | expect(unescape('fred, barney, & pebbles')).toBe( 6 | 'fred, barney, & pebbles' 7 | ); 8 | 9 | expect(unescape('&<>"'/')).toEqual('&<>"\'/'); 10 | }); 11 | 12 | it('handles strings with nothing to unescape', () => { 13 | expect(unescape('abc')).toEqual('abc'); 14 | }); 15 | 16 | it('does not unescape the "`" character', () => { 17 | expect(unescape('`')).toEqual('`'); 18 | }); 19 | 20 | it('does not unescape the "/" character', () => { 21 | expect(unescape('/')).toEqual('/'); 22 | }); 23 | 24 | it('handles strings with tags', () => { 25 | expect(unescape('TV & Home Theater')).toBe( 26 | 'TV & Home Theater' 27 | ); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/util/__tests__/warn.test.js: -------------------------------------------------------------------------------- 1 | import { warn } from '../warn'; 2 | const noop = () => {}; 3 | 4 | it('calls console.warn first time', () => { 5 | const spy = jest.spyOn(console, 'warn').mockImplementation(noop); 6 | warn('hello this is my warning'); 7 | expect(spy).toHaveBeenCalledTimes(1); 8 | }); 9 | 10 | it("doesn't call console.warn second time", () => { 11 | const spy = jest.spyOn(console, 'warn').mockImplementation(noop); 12 | warn('hello this is my warning'); 13 | warn('hello this is my warning'); 14 | expect(spy).toHaveBeenCalledTimes(1); 15 | }); 16 | 17 | it('calls console.warn for each message', () => { 18 | const spy = jest.spyOn(console, 'warn').mockImplementation(noop); 19 | warn('hello this is my warning'); 20 | warn('hello this is my other warning'); 21 | expect(spy).toHaveBeenCalledTimes(2); 22 | }); 23 | 24 | it('calls console.warn for each message once', () => { 25 | const spy = jest.spyOn(console, 'warn').mockImplementation(noop); 26 | warn('hello this is my warning'); 27 | warn('hello this is my other warning'); 28 | warn('hello this is my other warning'); 29 | warn('hello this is my other warning'); 30 | expect(spy).toHaveBeenCalledTimes(2); 31 | }); 32 | -------------------------------------------------------------------------------- /src/util/polyfills.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | // source: @babel/plugin-proposal-object-rest-spread@7.2.0 4 | // prettier-ignore 5 | export function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; } 6 | 7 | // source: @babel/plugin-proposal-object-rest-spread@7.2.0 8 | // prettier-ignore 9 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 10 | -------------------------------------------------------------------------------- /src/util/suit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create class names like ais-widgetName-element--modifier 3 | * 4 | * @param {string} widgetName first part 5 | * @param {string} element part separated by - 6 | * @param {string} modifier final part, separated by -- 7 | * 8 | * @returns {string} the composed class name 9 | */ 10 | export default function suit(widgetName, element, modifier) { 11 | if (!widgetName) { 12 | throw new Error('You need to provide `widgetName` in your data'); 13 | } 14 | 15 | const elements = [`ais-${widgetName}`]; 16 | 17 | if (element) { 18 | elements.push(`-${element}`); 19 | } 20 | 21 | if (modifier) { 22 | elements.push(`--${modifier}`); 23 | } 24 | 25 | return elements.join(''); 26 | } 27 | -------------------------------------------------------------------------------- /src/util/testutils/client.js: -------------------------------------------------------------------------------- 1 | export const createFakeClient = () => ({ 2 | search: jest.fn(requests => 3 | Promise.resolve({ 4 | results: requests.map(({ params: { query } }) => ({ query })), 5 | }) 6 | ), 7 | }); 8 | -------------------------------------------------------------------------------- /src/util/testutils/helper.js: -------------------------------------------------------------------------------- 1 | export const createSerializedState = () => ({ 2 | results: [ 3 | { 4 | hits: [ 5 | { 6 | objectID: 'doggos', 7 | name: 'the dog', 8 | }, 9 | ], 10 | nbHits: 1071, 11 | page: 0, 12 | nbPages: 200, 13 | hitsPerPage: 5, 14 | processingTimeMS: 3, 15 | facets: { 16 | genre: { 17 | Comedy: 1071, 18 | Drama: 290, 19 | Romance: 202, 20 | }, 21 | }, 22 | exhaustiveFacetsCount: true, 23 | exhaustiveNbHits: true, 24 | query: 'hi', 25 | queryAfterRemoval: 'hi', 26 | params: 27 | 'query=hi&hitsPerPage=5&page=0&highlightPreTag=__ais-highlight__&highlightPostTag=__%2Fais-highlight__&facets=%5B%22genre%22%5D&tagFilters=&facetFilters=%5B%5B%22genre%3AComedy%22%5D%5D', 28 | index: 'movies', 29 | }, 30 | { 31 | hits: [{ objectID: 'doggos' }], 32 | nbHits: 5131, 33 | page: 0, 34 | nbPages: 1000, 35 | hitsPerPage: 1, 36 | processingTimeMS: 7, 37 | facets: { 38 | genre: { 39 | Comedy: 1071, 40 | Drama: 1642, 41 | Romance: 474, 42 | }, 43 | }, 44 | exhaustiveFacetsCount: true, 45 | exhaustiveNbHits: true, 46 | query: 'hi', 47 | queryAfterRemoval: 'hi', 48 | params: 49 | 'query=hi&hitsPerPage=1&page=0&highlightPreTag=__ais-highlight__&highlightPostTag=__%2Fais-highlight__&attributesToRetrieve=%5B%5D&attributesToHighlight=%5B%5D&attributesToSnippet=%5B%5D&tagFilters=&analytics=false&clickAnalytics=false&facets=genre', 50 | index: 'movies', 51 | }, 52 | ], 53 | state: { 54 | index: 'movies', 55 | query: 'hi', 56 | facets: [], 57 | disjunctiveFacets: ['genre'], 58 | hierarchicalFacets: [], 59 | facetsRefinements: {}, 60 | facetsExcludes: {}, 61 | disjunctiveFacetsRefinements: { genre: ['Comedy'] }, 62 | numericRefinements: {}, 63 | tagRefinements: [], 64 | hierarchicalFacetsRefinements: {}, 65 | hitsPerPage: 5, 66 | page: 0, 67 | highlightPreTag: '__ais-highlight__', 68 | highlightPostTag: '__/ais-highlight__', 69 | }, 70 | }); 71 | -------------------------------------------------------------------------------- /src/util/unescape.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This implementation is taken from Lodash implementation. 3 | * See: https://github.com/lodash/lodash/blob/4.17.11-npm/unescape.js 4 | */ 5 | 6 | /** Used to map HTML entities to characters. */ 7 | const htmlUnescapes = { 8 | '&': '&', 9 | '<': '<', 10 | '>': '>', 11 | '"': '"', 12 | ''': "'", 13 | }; 14 | 15 | /** Used to match HTML entities and HTML characters. */ 16 | const reEscapedHtml = /&(?:amp|lt|gt|quot|#39);/g; 17 | const reHasEscapedHtml = RegExp(reEscapedHtml.source); 18 | 19 | /** 20 | * The inverse of `_.escape`; this method converts the HTML entities 21 | * `&`, `<`, `>`, `"`, and `'` in `string` to 22 | * their corresponding characters. 23 | * 24 | * **Note:** No other HTML entities are unescaped. To unescape additional 25 | * HTML entities use a third-party library like [_he_](https://mths.be/he). 26 | * 27 | * @static 28 | * @memberOf _ 29 | * @since 0.6.0 30 | * @category String 31 | * @param {string} [string=''] The string to unescape. 32 | * @returns {string} Returns the unescaped string. 33 | * @example 34 | * 35 | * _.unescape('fred, barney, & pebbles'); 36 | * // => 'fred, barney, & pebbles' 37 | */ 38 | export function unescape(string) { 39 | return string && reHasEscapedHtml.test(string) 40 | ? string.replace(reEscapedHtml, character => htmlUnescapes[character]) 41 | : string; 42 | } 43 | -------------------------------------------------------------------------------- /src/util/vue-compat/index-vue2.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | const isVue2 = true; 4 | const isVue3 = false; 5 | const Vue2 = Vue; 6 | const version = Vue.version; 7 | 8 | export { Vue, Vue2, isVue2, isVue3, version }; 9 | 10 | export function renderCompat(fn) { 11 | return function(createElement) { 12 | return fn.call(this, createElement); 13 | }; 14 | } 15 | 16 | export function getDefaultSlot(component) { 17 | return component.$slots.default; 18 | } 19 | 20 | // Vue3-only APIs 21 | export const computed = undefined; 22 | export const createApp = undefined; 23 | export const createSSRApp = undefined; 24 | export const createRef = undefined; 25 | export const customRef = undefined; 26 | export const defineAsyncComponent = undefined; 27 | export const defineComponent = undefined; 28 | export const del = undefined; 29 | export const getCurrentInstance = undefined; 30 | export const h = undefined; 31 | export const inject = undefined; 32 | export const isRaw = undefined; 33 | export const isReactive = undefined; 34 | export const isReadonly = undefined; 35 | export const isRef = undefined; 36 | export const markRaw = undefined; 37 | export const nextTick = undefined; 38 | export const onActivated = undefined; 39 | export const onBeforeMount = undefined; 40 | export const onBeforeUnmount = undefined; 41 | export const onBeforeUpdate = undefined; 42 | export const onDeactivated = undefined; 43 | export const onErrorCaptured = undefined; 44 | export const onMounted = undefined; 45 | export const onServerPrefetch = undefined; 46 | export const onUnmounted = undefined; 47 | export const onUpdated = undefined; 48 | export const provide = undefined; 49 | export const proxyRefs = undefined; 50 | export const reactive = undefined; 51 | export const readonly = undefined; 52 | export const ref = undefined; 53 | export const set = undefined; 54 | export const shallowReactive = undefined; 55 | export const shallowReadonly = undefined; 56 | export const shallowRef = undefined; 57 | export const toRaw = undefined; 58 | export const toRef = undefined; 59 | export const toRefs = undefined; 60 | export const triggerRef = undefined; 61 | export const unref = undefined; 62 | export const useCSSModule = undefined; 63 | export const useCssModule = undefined; 64 | export const warn = undefined; 65 | export const watch = undefined; 66 | export const watchEffect = undefined; 67 | -------------------------------------------------------------------------------- /src/util/vue-compat/index-vue3.js: -------------------------------------------------------------------------------- 1 | import * as Vue from 'vue'; 2 | 3 | const isVue2 = false; 4 | const isVue3 = true; 5 | const Vue2 = undefined; 6 | 7 | export { createApp, createSSRApp, h, version, nextTick } from 'vue'; 8 | export { Vue, Vue2, isVue2, isVue3 }; 9 | 10 | export function renderCompat(fn) { 11 | function h(tag, props, children) { 12 | if (typeof props === 'object' && (props.attrs || props.props)) { 13 | // In vue 3, we no longer wrap with `attrs` or `props` key. 14 | const flatProps = Object.assign({}, props, props.attrs, props.props); 15 | delete flatProps.attrs; 16 | delete flatProps.props; 17 | 18 | return Vue.h(tag, flatProps, children); 19 | } 20 | 21 | return Vue.h(tag, props, children); 22 | } 23 | 24 | return function() { 25 | return fn.call(this, h); 26 | }; 27 | } 28 | 29 | export function getDefaultSlot(component) { 30 | return component.$slots.default && component.$slots.default(); 31 | } 32 | -------------------------------------------------------------------------------- /src/util/vue-compat/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | By default, we maintain this repository based on Vue 2. 3 | That's why this file is exporting from `index-vue2`, 4 | which includes all the variables and methods for Vue 2. 5 | When `scripts/build-vue3.sh` runs, it will replace with 6 | > export * from './index-vue3'; 7 | and revert it back after finished. 8 | */ 9 | export * from './index-vue2'; 10 | -------------------------------------------------------------------------------- /src/util/warn.js: -------------------------------------------------------------------------------- 1 | const cache = new Set(); 2 | 3 | export function warn(message) { 4 | if (cache.has(message)) return; 5 | cache.add(message); 6 | // eslint-disable-next-line no-console 7 | console.warn(message); 8 | } 9 | -------------------------------------------------------------------------------- /src/widgets.js: -------------------------------------------------------------------------------- 1 | export { default as AisAutocomplete } from './components/Autocomplete.vue'; 2 | export { default as AisBreadcrumb } from './components/Breadcrumb.vue'; 3 | export { 4 | default as AisClearRefinements, 5 | } from './components/ClearRefinements.vue'; 6 | export { default as AisConfigure } from './components/Configure'; 7 | export { 8 | default as AisExperimentalConfigureRelatedItems, 9 | } from './components/ConfigureRelatedItems'; 10 | export { 11 | default as AisCurrentRefinements, 12 | } from './components/CurrentRefinements.vue'; 13 | export { 14 | default as AisHierarchicalMenu, 15 | } from './components/HierarchicalMenu.vue'; 16 | export { default as AisHighlight } from './components/Highlight.vue'; 17 | export { default as AisHits } from './components/Hits.vue'; 18 | export { default as AisHitsPerPage } from './components/HitsPerPage.vue'; 19 | export { default as AisIndex } from './components/Index'; 20 | export { default as AisInstantSearch } from './components/InstantSearch'; 21 | export { default as AisInstantSearchSsr } from './components/InstantSearchSsr'; 22 | export { default as AisInfiniteHits } from './components/InfiniteHits.vue'; 23 | export { default as AisMenu } from './components/Menu.vue'; 24 | export { default as AisMenuSelect } from './components/MenuSelect.vue'; 25 | export { default as AisNumericMenu } from './components/NumericMenu.vue'; 26 | export { default as AisPagination } from './components/Pagination.vue'; 27 | export { default as AisPanel } from './components/Panel.vue'; 28 | export { default as AisPoweredBy } from './components/PoweredBy.vue'; 29 | export { default as AisQueryRuleContext } from './components/QueryRuleContext'; 30 | export { 31 | default as AisQueryRuleCustomData, 32 | } from './components/QueryRuleCustomData.vue'; 33 | export { default as AisRangeInput } from './components/RangeInput.vue'; 34 | export { default as AisRatingMenu } from './components/RatingMenu.vue'; 35 | export { default as AisRefinementList } from './components/RefinementList.vue'; 36 | export { default as AisStateResults } from './components/StateResults.vue'; 37 | export { default as AisSearchBox } from './components/SearchBox.vue'; 38 | export { default as AisSnippet } from './components/Snippet.vue'; 39 | export { default as AisSortBy } from './components/SortBy.vue'; 40 | export { default as AisStats } from './components/Stats.vue'; 41 | export { 42 | default as AisToggleRefinement, 43 | } from './components/ToggleRefinement.vue'; 44 | export { default as AisVoiceSearch } from './components/VoiceSearch.vue'; 45 | export { default as AisRelevantSort } from './components/RelevantSort.vue'; 46 | export { default as AisDynamicWidgets } from './components/DynamicWidgets'; 47 | export { 48 | default as AisExperimentalDynamicWidgets, 49 | } from './components/ExperimentalDynamicWidgets'; 50 | -------------------------------------------------------------------------------- /stories/ClearRefinements.stories.js: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/vue'; 2 | import { previewWrapper } from './utils'; 3 | 4 | storiesOf('ais-clear-refinements', module) 5 | .addDecorator(previewWrapper()) 6 | .add('default', () => ({ 7 | template: ` 8 | 9 | `, 10 | })) 11 | .add('also clearing query', () => ({ 12 | template: ` 13 | 14 |20 | `, 21 | })) 22 | .add('not clearing "brand"', () => ({ 23 | template: ` 24 |15 |17 | 18 | TIP: type something first 19 |16 | 25 |39 | `, 40 | })) 41 | .add('with a custom label', () => ({ 42 | template: ` 43 |26 |38 |27 |
28 |35 |
36 |37 | 44 | Remove the refinements 45 | 46 | `, 47 | })) 48 | .add('with a custom render', () => ({ 49 | template: ` 50 |51 | 52 | 58 | 59 | 60 | `, 61 | })) 62 | .add('with a Panel', () => ({ 63 | template: ` 64 |65 | Clear refinements 66 | 69 | `, 70 | })); 71 | -------------------------------------------------------------------------------- /stories/Configure.stories.js: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/vue'; 2 | import { previewWrapper } from './utils'; 3 | import { withKnobs, object } from '@storybook/addon-knobs/vue'; 4 | 5 | storiesOf('ais-configure', module) 6 | .addDecorator( 7 | previewWrapper({ 8 | filters: '67 | Footer 68 | ', 9 | }) 10 | ) 11 | .addDecorator(withKnobs) 12 | .add('default', () => ({ 13 | template: ` 14 | 15 | `, 16 | })) 17 | .add('with 1 hit per page', () => ({ 18 | template: ` 19 | 20 | `, 21 | })) 22 | .add('with 1 hit per page (kebab)', () => ({ 23 | template: ` 24 | 25 | `, 26 | })) 27 | .add('external toggler', () => ({ 28 | template: ` 29 | 30 |33 | `, 34 | data() { 35 | return { hitsPerPage: 1 }; 36 | }, 37 | methods: { 38 | toggleHitsPerPage() { 39 | this.hitsPerPage = this.hitsPerPage === 1 ? 5 : 1; 40 | }, 41 | }, 42 | })) 43 | .add('inline toggler', () => ({ 44 | template: ` 45 |31 | 32 | 46 | 47 | 53 | `, 54 | })) 55 | .add('with display of the parameters', () => ({ 56 | template: ` 57 |{{JSON.stringify(searchParameters, null, 2)}}48 | 51 | 52 |58 | 59 | 62 | `, 63 | })) 64 | .add('merging parameters', () => ({ 65 | template: ` 66 |{{ searchParameters }}60 | 61 |67 | 68 | 79 | currently applied filters: 82 | `, 83 | })) 84 | .add('playground', () => ({ 85 | template: ` 86 |{{searchParameters}}80 | 81 |87 | 88 | 91 | `, 92 | data() { 93 | return { 94 | knobs: object('search parameters', { 95 | hitsPerPage: 1, 96 | }), 97 | }; 98 | }, 99 | })); 100 | -------------------------------------------------------------------------------- /stories/DynamicWidgets.stories.js: -------------------------------------------------------------------------------- 1 | import { previewWrapper } from './utils'; 2 | import { storiesOf } from '@storybook/vue'; 3 | 4 | storiesOf('ais-dynamic-widgets', module) 5 | .addDecorator(previewWrapper()) 6 | .add('simple usage', () => ({ 7 | template: ` 8 |{{ searchParameters }}89 | 90 |9 | `, 16 | data() { 17 | return { 18 | hierarchicalCategories: [ 19 | 'hierarchicalCategories.lvl0', 20 | 'hierarchicalCategories.lvl1', 21 | 'hierarchicalCategories.lvl2', 22 | ], 23 | }; 24 | }, 25 | })); 26 | -------------------------------------------------------------------------------- /stories/Highlight.stories.js: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/vue'; 2 | import { previewWrapper } from './utils'; 3 | 4 | storiesOf('ais-highlight', module) 5 | .addDecorator(previewWrapper()) 6 | .add('default', () => ({ 7 | template: ` 8 |10 | 11 | 12 | hierarchy 13 | 15 |14 | 9 |18 | `, 19 | })) 20 | .add('with array value', () => ({ 21 | template: ` 22 |10 | 11 | 17 |12 |15 | 16 |13 |
14 | 23 |33 | `, 34 | })) 35 | .add('with highlighted tag name', () => ({ 36 | template: ` 37 |24 | 25 | 32 |26 |30 | 31 |27 |
28 | 29 |38 |51 | `, 52 | })); 53 | -------------------------------------------------------------------------------- /stories/HitsPerPage.stories.js: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/vue'; 2 | import { previewWrapper } from './utils'; 3 | 4 | storiesOf('ais-hits-per-page', module) 5 | .addDecorator(previewWrapper()) 6 | .add('default', () => ({ 7 | template: ` 8 |39 | 40 | 50 |41 |48 | 49 |42 |
44 | 45 |43 | 46 | 47 | 14 | `, 15 | })) 16 | .add('with different default', () => ({ 17 | template: ` 18 | 24 | `, 25 | })) 26 | .add('with transform items', () => ({ 27 | template: ` 28 | 35 | `, 36 | methods: { 37 | transformItems(items) { 38 | return items.map(item => 39 | Object.assign({}, item, { 40 | label: item.label.toUpperCase(), 41 | }) 42 | ); 43 | }, 44 | }, 45 | })) 46 | .add('with a custom render', () => ({ 47 | template: ` 48 | 54 | 55 | `, 69 | })) 70 | .add('with a Panel', () => ({ 71 | template: ` 72 |56 | 66 |67 | 68 |73 | Hits per page 74 | 82 | `, 83 | })); 84 | -------------------------------------------------------------------------------- /stories/Index.stories.js: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/vue'; 2 | import algoliasearch from 'algoliasearch'; 3 | 4 | storiesOf('ais-index', module) 5 | .add('default', () => ({ 6 | template: ` 7 |80 | Footer 81 | 8 |28 | `, 29 | data() { 30 | return { 31 | searchClient: algoliasearch( 32 | 'latency', 33 | '6be0576ff61c053d5f9a3225e2a90f76' 34 | ), 35 | }; 36 | }, 37 | })) 38 | .add('shared and individual widgets', () => ({ 39 | template: ` 40 |9 | 27 |10 | 11 | 12 |18 |13 | 14 | 17 |15 | 16 |
19 |20 | 26 |21 | 22 | 25 |23 | 24 |
41 |67 | `, 68 | data() { 69 | return { 70 | searchClient: algoliasearch( 71 | 'latency', 72 | '6be0576ff61c053d5f9a3225e2a90f76' 73 | ), 74 | }; 75 | }, 76 | })); 77 | -------------------------------------------------------------------------------- /stories/MemoryRouter.js: -------------------------------------------------------------------------------- 1 | export class MemoryRouter { 2 | constructor(initialState = {}) { 3 | this._memoryState = initialState; 4 | } 5 | write(routeState) { 6 | this._memoryState = routeState; 7 | } 8 | read() { 9 | return this._memoryState; 10 | } 11 | createURL() { 12 | return ''; 13 | } 14 | onUpdate() { 15 | return {}; 16 | } 17 | dispose() {} 18 | } 19 | -------------------------------------------------------------------------------- /stories/MenuSelect.stories.js: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/vue'; 2 | import { previewWrapper } from './utils'; 3 | 4 | storiesOf('ais-menu-select', module) 5 | .addDecorator(previewWrapper()) 6 | .add('default', () => ({ 7 | template: ` 8 |42 | 66 |43 | 44 |
45 |46 | 50 | 51 | 52 | 55 |53 | 54 |
56 |57 | 64 |58 | 59 | 60 | 63 |61 | 62 |
65 | 9 | `, 10 | })) 11 | .add('with a limit', () => ({ 12 | template: ` 13 | 17 | `, 18 | })) 19 | .add('with a custom sort', () => ({ 20 | template: ` 21 | 25 | `, 26 | })) 27 | .add('with a custom label', () => ({ 28 | template: ` 29 | 33 | `, 34 | })) 35 | .add('with a custom item slot', () => ({ 36 | template: ` 37 | 38 | 39 | {{ item.label }} 40 | 41 | 42 | `, 43 | })) 44 | .add('with transform items', () => ({ 45 | template: ` 46 |51 | `, 52 | methods: { 53 | transformItems(items) { 54 | return items.map(item => 55 | Object.assign({}, item, { 56 | label: item.label.toUpperCase(), 57 | }) 58 | ); 59 | }, 60 | }, 61 | })) 62 | .add('with a custom rendering', () => ({ 63 | template: ` 64 | 65 | 66 | 82 | 83 | 84 | `, 85 | })) 86 | .add('with a Panel', () => ({ 87 | template: ` 88 |89 | Menu Select 90 | 93 | `, 94 | })); 95 | -------------------------------------------------------------------------------- /stories/NumericMenu.stories.js: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/vue'; 2 | import { previewWrapper } from './utils'; 3 | 4 | storiesOf('ais-numeric-menu', module) 5 | .addDecorator(previewWrapper()) 6 | .add('default', () => ({ 7 | template: ` 8 |91 | Footer 92 | 18 | `, 19 | })) 20 | .add('with transform items', () => ({ 21 | template: ` 22 | 33 | `, 34 | methods: { 35 | transformItems(items) { 36 | return items.map(item => 37 | Object.assign({}, item, { label: `👉 ${item.label}` }) 38 | ); 39 | }, 40 | }, 41 | })) 42 | .add('with a custom render', () => ({ 43 | template: ` 44 | 54 | 55 | 71 | `, 72 | })) 73 | .add('with a Panel', () => ({ 74 | template: ` 75 |56 |
69 | 70 |- 61 | 65 | {{ item.label }} 66 | 67 |
68 |76 | Numeric Menu 77 | 89 | `, 90 | })); 91 | -------------------------------------------------------------------------------- /stories/Pagination.stories.js: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/vue'; 2 | import { previewWrapper } from './utils'; 3 | 4 | storiesOf('ais-pagination', module) 5 | .addDecorator(previewWrapper()) 6 | .add('default', () => ({ 7 | template: ` 8 |87 | Footer 88 | 9 | `, 10 | })) 11 | .add('with a padding', () => ({ 12 | template: ` 13 | 14 | `, 15 | })) 16 | .add('with a total pages', () => ({ 17 | template: ` 18 | 19 | `, 20 | })) 21 | .add('complete custom rendering', () => ({ 22 | template: ` 23 | 27 | 34 | 43 | 44 | `, 45 | })) 46 | .add('with named slots', () => ({ 47 | template: ` 48 |49 | 50 | 56 | 57 | 58 | 64 | 65 | 66 | 67 | 72 | {{page}} 73 | 74 | 75 | 76 | 82 | 83 | 84 | 90 | 91 | `, 92 | })) 93 | .add('with a Panel', () => ({ 94 | template: ` 95 |96 | Pagination 97 | 100 | `, 101 | })); 102 | -------------------------------------------------------------------------------- /stories/Panel.stories.js: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/vue'; 2 | import { previewWrapper } from './utils'; 3 | 4 | storiesOf('ais-panel', module) 5 | .addDecorator(previewWrapper()) 6 | .add('default', () => ({ 7 | template: ` 8 |98 | Footer 99 | 9 | Brand 10 | 12 | `, 13 | })) 14 | .add('text content', () => ({ 15 | template: ` 16 |11 | 17 | This is the body of the Panel. 18 | 19 | `, 20 | })) 21 | .add('with header', () => ({ 22 | template: ` 23 |24 | Header 25 | This is the body of the Panel. 26 | 27 | `, 28 | })) 29 | .add('with footer', () => ({ 30 | template: ` 31 |32 | This is the body of the Panel. 33 | Footer 34 | 35 | `, 36 | })) 37 | .add('with header & footer', () => ({ 38 | template: ` 39 |40 | Header 41 | This is the body of the Panel. 42 | Footer 43 | 44 | `, 45 | })); 46 | -------------------------------------------------------------------------------- /stories/PoweredBy.stories.js: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/vue'; 2 | import { previewWrapper } from './utils'; 3 | 4 | storiesOf('ais-powered-by', module) 5 | .addDecorator(previewWrapper()) 6 | .add('default', () => ({ 7 | template: ` 8 |9 |`, 11 | })) 12 | .add('dark', () => ({ 13 | template: ` 14 |10 | 15 |`, 17 | })); 18 | -------------------------------------------------------------------------------- /stories/RatingMenu.stories.js: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/vue'; 2 | import { previewWrapper } from './utils'; 3 | 4 | storiesOf('ais-rating-menu', module) 5 | .addDecorator( 6 | previewWrapper({ 7 | indexName: 'instant_search_rating_asc', 8 | }) 9 | ) 10 | .add('default', () => ({ 11 | template: ` 12 |16 | 13 | `, 14 | })) 15 | .add('custom rendering', () => ({ 16 | template: ` 17 | 18 |`, 31 | })) 32 | .add('with a Panel', () => ({ 33 | template: ` 34 |19 | 20 | rating 21 | 30 |22 |
28 | 29 |- 23 | 26 |
27 |35 | Rating Menu 36 | 39 | `, 40 | })); 41 | -------------------------------------------------------------------------------- /stories/RefinementList.stories.js: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/vue'; 2 | import { previewWrapper } from './utils'; 3 | 4 | storiesOf('ais-refinement-list', module) 5 | .addDecorator(previewWrapper({ filters: '' })) 6 | .add('default', () => ({ 7 | template: ` 8 |37 | Footer 38 | 9 | `, 10 | })) 11 | .add('with searchbox', () => ({ 12 | template: ` 13 | 17 | `, 18 | })) 19 | .add('with show more', () => ({ 20 | template: ` 21 | 25 | `, 26 | })) 27 | .add('with transform items', () => ({ 28 | template: ` 29 | 33 | `, 34 | methods: { 35 | transformItems(items) { 36 | return items.map(item => 37 | Object.assign(item, { 38 | label: item.label.toLocaleUpperCase(), 39 | }) 40 | ); 41 | }, 42 | }, 43 | })) 44 | .add('item custom rendering', () => ({ 45 | template: ` 46 | 47 | 48 | 53 | 54 | `, 55 | })) 56 | .add('full custom rendering', () => ({ 57 | template: ` 58 |59 | 66 | 67 | 76 | 77 | 80 | 81 | `, 82 | })); 83 | -------------------------------------------------------------------------------- /stories/RelevantSort.stories.js: -------------------------------------------------------------------------------- 1 | import algoliasearch from 'algoliasearch/lite'; 2 | import { storiesOf } from '@storybook/vue'; 3 | import { previewWrapper } from './utils'; 4 | 5 | storiesOf('ais-relevant-sort', module) 6 | .addDecorator( 7 | previewWrapper({ 8 | searchClient: algoliasearch( 9 | 'C7RIRJRYR9', 10 | '77af6d5ffb27caa5ff4937099fcb92e8' 11 | ), 12 | indexName: 'test_Bestbuy_vr_price_asc', 13 | }) 14 | ) 15 | .add('default', () => ({ 16 | template: '', 17 | })) 18 | .add('with custom text', () => ({ 19 | template: ` 20 | 21 | 22 | 23 | We removed some search results to show you the most relevant ones 24 | 25 | 26 | Currently showing all results 27 | 28 | 29 | 30 | 31 | See all results 32 | 33 | 34 | See relevant results 35 | 36 | 37 | 38 | `, 39 | })); 40 | -------------------------------------------------------------------------------- /stories/SearchBox.stories.js: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/vue'; 2 | import { previewWrapper } from './utils'; 3 | 4 | storiesOf('ais-search-box', module) 5 | .addDecorator(previewWrapper()) 6 | .add('default', () => ({ 7 | template: '', 8 | })) 9 | .add('with loading indicator', () => ({ 10 | template: ' ', 11 | })) 12 | .add('with autofocus', () => ({ 13 | template: ' ', 14 | })) 15 | .add('with custom rendering', () => ({ 16 | template: ` 17 | 18 | 19 | 24 | 25 | 26 | `, 27 | })) 28 | .add('with custom rendering of icons', () => ({ 29 | template: ` 30 |31 | 32 | ❌ 33 | 34 | 35 | 🔎 36 | 37 | 38 | 🔄 39 | 40 | 41 | `, 42 | })) 43 | .add('with a Panel', () => ({ 44 | template: ` 45 |46 | SearchBox 47 | 50 | `, 51 | })); 52 | -------------------------------------------------------------------------------- /stories/Snippet.stories.js: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/vue'; 2 | import { previewWrapper } from './utils'; 3 | 4 | storiesOf('ais-snippet', module) 5 | .addDecorator(previewWrapper()) 6 | .add('default', () => ({ 7 | template: ` 8 |48 | Footer 49 | 9 |25 | `, 26 | })) 27 | .add('with highlighted tag name', () => ({ 28 | template: ` 29 |13 | 14 | 15 | 16 | 24 |17 |22 | 23 |18 | 19 |
20 | 21 | 30 |46 | `, 47 | })); 48 | -------------------------------------------------------------------------------- /stories/SortBy.stories.js: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/vue'; 2 | import { previewWrapper } from './utils'; 3 | 4 | storiesOf('ais-sort-by', module) 5 | .addDecorator(previewWrapper()) 6 | .add('default', () => ({ 7 | template: ` 8 |34 | 35 | 36 | 37 | 45 |38 |43 | 44 |39 | 40 |
41 | 42 | 15 | `, 16 | })) 17 | .add('with transform items', () => ({ 18 | template: ` 19 | 27 | `, 28 | methods: { 29 | transformItems(items) { 30 | return items.map(item => 31 | Object.assign({}, item, { 32 | label: item.label.toUpperCase(), 33 | }) 34 | ); 35 | }, 36 | }, 37 | })) 38 | .add('with custom render', () => ({ 39 | template: ` 40 | 47 | 48 | 57 | `, 58 | })) 59 | .add('with a Panel', () => ({ 60 | template: ` 61 |49 |
55 | 56 |- 50 | 53 |
54 |62 | Sort By 63 | 72 | `, 73 | })); 74 | -------------------------------------------------------------------------------- /stories/Stats.stories.js: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/vue'; 2 | import { previewWrapper } from './utils'; 3 | 4 | storiesOf('ais-stats', module) 5 | .addDecorator(previewWrapper()) 6 | .add('default', () => ({ 7 | template: ` 8 |70 | Footer 71 | 9 | `, 10 | })) 11 | .add('custom rendering', () => ({ 12 | template: ` 13 | 14 | 15 | {{nbHits}} hits computed, in {{processingTimeMS}}ms 😲 Woh! 16 | 17 | 18 | `, 19 | })) 20 | .add('with a Panel', () => ({ 21 | template: ` 22 |23 | Stats 24 | 27 | `, 28 | })); 29 | -------------------------------------------------------------------------------- /stories/ToggleRefinement.stories.js: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/vue'; 2 | import { previewWrapper } from './utils'; 3 | 4 | storiesOf('ais-toggle-refinement', module) 5 | .addDecorator(previewWrapper()) 6 | .add('default', () => ({ 7 | template: ` 8 |25 | Footer 26 | 12 | `, 13 | })) 14 | .add('with an on value', () => ({ 15 | template: ` 16 | 21 | `, 22 | })) 23 | .add('with an on value (with multiple values)', () => ({ 24 | template: ` 25 | 30 | `, 31 | })) 32 | .add('with an off value', () => ({ 33 | template: ` 34 | 39 | `, 40 | })) 41 | .add('with a custom render', () => ({ 42 | template: ` 43 | 47 | 48 | 49 | {{ value.name }} 50 | {{ value.isRefined ? '(is enabled)' : '(is disabled)' }} 51 | 52 | 53 | 54 | `, 55 | })) 56 | .add('with a Panel', () => ({ 57 | template: ` 58 |59 | Toggle Refinement 60 | 66 | `, 67 | })); 68 | -------------------------------------------------------------------------------- /stories/__Template.stories.js: -------------------------------------------------------------------------------- 1 | // This component is not exported, so it's just an example! 2 | 3 | // import { previewWrapper } from './utils'; 4 | // import { storiesOf } from '@storybook/vue'; 5 | 6 | // storiesOf('__Template', module) 7 | // .addDecorator(previewWrapper()) 8 | // .add('simple usage', () => ({ 9 | // template: `64 | Footer 65 | `, 10 | // })) 11 | // .add('clearing query', () => ({ 12 | // template: ` 13 | //`, 15 | // })) 16 | // .add('custom rendering', () => ({ 17 | // template: `14 | // 18 | // `, 22 | // })); 23 | -------------------------------------------------------------------------------- /stories/utils.js: -------------------------------------------------------------------------------- 1 | import algoliasearch from 'algoliasearch/lite'; 2 | 3 | export const previewWrapper = ({ 4 | searchClient = algoliasearch('latency', '6be0576ff61c053d5f9a3225e2a90f76'), 5 | insightsClient, 6 | indexName = 'instant_search', 7 | hits = ` 8 | 9 |19 | // Clear search query 20 | //21 | //10 |
28 | 29 | `, 30 | filters = ` 31 |- 15 | 19 |
27 |20 |26 |21 |
23 |22 | Rating: {{ item.rating }}✭
24 |Price: {{ item.price }}$
25 |32 | 33 | `, 34 | routing, 35 | } = {}) => () => ({ 36 | template: ` 37 | 43 | 61 | `, 62 | data() { 63 | return { 64 | searchClient, 65 | routing, 66 | insightsClient, 67 | }; 68 | }, 69 | }); 70 | -------------------------------------------------------------------------------- /test/modules/vue2/package-is-cjs-module.cjs: -------------------------------------------------------------------------------- 1 | /* eslint import/extensions: ['error', 'always'], import/no-commonjs: ['error', {allowRequire: true}] */ 2 | const assert = require('assert'); 3 | 4 | const VueInstantSearch2 = require('../../../vue2/cjs/index.js'); 5 | 6 | assert.ok(VueInstantSearch2); 7 | -------------------------------------------------------------------------------- /test/modules/vue2/package-is-es-module.mjs: -------------------------------------------------------------------------------- 1 | /* eslint import/extensions: ['error', 'always'] */ 2 | import assert from 'assert'; 3 | 4 | import * as VueInstantSearch2 from '../../../vue2/es/index.js'; 5 | import * as Vue2Widgets from '../../../vue2/es/src/widgets.js'; 6 | 7 | assert.ok(VueInstantSearch2); 8 | assert.ok(Vue2Widgets); 9 | -------------------------------------------------------------------------------- /test/modules/vue3/package-is-cjs-module.cjs: -------------------------------------------------------------------------------- 1 | /* eslint import/extensions: ['error', 'always'], import/no-commonjs: ['error', {allowRequire: true}] */ 2 | const assert = require('assert'); 3 | 4 | const VueInstantSearch3 = require('../../../vue3/cjs/index.js'); 5 | 6 | assert.ok(VueInstantSearch3); 7 | -------------------------------------------------------------------------------- /test/modules/vue3/package-is-es-module.mjs: -------------------------------------------------------------------------------- 1 | /* eslint import/extensions: ['error', 'always'] */ 2 | import assert from 'assert'; 3 | 4 | import * as VueInstantSearch3 from '../../../vue3/es/index.js'; 5 | import * as Vue3Widgets from '../../../vue3/es/src/widgets.js'; 6 | 7 | assert.ok(VueInstantSearch3); 8 | assert.ok(Vue3Widgets); 9 | -------------------------------------------------------------------------------- /test/utils/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | isVue3, 3 | createApp as _createApp, 4 | createSSRApp as _createSSRApp, 5 | nextTick as _nextTick, 6 | Vue2, 7 | } from '../../src/util/vue-compat'; 8 | 9 | export const htmlCompat = function(html) { 10 | if (isVue3) { 11 | return html 12 | .replace(/disabled=""/g, 'disabled="disabled"') 13 | .replace(/hidden=""/g, 'hidden="hidden"') 14 | .replace(/novalidate=""/g, 'novalidate="novalidate"') 15 | .replace(/required=""/g, 'required="required"'); 16 | } else { 17 | return html; 18 | } 19 | }; 20 | 21 | export const mount = isVue3 22 | ? (component, options = {}) => { 23 | const { 24 | propsData, 25 | mixins, 26 | provide, 27 | slots, 28 | scopedSlots, 29 | stubs, 30 | ...restOptions 31 | } = options; 32 | // If we `import` this, it will try to import Vue3-only APIs like `defineComponent`, 33 | // and jest will fail. So we need to `require` it. 34 | const wrapper = require('@vue/test-utils2').mount(component, { 35 | ...restOptions, 36 | props: propsData, 37 | global: { 38 | mixins, 39 | provide, 40 | stubs, 41 | }, 42 | slots: { 43 | ...slots, 44 | ...scopedSlots, 45 | }, 46 | }); 47 | wrapper.destroy = wrapper.unmount; 48 | wrapper.htmlCompat = function() { 49 | return htmlCompat(this.html()); 50 | }; 51 | return wrapper; 52 | } 53 | : (component, options = {}) => { 54 | const wrapper = require('@vue/test-utils').mount(component, options); 55 | wrapper.htmlCompat = function() { 56 | return htmlCompat(this.html()); 57 | }; 58 | return wrapper; 59 | }; 60 | 61 | export const createApp = props => { 62 | if (isVue3) { 63 | return _createApp(props); 64 | } else { 65 | return new Vue2(props); 66 | } 67 | }; 68 | 69 | export const createSSRApp = props => { 70 | if (isVue3) { 71 | return _createSSRApp(props); 72 | } else { 73 | return new Vue2(props); 74 | } 75 | }; 76 | 77 | export const nextTick = () => (isVue3 ? _nextTick() : Vue2.nextTick()); 78 | -------------------------------------------------------------------------------- /wdio.local.conf.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-commonjs */ 2 | 3 | const { local } = require('instantsearch-e2e-tests'); 4 | 5 | exports.config = local; 6 | -------------------------------------------------------------------------------- /wdio.saucelabs.conf.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-commonjs */ 2 | 3 | const { saucelabs } = require('instantsearch-e2e-tests'); 4 | 5 | exports.config = saucelabs; 6 | -------------------------------------------------------------------------------- /website/_redirects: -------------------------------------------------------------------------------- 1 | / https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/vue/ 301! 2 | 3 | # Examples 4 | /examples/:name/* https://instantsearchjs.netlify.app/examples/vue/:name/:splat 301! 5 | /stories/* https://instantsearchjs.netlify.app/stories/vue/:splat 301! 6 | --------------------------------------------------------------------------------44 |46 | 47 |45 | 48 |60 |49 | ${filters} 50 |51 |52 |59 |53 | 54 | 55 | ${hits} 56 | 57 |58 |