Skip to content

Commit

Permalink
feat(panel): render templates on init with render state (#4845)
Browse files Browse the repository at this point in the history
* feat(panel): render templates on init with render state

Before this PR the initial render happens *before* widget init. This doesn't have a huge effect, although it got rendered with just an empty object. This makes things needlessly dynamic (more than the types were saying even, because the Template isn't super strict), and would make a template like `header({ widgetParams }) { return widgetParams.attribute }` throw, even though with this PR it is possible without conditionals or flashing.

Under very strict conditions this could be construed as a breakign change, although it's closer to a fix, therefore I have classified it as a new feature.

* undo some type changes (hidden doesn't get called on init)
  • Loading branch information
Haroenv committed Aug 24, 2021
1 parent f5bc9d2 commit 0e151a9
Show file tree
Hide file tree
Showing 3 changed files with 284 additions and 44 deletions.
9 changes: 3 additions & 6 deletions src/components/Panel/Panel.tsx
Expand Up @@ -6,13 +6,10 @@ import cx from 'classnames';
import Template from '../Template/Template';
import type {
PanelCSSClasses,
PanelSharedOptions,
PanelTemplates,
} from '../../widgets/panel/panel';
import type {
ComponentCSSClasses,
RenderOptions,
UnknownWidgetFactory,
} from '../../types';
import type { ComponentCSSClasses, UnknownWidgetFactory } from '../../types';

export type PanelComponentCSSClasses = ComponentCSSClasses<
// `collapseIcon` is only used in the default templates of the widget
Expand All @@ -26,7 +23,7 @@ export type PanelProps<TWidget extends UnknownWidgetFactory> = {
hidden: boolean;
collapsible: boolean;
isCollapsed: boolean;
data: RenderOptions | Record<string, never>;
data: PanelSharedOptions<TWidget>;
cssClasses: PanelComponentCSSClasses;
templates: PanelComponentTemplates<TWidget>;
bodyElement: HTMLElement;
Expand Down
251 changes: 231 additions & 20 deletions src/widgets/panel/__tests__/panel-test.ts
Expand Up @@ -114,10 +114,12 @@ describe('Templates', () => {
test('with default templates', () => {
const widgetWithPanel = panel()(widgetFactory);

widgetWithPanel({
const widget = widgetWithPanel({
container: document.createElement('div'),
});

widget.init(createInitOptions());

const firstRender = render.mock.calls[0][0] as VNode<
PanelProps<typeof widgetFactory>
>;
Expand All @@ -137,10 +139,12 @@ describe('Templates', () => {
},
})(widgetFactory);

widgetWithPanel({
const widget = widgetWithPanel({
container: document.createElement('div'),
});

widget.init(createInitOptions());

const firstRender = render.mock.calls[0][0] as VNode<
PanelProps<typeof widgetFactory>
>;
Expand All @@ -156,10 +160,12 @@ describe('Templates', () => {
},
})(widgetFactory);

widgetWithPanel({
const widget = widgetWithPanel({
container: document.createElement('div'),
});

widget.init(createInitOptions());

const firstRender = render.mock.calls[0][0] as VNode<
PanelProps<typeof widgetFactory>
>;
Expand All @@ -175,10 +181,12 @@ describe('Templates', () => {
},
})(widgetFactory);

widgetWithPanel({
const widget = widgetWithPanel({
container: document.createElement('div'),
});

widget.init(createInitOptions());

const firstRender = render.mock.calls[0][0] as VNode<
PanelProps<typeof widgetFactory>
>;
Expand Down Expand Up @@ -206,32 +214,235 @@ describe('Lifecycle', () => {
container: document.createElement('div'),
});

widgetWithPanel.init!(createInitOptions());
widgetWithPanel.render!(createRenderOptions());
widgetWithPanel.dispose!(createDisposeOptions());
widgetWithPanel.init(createInitOptions());
widgetWithPanel.render(createRenderOptions());
widgetWithPanel.dispose(createDisposeOptions());

expect(widget.init).toHaveBeenCalledTimes(1);
expect(widget.render).toHaveBeenCalledTimes(1);
expect(widget.dispose).toHaveBeenCalledTimes(1);
});

test('returns the `state` from the widget dispose function', () => {
const nextSearchParameters = new algoliasearchHelper.SearchParameters({
facets: ['brands'],
describe('init', () => {
test("calls the wrapped widget's init", () => {
const widget = {
$$type: 'mock.widget',
init: jest.fn(),
};
const widgetFactory = () => widget;

const widgetWithPanel = panel()(widgetFactory)({
container: document.createElement('div'),
});

const initOptions = createInitOptions();

widgetWithPanel.init(initOptions);

expect(widget.init).toHaveBeenCalledTimes(1);
expect(widget.init).toHaveBeenCalledWith(initOptions);
});
const widget = {
$$type: 'mock.widget',
init: jest.fn(),
dispose: jest.fn(() => nextSearchParameters),
};
const widgetFactory = () => widget;

const widgetWithPanel = panel()(widgetFactory)({
container: document.createElement('div'),
test('does not call hidden and collapsed yet', () => {
const renderState = {
widgetParams: {},
swag: true,
};

const widget = {
$$type: 'mock.widget',
render: jest.fn(),
getWidgetRenderState() {
return renderState;
},
};

const widgetFactory = () => widget;

const hiddenFn = jest.fn();
const collapsedFn = jest.fn();

const widgetWithPanel = panel({
hidden: hiddenFn,
collapsed: collapsedFn,
})(widgetFactory)({
container: document.createElement('div'),
});

const initOptions = createInitOptions();

widgetWithPanel.init(initOptions);

expect(hiddenFn).toHaveBeenCalledTimes(0);
expect(collapsedFn).toHaveBeenCalledTimes(0);
});

test('renders with render state', () => {
const renderState = {
widgetParams: {},
swag: true,
};

const widget = {
$$type: 'mock.widget',
render: jest.fn(),
getWidgetRenderState() {
return renderState;
},
};

const widgetFactory = () => widget;

const widgetWithPanel = panel()(widgetFactory)({
container: document.createElement('div'),
});

const initOptions = createInitOptions();

widgetWithPanel.init(initOptions);

const firstRender = render.mock.calls[0][0] as VNode<
PanelProps<typeof widgetFactory>
>;

expect(firstRender.props).toEqual(
expect.objectContaining({
hidden: true,
collapsible: false,
isCollapsed: false,
data: {
...renderState,
...initOptions,
},
})
);
});
});

describe('render', () => {
test("calls the wrapped widget's render", () => {
const widget = {
$$type: 'mock.widget',
render: jest.fn(),
};
const widgetFactory = () => widget;

const widgetWithPanel = panel()(widgetFactory)({
container: document.createElement('div'),
});

const renderOptions = createRenderOptions();

widgetWithPanel.render(renderOptions);

expect(widget.render).toHaveBeenCalledTimes(1);
expect(widget.render).toHaveBeenCalledWith(renderOptions);
});

test("calls hidden and collapsed with the wrapped widget's render state", () => {
const renderState = {
widgetParams: {},
swag: true,
};

const widget = {
$$type: 'mock.widget',
render: jest.fn(),
getWidgetRenderState() {
return renderState;
},
};

const widgetFactory = () => widget;

const hiddenFn = jest.fn();
const collapsedFn = jest.fn();

const widgetWithPanel = panel({
hidden: hiddenFn,
collapsed: collapsedFn,
})(widgetFactory)({
container: document.createElement('div'),
});

const renderOptions = createRenderOptions();

widgetWithPanel.render(renderOptions);

expect(hiddenFn).toHaveBeenCalledTimes(1);
expect(hiddenFn).toHaveBeenCalledWith({
...renderState,
...renderOptions,
});

expect(collapsedFn).toHaveBeenCalledTimes(1);
expect(collapsedFn).toHaveBeenCalledWith({
...renderState,
...renderOptions,
});
});

test('renders with render state', () => {
const renderState = {
widgetParams: {},
swag: true,
};

const widget = {
$$type: 'mock.widget',
render: jest.fn(),
getWidgetRenderState() {
return renderState;
},
};

const widgetFactory = () => widget;

const widgetWithPanel = panel()(widgetFactory)({
container: document.createElement('div'),
});

const renderOptions = createRenderOptions();

widgetWithPanel.render(renderOptions);

const firstRender = render.mock.calls[0][0] as VNode<
PanelProps<typeof widgetFactory>
>;

expect(firstRender.props).toEqual(
expect.objectContaining({
hidden: false,
collapsible: false,
isCollapsed: false,
data: {
...renderState,
...renderOptions,
},
})
);
});
});

const nextState = widgetWithPanel.dispose!(createDisposeOptions({}));
describe('dispose', () => {
test("returns the state from the widget's dispose function", () => {
const nextSearchParameters = new algoliasearchHelper.SearchParameters({
facets: ['brands'],
});
const widget = {
$$type: 'mock.widget',
init: jest.fn(),
dispose: jest.fn(() => nextSearchParameters),
};
const widgetFactory = () => widget;

const widgetWithPanel = panel()(widgetFactory)({
container: document.createElement('div'),
});

const nextState = widgetWithPanel.dispose(createDisposeOptions());

expect(nextState).toEqual(nextSearchParameters);
expect(nextState).toEqual(nextSearchParameters);
});
});
});

0 comments on commit 0e151a9

Please sign in to comment.