Skip to content

Commit

Permalink
react-instantsearch: fix DynamicWidgets types (DefinitelyTyped#60050)
Browse files Browse the repository at this point in the history
* react-instantsearch: fix DynamicWidgets types

* react-instantsearch: apply suggestions from code review
  • Loading branch information
FabienMotte authored Apr 26, 2022
1 parent 24bc397 commit 4575cb0
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 34 deletions.
70 changes: 56 additions & 14 deletions types/react-instantsearch-core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -748,37 +748,79 @@ export type InsightsClient = (method: InsightsClientMethod, payload: InsightsCli
export type InsightsClientMethod = 'clickedObjectIDsAfterSearch' | 'convertedObjectIDsAfterSearch';

export interface InsightsClientPayload {
index: string;
queryID: string;
eventName: string;
objectIDs: string[];
positions?: number[] | undefined;
index: string;
queryID: string;
eventName: string;
objectIDs: string[];
positions?: number[] | undefined;
}

export type WrappedInsightsClient = (method: InsightsClientMethod, payload: Partial<InsightsClientPayload>) => void;
export interface ConnectHitInsightsProvided {
hit: Hit;
insights: WrappedInsightsClient;
hit: Hit;
insights: WrappedInsightsClient;
}

export function EXPERIMENTAL_connectConfigureRelatedItems(
Composed: React.ComponentType<any>
): React.ComponentClass<any>;
export function connectQueryRules(Composed: React.ComponentType<any>): React.ComponentClass<any>;
export function connectHitInsights(
insightsClient: InsightsClient,
insightsClient: InsightsClient,
): (
hitComponent: React.ComponentType<any>,
hitComponent: React.ComponentType<any>,
) => React.ComponentType<Omit<ConnectHitInsightsProvided, { insights: WrappedInsightsClient }>>;
export function connectVoiceSearch(Composed: React.ComponentType<any>): React.ComponentClass<any>;

export interface DynamicWidgetsProps {
children: React.ReactNode;
attributesToRender: string[];
fallbackComponent?: React.ComponentType<{ attribute: string }>;
export interface DynamicWidgetsExposed {
/**
* The children of this component will be displayed dynamically based
* on the result of facetOrdering. This means that any child needs
* to have either the “attribute” or “attributes” prop.
*/
children?: React.ReactChild;
/**
* A function to transform the attributes to render,
* or using a different source to determine the attributes to render.
*/
transformItems?: (items: string[], meta: { results: SearchResults }) => any;
/**
* The fallbackComponent prop is used if no widget from children matches.
* The component gets called with an attribute prop.
*/
fallbackComponent?: React.ComponentType<{ attribute: string }>;
/**
* The facets to apply before dynamic widgets get mounted.
* Setting the value to ['*'] will request all facets
* and avoid an additional network request once the widgets are added.
* @default ['*']
*/
facets?: never[] | ['*'];
/**
* The default number of facet values to request.
* It’s recommended to have this value at least as high as the highest limit
* and showMoreLimit of dynamic widgets, as this will prevent
* a second network request once that widget mounts.
* To avoid pinned items not showing in the result, make sure you choose
* a maxValuesPerFacet at least as high as all the most pinned items you have.
* @default 20
*/
maxValuesPerFacet?: number;
}

export class DynamicWidgets extends React.Component<DynamicWidgetsProps> {}
export type DynamicWidgetsProvided = Pick<DynamicWidgetsExposed, 'children' | 'fallbackComponent'> & {
/** The list of refinement values to display returned from the Algolia API. */
attributesToRender: string[]
};

export class DynamicWidgets extends React.Component<DynamicWidgetsExposed> {}

export function connectDynamicWidgets(
stateless: React.FunctionComponent<DynamicWidgetsProvided>
): React.ComponentClass<DynamicWidgetsExposed>;
export function connectDynamicWidgets<TProps extends Partial<DynamicWidgetsProvided>>(
Composed: React.ComponentType<TProps>
): ConnectedComponentClass<TProps, DynamicWidgetsProvided, DynamicWidgetsExposed>;

// Turn off automatic exports - so we don't export internal types like Omit<>
export {};
95 changes: 81 additions & 14 deletions types/react-instantsearch-core/react-instantsearch-core-tests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import {
connectSearchBox,
connectStateResults,
connectStats,
connectDynamicWidgets,
createConnector,
CurrentRefinementsProvided,
DynamicWidgets,
HighlightProps,
HighlightProvided,
Hit,
Expand All @@ -32,6 +32,8 @@ import {
StatsProvided,
translatable,
TranslatableProvided,
DynamicWidgetsProvided,
DynamicWidgets,
} from 'react-instantsearch-core';

import { Hits, RefinementList } from 'react-instantsearch-dom';
Expand Down Expand Up @@ -648,23 +650,88 @@ import { Hits, RefinementList } from 'react-instantsearch-dom';
};

() => {
const HitComponent = ({ hit, insights }: ConnectHitInsightsProvided) => (
<button
onClick={() => {
insights('clickedObjectIDsAfterSearch', { eventName: 'hit clicked' });
}}
>
<article>
<h1>{hit.name}</h1>
</article>
</button>
const HitComponent = ({ hit, insights }: ConnectHitInsightsProvided) => (
<button
onClick={() => {
insights('clickedObjectIDsAfterSearch', { eventName: 'hit clicked' });
}}
>
<article>
<h1>{hit.name}</h1>
</article>
</button>
);

const HitWithInsights = connectHitInsights(() => {})(HitComponent);

<Hits hitComponent={HitWithInsights} />;
};

() => {
function getAttribute(component: React.ReactChild): string | undefined {
if (typeof component !== 'object') {
return undefined;
}

if (component.props.attribute) {
return component.props.attribute;
}
if (Array.isArray(component.props.attributes)) {
return component.props.attributes[0];
}
if (component.props.children) {
return getAttribute(React.Children.only(component.props.children));
}

return undefined;
}

const MyDynamicWidgets = ({
attributesToRender,
fallbackComponent: Fallback = () => null,
children
}: DynamicWidgetsProvided) => {
const widgets = new Map();

React.Children.forEach(children, (child) => {
const attribute = getAttribute(child as React.ReactChild);
if (!attribute) {
throw new Error('Could not find "attribute" prop');
}
widgets.set(attribute, child);
});

return (
<>
{attributesToRender.map((attribute) => (
<React.Fragment key={attribute}>
{widgets.get(attribute) || <Fallback attribute={attribute} />}
</React.Fragment>
))}
</>
);
};

const HitWithInsights = connectHitInsights(() => {})(HitComponent);
const ConnectedDynamicWidgets = connectDynamicWidgets(MyDynamicWidgets);

<Hits hitComponent={HitWithInsights} />;
<ConnectedDynamicWidgets
transformItems={item => item}
fallbackComponent={RefinementList}
facets={['*']}
maxValuesPerFacet={20}
>
<RefinementList attribute="brand" />
</ConnectedDynamicWidgets>;
};

() => {
<DynamicWidgets fallbackComponent={RefinementList} attributesToRender={['']}><RefinementList attribute="brand"/></DynamicWidgets>;
// https://www.algolia.com/doc/api-reference/widgets/dynamic-facets/react/
<DynamicWidgets
transformItems={item => item}
fallbackComponent={RefinementList}
facets={['*']}
maxValuesPerFacet={20}
>
<RefinementList attribute="brand"/>
</DynamicWidgets>;
};
8 changes: 4 additions & 4 deletions types/react-instantsearch-dom/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import * as React from 'react';

import { Hit, BasicDoc, DynamicWidgetsProps as CoreDynamicWidgetsProps } from 'react-instantsearch-core';
import { Hit, BasicDoc, DynamicWidgetsExposed } from 'react-instantsearch-core';

// Core
export { createConnector } from 'react-instantsearch-core';
Expand Down Expand Up @@ -51,6 +51,7 @@ export { connectToggleRefinement } from 'react-instantsearch-core';
export { EXPERIMENTAL_connectConfigureRelatedItems } from 'react-instantsearch-core';
export { connectHitInsights } from 'react-instantsearch-core';
export { connectQueryRules } from 'react-instantsearch-core';
export { connectDynamicWidgets } from 'react-instantsearch-core';

// DOM
interface CommonWidgetProps {
Expand Down Expand Up @@ -208,8 +209,7 @@ export function createVoiceSearchHelper(params: VoiceSearchHelperParams): VoiceS

export function createInfiniteHitsSessionStorageCache(...args: any[]): any;

export interface DynamicWidgetsProps extends CoreDynamicWidgetsProps {
className?: string | undefined;
export interface DynamicWidgetsProps extends DynamicWidgetsExposed {
className?: string;
}

export class DynamicWidgets extends React.Component<DynamicWidgetsProps> {}
13 changes: 11 additions & 2 deletions types/react-instantsearch-dom/react-instantsearch-dom-tests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
SearchBox,
SortBy,
} from 'react-instantsearch/dom';
import { Hit, connectRefinementList, connectMenu, InstantSearchProps } from 'react-instantsearch-core';
import { Hit, InstantSearchProps } from 'react-instantsearch-core';

// DOM
() => {
Expand Down Expand Up @@ -336,5 +336,14 @@ const test = () => {
};

() => {
<DynamicWidgets fallbackComponent={RefinementList} className="test" attributesToRender={['']}><RefinementList attribute="brand"/></DynamicWidgets>;
// https://www.algolia.com/doc/api-reference/widgets/dynamic-facets/react/
<DynamicWidgets
transformItems={item => item}
fallbackComponent={RefinementList}
facets={['*']}
maxValuesPerFacet={20}
className="test"
>
<RefinementList attribute="brand"/>
</DynamicWidgets>;
};

0 comments on commit 4575cb0

Please sign in to comment.