From a0cd78696a07f9f985c0d7258fa8336b64439345 Mon Sep 17 00:00:00 2001 From: Aymeric Giraudet Date: Tue, 5 Dec 2023 15:19:02 +0100 Subject: [PATCH 1/4] poc: common JSX components --- packages/instantsearch-jsx/package.json | 52 ++++++++++ packages/instantsearch-jsx/src/Hits.tsx | 94 +++++++++++++++++++ packages/instantsearch-jsx/src/index.ts | 1 + .../tsconfig.declaration.json | 3 + packages/react-instantsearch/package.json | 1 + packages/react-instantsearch/src/ui/Hits.tsx | 84 +---------------- .../react-instantsearch/src/widgets/Hits.tsx | 2 +- packages/vue-instantsearch/package.json | 1 + .../vue-instantsearch/src/components/Hits.js | 57 +++++++++++ .../vue-instantsearch/src/components/Hits.vue | 72 -------------- packages/vue-instantsearch/src/widgets.js | 2 +- 11 files changed, 216 insertions(+), 153 deletions(-) create mode 100644 packages/instantsearch-jsx/package.json create mode 100644 packages/instantsearch-jsx/src/Hits.tsx create mode 100644 packages/instantsearch-jsx/src/index.ts create mode 100644 packages/instantsearch-jsx/tsconfig.declaration.json create mode 100644 packages/vue-instantsearch/src/components/Hits.js delete mode 100644 packages/vue-instantsearch/src/components/Hits.vue diff --git a/packages/instantsearch-jsx/package.json b/packages/instantsearch-jsx/package.json new file mode 100644 index 0000000000..a5d62919f8 --- /dev/null +++ b/packages/instantsearch-jsx/package.json @@ -0,0 +1,52 @@ +{ + "name": "instantsearch-jsx", + "version": "1.0.0", + "description": "Common JSX components for InstantSearch flavors", + "source": "src/index.ts", + "types": "dist/es/index.d.ts", + "main": "dist/cjs/index.js", + "module": "dist/es/index.js", + "type": "module", + "exports": { + ".": { + "import": "./dist/es/index.js", + "require": "./dist/cjs/index.js" + } + }, + "sideEffects": false, + "license": "MIT", + "homepage": "https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/react/", + "repository": { + "type": "git", + "url": "https://github.com/algolia/instantsearch" + }, + "author": { + "name": "Algolia, Inc.", + "url": "https://www.algolia.com" + }, + "keywords": [ + "algolia", + "components", + "fast", + "instantsearch", + "react", + "search" + ], + "files": [ + "README.md", + "dist" + ], + "scripts": { + "clean": "rm -rf dist", + "watch": "yarn build:cjs --watch", + "build": "yarn build:cjs && yarn build:es && yarn build:types", + "build:cjs": "BABEL_ENV=cjs babel src --root-mode upward --extensions '.js,.ts,.tsx' --out-dir dist/cjs --ignore '**/__tests__/**/*','**/__mocks__/**/*' --quiet && ../../scripts/prepare-cjs.sh", + "build:es": "BABEL_ENV=es babel src --root-mode upward --extensions '.js,.ts,.tsx' --out-dir dist/es --ignore '**/__tests__/**/*','**/__mocks__/**/*' --quiet", + "build:types": "tsc -p ./tsconfig.declaration.json --outDir ./dist/es", + "test:exports": "node ./test/module/is-es-module.mjs && node ./test/module/is-cjs-module.cjs", + "version": "./scripts/version.cjs" + }, + "dependencies": { + "@babel/runtime": "^7.1.2" + } +} diff --git a/packages/instantsearch-jsx/src/Hits.tsx b/packages/instantsearch-jsx/src/Hits.tsx new file mode 100644 index 0000000000..2c59de0e45 --- /dev/null +++ b/packages/instantsearch-jsx/src/Hits.tsx @@ -0,0 +1,94 @@ +/* eslint-disable no-nested-ternary */ +/** @jsx createElement */ + +export type HitsClassNames = { + /** + * Class names to apply to the root element + */ + root: string; + /** + * Class names to apply to the root element without results + */ + emptyRoot: string; + /** + * Class names to apply to the list element + */ + list: string; + /** + * Class names to apply to each item element + */ + item: string; +}; + +type BaseHit = { + objectID: string; +}; + +export type HitsProps = { + hitComponent: any; + hitSlot?: any; + hits: T[]; + className?: string; + classNames?: Partial; + sendEvent: (eventName: string, hit: T, event: string) => void; +}; + +export function cx( + ...classNames: Array +) { + return classNames.filter(Boolean).join(' '); +} + +export function createHits({ createElement }: any) { + function DefaultHitComponent({ hit }: { hit: BaseHit }) { + return ( +
+ {JSON.stringify(hit).slice(0, 100)}… +
+ ); + } + + return function Hits({ + hitComponent: HitComponent, + hitSlot, + classNames = {}, + hits, + sendEvent, + ...props + }: HitsProps) { + return ( +
+
    + {hits.map((hit) => ( +
  1. { + sendEvent('click:internal', hit, 'Hit Clicked'); + }} + onAuxClick={() => { + sendEvent('click:internal', hit, 'Hit Clicked'); + }} + > + {HitComponent ? ( + + ) : hitSlot ? ( + hitSlot({ item: hit }) + ) : ( + + )} +
  2. + ))} +
+
+ ); + }; +} diff --git a/packages/instantsearch-jsx/src/index.ts b/packages/instantsearch-jsx/src/index.ts new file mode 100644 index 0000000000..c01658da47 --- /dev/null +++ b/packages/instantsearch-jsx/src/index.ts @@ -0,0 +1 @@ +export * from './Hits'; diff --git a/packages/instantsearch-jsx/tsconfig.declaration.json b/packages/instantsearch-jsx/tsconfig.declaration.json new file mode 100644 index 0000000000..1e0c6449f8 --- /dev/null +++ b/packages/instantsearch-jsx/tsconfig.declaration.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.declaration" +} diff --git a/packages/react-instantsearch/package.json b/packages/react-instantsearch/package.json index 1a453ba081..2b3842821e 100644 --- a/packages/react-instantsearch/package.json +++ b/packages/react-instantsearch/package.json @@ -49,6 +49,7 @@ "dependencies": { "@babel/runtime": "^7.1.2", "instantsearch.js": "4.61.0", + "instantsearch-jsx": "1.0.0", "react-instantsearch-core": "7.4.0" }, "peerDependencies": { diff --git a/packages/react-instantsearch/src/ui/Hits.tsx b/packages/react-instantsearch/src/ui/Hits.tsx index 19c44ebcf7..c6a10c44fc 100644 --- a/packages/react-instantsearch/src/ui/Hits.tsx +++ b/packages/react-instantsearch/src/ui/Hits.tsx @@ -1,80 +1,6 @@ -import React from 'react'; +import { createHits } from 'instantsearch-jsx'; +import { createElement } from 'react'; -import { cx } from './lib/cx'; - -import type { Hit } from 'instantsearch.js'; -import type { SendEventForHits } from 'instantsearch.js/es/lib/utils'; - -export type HitsProps = React.ComponentProps<'div'> & { - hits: THit[]; - sendEvent: SendEventForHits; - hitComponent?: React.JSXElementConstructor<{ - hit: THit; - sendEvent: SendEventForHits; - }>; - classNames?: Partial; -}; - -function DefaultHitComponent({ hit }: { hit: Hit }) { - return ( -
- {JSON.stringify(hit).slice(0, 100)}… -
- ); -} - -export type HitsClassNames = { - /** - * Class names to apply to the root element - */ - root: string; - /** - * Class names to apply to the root element without results - */ - emptyRoot: string; - /** - * Class names to apply to the list element - */ - list: string; - /** - * Class names to apply to each item element - */ - item: string; -}; - -export function Hits({ - hits, - sendEvent, - hitComponent: HitComponent = DefaultHitComponent, - classNames = {}, - ...props -}: HitsProps) { - return ( -
-
    - {hits.map((hit) => ( -
  1. { - sendEvent('click:internal', hit, 'Hit Clicked'); - }} - onAuxClick={() => { - sendEvent('click:internal', hit, 'Hit Clicked'); - }} - > - -
  2. - ))} -
-
- ); -} +export const Hits = createHits({ + createElement, +}); diff --git a/packages/react-instantsearch/src/widgets/Hits.tsx b/packages/react-instantsearch/src/widgets/Hits.tsx index 5a99c67b67..e7cff6cbe3 100644 --- a/packages/react-instantsearch/src/widgets/Hits.tsx +++ b/packages/react-instantsearch/src/widgets/Hits.tsx @@ -3,7 +3,7 @@ import { useHits } from 'react-instantsearch-core'; import { Hits as HitsUiComponent } from '../ui/Hits'; -import type { HitsProps as HitsUiComponentProps } from '../ui/Hits'; +import type { HitsProps as HitsUiComponentProps } from 'instantsearch-jsx'; import type { Hit, BaseHit } from 'instantsearch.js'; import type { UseHitsProps } from 'react-instantsearch-core'; diff --git a/packages/vue-instantsearch/package.json b/packages/vue-instantsearch/package.json index c4f1a6f3d7..1e9d66fe31 100644 --- a/packages/vue-instantsearch/package.json +++ b/packages/vue-instantsearch/package.json @@ -36,6 +36,7 @@ }, "dependencies": { "instantsearch.js": "4.61.0", + "instantsearch-jsx": "1.0.0", "mitt": "^2.1.0" }, "peerDependencies": { diff --git a/packages/vue-instantsearch/src/components/Hits.js b/packages/vue-instantsearch/src/components/Hits.js new file mode 100644 index 0000000000..f4edbfd50c --- /dev/null +++ b/packages/vue-instantsearch/src/components/Hits.js @@ -0,0 +1,57 @@ +import { createHits } from 'instantsearch-jsx'; +import { connectHitsWithInsights } from 'instantsearch.js/es/connectors'; + +import { createSuitMixin } from '../mixins/suit'; +import { createWidgetMixin } from '../mixins/widget'; +import { renderCompat } from '../util/vue-compat'; + +const augmentH = (baseH) => (tag, propsWithClassName, children) => { + const { className, ...props } = propsWithClassName; + return baseH(tag, Object.assign(props, { class: className }), [children]); +}; + +export default { + name: 'AisHits', + mixins: [ + createWidgetMixin( + { + connector: connectHitsWithInsights, + }, + { + $$widgetType: 'ais.hits', + } + ), + createSuitMixin({ name: 'Hits' }), + ], + props: { + escapeHTML: { + type: Boolean, + default: true, + }, + transformItems: { + type: Function, + default: undefined, + }, + }, + computed: { + items() { + return this.state.hits; + }, + widgetParams() { + return { + escapeHTML: this.escapeHTML, + transformItems: this.transformItems, + }; + }, + }, + render: renderCompat(function (baseH) { + if (!this.state) return null; + + const h = augmentH(baseH); + + return createHits({ createElement: h })({ + hits: this.state.hits, + hitSlot: this.$scopedSlots.item, + }); + }), +}; diff --git a/packages/vue-instantsearch/src/components/Hits.vue b/packages/vue-instantsearch/src/components/Hits.vue deleted file mode 100644 index 3139b09beb..0000000000 --- a/packages/vue-instantsearch/src/components/Hits.vue +++ /dev/null @@ -1,72 +0,0 @@ - - - diff --git a/packages/vue-instantsearch/src/widgets.js b/packages/vue-instantsearch/src/widgets.js index 8f0b562738..1a04225e07 100644 --- a/packages/vue-instantsearch/src/widgets.js +++ b/packages/vue-instantsearch/src/widgets.js @@ -6,7 +6,7 @@ export { default as AisExperimentalConfigureRelatedItems } from './components/Co export { default as AisCurrentRefinements } from './components/CurrentRefinements.vue'; export { default as AisHierarchicalMenu } from './components/HierarchicalMenu.vue'; export { default as AisHighlight } from './components/Highlight.vue'; -export { default as AisHits } from './components/Hits.vue'; +export { default as AisHits } from './components/Hits'; export { default as AisHitsPerPage } from './components/HitsPerPage.vue'; export { default as AisIndex } from './components/Index'; export { default as AisInstantSearch } from './components/InstantSearch'; From a4b6ff7d4efff1f8399676b053e5765e822507d7 Mon Sep 17 00:00:00 2001 From: Aymeric Giraudet Date: Wed, 6 Dec 2023 11:41:07 +0100 Subject: [PATCH 2/4] vue3 support --- .../vue-instantsearch/src/components/Hits.js | 21 +++++++++---------- .../src/util/vue-compat/index-vue2.js | 15 ++++++++++++- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/packages/vue-instantsearch/src/components/Hits.js b/packages/vue-instantsearch/src/components/Hits.js index f4edbfd50c..fcc129f2a1 100644 --- a/packages/vue-instantsearch/src/components/Hits.js +++ b/packages/vue-instantsearch/src/components/Hits.js @@ -3,12 +3,7 @@ import { connectHitsWithInsights } from 'instantsearch.js/es/connectors'; import { createSuitMixin } from '../mixins/suit'; import { createWidgetMixin } from '../mixins/widget'; -import { renderCompat } from '../util/vue-compat'; - -const augmentH = (baseH) => (tag, propsWithClassName, children) => { - const { className, ...props } = propsWithClassName; - return baseH(tag, Object.assign(props, { class: className }), [children]); -}; +import { renderCompat, isVue2 } from '../util/vue-compat'; export default { name: 'AisHits', @@ -44,14 +39,18 @@ export default { }; }, }, - render: renderCompat(function (baseH) { + render: renderCompat(function (h) { if (!this.state) return null; - const h = augmentH(baseH); - - return createHits({ createElement: h })({ + return h(createHits({ createElement: h }), { hits: this.state.hits, - hitSlot: this.$scopedSlots.item, + hitSlot: isVue2 ? this.$scopedSlots.item : this.$slots.item, + sendEvent: this.state.sendEvent, + classNames: { + item: this.suit('item'), + list: this.suit('list'), + root: this.suit(), + }, }); }), }; diff --git a/packages/vue-instantsearch/src/util/vue-compat/index-vue2.js b/packages/vue-instantsearch/src/util/vue-compat/index-vue2.js index 530fb21a45..7695cf044d 100644 --- a/packages/vue-instantsearch/src/util/vue-compat/index-vue2.js +++ b/packages/vue-instantsearch/src/util/vue-compat/index-vue2.js @@ -7,9 +7,22 @@ const version = Vue.version; export { Vue, Vue2, isVue2, isVue3, version }; +const augmentCreateElement = + (createElement) => (tag, propsWithClassName, children) => { + const { className, ...props } = propsWithClassName; + + if (typeof tag === 'function') { + return tag(Object.assign(props, { class: className, children })); + } + + return createElement(tag, Object.assign(props, { class: className }), [ + children, + ]); + }; + export function renderCompat(fn) { return function (createElement) { - return fn.call(this, createElement); + return fn.call(this, augmentCreateElement(createElement)); }; } From 6ba6407c0c3183f4ddbca40ff0f90cd0bbf1d9e8 Mon Sep 17 00:00:00 2001 From: Aymeric Giraudet Date: Thu, 7 Dec 2023 11:15:28 +0100 Subject: [PATCH 3/4] preact --- babel.config.js | 3 + packages/instantsearch-jsx/src/Hits.tsx | 54 +++++++------ packages/instantsearch.js/package.json | 1 + .../src/components/Hits/Hits.tsx | 76 ++++++++++--------- .../vue-instantsearch/src/components/Hits.js | 2 +- 5 files changed, 75 insertions(+), 61 deletions(-) diff --git a/babel.config.js b/babel.config.js index facc811fe7..970ed58edc 100644 --- a/babel.config.js +++ b/babel.config.js @@ -79,6 +79,9 @@ module.exports = (api) => { // false positive (babel doesn't know types) // this is actually only called on arrays 'String.prototype.includes', + + // just for the PoC + 'Object.assign', ]; if (defaultShouldInject && !exclude.includes(name)) { throw new Error( diff --git a/packages/instantsearch-jsx/src/Hits.tsx b/packages/instantsearch-jsx/src/Hits.tsx index 2c59de0e45..ffa8015a4c 100644 --- a/packages/instantsearch-jsx/src/Hits.tsx +++ b/packages/instantsearch-jsx/src/Hits.tsx @@ -1,4 +1,3 @@ -/* eslint-disable no-nested-ternary */ /** @jsx createElement */ export type HitsClassNames = { @@ -25,8 +24,12 @@ type BaseHit = { }; export type HitsProps = { - hitComponent: any; - hitSlot?: any; + hitComponent?: (props: { + hit: T; + item: T; + sendEvent: (eventName: string, hit: T, event: string) => void; + }) => JSX.Element; + itemComponent?: (props: { hit: T; index: number }) => JSX.Element; hits: T[]; className?: string; classNames?: Partial; @@ -50,7 +53,7 @@ export function createHits({ createElement }: any) { return function Hits({ hitComponent: HitComponent, - hitSlot, + itemComponent: ItemComponent, classNames = {}, hits, sendEvent, @@ -67,26 +70,29 @@ export function createHits({ createElement }: any) { )} >
    - {hits.map((hit) => ( -
  1. { - sendEvent('click:internal', hit, 'Hit Clicked'); - }} - onAuxClick={() => { - sendEvent('click:internal', hit, 'Hit Clicked'); - }} - > - {HitComponent ? ( - - ) : hitSlot ? ( - hitSlot({ item: hit }) - ) : ( - - )} -
  2. - ))} + {hits.map((hit, index) => + ItemComponent ? ( + + ) : ( +
  3. { + sendEvent('click:internal', hit, 'Hit Clicked'); + }} + onAuxClick={() => { + sendEvent('click:internal', hit, 'Hit Clicked'); + }} + > + {HitComponent ? ( + // Vue uses `item` and React uses `hit` + + ) : ( + + )} +
  4. + ) + )}
); diff --git a/packages/instantsearch.js/package.json b/packages/instantsearch.js/package.json index 071c9e63b2..71626c1893 100644 --- a/packages/instantsearch.js/package.json +++ b/packages/instantsearch.js/package.json @@ -34,6 +34,7 @@ "@types/hogan.js": "^3.0.0", "@types/qs": "^6.5.3", "algoliasearch-helper": "3.15.0", + "instantsearch-jsx": "1.0.0", "hogan.js": "^3.0.2", "htm": "^3.0.0", "preact": "^10.10.0", diff --git a/packages/instantsearch.js/src/components/Hits/Hits.tsx b/packages/instantsearch.js/src/components/Hits/Hits.tsx index 414757f00b..b3cedc7259 100644 --- a/packages/instantsearch.js/src/components/Hits/Hits.tsx +++ b/packages/instantsearch.js/src/components/Hits/Hits.tsx @@ -1,6 +1,7 @@ /** @jsx h */ import { cx } from '@algolia/ui-components-shared'; +import { createHits } from 'instantsearch-jsx'; import { h } from 'preact'; import { createInsightsEventHandler } from '../../lib/insights/listener'; @@ -26,13 +27,15 @@ export type HitsProps = { templateProps: PreparedTemplateProps; }; +const UiHits = createHits({ createElement: h }); + export default function Hits({ results, hits, insights, - bindEvent, sendEvent, cssClasses, + bindEvent, templateProps, }: HitsProps) { const handleInsightsClick = createInsightsEventHandler({ @@ -55,40 +58,41 @@ export default function Hits({ } return ( -
-
    - {hits.map((hit, index) => ( -