diff --git a/package.json b/package.json
index e164ef427..ef7023a81 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/__tests__/index.js b/src/__tests__/index.js
index 7917651e8..9b2aa4652 100644
--- a/src/__tests__/index.js
+++ b/src/__tests__/index.js
@@ -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)}`);
}
diff --git a/src/components/DynamicWidgets.js b/src/components/DynamicWidgets.js
new file mode 100644
index 000000000..42007c714
--- /dev/null
+++ b/src/components/DynamicWidgets.js
@@ -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: [],
+ };
+ },
+ },
+};
diff --git a/src/components/__tests__/DynamicWidgets.js b/src/components/__tests__/DynamicWidgets.js
new file mode 100644
index 000000000..bd23d0853
--- /dev/null
+++ b/src/components/__tests__/DynamicWidgets.js
@@ -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: `
+