Skip to content

Commit

Permalink
feat(panel): render templates on init with render state
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Haroenv committed Aug 24, 2021
1 parent 047acd0 commit effa71b
Show file tree
Hide file tree
Showing 3 changed files with 285 additions and 48 deletions.
9 changes: 3 additions & 6 deletions src/components/Panel/Panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@ import cx from 'classnames';
import Template from '../Template/Template';
import type {
PanelCSSClasses,
PanelRenderOptions,
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: PanelRenderOptions<TWidget>;
cssClasses: PanelComponentCSSClasses;
templates: PanelComponentTemplates<TWidget>;
bodyElement: HTMLElement;
Expand Down
251 changes: 231 additions & 20 deletions src/widgets/panel/__tests__/panel-test.ts
Original file line number Diff line number Diff line change
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);
});
});
});
Loading

0 comments on commit effa71b

Please sign in to comment.