├── .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 | default-theme 10 | 11 | 12 | 13 | 17 |
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/default-theme/src/App.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | -ms-box-sizing: border-box; 5 | box-sizing: border-box; 6 | } 7 | 8 | html, 9 | body { 10 | margin: 0; 11 | padding: 0; 12 | font-size: 16px; 13 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, 14 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 15 | height: 100%; 16 | } 17 | 18 | .header { 19 | display: flex; 20 | align-items: center; 21 | z-index: 99; 22 | width: 100%; 23 | padding: 10px; 24 | background: #222f3f; 25 | } 26 | 27 | .logo { 28 | display: block; 29 | float: left; 30 | margin-right: 12px; 31 | } 32 | 33 | main { 34 | display: flex; 35 | } 36 | 37 | aside, 38 | section { 39 | padding: 10px; 40 | } 41 | 42 | section { 43 | width: 100%; 44 | } 45 | 46 | .thank-you { 47 | font-size: 12px; 48 | text-align: center; 49 | margin-top: 20px; 50 | } 51 | 52 | .ais-Panel { 53 | margin-top: 20px; 54 | } 55 | 56 | .section-header { 57 | display: flex; 58 | justify-content: space-between; 59 | align-items: center; 60 | margin-bottom: 10px; 61 | } 62 | 63 | .sort-by { 64 | display: flex; 65 | align-items: center; 66 | } 67 | 68 | .sort-by label { 69 | margin-right: 5px; 70 | } 71 | 72 | footer { 73 | margin-top: 25px; 74 | } 75 | 76 | .no-results { 77 | text-align: center; 78 | } 79 | -------------------------------------------------------------------------------- /examples/default-theme/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import InstantSearch from 'vue-instantsearch'; 3 | import App from './App.vue'; 4 | import 'instantsearch.css/themes/algolia-min.css'; 5 | 6 | Vue.use(InstantSearch); 7 | 8 | Vue.config.productionTip = false; 9 | 10 | new Vue({ 11 | render: h => h(App), 12 | }).$mount('#app'); 13 | -------------------------------------------------------------------------------- /examples/default-theme/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: './', 3 | }; 4 | -------------------------------------------------------------------------------- /examples/e-commerce/.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 | -------------------------------------------------------------------------------- /examples/e-commerce/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: ['plugin:vue/essential', 'eslint:recommended'], 7 | rules: {}, 8 | parserOptions: { 9 | parser: 'babel-eslint', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /examples/e-commerce/.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/e-commerce/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "proseWrap": "never", 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /examples/e-commerce/README.md: -------------------------------------------------------------------------------- 1 | # E-commerce demo 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/e-commerce/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/app'], 3 | }; 4 | -------------------------------------------------------------------------------- /examples/e-commerce/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-instantsearch-e-commerce", 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 | "classnames": "2.2.6", 13 | "instantsearch.js": "^4.5.0", 14 | "vue": "2.6.10", 15 | "vue-instantsearch": "file:../../", 16 | "vue-slider-component": "3.0.32" 17 | }, 18 | "devDependencies": { 19 | "@vue/cli-plugin-babel": "3.8.0", 20 | "@vue/cli-plugin-eslint": "3.8.0", 21 | "@vue/cli-service": "3.8.0", 22 | "vue-template-compiler": "2.6.10" 23 | }, 24 | "postcss": { 25 | "plugins": { 26 | "autoprefixer": {} 27 | } 28 | }, 29 | "browserslist": [ 30 | "> 1%", 31 | "last 2 versions", 32 | "not ie <= 8" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /examples/e-commerce/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/vue-instantsearch/f9dd4b0b33d47ad0d34292f8d826b6b5882e930d/examples/e-commerce/public/favicon.png -------------------------------------------------------------------------------- /examples/e-commerce/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 22 | 23 | 24 | 25 | E-commerce demo | Algolia 26 | 27 | 28 | 29 | 30 | 31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /examples/e-commerce/public/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "E-commerce", 3 | "name": "Vue InstantSearch E-commerce Demo", 4 | "icons": [ 5 | { 6 | "src": "favicon.png", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#e2a400", 14 | "background_color": "#fff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/e-commerce/src/images/cover-mobile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/vue-instantsearch/f9dd4b0b33d47ad0d34292f8d826b6b5882e930d/examples/e-commerce/src/images/cover-mobile.jpg -------------------------------------------------------------------------------- /examples/e-commerce/src/images/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/vue-instantsearch/f9dd4b0b33d47ad0d34292f8d826b6b5882e930d/examples/e-commerce/src/images/cover.jpg -------------------------------------------------------------------------------- /examples/e-commerce/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import InstantSearch from 'vue-instantsearch'; 3 | import App from './App.vue'; 4 | 5 | Vue.use(InstantSearch); 6 | 7 | Vue.config.productionTip = false; 8 | 9 | new Vue({ 10 | render: h => h(App), 11 | }).$mount('#app'); 12 | -------------------------------------------------------------------------------- /examples/e-commerce/src/utils.js: -------------------------------------------------------------------------------- 1 | export function formatNumber(value) { 2 | return Number(value).toLocaleString(); 3 | } 4 | -------------------------------------------------------------------------------- /examples/e-commerce/src/widgets/ClearRefinements.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 33 | -------------------------------------------------------------------------------- /examples/e-commerce/src/widgets/PriceSlider.css: -------------------------------------------------------------------------------- 1 | .vue-slider { 2 | display: flex; 3 | justify-content: center; 4 | margin-top: 1.5rem; 5 | } 6 | 7 | .vue-slider-rail { 8 | background-color: rgba(65, 66, 71, 0.08); 9 | border-radius: 3px; 10 | cursor: pointer; 11 | height: 4px; 12 | width: calc(100% - 10px); 13 | } 14 | 15 | .vue-slider-process { 16 | background-color: #e2a400; 17 | border-radius: 3px; 18 | } 19 | 20 | .vue-slider-dot-tooltip { 21 | cursor: grab; 22 | display: flex; 23 | font-size: 0.75rem; 24 | font-weight: bold; 25 | margin-top: 5px; 26 | } 27 | 28 | .vue-slider-dot-tooltip::before { 29 | color: #e2a400; 30 | content: '$'; 31 | font-size: 0.6; 32 | margin-right: 4px; 33 | } 34 | 35 | .vue-slider-dot-tooltip-text .vue-slider-dot-tooltip-inner { 36 | cursor: -webkit-grabbing; 37 | cursor: -moz-grabbing; 38 | text-align: center; 39 | white-space: nowrap; 40 | } 41 | 42 | .vue-slider-dot-handle { 43 | background-image: linear-gradient(to top, #f5f5fa, #fff); 44 | border-radius: 50%; 45 | box-shadow: 0 4px 11px 0 rgba(37, 44, 97, 0.15), 46 | 0 2px 3px 0 rgba(93, 100, 148, 0.2); 47 | cursor: -webkit-grabbing; 48 | cursor: -moz-grabbing; 49 | height: 100%; 50 | width: 100%; 51 | } 52 | 53 | @media (max-width: 899px) { 54 | .vue-slider-dot { 55 | height: 24px !important; 56 | width: 24px !important; 57 | } 58 | } -------------------------------------------------------------------------------- /examples/e-commerce/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: '/examples/e-commerce', 3 | }; 4 | -------------------------------------------------------------------------------- /examples/media/.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/media/README.md: -------------------------------------------------------------------------------- 1 | # media 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/media/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/app'], 3 | }; 4 | -------------------------------------------------------------------------------- /examples/media/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "media", 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.js": "^4.2.0", 13 | "vue": "^2.6.7", 14 | "vue-instantsearch": "^3.0.0-beta.0" 15 | }, 16 | "devDependencies": { 17 | "@vue/cli-plugin-babel": "^3.4.1", 18 | "@vue/cli-plugin-eslint": "^3.4.1", 19 | "@vue/cli-service": "^3.4.1", 20 | "vue-template-compiler": "^2.6.7" 21 | }, 22 | "eslintConfig": { 23 | "root": true, 24 | "env": { 25 | "node": true 26 | }, 27 | "extends": [ 28 | "plugin:vue/essential", 29 | "eslint:recommended" 30 | ], 31 | "rules": {}, 32 | "parserOptions": { 33 | "parser": "babel-eslint" 34 | } 35 | }, 36 | "postcss": { 37 | "plugins": { 38 | "autoprefixer": {} 39 | } 40 | }, 41 | "browserslist": [ 42 | "> 1%", 43 | "last 2 versions", 44 | "not ie <= 8" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /examples/media/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/vue-instantsearch/f9dd4b0b33d47ad0d34292f8d826b6b5882e930d/examples/media/public/favicon.ico -------------------------------------------------------------------------------- /examples/media/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | media 13 | 14 | 15 | 16 | 19 |
20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/media/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import InstantSearch from 'vue-instantsearch'; 3 | import App from './App.vue'; 4 | 5 | Vue.use(InstantSearch); 6 | 7 | Vue.config.productionTip = false; 8 | 9 | new Vue({ 10 | render: h => h(App), 11 | }).$mount('#app'); 12 | -------------------------------------------------------------------------------- /examples/media/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: './', 3 | }; 4 | -------------------------------------------------------------------------------- /examples/nuxt/README.md: -------------------------------------------------------------------------------- 1 | # nuxt 2 | 3 | > Vue InstantSearch & Nuxt 4 | 5 | ## Build Setup 6 | 7 | ```bash 8 | # install dependencies 9 | $ yarn install 10 | 11 | # serve with hot reload at localhost:3000 12 | $ yarn run dev 13 | 14 | # build for production and launch server 15 | $ yarn run build 16 | $ yarn start 17 | 18 | # generate static project 19 | $ yarn run generate 20 | ``` 21 | 22 | For detailed explanation on how things work, checkout [Nuxt.js docs](https://nuxtjs.org). 23 | -------------------------------------------------------------------------------- /examples/nuxt/layouts/README.md: -------------------------------------------------------------------------------- 1 | # LAYOUTS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your Application Layouts. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/views#layouts). 8 | -------------------------------------------------------------------------------- /examples/nuxt/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | -------------------------------------------------------------------------------- /examples/nuxt/nuxt.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-commonjs */ 2 | 3 | module.exports = { 4 | build: { 5 | transpile: ['vue-instantsearch', 'instantsearch.js/es'], 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /examples/nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt", 3 | "version": "1.0.0", 4 | "description": "Vue InstantSearch & Nuxt", 5 | "private": true, 6 | "scripts": { 7 | "serve": "yarn dev", 8 | "dev": "nuxt", 9 | "build": "nuxt build", 10 | "start": "nuxt start", 11 | "generate": "nuxt generate" 12 | }, 13 | "dependencies": { 14 | "algoliasearch": "^4.0.1", 15 | "cross-env": "^5.2.0", 16 | "nuxt": "^2.4.5", 17 | "vue-instantsearch": "file:../../", 18 | "vue-server-renderer": "2.6.11" 19 | }, 20 | "devDependencies": {} 21 | } 22 | -------------------------------------------------------------------------------- /examples/nuxt/pages/README.md: -------------------------------------------------------------------------------- 1 | # PAGES 2 | 3 | This directory contains your Application Views and Routes. 4 | The framework reads all the `*.vue` files inside this directory and create the router of your application. 5 | 6 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing). 7 | -------------------------------------------------------------------------------- /examples/nuxt/pages/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /examples/nuxt/static/README.md: -------------------------------------------------------------------------------- 1 | # STATIC 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your static files. 6 | Each file inside this directory is mapped to `/`. 7 | 8 | Example: `/static/robots.txt` is mapped as `/robots.txt`. 9 | 10 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#static). 11 | -------------------------------------------------------------------------------- /examples/nuxt/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/vue-instantsearch/f9dd4b0b33d47ad0d34292f8d826b6b5882e930d/examples/nuxt/static/favicon.ico -------------------------------------------------------------------------------- /examples/ssr/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /examples/ssr/.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/ssr/README.md: -------------------------------------------------------------------------------- 1 | # ssr 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 | ### Run your tests 22 | 23 | ``` 24 | yarn run test 25 | ``` 26 | 27 | ### Lints and fixes files 28 | 29 | ``` 30 | yarn run lint 31 | ``` 32 | -------------------------------------------------------------------------------- /examples/ssr/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/app'], 3 | }; 4 | -------------------------------------------------------------------------------- /examples/ssr/not.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: ['plugin:vue/essential', '@vue/prettier'], 7 | rules: { 8 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 9 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 10 | }, 11 | parserOptions: { 12 | parser: 'babel-eslint', 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /examples/ssr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssr", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "lint": "vue-cli-service lint", 7 | "build": "vue-cli-service ssr:build", 8 | "serve": "vue-cli-service ssr:serve", 9 | "start": "cross-env NODE_ENV=production vue-cli-service ssr:serve --mode production" 10 | }, 11 | "dependencies": { 12 | "algoliasearch": "^4.0.1", 13 | "instantsearch.css": "^7.1.1", 14 | "vue": "^2.6.7", 15 | "vue-instantsearch": "file:../../", 16 | "vue-router": "^3.0.2", 17 | "vue-server-renderer": "^2.6.11" 18 | }, 19 | "devDependencies": { 20 | "@akryum/vue-cli-plugin-ssr": "^0.5.0", 21 | "@vue/cli-plugin-babel": "^3.4.1", 22 | "@vue/cli-plugin-eslint": "^3.4.1", 23 | "@vue/cli-service": "^3.4.1", 24 | "@vue/eslint-config-prettier": "^4.0.0", 25 | "babel-eslint": "^10.0.1", 26 | "eslint": "^5.14.1", 27 | "eslint-plugin-vue": "^5.2.2", 28 | "vue-template-compiler": "^2.6.7" 29 | }, 30 | "eslintConfig": { 31 | "root": true, 32 | "env": { 33 | "node": true 34 | }, 35 | "extends": [ 36 | "plugin:vue/essential", 37 | "eslint:recommended" 38 | ], 39 | "rules": { 40 | "no-console": "off" 41 | }, 42 | "parserOptions": { 43 | "parser": "babel-eslint" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/ssr/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /examples/ssr/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/vue-instantsearch/f9dd4b0b33d47ad0d34292f8d826b6b5882e930d/examples/ssr/public/favicon.ico -------------------------------------------------------------------------------- /examples/ssr/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{ title }} 10 | {{{ renderResourceHints() }}} 11 | {{{ renderStyles() }}} 12 | 13 | 14 | 15 | {{{ renderState() }}} 16 | {{{ renderState({ contextKey: 'algoliaState', windowKey: '__ALGOLIA_STATE__' }) }}} 17 | {{{ renderScripts() }}} 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/ssr/src/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 22 | -------------------------------------------------------------------------------- /examples/ssr/src/entry-client.js: -------------------------------------------------------------------------------- 1 | import { loadAsyncComponents } from '@akryum/vue-cli-plugin-ssr/client'; 2 | 3 | import { createApp } from './main'; 4 | 5 | createApp({ 6 | async beforeApp({ router }) { 7 | await loadAsyncComponents({ router }); 8 | }, 9 | 10 | afterApp({ app }) { 11 | app.$mount('#app'); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /examples/ssr/src/entry-server.js: -------------------------------------------------------------------------------- 1 | import { createApp } from './main'; 2 | 3 | export default context => 4 | new Promise(async (resolve, reject) => { 5 | const { app, router } = await createApp({ context }); 6 | 7 | router.push(context.url); 8 | 9 | // wait until router has resolved possible async components and hooks 10 | router.onReady(() => { 11 | // This `rendered` hook is called when the app has finished rendering 12 | // After the app is rendered, our store is now 13 | // filled with the state from our components. 14 | // When we attach the state to the context, and the `template` option 15 | // is used for the renderer, the state will automatically be 16 | // serialized and injected into the HTML as `window.__INITIAL_STATE__`. 17 | context.rendered = () => { 18 | context.algoliaState = app.instantsearch.getState(); 19 | }; 20 | 21 | // no matched routes, reject with 404 22 | const matchedComponents = router.getMatchedComponents(); 23 | if (matchedComponents.length === 0) { 24 | return reject({ code: 404 }); 25 | } 26 | 27 | resolve(app); 28 | }, reject); 29 | }); 30 | -------------------------------------------------------------------------------- /examples/ssr/src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import Home from './views/Home.vue'; 4 | 5 | Vue.use(Router); 6 | 7 | export function createRouter() { 8 | return new Router({ 9 | mode: 'history', 10 | base: process.env.BASE_URL, 11 | routes: [ 12 | { 13 | path: '/', 14 | name: 'home', 15 | component: Home, 16 | }, 17 | { 18 | path: '/about', 19 | name: 'about', 20 | // route level code-splitting 21 | // this generates a separate chunk (about.[hash].js) for this route 22 | // which is lazy-loaded when the route is visited. 23 | component: () => 24 | import(/* webpackChunkName: "about" */ './views/About.vue'), 25 | }, 26 | { 27 | path: '/search', 28 | name: 'search', 29 | // route level code-splitting 30 | // this generates a separate chunk (search.[hash].js) for this route 31 | // which is lazy-loaded when the route is visited. 32 | component: () => 33 | import(/* webpackChunkName: "search" */ './views/Search.vue'), 34 | }, 35 | ], 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /examples/ssr/src/views/About.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/ssr/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /examples/ssr/src/views/Search.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 49 | 50 | 55 | -------------------------------------------------------------------------------- /examples/ssr/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | pluginOptions: { 3 | ssr: { 4 | nodeExternalsWhitelist: [ 5 | /\.css$/, 6 | /\?vue&type=style/, 7 | /vue-instantsearch/, 8 | /instantsearch.js/, 9 | ], 10 | }, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import createSerializer from 'jest-serializer-html/createSerializer'; 2 | import { isVue3, isVue2, Vue2 } from './src/util/vue-compat'; 3 | 4 | if (isVue2) { 5 | Vue2.config.productionTip = false; 6 | } 7 | 8 | expect.addSnapshotSerializer( 9 | createSerializer({ 10 | print: { 11 | sortAttributes: names => names.sort(), 12 | }, 13 | }) 14 | ); 15 | 16 | const toHaveEmptyHTML = wrapper => { 17 | const html = wrapper.html(); 18 | if ( 19 | (isVue2 && html === '') || 20 | (isVue3 && ['', ''].includes(html)) 21 | ) { 22 | return { 23 | pass: true, 24 | }; 25 | } else { 26 | return { 27 | pass: false, 28 | message: () => `expected ${html} to be an empty HTML string`, 29 | }; 30 | } 31 | }; 32 | 33 | const toHaveBooleanAttribute = attribute => wrapper => { 34 | // :hidden="true" becomes 35 | // hidden="hidden" in Vue 2 and 36 | // hidden="" in Vue 3. 37 | 38 | // So we need this to write correct tests to match them in both versions. 39 | const value = wrapper.attributes(attribute); 40 | if ((isVue2 && value === attribute) || (isVue3 && value === '')) { 41 | return { pass: true }; 42 | } else { 43 | return { 44 | pass: false, 45 | message: () => `expected ${wrapper} to have \`${attribute}\` attribute`, 46 | }; 47 | } 48 | }; 49 | 50 | expect.extend({ 51 | toHaveEmptyHTML, 52 | toBeDisabled: toHaveBooleanAttribute('disabled'), 53 | toBeHidden: toHaveBooleanAttribute('hidden'), 54 | toBeAutofocused: toHaveBooleanAttribute('autofocus'), 55 | }); 56 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "echo 'no build needed'" 3 | publish = "website" 4 | [build.environment] 5 | YARN_VERSION = "1.6.0" 6 | NODE_ENV = "development" 7 | VUE_CLI_TEST = "true" 8 | -------------------------------------------------------------------------------- /scripts/__mocks__/fs.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-commonjs */ 2 | const fs = jest.requireActual('fs'); 3 | 4 | let mockFiles = new Set(); 5 | function __setMockFiles(newMockFiles) { 6 | mockFiles = new Set(newMockFiles); 7 | } 8 | 9 | const realStatSync = fs.statSync; 10 | function statSync(pathName, ...args) { 11 | try { 12 | return realStatSync(pathName, ...args); 13 | } catch (e) { 14 | if (e && (e.code === 'ENOENT' || e.code === 'ENOTDIR')) { 15 | if (mockFiles.has(pathName)) { 16 | return { 17 | isFile() { 18 | return true; 19 | }, 20 | }; 21 | } 22 | } 23 | throw e; 24 | } 25 | } 26 | 27 | fs.__setMockFiles = __setMockFiles; 28 | fs.statSync = statSync; 29 | 30 | module.exports = fs; 31 | -------------------------------------------------------------------------------- /scripts/clean-node-modules.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # we have something weird going on, where installing via `file:../..` doesn't take "files" in account, and thus copies recursively. This causes cache to take many minutes and us to time-out. The long-term solution is to use prevent the recursive copying too, which will make the install faster. 4 | 5 | mkdir /tmp/empty 6 | 7 | rsync -a --delete /tmp/empty node_modules 8 | rsync -a --delete /tmp/empty examples/default-theme/node_modules 9 | rsync -a --delete /tmp/empty examples/e-commerce/node_modules 10 | rsync -a --delete /tmp/empty examples/media/node_modules 11 | rsync -a --delete /tmp/empty examples/nuxt/node_modules 12 | rsync -a --delete /tmp/empty examples/ssr/node_modules 13 | -------------------------------------------------------------------------------- /scripts/test-vue3.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | yarn remove vue vue-jest vue-template-compiler babel-preset-es2015 5 | yarn add vue@3.1.2 @vue/server-renderer@3.1.2 vue-jest@5.0.0-alpha.10 vue-loader@16.1.2 jest@26.6.3 babel-jest@25.2.6 @babel/preset-env@7.14.5 @babel/plugin-transform-runtime@7.14.5 @babel/core@7.14.5 -D -E 6 | echo "export * from './index-vue3';" > src/util/vue-compat/index.js 7 | rm .babelrc 8 | cat < babel.config.js 9 | // eslint-disable-next-line import/no-commonjs 10 | module.exports = { 11 | plugins: ['@babel/plugin-transform-runtime'], 12 | presets: ['@babel/preset-env'], 13 | }; 14 | EOT 15 | -------------------------------------------------------------------------------- /ship.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-commonjs */ 2 | module.exports = { 3 | pullRequestTeamReviewer: ['instantsearch-for-websites'], 4 | }; 5 | -------------------------------------------------------------------------------- /src/components/Autocomplete.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 57 | -------------------------------------------------------------------------------- /src/components/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 105 | -------------------------------------------------------------------------------- /src/components/ClearRefinements.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 71 | -------------------------------------------------------------------------------- /src/components/Configure.js: -------------------------------------------------------------------------------- 1 | import { createWidgetMixin } from '../mixins/widget'; 2 | import { createSuitMixin } from '../mixins/suit'; 3 | import { connectConfigure } from 'instantsearch.js/es/connectors'; 4 | import { isVue3, renderCompat } from '../util/vue-compat'; 5 | 6 | export default { 7 | inheritAttrs: false, 8 | name: 'AisConfigure', 9 | mixins: [ 10 | createSuitMixin({ name: 'Configure' }), 11 | createWidgetMixin( 12 | { 13 | connector: connectConfigure, 14 | }, 15 | { 16 | $$widgetType: 'ais.configure', 17 | } 18 | ), 19 | ], 20 | computed: { 21 | widgetParams() { 22 | return { 23 | searchParameters: this.$attrs, 24 | }; 25 | }, 26 | }, 27 | render: renderCompat(function(h) { 28 | const slot = isVue3 ? this.$slots.default : this.$scopedSlots.default; 29 | 30 | if (!this.state || !slot) { 31 | return null; 32 | } 33 | 34 | return h( 35 | 'div', 36 | { 37 | class: this.suit(), 38 | }, 39 | [ 40 | slot({ 41 | refine: this.state.refine, 42 | searchParameters: this.state.widgetParams.searchParameters, 43 | }), 44 | ] 45 | ); 46 | }), 47 | }; 48 | -------------------------------------------------------------------------------- /src/components/ConfigureRelatedItems.js: -------------------------------------------------------------------------------- 1 | import { createWidgetMixin } from '../mixins/widget'; 2 | import { EXPERIMENTAL_connectConfigureRelatedItems } from 'instantsearch.js/es/connectors'; 3 | 4 | export default { 5 | inheritAttrs: false, 6 | name: 'AisExperimentalConfigureRelatedItems', 7 | mixins: [ 8 | createWidgetMixin( 9 | { 10 | connector: EXPERIMENTAL_connectConfigureRelatedItems, 11 | }, 12 | { 13 | $$widgetType: 'ais.configureRelatedItems', 14 | } 15 | ), 16 | ], 17 | props: { 18 | hit: { 19 | type: Object, 20 | required: true, 21 | }, 22 | matchingPatterns: { 23 | type: Object, 24 | required: true, 25 | }, 26 | transformSearchParameters: { 27 | type: Function, 28 | required: false, 29 | }, 30 | }, 31 | computed: { 32 | widgetParams() { 33 | return { 34 | hit: this.hit, 35 | matchingPatterns: this.matchingPatterns, 36 | transformSearchParameters: this.transformSearchParameters, 37 | }; 38 | }, 39 | }, 40 | render() { 41 | return null; 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/ExperimentalDynamicWidgets.js: -------------------------------------------------------------------------------- 1 | import AisDynamicWidgets from './DynamicWidgets'; 2 | import { warn } from '../util/warn'; 3 | 4 | // @MAJOR remove this file 5 | export default Object.assign({}, AisDynamicWidgets, { 6 | name: 'AisExperimentalDynamicWidgets', 7 | mounted() { 8 | warn('Use AisDynamicWidgets instead of AisExperimentalDynamicWidgets.'); 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/HierarchicalMenuList.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 69 | -------------------------------------------------------------------------------- /src/components/Highlight.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 37 | -------------------------------------------------------------------------------- /src/components/Highlighter.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 74 | -------------------------------------------------------------------------------- /src/components/Hits.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 70 | -------------------------------------------------------------------------------- /src/components/HitsPerPage.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 78 | -------------------------------------------------------------------------------- /src/components/Index.js: -------------------------------------------------------------------------------- 1 | import { createSuitMixin } from '../mixins/suit'; 2 | import { createWidgetMixin } from '../mixins/widget'; 3 | import indexWidget from 'instantsearch.js/es/widgets/index/index'; 4 | import { renderCompat, getDefaultSlot } from '../util/vue-compat'; 5 | 6 | // wrapped in a dummy function, since indexWidget doesn't render 7 | const connectIndex = () => indexWidget; 8 | 9 | export default { 10 | name: 'AisIndex', 11 | mixins: [ 12 | createSuitMixin({ name: 'Index' }), 13 | createWidgetMixin( 14 | { connector: connectIndex }, 15 | { 16 | $$widgetType: 'ais.index', 17 | } 18 | ), 19 | ], 20 | provide() { 21 | return { 22 | // The widget is created & registered by widgetMixin, accessor is needed 23 | // because provide is not reactive. 24 | $_ais_getParentIndex: () => this.widget, 25 | }; 26 | }, 27 | props: { 28 | indexName: { 29 | type: String, 30 | required: true, 31 | }, 32 | indexId: { 33 | type: String, 34 | required: false, 35 | }, 36 | }, 37 | render: renderCompat(function(h) { 38 | return h('div', {}, getDefaultSlot(this)); 39 | }), 40 | computed: { 41 | widgetParams() { 42 | return { 43 | indexName: this.indexName, 44 | indexId: this.indexId, 45 | }; 46 | }, 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/InstantSearch.js: -------------------------------------------------------------------------------- 1 | import instantsearch from 'instantsearch.js/es'; 2 | import { createInstantSearchComponent } from '../util/createInstantSearchComponent'; 3 | import { warn } from '../util/warn'; 4 | import { renderCompat, getDefaultSlot } from '../util/vue-compat'; 5 | 6 | const oldApiWarning = `Vue InstantSearch: You used the prop api-key or app-id. 7 | These have been replaced by search-client. 8 | 9 | See more info here: https://www.algolia.com/doc/api-reference/widgets/instantsearch/vue/#widget-param-search-client`; 10 | 11 | export default createInstantSearchComponent({ 12 | name: 'AisInstantSearch', 13 | props: { 14 | searchClient: { 15 | type: Object, 16 | required: true, 17 | }, 18 | insightsClient: { 19 | type: Function, 20 | default: undefined, 21 | }, 22 | indexName: { 23 | type: String, 24 | required: true, 25 | }, 26 | routing: { 27 | default: undefined, 28 | validator(value) { 29 | if ( 30 | typeof value === 'boolean' || 31 | (!value.router && !value.stateMapping) 32 | ) { 33 | warn( 34 | 'The `routing` option expects an object with `router` and/or `stateMapping`.\n\nSee https://www.algolia.com/doc/api-reference/widgets/instantsearch/vue/#widget-param-routing' 35 | ); 36 | return false; 37 | } 38 | return true; 39 | }, 40 | }, 41 | stalledSearchDelay: { 42 | type: Number, 43 | default: undefined, 44 | }, 45 | searchFunction: { 46 | type: Function, 47 | default: undefined, 48 | }, 49 | onStateChange: { 50 | type: Function, 51 | default: undefined, 52 | }, 53 | initialUiState: { 54 | type: Object, 55 | default: undefined, 56 | }, 57 | apiKey: { 58 | type: String, 59 | default: undefined, 60 | validator(value) { 61 | if (value) { 62 | warn(oldApiWarning); 63 | } 64 | return false; 65 | }, 66 | }, 67 | appId: { 68 | type: String, 69 | default: undefined, 70 | validator(value) { 71 | if (value) { 72 | warn(oldApiWarning); 73 | } 74 | return false; 75 | }, 76 | }, 77 | middlewares: { 78 | type: Array, 79 | default: null, 80 | }, 81 | }, 82 | data() { 83 | return { 84 | instantSearchInstance: instantsearch({ 85 | searchClient: this.searchClient, 86 | insightsClient: this.insightsClient, 87 | indexName: this.indexName, 88 | routing: this.routing, 89 | stalledSearchDelay: this.stalledSearchDelay, 90 | searchFunction: this.searchFunction, 91 | onStateChange: this.onStateChange, 92 | initialUiState: this.initialUiState, 93 | }), 94 | }; 95 | }, 96 | render: renderCompat(function(h) { 97 | return h( 98 | 'div', 99 | { 100 | class: { 101 | [this.suit()]: true, 102 | [this.suit('', 'ssr')]: false, 103 | }, 104 | }, 105 | getDefaultSlot(this) 106 | ); 107 | }), 108 | }); 109 | -------------------------------------------------------------------------------- /src/components/InstantSearchSsr.js: -------------------------------------------------------------------------------- 1 | import { createInstantSearchComponent } from '../util/createInstantSearchComponent'; 2 | import { renderCompat, getDefaultSlot } from '../util/vue-compat'; 3 | 4 | export default createInstantSearchComponent({ 5 | name: 'AisInstantSearchSsr', 6 | inject: { 7 | $_ais_ssrInstantSearchInstance: { 8 | default() { 9 | throw new Error('`createServerRootMixin` is required when using SSR.'); 10 | }, 11 | }, 12 | }, 13 | data() { 14 | return { 15 | instantSearchInstance: this.$_ais_ssrInstantSearchInstance, 16 | }; 17 | }, 18 | render: renderCompat(function(h) { 19 | return h( 20 | 'div', 21 | { 22 | class: { 23 | [this.suit()]: true, 24 | [this.suit('', 'ssr')]: true, 25 | }, 26 | }, 27 | getDefaultSlot(this) 28 | ); 29 | }), 30 | }); 31 | -------------------------------------------------------------------------------- /src/components/MenuSelect.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 95 | -------------------------------------------------------------------------------- /src/components/NumericMenu.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 81 | -------------------------------------------------------------------------------- /src/components/Panel.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 44 | -------------------------------------------------------------------------------- /src/components/QueryRuleContext.js: -------------------------------------------------------------------------------- 1 | import { createSuitMixin } from '../mixins/suit'; 2 | import { createWidgetMixin } from '../mixins/widget'; 3 | import { connectQueryRules } from 'instantsearch.js/es/connectors'; 4 | 5 | export default { 6 | name: 'AisQueryRuleContext', 7 | mixins: [ 8 | createSuitMixin({ name: 'QueryRuleContext' }), 9 | createWidgetMixin( 10 | { 11 | connector: connectQueryRules, 12 | }, 13 | { 14 | $$widgetType: 'ais.queryRuleContext', 15 | } 16 | ), 17 | ], 18 | props: { 19 | trackedFilters: { 20 | type: Object, 21 | required: true, 22 | }, 23 | transformRuleContexts: { 24 | type: Function, 25 | required: false, 26 | default: undefined, 27 | }, 28 | }, 29 | computed: { 30 | widgetParams() { 31 | return { 32 | trackedFilters: this.trackedFilters, 33 | transformRuleContexts: this.transformRuleContexts, 34 | }; 35 | }, 36 | }, 37 | render() { 38 | return null; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/QueryRuleCustomData.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 56 | -------------------------------------------------------------------------------- /src/components/RelevantSort.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 59 | -------------------------------------------------------------------------------- /src/components/Snippet.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 37 | -------------------------------------------------------------------------------- /src/components/SortBy.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 68 | -------------------------------------------------------------------------------- /src/components/StateResults.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 91 | -------------------------------------------------------------------------------- /src/components/Stats.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 38 | -------------------------------------------------------------------------------- /src/components/ToggleRefinement.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 84 | -------------------------------------------------------------------------------- /src/components/__Template.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 65 | -------------------------------------------------------------------------------- /src/components/__tests__/Autocomplete.js: -------------------------------------------------------------------------------- 1 | import { mount } from '../../../test/utils'; 2 | import Autocomplete from '../Autocomplete.vue'; 3 | import { __setState } from '../../mixins/widget'; 4 | jest.mock('../../mixins/widget'); 5 | 6 | const defaultState = { 7 | refine: jest.fn(), 8 | currentRefinement: '', 9 | indices: [ 10 | { 11 | index: 'bla', 12 | label: 'bla bla bla ', 13 | hits: [{ objectID: 1, name: 'hi' }], 14 | results: {}, 15 | }, 16 | ], 17 | }; 18 | 19 | it('renders correctly', () => { 20 | __setState({ 21 | ...defaultState, 22 | }); 23 | const wrapper = mount(Autocomplete); 24 | 25 | expect(wrapper.html()).toMatchSnapshot(); 26 | }); 27 | 28 | it('gives the correct props to the default slot', done => { 29 | __setState({ 30 | ...defaultState, 31 | }); 32 | mount(Autocomplete, { 33 | scopedSlots: { 34 | default(props) { 35 | expect(props).toEqual( 36 | expect.objectContaining({ 37 | currentRefinement: defaultState.currentRefinement, 38 | refine: defaultState.refine, 39 | indices: defaultState.indices, 40 | }) 41 | ); 42 | done(); 43 | }, 44 | }, 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/components/__tests__/Configure.js: -------------------------------------------------------------------------------- 1 | import { mount } from '../../../test/utils'; 2 | import { __setState } from '../../mixins/widget'; 3 | import Configure from '../Configure'; 4 | 5 | jest.mock('../../mixins/widget'); 6 | 7 | const defaultState = { 8 | widgetParams: { 9 | searchParameters: { 10 | hitsPerPage: 5, 11 | }, 12 | }, 13 | }; 14 | 15 | const defaultProps = { 16 | hitsPerPage: 5, 17 | }; 18 | 19 | const defaultSlot = ` 20 | 25 | `; 26 | 27 | it('accepts SearchParameters from attributes', () => { 28 | const wrapper = mount(Configure, { 29 | propsData: { 30 | ...defaultProps, 31 | distinct: true, 32 | }, 33 | }); 34 | 35 | expect(wrapper.vm.widgetParams.searchParameters).toEqual({ 36 | hitsPerPage: 5, 37 | distinct: true, 38 | }); 39 | }); 40 | 41 | it('renders null without default slot', () => { 42 | __setState(null); 43 | 44 | const wrapper = mount(Configure, { 45 | propsData: defaultProps, 46 | }); 47 | 48 | expect(wrapper).toHaveEmptyHTML(); 49 | }); 50 | 51 | it('renders null without state', () => { 52 | __setState(null); 53 | 54 | const wrapper = mount({ 55 | components: { Configure }, 56 | data() { 57 | return { props: defaultProps }; 58 | }, 59 | template: ` 60 | 61 | ${defaultSlot} 62 | 63 | `, 64 | }); 65 | 66 | expect(wrapper).toHaveEmptyHTML(); 67 | }); 68 | 69 | it('renders with scoped slots', () => { 70 | __setState({ ...defaultState }); 71 | 72 | const wrapper = mount({ 73 | components: { Configure }, 74 | data() { 75 | return { props: defaultProps }; 76 | }, 77 | template: ` 78 | 79 | ${defaultSlot} 80 | 81 | `, 82 | }); 83 | 84 | expect(wrapper.html()).toMatchSnapshot(); 85 | }); 86 | -------------------------------------------------------------------------------- /src/components/__tests__/ConfigureRelatedItems.js: -------------------------------------------------------------------------------- 1 | import { mount } from '../../../test/utils'; 2 | import ConfigureRelatedItems from '../ConfigureRelatedItems'; 3 | 4 | jest.mock('../../mixins/widget'); 5 | 6 | it('accepts options from props', () => { 7 | const props = { 8 | hit: { objectID: '1' }, 9 | matchingPatterns: {}, 10 | transformSearchParameters: x => x, 11 | }; 12 | 13 | const wrapper = mount(ConfigureRelatedItems, { 14 | propsData: props, 15 | }); 16 | 17 | expect(wrapper.vm.widgetParams).toEqual(props); 18 | }); 19 | 20 | it('renders nothing', () => { 21 | const props = { 22 | hit: { objectID: '1' }, 23 | matchingPatterns: {}, 24 | }; 25 | 26 | const wrapper = mount(ConfigureRelatedItems, { 27 | propsData: props, 28 | }); 29 | 30 | expect(wrapper).toHaveEmptyHTML(); 31 | }); 32 | -------------------------------------------------------------------------------- /src/components/__tests__/ExperimentalDynamicWidgets.js: -------------------------------------------------------------------------------- 1 | import { mount } from '../../../test/utils'; 2 | import ExperimentalDynamicWidgets from '../ExperimentalDynamicWidgets'; 3 | import { warn } from '../../util/warn'; 4 | import { __setState } from '../../mixins/widget'; 5 | import DynamicWidgets from '../DynamicWidgets'; 6 | 7 | jest.mock('../../mixins/widget'); 8 | jest.mock('../../util/warn', () => ({ warn: jest.fn() })); 9 | 10 | it('warns on mount', () => { 11 | __setState(null); 12 | 13 | mount({ 14 | template: '', 15 | components: { 16 | ExperimentalDynamicWidgets, 17 | }, 18 | }); 19 | expect(warn).toHaveBeenCalledTimes(1); 20 | expect(warn.mock.calls[0][0]).toMatchInlineSnapshot( 21 | `"Use AisDynamicWidgets instead of AisExperimentalDynamicWidgets."` 22 | ); 23 | }); 24 | 25 | it('behaves the same as DynamicWidgets', () => { 26 | Object.keys(ExperimentalDynamicWidgets).forEach(key => { 27 | if (key === 'name') { 28 | // name is different 29 | expect(ExperimentalDynamicWidgets[key]).toBe( 30 | 'AisExperimentalDynamicWidgets' 31 | ); 32 | } else if (key === 'mounted') { 33 | // mounted has the warning 34 | expect(ExperimentalDynamicWidgets[key]).toEqual(expect.any(Function)); 35 | } else if (key[0] === '_') { 36 | // private Vue behavior, not tested 37 | } else { 38 | expect(ExperimentalDynamicWidgets[key]).toEqual(DynamicWidgets[key]); 39 | } 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/components/__tests__/Highlight.js: -------------------------------------------------------------------------------- 1 | import { mount } from '../../../test/utils'; 2 | import Highlight from '../Highlight.vue'; 3 | 4 | jest.unmock('instantsearch.js/es'); 5 | 6 | test('renders proper HTML', () => { 7 | const hit = { 8 | _highlightResult: { 9 | attr: { 10 | value: `content`, 11 | }, 12 | }, 13 | }; 14 | 15 | const wrapper = mount(Highlight, { 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 | _highlightResult: { 28 | attr: { 29 | value: `content`, 30 | }, 31 | }, 32 | }; 33 | 34 | const wrapper = mount(Highlight, { 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 highlighted', () => { 46 | process.env.NODE_ENV = 'production'; 47 | const hit = { 48 | _highlightResult: {}, 49 | }; 50 | 51 | const wrapper = mount(Highlight, { 52 | propsData: { 53 | attribute: 'attr', 54 | hit, 55 | }, 56 | }); 57 | 58 | expect(wrapper.html()).toMatchSnapshot(); 59 | }); 60 | 61 | test('allows usage of dot delimited path to access nested attribute', () => { 62 | const hit = { 63 | _highlightResult: { 64 | attr: { 65 | nested: { 66 | value: `nested val`, 67 | }, 68 | }, 69 | }, 70 | }; 71 | 72 | const wrapper = mount(Highlight, { 73 | propsData: { 74 | attribute: 'attr.nested', 75 | hit, 76 | }, 77 | }); 78 | 79 | expect(wrapper.html()).toMatchSnapshot(); 80 | }); 81 | 82 | test('retains whitespace nodes', () => { 83 | const hit = { 84 | _highlightResult: { 85 | attr: { 86 | value: `content searching`, 87 | }, 88 | }, 89 | }; 90 | 91 | const wrapper = mount(Highlight, { 92 | propsData: { 93 | attribute: 'attr', 94 | highlightedTagName: 'marquee', 95 | hit, 96 | }, 97 | }); 98 | 99 | expect(wrapper.html()).toBe( 100 | `content searching` 101 | ); 102 | }); 103 | -------------------------------------------------------------------------------- /src/components/__tests__/HitsPerPage.js: -------------------------------------------------------------------------------- 1 | import { mount } from '../../../test/utils'; 2 | import { __setState } from '../../mixins/widget'; 3 | import HitsPerPage from '../HitsPerPage.vue'; 4 | 5 | jest.mock('../../mixins/widget'); 6 | jest.mock('../../mixins/panel'); 7 | 8 | const defaultState = { 9 | items: [ 10 | { 11 | label: '10 results', 12 | value: 10, 13 | default: true, 14 | }, 15 | { 16 | label: '20 results', 17 | value: 20, 18 | }, 19 | ], 20 | }; 21 | 22 | const defaultProps = { 23 | items: [ 24 | { 25 | label: '10 results', 26 | value: 10, 27 | default: true, 28 | }, 29 | { 30 | label: '20 results', 31 | value: 20, 32 | }, 33 | ], 34 | }; 35 | 36 | it('accepts a transformItems prop', () => { 37 | __setState({ ...defaultState }); 38 | 39 | const transformItems = () => {}; 40 | 41 | const wrapper = mount(HitsPerPage, { 42 | propsData: { 43 | ...defaultProps, 44 | transformItems, 45 | }, 46 | }); 47 | 48 | expect(wrapper.vm.widgetParams.transformItems).toBe(transformItems); 49 | }); 50 | 51 | it('renders correctly', () => { 52 | __setState({ ...defaultState }); 53 | 54 | const wrapper = mount(HitsPerPage, { 55 | propsData: defaultProps, 56 | }); 57 | 58 | expect(wrapper.html()).toMatchSnapshot(); 59 | }); 60 | 61 | it('calls `refine` with the `value` on `change`', async () => { 62 | __setState({ 63 | ...defaultState, 64 | refine: jest.fn(), 65 | }); 66 | 67 | const wrapper = mount(HitsPerPage, { 68 | propsData: defaultProps, 69 | }); 70 | 71 | await wrapper.setData({ 72 | selected: 20, 73 | }); 74 | 75 | await wrapper.find('select').trigger('change'); 76 | 77 | expect(wrapper.vm.state.refine).toHaveBeenLastCalledWith(20); 78 | }); 79 | -------------------------------------------------------------------------------- /src/components/__tests__/Index-integration.js: -------------------------------------------------------------------------------- 1 | jest.unmock('instantsearch.js/es'); 2 | import { mount } from '../../../test/utils'; 3 | import Index from '../Index'; 4 | import instantsearch from 'instantsearch.js/es'; 5 | import { createWidgetMixin } from '../../mixins/widget'; 6 | import { createFakeClient } from '../../util/testutils/client'; 7 | 8 | it('child widgets get added to their parent index', () => { 9 | const widgetInstance = { 10 | render() {}, 11 | }; 12 | 13 | const ChildComponent = { 14 | name: 'child', 15 | mixins: [createWidgetMixin({ connector: () => () => widgetInstance })], 16 | render() { 17 | return null; 18 | }, 19 | }; 20 | 21 | const rootAddWidgets = jest.fn(); 22 | 23 | const wrapper = mount({ 24 | components: { Index, ChildComponent }, 25 | data() { 26 | return { props: { indexName: 'something' } }; 27 | }, 28 | provide: { 29 | $_ais_instantSearchInstance: { 30 | mainIndex: { addWidgets: rootAddWidgets }, 31 | }, 32 | }, 33 | template: ` 34 | 35 | 36 | 37 | `, 38 | }); 39 | 40 | const indexWidget = wrapper.findComponent(Index).vm.widget; 41 | expect(indexWidget.getWidgets()).toContain(widgetInstance); 42 | 43 | expect(rootAddWidgets).toHaveBeenCalledTimes(1); 44 | expect(rootAddWidgets).toHaveBeenCalledWith([ 45 | expect.objectContaining({ $$type: 'ais.index' }), 46 | ]); 47 | }); 48 | 49 | it('child widgets render with right data', () => { 50 | const widgetInstance = { 51 | init: jest.fn(), 52 | render: jest.fn(), 53 | }; 54 | 55 | const ChildComponent = { 56 | name: 'child', 57 | mixins: [createWidgetMixin({ connector: () => () => widgetInstance })], 58 | render() { 59 | return null; 60 | }, 61 | }; 62 | 63 | const search = instantsearch({ 64 | indexName: 'root index', 65 | searchClient: createFakeClient(), 66 | }); 67 | 68 | const wrapper = mount({ 69 | components: { Index, ChildComponent }, 70 | data() { 71 | return { props: { indexName: 'something' } }; 72 | }, 73 | provide: { 74 | $_ais_instantSearchInstance: search, 75 | }, 76 | template: ` 77 | 78 | 79 | 80 | `, 81 | }); 82 | 83 | search.start(); 84 | 85 | const indexWidget = wrapper.findComponent(Index).vm.widget; 86 | 87 | expect(indexWidget.getWidgets()).toContain(widgetInstance); 88 | 89 | expect(widgetInstance.render).not.toHaveBeenCalled(); 90 | expect(widgetInstance.init).toHaveBeenCalledTimes(1); 91 | 92 | expect(widgetInstance.init).toHaveBeenCalledWith( 93 | expect.objectContaining({ 94 | createURL: expect.any(Function), 95 | helper: expect.any(Object), 96 | instantSearchInstance: search, 97 | parent: indexWidget, 98 | state: expect.any(Object), 99 | templatesConfig: expect.any(Object), 100 | uiState: {}, 101 | }) 102 | ); 103 | }); 104 | -------------------------------------------------------------------------------- /src/components/__tests__/Index.js: -------------------------------------------------------------------------------- 1 | import { mount } from '../../../test/utils'; 2 | import Index from '../Index'; 3 | import { __setWidget } from '../../mixins/widget'; 4 | import { Vue2, isVue3, isVue2 } from '../../util/vue-compat'; 5 | jest.mock('../../mixins/widget'); 6 | 7 | beforeEach(() => { 8 | jest.resetAllMocks(); 9 | }); 10 | 11 | it('passes props to widgetParams', () => { 12 | const wrapper = mount(Index, { 13 | propsData: { 14 | indexName: 'the name', 15 | indexId: 'the id', 16 | }, 17 | }); 18 | 19 | expect(wrapper.vm.widgetParams).toEqual({ 20 | indexName: 'the name', 21 | indexId: 'the id', 22 | }); 23 | }); 24 | 25 | it('renders just a div by default', () => { 26 | const wrapper = mount(Index, { 27 | propsData: { 28 | indexName: 'index name', 29 | }, 30 | }); 31 | 32 | expect(wrapper.html()).toMatchInlineSnapshot(` 33 |
34 |
35 | `); 36 | }); 37 | 38 | it('renders its children', () => { 39 | const wrapper = mount(Index, { 40 | propsData: { 41 | indexName: 'index name', 42 | }, 43 | slots: { 44 | default: '
hi there!
', 45 | }, 46 | }); 47 | 48 | expect(wrapper.html()).toMatchInlineSnapshot(` 49 |
50 |
51 | hi there! 52 |
53 |
54 | `); 55 | }); 56 | 57 | it('provides the index widget', done => { 58 | const indexWidget = { $$type: 'ais.index' }; 59 | __setWidget(indexWidget); 60 | 61 | const ChildComponent = { 62 | inject: ['$_ais_getParentIndex'], 63 | mounted() { 64 | this.$nextTick(() => { 65 | expect(typeof this.$_ais_getParentIndex).toBe('function'); 66 | expect(this.$_ais_getParentIndex()).toEqual(indexWidget); 67 | done(); 68 | }); 69 | }, 70 | render() { 71 | return null; 72 | }, 73 | }; 74 | 75 | if (isVue2) { 76 | Vue2.config.errorHandler = done; 77 | } 78 | 79 | mount( 80 | { 81 | components: { Index, ChildComponent }, 82 | template: ` 83 | 84 | 85 | 86 | `, 87 | }, 88 | isVue3 && { 89 | global: { 90 | config: { 91 | errorHandler: done, 92 | }, 93 | }, 94 | } 95 | ); 96 | }); 97 | -------------------------------------------------------------------------------- /src/components/__tests__/Panel.js: -------------------------------------------------------------------------------- 1 | import { mount } from '../../../test/utils'; 2 | import Panel from '../Panel.vue'; 3 | 4 | describe('default render', () => { 5 | const defaultSlot = ` 6 |

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 of
 by 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 |
17 |
18 |       {
19 |   "text": "this is user data"
20 | }
21 |     
22 |
23 |
24 |
25 |       {
26 |   "text": "this too!"
27 | }
28 |     
29 |
30 |
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 |
24 |
25 |
26 | 31 |
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 |
43 |
44 |
45 | 50 |
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 | 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 | 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 |

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 | 19 | 20 | indices 21 | 22 | : 23 | 24 |
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 |
40 |
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 | 11 | `; 12 | 13 | exports[`custom default render renders correctly with an URL for the href 1`] = ` 14 |
15 |
16 | 17 | Clear refinements 18 | 19 |
20 |
21 | `; 22 | 23 | exports[`custom default render renders correctly without refinement 1`] = ` 24 |
25 | 30 |
31 | `; 32 | 33 | exports[`custom resetLabel render renders correctly with a custom reset label 1`] = ` 34 |
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 | 26 | ten 27 | 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 |
    6 |
  1. 7 | objectID: one, index: 0 8 |
  2. 9 |
  3. 10 | objectID: two, index: 1 11 |
  4. 12 |
13 |
14 | `; 15 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/HitsPerPage.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 |
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 |
11 | Hi there, this is the main slot 12 |
13 |
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 |
5 |
6 |

7 | This is the body of the Panel. 8 |

9 |
10 |
11 | `; 12 | 13 | exports[`default render renders correctly with footer 1`] = ` 14 |
15 |
16 |

17 | This is the body of the Panel. 18 |

19 |
20 | 25 |
26 | `; 27 | 28 | exports[`default render renders correctly with header 1`] = ` 29 |
30 |
31 | 32 | Header 33 | 34 |
35 |
36 |

37 | This is the body of the Panel. 38 |

39 |
40 |
41 | `; 42 | 43 | exports[`default render renders correctly without refinement 1`] = ` 44 |
45 |
46 |

47 | This is the body of the Panel. 48 |

49 |
50 |
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 | 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 | 26 | ten 27 | 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 |

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 |
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 |
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 |
40 |

41 |

42 |
43 |
44 | `; 45 | 46 | exports[`Rendering with custom template for status 1`] = ` 47 |
48 |
49 |

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 |
68 |
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 "" 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 |
15 | 16 |
17 | 18 | TIP: type something first 19 |
20 | `, 21 | })) 22 | .add('not clearing "brand"', () => ({ 23 | template: ` 24 |
25 |
26 | 27 |
28 | 35 |
36 | 37 |
38 |
39 | `, 40 | })) 41 | .add('with a custom label', () => ({ 42 | template: ` 43 | 44 | 45 | 46 | `, 47 | })) 48 | .add('with a custom render', () => ({ 49 | template: ` 50 | 51 | 59 | 60 | `, 61 | })) 62 | .add('with a Panel', () => ({ 63 | template: ` 64 | 65 | 66 | 67 | 68 | 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: '', 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 | 31 | 32 |
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 | 46 | 52 | 53 | `, 54 | })) 55 | .add('with display of the parameters', () => ({ 56 | template: ` 57 | 58 | 61 | 62 | `, 63 | })) 64 | .add('merging parameters', () => ({ 65 | template: ` 66 | 67 | 81 | 82 | `, 83 | })) 84 | .add('playground', () => ({ 85 | template: ` 86 | 87 | 90 | 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 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | `, 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 |
9 | 10 | 16 | 17 |
18 | `, 19 | })) 20 | .add('with array value', () => ({ 21 | template: ` 22 |
23 | 24 | 31 | 32 |
33 | `, 34 | })) 35 | .add('with highlighted tag name', () => ({ 36 | template: ` 37 |
38 | 39 | 49 | 50 |
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 | 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 | 68 | `, 69 | })) 70 | .add('with a Panel', () => ({ 71 | template: ` 72 | 73 | 74 | 80 | 81 | 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 |
8 | 9 | 10 | 11 |
12 | 13 | 16 | 17 |
18 |
19 | 20 | 21 | 24 | 25 | 26 |
27 |
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 |
41 | 42 | 43 | 44 |
45 | 46 | 50 | 51 | 54 | 55 |
56 | 57 | 58 | 59 | 62 | 63 | 64 | 65 |
66 |
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 | 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 | 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 | 83 | 84 | `, 85 | })) 86 | .add('with a Panel', () => ({ 87 | template: ` 88 | 89 | 90 | 91 | 92 | 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 | 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 | 70 | 71 | `, 72 | })) 73 | .add('with a Panel', () => ({ 74 | template: ` 75 | 76 | 77 | 87 | 88 | 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 | 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 | 44 | `, 45 | })) 46 | .add('with named slots', () => ({ 47 | template: ` 48 | 49 | 57 | 65 | 66 | 75 | 83 | 91 | `, 92 | })) 93 | .add('with a Panel', () => ({ 94 | template: ` 95 | 96 | 97 | 98 | 99 | 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 | 9 | 10 | 11 | 12 | `, 13 | })) 14 | .add('text content', () => ({ 15 | template: ` 16 | 17 | This is the body of the Panel. 18 | 19 | `, 20 | })) 21 | .add('with header', () => ({ 22 | template: ` 23 | 24 | 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 | 34 | 35 | `, 36 | })) 37 | .add('with header & footer', () => ({ 38 | template: ` 39 | 40 | 41 | This is the body of the Panel. 42 | 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 | 10 |
`, 11 | })) 12 | .add('dark', () => ({ 13 | template: ` 14 |
15 | 16 |
`, 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 | 13 | `, 14 | })) 15 | .add('custom rendering', () => ({ 16 | template: ` 17 |
18 | 19 | 29 | 30 |
`, 31 | })) 32 | .add('with a Panel', () => ({ 33 | template: ` 34 | 35 | 36 | 37 | 38 | 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 | 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 | 54 | `, 55 | })) 56 | .add('full custom rendering', () => ({ 57 | template: ` 58 | 59 | 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 | 29 | 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 | 25 | 26 | `, 27 | })) 28 | .add('with custom rendering of icons', () => ({ 29 | template: ` 30 | 31 | 34 | 37 | 40 | 41 | `, 42 | })) 43 | .add('with a Panel', () => ({ 44 | template: ` 45 | 46 | 47 | 48 | 49 | 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 |
9 | 13 | 14 | 15 | 23 | 24 |
25 | `, 26 | })) 27 | .add('with highlighted tag name', () => ({ 28 | template: ` 29 |
30 | 34 | 35 | 36 | 44 | 45 |
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 | 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 | 56 | 57 | `, 58 | })) 59 | .add('with a Panel', () => ({ 60 | template: ` 61 | 62 | 63 | 70 | 71 | 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 | 9 | `, 10 | })) 11 | .add('custom rendering', () => ({ 12 | template: ` 13 | 14 | 17 | 18 | `, 19 | })) 20 | .add('with a Panel', () => ({ 21 | template: ` 22 | 23 | 24 | 25 | 26 | 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 | 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 | 53 | 54 | `, 55 | })) 56 | .add('with a Panel', () => ({ 57 | template: ` 58 | 59 | 60 | 64 | 65 | 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: ``, 10 | // })) 11 | // .add('clearing query', () => ({ 12 | // template: `
13 | // 14 | //
`, 15 | // })) 16 | // .add('custom rendering', () => ({ 17 | // template: ` 18 | //
19 | // Clear search query 20 | //
21 | //
`, 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 | 29 | `, 30 | filters = ` 31 | 32 | 33 | `, 34 | routing, 35 | } = {}) => () => ({ 36 | template: ` 37 | 43 |
44 | 45 |
46 | 47 |
48 |
49 | ${filters} 50 |
51 |
52 | 53 | 54 | 55 | ${hits} 56 | 57 | 58 |
59 |
60 |
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 | --------------------------------------------------------------------------------