Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

poc: common JSX components #5952

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
52 changes: 52 additions & 0 deletions packages/instantsearch-jsx/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
94 changes: 94 additions & 0 deletions packages/instantsearch-jsx/src/Hits.tsx
Original file line number Diff line number Diff line change
@@ -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<T extends BaseHit> = {
hitComponent: any;
hitSlot?: any;
hits: T[];
className?: string;
classNames?: Partial<HitsClassNames>;
sendEvent: (eventName: string, hit: T, event: string) => void;
};

export function cx(
...classNames: Array<string | number | boolean | undefined | null>
) {
return classNames.filter(Boolean).join(' ');
}

export function createHits({ createElement }: any) {
function DefaultHitComponent({ hit }: { hit: BaseHit }) {
return (
<div style={{ wordBreak: 'break-all' }}>
{JSON.stringify(hit).slice(0, 100)}…
</div>
);
}

return function Hits<T extends BaseHit>({
hitComponent: HitComponent,
hitSlot,
classNames = {},
hits,
sendEvent,
...props
}: HitsProps<T>) {
return (
<div
{...props}
className={cx(
'ais-Hits',
classNames.root,
hits.length === 0 && cx('ais-Hits--empty', classNames.emptyRoot),
props.className
)}
>
<ol className={cx('ais-Hits-list', classNames.list)}>
{hits.map((hit) => (
<li
key={hit.objectID}
className={cx('ais-Hits-item', classNames.item)}
onClick={() => {
sendEvent('click:internal', hit, 'Hit Clicked');
}}
onAuxClick={() => {
sendEvent('click:internal', hit, 'Hit Clicked');
}}
>
{HitComponent ? (
<HitComponent hit={hit} sendEvent={sendEvent} />
) : hitSlot ? (
hitSlot({ item: hit })
) : (
<DefaultHitComponent hit={hit} />
)}
</li>
))}
</ol>
</div>
);
};
}
1 change: 1 addition & 0 deletions packages/instantsearch-jsx/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Hits';
3 changes: 3 additions & 0 deletions packages/instantsearch-jsx/tsconfig.declaration.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.declaration"
}
1 change: 1 addition & 0 deletions packages/react-instantsearch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
84 changes: 5 additions & 79 deletions packages/react-instantsearch/src/ui/Hits.tsx
Original file line number Diff line number Diff line change
@@ -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<THit> = React.ComponentProps<'div'> & {
hits: THit[];
sendEvent: SendEventForHits;
hitComponent?: React.JSXElementConstructor<{
hit: THit;
sendEvent: SendEventForHits;
}>;
classNames?: Partial<HitsClassNames>;
};

function DefaultHitComponent({ hit }: { hit: Hit }) {
return (
<div style={{ wordBreak: 'break-all' }}>
{JSON.stringify(hit).slice(0, 100)}…
</div>
);
}

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<THit extends Hit>({
hits,
sendEvent,
hitComponent: HitComponent = DefaultHitComponent,
classNames = {},
...props
}: HitsProps<THit>) {
return (
<div
{...props}
className={cx(
'ais-Hits',
classNames.root,
hits.length === 0 && cx('ais-Hits--empty', classNames.emptyRoot),
props.className
)}
>
<ol className={cx('ais-Hits-list', classNames.list)}>
{hits.map((hit) => (
<li
key={hit.objectID}
className={cx('ais-Hits-item', classNames.item)}
onClick={() => {
sendEvent('click:internal', hit, 'Hit Clicked');
}}
onAuxClick={() => {
sendEvent('click:internal', hit, 'Hit Clicked');
}}
>
<HitComponent hit={hit} sendEvent={sendEvent} />
</li>
))}
</ol>
</div>
);
}
export const Hits = createHits({
createElement,
});
2 changes: 1 addition & 1 deletion packages/react-instantsearch/src/widgets/Hits.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
1 change: 1 addition & 0 deletions packages/vue-instantsearch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
},
"dependencies": {
"instantsearch.js": "4.61.0",
"instantsearch-jsx": "1.0.0",
"mitt": "^2.1.0"
},
"peerDependencies": {
Expand Down
57 changes: 57 additions & 0 deletions packages/vue-instantsearch/src/components/Hits.js
Original file line number Diff line number Diff line change
@@ -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]);
};
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to map className to class and children has to be an array for Vue's h function (they don't have fragments)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vue 2 and 3 are quite different, we have renderCompat, but I wonder if we should just have separate files for vue 2 and vue 3 once we simplify the rendering?


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 })({
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call to createHits could be externalized so that we don't call it every render

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how can it be externalised? if I remember it correctly in vue 2 you only have access to h in render's arguments

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering Vue 2 will soon reach EOL, we might want to only support Vue 3 so this wouldn't be a problem.
Else we can make 1 file per version, we'll see what's best !

hits: this.state.hits,
hitSlot: this.$scopedSlots.item,
});
}),
};
72 changes: 0 additions & 72 deletions packages/vue-instantsearch/src/components/Hits.vue

This file was deleted.