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

Commit

Permalink
feat(ais-dynamic-widgets): add implementation
Browse files Browse the repository at this point in the history
Adds a new widget _ais-experimental-dynamic-widgets_ that can be used to conditionally render other widgets. This condition is based on the search results. At the moment there isn't yet a default way to enforce facet ordering in the Algolia engine, thus the data available to `transformItems` is an empty array. This will change once the Algolia engine adds support for facet ordering and this widget will move out of experimental mode.

```vue
<template>
  <ais-experimental-dynamic-widgets :transform-items="transformItems">
    <ais-refinement-list attribute="test1" />
    <ais-menu attribute="test2" />
    <ais-panel>
      <ais-hierarchical-menu :attributes="hierarchicalAttributes" />
    </ais-panel>
  </ais-experimental-dynamic-widgets>
</template>

<script>
export default {
  data() {
    return {
      hierarchicalAttributes: ["test3", "unused"],
    };
  },
  methods(_attributes, { results }) {
    // add a condition based on the results, eg. if you add the ordering via a query rule:
    return results.userData[0].facetOrdering;
  },
};
</script>
```

See also: algolia/instantsearch#4687
  • Loading branch information
Haroenv committed May 7, 2021
1 parent 118e0a3 commit 27ecb1f
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 5 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
},
"dependencies": {
"algoliasearch-helper": "^3.1.0",
"instantsearch.js": "^4.16.1"
"instantsearch.js": "^4.22.0"
},
"peerDependencies": {
"algoliasearch": ">= 3.32.0 < 5",
Expand Down
2 changes: 2 additions & 0 deletions src/__tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ it('should have `name` the same as the suit class name everywhere', () => {
expect(installedName).toBe(name);
if (name === 'AisInstantSearchSsr') {
expect(suitClass).toBe(`ais-InstantSearch`);
} else if (name === 'AisExperimentalDynamicWidgets') {
expect(suitClass).toBe(`ais-DynamicWidgets`);
} else {
expect(suitClass).toBe(`ais-${name.substr(3)}`);
}
Expand Down
89 changes: 89 additions & 0 deletions src/components/DynamicWidgets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { createWidgetMixin } from '../mixins/widget';
import { EXPERIMENTAL_connectDynamicWidgets } from 'instantsearch.js/es/connectors';
import { createSuitMixin } from '../mixins/suit';

function getVueAttribute(vnode) {
if (vnode.componentOptions && vnode.componentOptions.propsData) {
if (vnode.componentOptions.propsData.attribute) {
return vnode.componentOptions.propsData.attribute;
}
if (
vnode.componentOptions &&
Array.isArray(vnode.componentOptions.propsData.attributes)
) {
return vnode.componentOptions.propsData.attributes[0];
}
}

const children =
vnode.componentOptions && vnode.componentOptions.children
? vnode.componentOptions.children
: vnode.children;

if (Array.isArray(children)) {
// return first child with a truthy attribute
return children.reduce(
(acc, curr) => acc || getVueAttribute(curr),
undefined
);
}

return undefined;
}

export default {
name: 'AisExperimentalDynamicWidgets',
mixins: [
createWidgetMixin({ connector: EXPERIMENTAL_connectDynamicWidgets }),
createSuitMixin({ name: 'DynamicWidgets' }),
],
props: {
transformItems: {
type: Function,
required: true,
},
},
render(createElement) {
const components = new Map();
(this.$slots.default || []).forEach(vnode => {
const attribute = getVueAttribute(vnode);
if (attribute) {
components.set(
attribute,
createElement(
'div',
{ key: attribute, class: [this.suit('widget')] },
[vnode]
)
);
}
});

// by default, render everything, but hidden so that the routing doesn't disappear
if (!this.state) {
const allComponents = [];
components.forEach(vnode => allComponents.push(vnode));

return createElement(
'div',
{ attrs: { hidden: true }, class: [this.suit()] },
allComponents
);
}

return createElement(
'div',
{ class: [this.suit()] },
this.state.attributesToRender.map(attribute => components.get(attribute))
);
},
computed: {
widgetParams() {
return {
transformItems: this.transformItems,
// we do not pass "widgets" to the connector, since Vue is in charge of rendering
widgets: [],
};
},
},
};
178 changes: 178 additions & 0 deletions src/components/__tests__/DynamicWidgets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { createLocalVue, mount } from '@vue/test-utils';
import DynamicWidgets from '../DynamicWidgets';
import { __setState } from '../../mixins/widget';
import { plugin } from '../../plugin';
jest.mock('../../mixins/widget');

it('renders all children without state', () => {
const localVue = createLocalVue();

localVue.use(plugin);

__setState(null);

const wrapper = mount(DynamicWidgets, {
localVue,
propsData: {
transformItems: items => items,
},
slots: {
default: `
<ais-refinement-list attribute="test1"/>
<ais-refinement-list attribute="test2"/>
<ais-panel>
<ais-hierarchical-menu :attributes="['test3', 'test3']" />
</ais-panel>
`,
},
});

// the inner widgets don't render anything because state is falsy, but they are mounted
expect(wrapper.html()).toMatchInlineSnapshot(`
<div hidden="hidden"
class="ais-DynamicWidgets"
>
<div class="ais-DynamicWidgets-widget">
</div>
<div class="ais-DynamicWidgets-widget">
</div>
<div class="ais-DynamicWidgets-widget">
<div class="ais-Panel">
<div class="ais-Panel-body">
</div>
</div>
</div>
</div>
`);
});

it('renders nothing without children', () => {
__setState({
attributesToRender: [],
});

const wrapper = mount(DynamicWidgets, {
propsData: {
transformItems: items => items,
},
});
expect(wrapper.html()).toMatchInlineSnapshot(`
<div class="ais-DynamicWidgets">
</div>
`);
});

it('renders nothing with empty items', () => {
const localVue = createLocalVue();

localVue.use(plugin);

__setState({
attributesToRender: [],
items: [],
});

const wrapper = mount(DynamicWidgets, {
localVue,
propsData: {
transformItems: items => items,
},
slots: {
default: `<ais-refinement-list attribute="test1"/>`,
},
});

expect(wrapper.html()).toMatchInlineSnapshot(`
<div class="ais-DynamicWidgets">
</div>
`);
});

it('renders attributesToRender 1', () => {
const localVue = createLocalVue();

localVue.use(plugin);

__setState({
attributesToRender: ['test1'],
items: [],
});

localVue.component('test-component', {
props: { attribute: { type: String } },
render(h) {
return h('div', {}, this.attribute);
},
});

const wrapper = mount(DynamicWidgets, {
localVue,
propsData: {
transformItems: items => items,
},
slots: {
default: `
<test-component attribute="test1" />
<ais-refinement-list attribute="test2" />
`,
},
});

expect(wrapper.html()).toMatchInlineSnapshot(`
<div class="ais-DynamicWidgets">
<div class="ais-DynamicWidgets-widget">
<div>
test1
</div>
</div>
</div>
`);
});

it('renders attributesToRender 2', () => {
const localVue = createLocalVue();

localVue.use(plugin);

__setState({
attributesToRender: ['test2'],
items: [],
});

localVue.component('test-component', {
props: { attribute: { type: String } },
render(h) {
return h('div', {}, this.attribute);
},
});

const wrapper = mount(DynamicWidgets, {
localVue,
propsData: {
transformItems: items => items,
},
slots: {
default: `
<test-component attribute="test1" />
<ais-refinement-list attribute="test2" />
`,
},
});

expect(wrapper.html()).toMatchInlineSnapshot(`
<div class="ais-DynamicWidgets">
<div class="ais-DynamicWidgets-widget">
</div>
</div>
`);
});
1 change: 1 addition & 0 deletions src/widgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ export {
} from './components/ToggleRefinement.vue';
export { default as AisVoiceSearch } from './components/VoiceSearch.vue';
export { default as AisRelevantSort } from './components/RelevantSort.vue';
export { default as AisDynamicWidgets } from './components/DynamicWidgets';
36 changes: 36 additions & 0 deletions stories/DynamicWidgets.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { previewWrapper } from './utils';
import { storiesOf } from '@storybook/vue';

storiesOf('ais-dynamic-widgets', module)
.addDecorator(previewWrapper())
.add('simple usage', () => ({
template: `
<ais-experimental-dynamic-widgets :transform-items="transformItems">
<ais-refinement-list attribute="brand"></ais-refinement-list>
<ais-menu attribute="categories"></ais-menu>
<ais-panel>
<template slot="header">hierarchy</template>
<ais-hierarchical-menu :attributes="hierarchicalCategories"></ais-hierarchical-menu>
</ais-panel>
</ais-experimental-dynamic-widgets>`,
data() {
return {
hierarchicalCategories: [
'hierarchicalCategories.lvl0',
'hierarchicalCategories.lvl1',
'hierarchicalCategories.lvl2',
],
};
},
methods: {
transformItems(_attributes, { results }) {
if (results._state.query === 'dog') {
return ['categories'];
}
if (results._state.query === 'lego') {
return ['categories', 'brand'];
}
return ['brand', 'hierarchicalCategories.lvl0', 'categories'];
},
},
}));
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7892,10 +7892,10 @@ instantsearch.css@7.3.1:
resolved "https://registry.yarnpkg.com/instantsearch.css/-/instantsearch.css-7.3.1.tgz#7ab74a8f355091ae040947a9cf5438f379026622"
integrity sha512-/kaMDna5D+Q9mImNBHEhb9HgHATDOFKYii7N1Iwvrj+lmD9gBJLqVhUw67gftq2O0QI330pFza+CRscIwB1wQQ==

instantsearch.js@^4.16.1:
version "4.16.1"
resolved "https://registry.yarnpkg.com/instantsearch.js/-/instantsearch.js-4.16.1.tgz#78e00d2256c89693a94f58ebfc5f0864953b7ec8"
integrity sha512-NfwNOb+Ftj7Y+h6lW7iCd5SXWKIHQZ981ldSddEHbgTexbdJEyxgWhaF8c3HajCySp8wZPD2gj9KpNQy/BsUgQ==
instantsearch.js@^4.22.0:
version "4.22.0"
resolved "https://registry.yarnpkg.com/instantsearch.js/-/instantsearch.js-4.22.0.tgz#d28c7a2be6b399736f8f2edd6f59d474a0f661a4"
integrity sha512-IPjg/EwhDqFpvCJC1g3DF+i2cAr3SQZ9JPFhuKWC2apnGNfiFkPIQKvHx1pFTQm7FlZe262Xcpqi1ag1KVPGpA==
dependencies:
"@types/googlemaps" "^3.39.6"
algoliasearch-helper "^3.4.4"
Expand Down

0 comments on commit 27ecb1f

Please sign in to comment.