diff --git a/package.json b/package.json index 3eb3882..b2cf20b 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ "files": [ "dist/" ], + "exports": { + ".": "./dist/index.js" + }, "scripts": { "test": "jest", "test:watch": "jest --watch", diff --git a/src/hooks.ts b/src/hooks.ts deleted file mode 100644 index f8c5034..0000000 --- a/src/hooks.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useEffect, useState } from "react" - -/** - * Like `useEffect`, but only runs after component has rendered at least once. - */ -export function useEffectAfterMount(fn: () => void, deps: any[]): void { - const [hasRendered, setHasRendered] = useState(false) - - useEffect(() => { - if (hasRendered) { - fn() - } else { - setHasRendered(true) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [hasRendered, ...deps]) -} diff --git a/src/hooks/index.ts b/src/hooks/index.ts deleted file mode 100644 index 28bda1a..0000000 --- a/src/hooks/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export { default as useSearchQueryParams } from "./useSearchQueryParams" -export type { - UseSearchQueryParamsProps, - UseSearchQueryParamsResult -} from "./useSearchQueryParams" - -export type { SearchParams, Endpoint, FacetName } from "./configs" - -export { default as useInfiniteSearch } from "./useInfiniteSearch" -export type { - UseInfiniteSearchProps, - UseInfiniteSearchResult -} from "./useInfiniteSearch" diff --git a/src/hooks/useInfiniteSearch.test.ts b/src/hooks/useInfiniteSearch.test.ts index 9970541..cef52a4 100644 --- a/src/hooks/useInfiniteSearch.test.ts +++ b/src/hooks/useInfiniteSearch.test.ts @@ -314,7 +314,7 @@ describe("useInfiniteSearchApi", () => { "Makes requests with aggregations based on endpoint", async ({ expected, endpoint }) => { const fetcher = getDefferedFetcher({ count: 23 }) - const { rerender } = renderHook(useInfiniteSearch, { + renderHook(useInfiniteSearch, { initialProps: { params: { queryText: "one", diff --git a/src/index.ts b/src/index.ts index adf3a07..4e3f612 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,592 +1,35 @@ -import React, { - useState, - useCallback, - useEffect, - MouseEvent, - useMemo, - useRef -} from "react" -import { clone } from "ramda" -import _ from "lodash" -import type { History as HHistory } from "history" +export * from "./constants"; -import { INITIAL_FACET_STATE } from "./constants" -import { - FacetsAndSort, - deserializeSearchParams, - serializeSearchParams, - SearchParams -} from "./url_utils" -import { - Facets, - Aggregation, - Aggregations, - GetSearchPageSize -} from "./facet_display/types" -import { useEffectAfterMount } from "./hooks" - -export * from "./constants" - -export * from "./url_utils" -export * from "./open_api_generated/api" -export * from "./facet_display/types" +export * from "./old_hooks/url_utils"; +export * from "./open_api_generated/api"; +export * from "./facet_display/types"; export { default as FacetDisplay, getDepartmentName, - getLevelName -} from "./facet_display/FacetDisplay" -export { default as FilterableFacet } from "./facet_display/FilterableFacet" -export { sanitizeFacets } from "./facet_display/SanitizeFacets" - -export { buildSearchUrl, SearchQueryParams } from "./search" - -/** - * Accounts for a difference in the listener API for v4 and v5. - * See https://github.com/remix-run/history/issues/811 - */ -const history4or5Listen = ( - history: HHistory, - listener: (loc: Location, action: string) => void -): (() => void) => { - // @ts-ignore - return history.listen((e1: any, e2: any) => { - if (e2) { - listener(e1, e2) - } else { - listener(e1.location, e1.action) - } - }) -} - -export const useFacetOptions = ( - aggregations: Aggregations, - activeFacets: Facets -): ((group: string) => Aggregation | null) => { - return useCallback( - (group: string) => { - const emptyActiveFacets = (activeFacets[group as keyof Facets] || []).map( - (facet: string) => ({ - key: facet, - doc_count: 0 - }) - ) - - if (!aggregations) { - return null - } - - return aggregations.get(group) || emptyActiveFacets - }, - [aggregations, activeFacets] - ) -} - -type UseSearchInputsResult = { - /** - * Parameters to be used for a search query. - * - * Typically, these are the parameters of the previous search query. Thus, - * `searchParams.text` may be different from `text` if the has typed new text - * without submitting the search. - */ - searchParams: SearchParams - setSearchParams: React.Dispatch> - /** - * `text` displayed in the UI. - * - * May be different from `searchParams.text` if user has typed new text - * without submitting the search. - */ - text: string - setText: (text: string) => void - /** - * Reset `searchParams` and `text`. - */ - clearAllFilters: () => void - /** - * Toggle a single facet; also sets text -> searchParams.text. - */ - toggleFacet: (name: string, value: string, isEnbaled: boolean) => void - /** - * Toggle multiple facets; also sets text -> searchParams.text. - */ - toggleFacets: (facets: [string, string, boolean][]) => void - /** - * Event handler for toggling a single facet; also sets text -> searchParams.text. - */ - onUpdateFacet: ({ - target - }: { - target: Pick - }) => void - /** - * Input handler for updating `text` (des NOT update `searchParams.text`). - */ - updateText: (event: { target: { value: string } }) => void - /** - * Event handler for clearing `text`; also clears searchParams.text. - */ - clearText: () => void - /** - * Event handler for updating `searchParams.sort`; also sets text -> searchParams.text. - */ - updateSort: (event: { target: { value: string } } | null) => void - /** - * Updates `searchParams.sort`; also sets text -> searchParams.text. - */ - updateUI: (newUI: string | null) => void - /** - * Set `searchParams.text` to the current value of `text`. - */ - submitText: () => void - updateEndpoint: (newEndpoint: string | null) => void -} - -/** - * Provides state and event handlers for learning resources search UI; state - * includes data for facets, query text, sort order, ui variant (e.g., - * 'compact'). - * - * Note that there are two different state values for search text, `text` and - * `searchParams.text`. In the typical setup: - * - `text` represents the text currently displayed in the UI and updates - * frequently (e.g., on every keypress). - * - `searchParams.text` represents the text used for the currently displayed - * search results and updates less often (e.g., when a user presses "submit" - * or on debounced keypresses). - * - * The provided event handlers for updating other search parameters (sort, ui, - * facets) sync `text` -> `searchParams.text`. - */ -export const useSearchInputs = (history: HHistory): UseSearchInputsResult => { - const [searchParamsInternal, setSearchParams] = useState(() => - deserializeSearchParams(history.location) - ) - - const searchParams = useMemo(() => { - return searchParamsInternal - // This is intentional: let's maintain referential equality when - // serialization is the same. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [serializeSearchParams(searchParamsInternal)]) - - /** - * Store text in state + ref. State for re-renders, and ref for render-stable - * callbacks. - */ - const textRef = useRef(searchParamsInternal.text) - const [text, setTextState] = useState(searchParamsInternal.text) - const setText = useCallback((val: string) => { - setTextState(val) - textRef.current = val - }, []) - - const clearAllFilters = useCallback(() => { - setSearchParams(current => { - return { - text: "", - sort: null, - ui: current.ui, - activeFacets: INITIAL_FACET_STATE, - endpoint: current.endpoint - } - }) - setText("") - }, [setText]) - - const toggleFacet: UseSearchInputsResult["toggleFacet"] = useCallback( - (name, value, isEnabled) => { - setSearchParams(current => { - const { activeFacets, sort, ui } = current - const newFacets = clone(activeFacets) - const facetName = name as keyof Facets - - if (isEnabled) { - newFacets[facetName] = _.union(newFacets[facetName] || [], [value]) - } else { - newFacets[facetName] = _.without(newFacets[facetName] || [], value) - } - return { - ...current, - activeFacets: newFacets, - sort, - ui, - text: textRef.current - } - }) - }, - [] - ) - - const toggleFacets: UseSearchInputsResult["toggleFacets"] = useCallback( - facets => { - setSearchParams(current => { - const { activeFacets, sort, ui } = current - const newFacets = clone(activeFacets) - - facets.forEach(([name, value, isEnabled]) => { - const facetName = name as keyof Facets - if (isEnabled) { - newFacets[facetName] = _.union(newFacets[facetName] || [], [value]) - } else { - newFacets[facetName] = _.without(newFacets[facetName] || [], value) - } - }) - return { - ...current, - activeFacets: newFacets, - sort, - ui, - text: textRef.current - } - }) - }, - [] - ) - - const onUpdateFacet: UseSearchInputsResult["onUpdateFacet"] = useCallback( - e => toggleFacet(e.target.name, e.target.value, e.target.checked), - [toggleFacet] - ) - - const updateText: UseSearchInputsResult["updateText"] = useCallback( - event => { - const text = event ? event.target.value : "" - setText(text) - }, - [setText] - ) - - const updateSort: UseSearchInputsResult["updateSort"] = useCallback( - (event): void => { - const newSort = event ? (event.target as HTMLSelectElement).value : "" - setSearchParams(current => ({ - ...current, - sort: newSort, - text: textRef.current - })) - }, - [] - ) - - const updateUI = useCallback((newUI: string | null): void => { - setSearchParams(current => ({ - ...current, - ui: newUI, - text: textRef.current - })) - }, []) - - const updateEndpoint = useCallback((newEndpoint: string | null): void => { - setSearchParams(current => ({ - ...current, - endpoint: newEndpoint, - text: textRef.current - })) - }, []) - - const clearText = useCallback(() => { - setText("") - setSearchParams(current => ({ ...current, text: "" })) - }, [setText, setSearchParams]) - - const submitText = useCallback(() => { - setSearchParams(current => ({ - ...current, - text: textRef.current - })) - }, []) - - return { - searchParams, - setSearchParams, - text, - setText, - clearAllFilters, - toggleFacet, - toggleFacets, - onUpdateFacet, - updateText, - updateSort, - clearText, - updateUI, - updateEndpoint, - submitText - } -} - -const setLocation = (history: HHistory, searchParams: SearchParams) => { - const currentSearch = serializeSearchParams( - deserializeSearchParams(history.location) - ) - const { activeFacets, sort, ui, text, endpoint } = searchParams - const newSearch = serializeSearchParams({ - text, - activeFacets, - sort, - ui, - endpoint - }) - if (currentSearch !== newSearch) { - const prefix = newSearch ? "?" : "" - history.push({ - search: `${prefix}${newSearch}` - }) - } -} + getLevelName, +} from "./facet_display/FacetDisplay"; +export { default as FilterableFacet } from "./facet_display/FilterableFacet"; +export { sanitizeFacets } from "./facet_display/SanitizeFacets"; -/** - * Sync changes to URL search parameters with `searchParams`, and vice versa. - * - * Pushes a new entry to the history stack every time the URL would change. - */ -export const useSyncUrlAndSearch = ( - history: HHistory, - { - searchParams, - setSearchParams, - setText - }: Pick -) => { - // sync URL to search - useEffect(() => { - const unlisten = history4or5Listen(history, location => { - const { activeFacets, sort, ui, text, endpoint } = - deserializeSearchParams(location) - setSearchParams({ activeFacets, sort, ui, text, endpoint }) - setText(text) - }) - return unlisten - }, [history, setSearchParams, setText]) +export { buildSearchUrl, SearchQueryParams } from "./old_hooks/search"; - useEffect(() => { - setLocation(history, searchParams) - }, [history, searchParams]) -} - -interface PreventableEvent { - preventDefault?: () => void - type?: string -} -interface CourseSearchResult { - facetOptions: (group: string) => Aggregation | null - clearAllFilters: UseSearchInputsResult["clearAllFilters"] - toggleFacet: UseSearchInputsResult["toggleFacet"] - toggleFacets: UseSearchInputsResult["toggleFacets"] - onUpdateFacets: UseSearchInputsResult["onUpdateFacet"] - updateText: UseSearchInputsResult["updateText"] - clearText: React.MouseEventHandler - updateSort: UseSearchInputsResult["updateSort"] - acceptSuggestion: (suggestion: string) => void - loadMore: () => void - incremental: boolean - text: string - sort: string | null - activeFacets: Facets - /** - * Callback that handles search submission. Pass this to your search input - * submission event target, e.g., `
` or - * `