Skip to content

Commit

Permalink
components: create ResultsContainer component
Browse files Browse the repository at this point in the history
The ResultsContainer component asbtracts away the internals
of working with the result set.
  • Loading branch information
Benjamin Hinchley committed Jul 10, 2018
1 parent ac79739 commit 2cacba3
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 79 deletions.
34 changes: 15 additions & 19 deletions src/components/Input/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Result } from "@sajari/sdk-js";
import * as React from "react";

import { Provider } from "./context";
import { Consumer, Provider } from "./context";

import { Config } from "../../config";
import { ClearFn, SearchFn } from "../context/pipeline/context";
Expand All @@ -26,7 +26,7 @@ enum InputKeyCodes {
}

export type InputMode = "standard" | "typeahead";
export type DropdownMode = "none" | "suggestions" | "results";
export type DropdownMode = "none" | "suggestions" | "custom";

export interface InputProps {
mode?: InputMode;
Expand Down Expand Up @@ -58,10 +58,7 @@ export interface InputProps {
search: SearchFn,
value: string
) => void;
ResultsDropdownRenderer?: React.ComponentType<{
highlightedIndex: number;
setHighlightedIndex: (index: number) => void;
}>;
DropdownRenderer?: React.ComponentType<InputContext>;
}

export class Input extends React.Component<InputProps> {
Expand All @@ -82,7 +79,7 @@ export class Input extends React.Component<InputProps> {
placeholder,
autoFocus,
enableVoiceSearch,
ResultsDropdownRenderer,
DropdownRenderer,
onFocus,
onBlur,
onDropdownClose,
Expand All @@ -105,7 +102,6 @@ export class Input extends React.Component<InputProps> {
getRootProps,
getInputProps,
setState,
setHighlightedIndex,
pipelines
}: InputContext) => {
return (
Expand Down Expand Up @@ -153,12 +149,12 @@ export class Input extends React.Component<InputProps> {
/>
<Dropdown isOpen={isDropdownOpen}>
{dropdownMode === "suggestions" ? <Suggestions /> : null}
{dropdownMode === "results" &&
ResultsDropdownRenderer !== undefined ? (
<ResultsDropdownRenderer
highlightedIndex={highlightedIndex}
setHighlightedIndex={setHighlightedIndex}
/>
{dropdownMode === "custom" && DropdownRenderer !== undefined ? (
<Consumer>
{(context: InputContext) => (
<DropdownRenderer {...context} />
)}
</Consumer>
) : null}
</Dropdown>
</Container>
Expand All @@ -182,8 +178,8 @@ export class Input extends React.Component<InputProps> {
search: { search: SearchFn },
instant: { search: SearchFn }
) => (event: InputChangeEvent) => {
const { mode, dropdownMode, instantSearch } = this.props;
if (instantSearch || dropdownMode === "results") {
const { mode, instantSearch } = this.props;
if (instantSearch) {
const value = event.target.value;
search.search(value, false);
} else if (mode === "typeahead") {
Expand All @@ -210,7 +206,7 @@ export class Input extends React.Component<InputProps> {
this.props.onKeyDown(event);
}

if (dropdownMode !== "results" && keyCode === InputKeyCodes.Return) {
if (dropdownMode !== "custom" && keyCode === InputKeyCodes.Return) {
const suggestion = suggestions[highlightedIndex - 1];

if (suggestion === undefined) {
Expand Down Expand Up @@ -240,7 +236,7 @@ export class Input extends React.Component<InputProps> {
event.preventDefault();
if (highlightedIndex === 0) {
let length = suggestions.length;
if (dropdownMode === "results") {
if (dropdownMode === "custom") {
length = results.length;
}

Expand All @@ -254,7 +250,7 @@ export class Input extends React.Component<InputProps> {

if (keyCode === InputKeyCodes.DownArrow) {
let length = suggestions.length;
if (dropdownMode === "results") {
if (dropdownMode === "custom") {
length = results.length;
}

Expand Down
4 changes: 2 additions & 2 deletions src/components/Input/components/Suggestions/Suggestions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import isEqual from "deep-is";
import * as React from "react";

import { Consumer } from "../../context";
import { SetStateFn } from "../../context/context";
import { InputContext, SetStateFn } from "../../context/context";
import { trimPrefix } from "../../utils";
import { Suggestion, SuggestionsContainer } from "./styled";

Expand Down Expand Up @@ -120,7 +120,7 @@ export default () => (
setHighlightedIndex,
setState,
pipelines
}) => (
}: InputContext) => (
<Suggestions
inputValue={inputValue}
isDropdownOpen={isDropdownOpen}
Expand Down
8 changes: 5 additions & 3 deletions src/components/Input/context/Provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export class Provider extends React.Component<ProviderProps, ProviderState> {
}
this.setState(state => ({ ...state, inputValue }));

if (dropdownMode === undefined || dropdownMode !== "results") {
if (dropdownMode === undefined || dropdownMode !== "custom") {
return;
}

Expand Down Expand Up @@ -184,7 +184,9 @@ export class Provider extends React.Component<ProviderProps, ProviderState> {

return (
<Context.Provider value={value as InputContext}>
<Context.Consumer>{context => children(context)}</Context.Consumer>
<Context.Consumer>
{(context: InputContext) => children(context)}
</Context.Consumer>
</Context.Provider>
);
}
Expand All @@ -207,7 +209,7 @@ export class Provider extends React.Component<ProviderProps, ProviderState> {
private getInputProps = (props: { [k: string]: any }) => {
return {
onBlur: (event: React.FocusEvent<HTMLInputElement>) => {
if (this.props.dropdownMode !== "results") {
if (this.props.dropdownMode !== "custom") {
// @ts-ignore: partial state update
this.handleSetState({
isDropdownOpen: false
Expand Down
152 changes: 99 additions & 53 deletions src/components/Results/Results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,96 @@ export interface ResultsProps {
};
}

export interface ResultsRenderFnProps {
error: RequestError | null;
results?: Array<{ key: string } & ResultProps>;
}

export type ResultsRenderFn = (props: ResultsRenderFnProps) => React.ReactNode;

export interface ResultsContainerProps {
children: ResultsRenderFn;

fields?: {
title?: string;
description?: string;
url?: string;
image?: string;
};
}

export const ResultsContainer: React.SFC<ResultsContainerProps> = ({
children,
fields
}) => {
return (
<Consumer>
{({ search: { response }, resultClicked }) => {
if (response === null || response === undefined || response.isEmpty()) {
return null;
}
if (response.isError()) {
const err = response.getError() as RequestError;
if (err.httpStatusCode === STATUS_UNAUTHORISED) {
err.message = i18n.t("errors:authorization");
}
if (err.transportErrorCode === TransportError.Connection) {
err.message = i18n.t("errors:connection");
} else if (err.transportErrorCode === TransportError.ParseResponse) {
err.message = i18n.t("errors:parseResponse");
}
return children({ error: err });
}

const results =
response !== undefined ? response.getResults() || [] : [];

if (results.length < 1) {
return children({ error: null, results: [] as any });
}

const res: Array<{ key: string } & ResultProps> = results.map(
(result: SDKResult, index: number) => {
const key =
(result.values._id as string) ||
(("" + index + result.values.url) as string);
const token = result.token && (result.token as ClickToken).click;

// tslint:disable:object-literal-sort-keys
const values = {
...result.values,
// @ts-ignore: idx
title: result.values[idx(fields, _ => _.title) || "title"],
description:
result.values[
// @ts-ignore: idx
idx(fields, _ => _.description) || "description"
],
// @ts-ignore: idx
url: result.values[idx(fields, _ => _.url) || "url"],
// @ts-ignore: idx
image: result.values[idx(fields, _ => _.image) || "image"]
};
// tslint:enable:object-literal-sort-keys

return {
key,
resultClicked,
token,
values,
indexScore: result.indexScore,
itemIndex: index,
score: result.score
} as { key: string } & ResultProps;
}
);

return children({ error: null, results: res });
}}
</Consumer>
);
};

export class Results extends React.Component<ResultsProps, {}> {
public render() {
const {
Expand All @@ -41,67 +131,23 @@ export class Results extends React.Component<ResultsProps, {}> {
} = this.props;

return (
<Consumer>
{({ search: { response }, resultClicked }) => {
if (
response === null ||
response === undefined ||
response.isEmpty()
) {
return null;
}

if (response.isError()) {
const error = response.getError() as RequestError;
if (error.httpStatusCode === STATUS_UNAUTHORISED) {
return <Error>{i18n.t("errors:authorization")}</Error>;
}
if (error.transportErrorCode === TransportError.Connection) {
return <Error>{i18n.t("errors:connection")}</Error>;
} else if (
error.transportErrorCode === TransportError.ParseResponse
) {
return <Error>{i18n.t("errors:parseResponse")}</Error>;
}
<ResultsContainer fields={fields}>
{({ error, results }) => {
if (error) {
return <Error>{error.message}</Error>;
}

const results =
response !== undefined ? response.getResults() || [] : [];
if (results === undefined) {
return null;
}

return results.length > 0 ? (
<Container styles={idx(styles, _ => _.container)}>
{results.map((result: SDKResult, index: number) => {
const key =
(result.values._id as string) ||
(("" + index + result.values.url) as string);
const token =
result.token && (result.token as ClickToken).click;

// tslint:disable:object-literal-sort-keys
const values = {
...result.values,
// @ts-ignore: idx
title: result.values[idx(fields, _ => _.title) || "title"],
description:
result.values[
// @ts-ignore: idx
idx(fields, _ => _.description) || "description"
],
// @ts-ignore: idx
url: result.values[idx(fields, _ => _.url) || "url"],
// @ts-ignore: idx
image: result.values[idx(fields, _ => _.image) || "image"]
};
// tslint:enable:object-literal-sort-keys

{results.map(({ key, ...result }) => {
return (
<ResultItem key={key} styles={idx(styles, _ => _.item)}>
<ResultRenderer
token={token}
itemIndex={index}
values={values}
resultClicked={resultClicked}
{...result}
showImage={showImages}
styles={idx(styles, _ => _.result)}
/>
Expand All @@ -111,7 +157,7 @@ export class Results extends React.Component<ResultsProps, {}> {
</Container>
) : null;
}}
</Consumer>
</ResultsContainer>
);
}
}
2 changes: 1 addition & 1 deletion src/components/Results/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { Results } from "./Results";
export { ResultsContainer, Results } from "./Results";
2 changes: 1 addition & 1 deletion src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export {
export { Input } from "./Input";

export { Response } from "./Response";
export { Results } from "./Results";
export { ResultsContainer, Results } from "./Results";
export { Result, TokenLink } from "./Result";

export { Summary } from "./Summary";
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export {
FilterProvider,
Input,
Response,
ResultsContainer,
Results,
TokenLink,
Summary,
Expand Down

0 comments on commit 2cacba3

Please sign in to comment.