Skip to content

Commit

Permalink
feat(headless): added defineResultList SSR controller (#3099)
Browse files Browse the repository at this point in the history
  • Loading branch information
btaillon-coveo authored Aug 15, 2023
1 parent 804af40 commit 9fce780
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 57 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {SearchEngine} from '../../../app/search-engine/search-engine';
import {ControllerDefinitionWithoutProps} from '../../../app/ssr-engine/types/common';
import {
ResultList,
ResultListProps,
buildResultList,
} from '../../result-list/headless-result-list';

export type {
ResultListOptions,
ResultListProps,
ResultListState,
ResultList,
} from '../../result-list/headless-result-list';

/**
* @internal
*/
export const defineResultList = (
props?: ResultListProps
): ControllerDefinitionWithoutProps<SearchEngine, ResultList> => ({
build: (engine) => buildResultList(engine, props),
});
8 changes: 8 additions & 0 deletions packages/headless/src/ssr.index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,11 @@ export type {
} from './app/ssr-engine/types/core-engine';

export {defineSearchEngine} from './app/ssr-engine/ssr-engine';

export type {
ResultList,
ResultListOptions,
ResultListProps,
ResultListState,
} from './controllers/ssr/result-list/headless-ssr-result-list';
export {defineResultList} from './controllers/ssr/result-list/headless-ssr-result-list';
17 changes: 17 additions & 0 deletions packages/samples/headless-ssr/cypress/e2e/ssr-e2e-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const ConsoleAliases = {
error: '@consoleError',
warn: '@consoleWarn',
log: '@consoleLog',
};

export function spyOnConsole() {
cy.window().then((win) => {
cy.spy(win.console, 'error').as(ConsoleAliases.error.substring(1));
cy.spy(win.console, 'warn').as(ConsoleAliases.warn.substring(1));
cy.spy(win.console, 'log').as(ConsoleAliases.log.substring(1));
});
}

export function waitForHydration() {
cy.get('#hydrated-indicator').should('be.checked');
}
12 changes: 11 additions & 1 deletion packages/samples/headless-ssr/cypress/e2e/ssr.cy.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import 'cypress-web-vitals';
import {ConsoleAliases, spyOnConsole, waitForHydration} from './ssr-e2e-utils';

describe('headless ssr example', () => {
const numResults = 10;
const numResultsMsg = `Hydrated engine with ${numResults} results`;
const numResultsMsg = `Rendered page with ${numResults} results`;
const msgSelector = '#hydrated-msg';
const timestampSelector = '#timestamp';
it('renders page in SSR as expected', () => {
Expand Down Expand Up @@ -54,4 +55,13 @@ describe('headless ssr example', () => {
cy.visit('/');
cy.vitals(VITALS_THRESHOLD);
});

it('should not log any error nor warning', () => {
spyOnConsole();
cy.visit('/');
waitForHydration();
cy.wait(1000);
cy.get(ConsoleAliases.error).should('not.be.called');
cy.get(ConsoleAliases.warn).should('not.be.called');
});
});
30 changes: 3 additions & 27 deletions packages/samples/headless-ssr/src/common/engine.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,17 @@
import {getSampleSearchEngineConfiguration} from '@coveo/headless';
import {
buildController,
Controller,
getSampleSearchEngineConfiguration,
Result,
SearchEngine,
} from '@coveo/headless';
import {
ControllerDefinitionWithoutProps,
defineSearchEngine,
defineResultList,
InferSSRState,
InferCSRState,
} from '@coveo/headless/ssr';

// Custom controller to fetch results from snapshot
// as snapshot doesn't have an engine that can be accessed directly.
function defineCustomResultList(): ControllerDefinitionWithoutProps<
SearchEngine,
Controller & {state: {results: Result[]}}
> {
return {
build(engine: SearchEngine) {
return {
...buildController(engine),
get state() {
return {results: engine.state.search.results};
},
};
},
};
}

const engineDefinition = defineSearchEngine({
configuration: {
...getSampleSearchEngineConfiguration(),
analytics: {enabled: false},
},
controllers: {resultList: defineCustomResultList()},
controllers: {resultList: defineResultList()},
});

export type SearchSSRState = InferSSRState<typeof engineDefinition>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {FunctionComponent} from 'react';
import {SearchCSRState, SearchSSRState} from '../common/engine';

export interface HydrationMetadataProps {
ssrState: SearchSSRState;
csrResult?: SearchCSRState;
}

export const HydrationMetadata: FunctionComponent<HydrationMetadataProps> = ({
ssrState,
csrResult,
}) => (
<>
<div>
Hydrated:{' '}
<input
id="hydrated-indicator"
type="checkbox"
readOnly
checked={!!csrResult}
/>
</div>
<span id="hydrated-msg">
Rendered page with{' '}
{(csrResult ?? ssrState).controllers.resultList.state.results.length}{' '}
results
</span>
<div>
Rendered on{' '}
<span id="timestamp" suppressHydrationWarning>
{new Date().toISOString()}
</span>
</div>
</>
);
48 changes: 26 additions & 22 deletions packages/samples/headless-ssr/src/components/result-list.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
'use client';
import {
ResultList as ResultListController,
ResultListState,
} from '@coveo/headless';
import {useEffect, useState, FunctionComponent} from 'react';

import {Result} from '@coveo/headless';
interface ResultListProps {
initialState: ResultListState;
controller?: ResultListController;
}

export const ResultList: FunctionComponent<ResultListProps> = (props) => {
const {initialState, controller} = props;
const [state, setState] = useState(initialState);

useEffect(
() => controller?.subscribe(() => setState({...controller.state})),
[controller]
);

export default function ResultList({results}: {results: Result[]}) {
return (
<div>
<span id="hydrated-msg">
Hydrated engine with {results?.length} results
</span>
<ul>
{results?.map((result) => (
<li key={result.uniqueId}>
<a href={result.clickUri}>{result.title}</a>
</li>
))}
</ul>
<div>
Rendered on{' '}
<span id="timestamp" suppressHydrationWarning>
{new Date().toISOString()}
</span>
</div>
</div>
<ul>
{state.results.map((result) => (
<li key={result.uniqueId}>
<h3>{result.title}</h3>
</li>
))}
</ul>
);
}
};
21 changes: 14 additions & 7 deletions packages/samples/headless-ssr/src/components/search-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,26 @@ import {
hydrateInitialState,
} from '@/src/common/engine';
import {useEffect, useState} from 'react';
import ResultList from './result-list';
import {HydrationMetadata} from './hydration-metadata';
import {ResultList} from './result-list';

export default function SearchPage({ssrState}: {ssrState: SearchSSRState}) {
const [csrResult, setCSRResult] = useState<SearchCSRState | null>(null);
const [csrResult, setCSRResult] = useState<SearchCSRState | undefined>(
undefined
);

useEffect(() => {
hydrateInitialState(ssrState).then(({engine, controllers}) => {
setCSRResult({engine, controllers});
});
}, [ssrState]);

const results = csrResult
? csrResult.controllers.resultList.state.results
: ssrState.controllers.resultList.state.results;
return <ResultList results={results}></ResultList>;
return (
<>
<HydrationMetadata ssrState={ssrState} csrResult={csrResult} />
<ResultList
initialState={ssrState.controllers.resultList.state}
controller={csrResult?.controllers.resultList}
/>
</>
);
}

0 comments on commit 9fce780

Please sign in to comment.