Skip to content

Commit 0e151a9

Browse files
authoredAug 24, 2021
feat(panel): render templates on init with render state (#4845)
* 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)
1 parent f5bc9d2 commit 0e151a9

File tree

3 files changed

+284
-44
lines changed

3 files changed

+284
-44
lines changed
 

‎src/components/Panel/Panel.tsx

+3-6
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,10 @@ import cx from 'classnames';
66
import Template from '../Template/Template';
77
import type {
88
PanelCSSClasses,
9+
PanelSharedOptions,
910
PanelTemplates,
1011
} from '../../widgets/panel/panel';
11-
import type {
12-
ComponentCSSClasses,
13-
RenderOptions,
14-
UnknownWidgetFactory,
15-
} from '../../types';
12+
import type { ComponentCSSClasses, UnknownWidgetFactory } from '../../types';
1613

1714
export type PanelComponentCSSClasses = ComponentCSSClasses<
1815
// `collapseIcon` is only used in the default templates of the widget
@@ -26,7 +23,7 @@ export type PanelProps<TWidget extends UnknownWidgetFactory> = {
2623
hidden: boolean;
2724
collapsible: boolean;
2825
isCollapsed: boolean;
29-
data: RenderOptions | Record<string, never>;
26+
data: PanelSharedOptions<TWidget>;
3027
cssClasses: PanelComponentCSSClasses;
3128
templates: PanelComponentTemplates<TWidget>;
3229
bodyElement: HTMLElement;

‎src/widgets/panel/__tests__/panel-test.ts

+231-20
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,12 @@ describe('Templates', () => {
114114
test('with default templates', () => {
115115
const widgetWithPanel = panel()(widgetFactory);
116116

117-
widgetWithPanel({
117+
const widget = widgetWithPanel({
118118
container: document.createElement('div'),
119119
});
120120

121+
widget.init(createInitOptions());
122+
121123
const firstRender = render.mock.calls[0][0] as VNode<
122124
PanelProps<typeof widgetFactory>
123125
>;
@@ -137,10 +139,12 @@ describe('Templates', () => {
137139
},
138140
})(widgetFactory);
139141

140-
widgetWithPanel({
142+
const widget = widgetWithPanel({
141143
container: document.createElement('div'),
142144
});
143145

146+
widget.init(createInitOptions());
147+
144148
const firstRender = render.mock.calls[0][0] as VNode<
145149
PanelProps<typeof widgetFactory>
146150
>;
@@ -156,10 +160,12 @@ describe('Templates', () => {
156160
},
157161
})(widgetFactory);
158162

159-
widgetWithPanel({
163+
const widget = widgetWithPanel({
160164
container: document.createElement('div'),
161165
});
162166

167+
widget.init(createInitOptions());
168+
163169
const firstRender = render.mock.calls[0][0] as VNode<
164170
PanelProps<typeof widgetFactory>
165171
>;
@@ -175,10 +181,12 @@ describe('Templates', () => {
175181
},
176182
})(widgetFactory);
177183

178-
widgetWithPanel({
184+
const widget = widgetWithPanel({
179185
container: document.createElement('div'),
180186
});
181187

188+
widget.init(createInitOptions());
189+
182190
const firstRender = render.mock.calls[0][0] as VNode<
183191
PanelProps<typeof widgetFactory>
184192
>;
@@ -206,32 +214,235 @@ describe('Lifecycle', () => {
206214
container: document.createElement('div'),
207215
});
208216

209-
widgetWithPanel.init!(createInitOptions());
210-
widgetWithPanel.render!(createRenderOptions());
211-
widgetWithPanel.dispose!(createDisposeOptions());
217+
widgetWithPanel.init(createInitOptions());
218+
widgetWithPanel.render(createRenderOptions());
219+
widgetWithPanel.dispose(createDisposeOptions());
212220

213221
expect(widget.init).toHaveBeenCalledTimes(1);
214222
expect(widget.render).toHaveBeenCalledTimes(1);
215223
expect(widget.dispose).toHaveBeenCalledTimes(1);
216224
});
217225

218-
test('returns the `state` from the widget dispose function', () => {
219-
const nextSearchParameters = new algoliasearchHelper.SearchParameters({
220-
facets: ['brands'],
226+
describe('init', () => {
227+
test("calls the wrapped widget's init", () => {
228+
const widget = {
229+
$$type: 'mock.widget',
230+
init: jest.fn(),
231+
};
232+
const widgetFactory = () => widget;
233+
234+
const widgetWithPanel = panel()(widgetFactory)({
235+
container: document.createElement('div'),
236+
});
237+
238+
const initOptions = createInitOptions();
239+
240+
widgetWithPanel.init(initOptions);
241+
242+
expect(widget.init).toHaveBeenCalledTimes(1);
243+
expect(widget.init).toHaveBeenCalledWith(initOptions);
221244
});
222-
const widget = {
223-
$$type: 'mock.widget',
224-
init: jest.fn(),
225-
dispose: jest.fn(() => nextSearchParameters),
226-
};
227-
const widgetFactory = () => widget;
228245

229-
const widgetWithPanel = panel()(widgetFactory)({
230-
container: document.createElement('div'),
246+
test('does not call hidden and collapsed yet', () => {
247+
const renderState = {
248+
widgetParams: {},
249+
swag: true,
250+
};
251+
252+
const widget = {
253+
$$type: 'mock.widget',
254+
render: jest.fn(),
255+
getWidgetRenderState() {
256+
return renderState;
257+
},
258+
};
259+
260+
const widgetFactory = () => widget;
261+
262+
const hiddenFn = jest.fn();
263+
const collapsedFn = jest.fn();
264+
265+
const widgetWithPanel = panel({
266+
hidden: hiddenFn,
267+
collapsed: collapsedFn,
268+
})(widgetFactory)({
269+
container: document.createElement('div'),
270+
});
271+
272+
const initOptions = createInitOptions();
273+
274+
widgetWithPanel.init(initOptions);
275+
276+
expect(hiddenFn).toHaveBeenCalledTimes(0);
277+
expect(collapsedFn).toHaveBeenCalledTimes(0);
278+
});
279+
280+
test('renders with render state', () => {
281+
const renderState = {
282+
widgetParams: {},
283+
swag: true,
284+
};
285+
286+
const widget = {
287+
$$type: 'mock.widget',
288+
render: jest.fn(),
289+
getWidgetRenderState() {
290+
return renderState;
291+
},
292+
};
293+
294+
const widgetFactory = () => widget;
295+
296+
const widgetWithPanel = panel()(widgetFactory)({
297+
container: document.createElement('div'),
298+
});
299+
300+
const initOptions = createInitOptions();
301+
302+
widgetWithPanel.init(initOptions);
303+
304+
const firstRender = render.mock.calls[0][0] as VNode<
305+
PanelProps<typeof widgetFactory>
306+
>;
307+
308+
expect(firstRender.props).toEqual(
309+
expect.objectContaining({
310+
hidden: true,
311+
collapsible: false,
312+
isCollapsed: false,
313+
data: {
314+
...renderState,
315+
...initOptions,
316+
},
317+
})
318+
);
319+
});
320+
});
321+
322+
describe('render', () => {
323+
test("calls the wrapped widget's render", () => {
324+
const widget = {
325+
$$type: 'mock.widget',
326+
render: jest.fn(),
327+
};
328+
const widgetFactory = () => widget;
329+
330+
const widgetWithPanel = panel()(widgetFactory)({
331+
container: document.createElement('div'),
332+
});
333+
334+
const renderOptions = createRenderOptions();
335+
336+
widgetWithPanel.render(renderOptions);
337+
338+
expect(widget.render).toHaveBeenCalledTimes(1);
339+
expect(widget.render).toHaveBeenCalledWith(renderOptions);
340+
});
341+
342+
test("calls hidden and collapsed with the wrapped widget's render state", () => {
343+
const renderState = {
344+
widgetParams: {},
345+
swag: true,
346+
};
347+
348+
const widget = {
349+
$$type: 'mock.widget',
350+
render: jest.fn(),
351+
getWidgetRenderState() {
352+
return renderState;
353+
},
354+
};
355+
356+
const widgetFactory = () => widget;
357+
358+
const hiddenFn = jest.fn();
359+
const collapsedFn = jest.fn();
360+
361+
const widgetWithPanel = panel({
362+
hidden: hiddenFn,
363+
collapsed: collapsedFn,
364+
})(widgetFactory)({
365+
container: document.createElement('div'),
366+
});
367+
368+
const renderOptions = createRenderOptions();
369+
370+
widgetWithPanel.render(renderOptions);
371+
372+
expect(hiddenFn).toHaveBeenCalledTimes(1);
373+
expect(hiddenFn).toHaveBeenCalledWith({
374+
...renderState,
375+
...renderOptions,
376+
});
377+
378+
expect(collapsedFn).toHaveBeenCalledTimes(1);
379+
expect(collapsedFn).toHaveBeenCalledWith({
380+
...renderState,
381+
...renderOptions,
382+
});
383+
});
384+
385+
test('renders with render state', () => {
386+
const renderState = {
387+
widgetParams: {},
388+
swag: true,
389+
};
390+
391+
const widget = {
392+
$$type: 'mock.widget',
393+
render: jest.fn(),
394+
getWidgetRenderState() {
395+
return renderState;
396+
},
397+
};
398+
399+
const widgetFactory = () => widget;
400+
401+
const widgetWithPanel = panel()(widgetFactory)({
402+
container: document.createElement('div'),
403+
});
404+
405+
const renderOptions = createRenderOptions();
406+
407+
widgetWithPanel.render(renderOptions);
408+
409+
const firstRender = render.mock.calls[0][0] as VNode<
410+
PanelProps<typeof widgetFactory>
411+
>;
412+
413+
expect(firstRender.props).toEqual(
414+
expect.objectContaining({
415+
hidden: false,
416+
collapsible: false,
417+
isCollapsed: false,
418+
data: {
419+
...renderState,
420+
...renderOptions,
421+
},
422+
})
423+
);
231424
});
425+
});
232426

233-
const nextState = widgetWithPanel.dispose!(createDisposeOptions({}));
427+
describe('dispose', () => {
428+
test("returns the state from the widget's dispose function", () => {
429+
const nextSearchParameters = new algoliasearchHelper.SearchParameters({
430+
facets: ['brands'],
431+
});
432+
const widget = {
433+
$$type: 'mock.widget',
434+
init: jest.fn(),
435+
dispose: jest.fn(() => nextSearchParameters),
436+
};
437+
const widgetFactory = () => widget;
438+
439+
const widgetWithPanel = panel()(widgetFactory)({
440+
container: document.createElement('div'),
441+
});
442+
443+
const nextState = widgetWithPanel.dispose(createDisposeOptions());
234444

235-
expect(nextState).toEqual(nextSearchParameters);
445+
expect(nextState).toEqual(nextSearchParameters);
446+
});
236447
});
237448
});

‎src/widgets/panel/panel.tsx

+50-18
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ import {
1111
import { component } from '../../lib/suit';
1212
import type { PanelComponentCSSClasses } from '../../components/Panel/Panel';
1313
import Panel from '../../components/Panel/Panel';
14-
import type { Template, RenderOptions, WidgetFactory } from '../../types';
14+
import type {
15+
Template,
16+
RenderOptions,
17+
WidgetFactory,
18+
InitOptions,
19+
Widget,
20+
} from '../../types';
1521

1622
export type PanelCSSClasses = Partial<{
1723
/**
@@ -100,6 +106,12 @@ type GetWidgetRenderState<TWidgetFactory extends AnyWidgetFactory> =
100106
export type PanelRenderOptions<TWidgetFactory extends AnyWidgetFactory> =
101107
RenderOptions & GetWidgetRenderState<TWidgetFactory>;
102108

109+
export type PanelSharedOptions<TWidgetFactory extends AnyWidgetFactory> = (
110+
| InitOptions
111+
| RenderOptions
112+
) &
113+
GetWidgetRenderState<TWidgetFactory>;
114+
103115
export type PanelWidgetParams<TWidgetFactory extends AnyWidgetFactory> = {
104116
/**
105117
* A function that is called on each render to determine if the
@@ -145,7 +157,7 @@ const renderer =
145157
collapsible,
146158
collapsed,
147159
}: {
148-
options: RenderOptions | Record<string, never>;
160+
options: PanelSharedOptions<TWidget>;
149161
hidden: boolean;
150162
collapsible: boolean;
151163
collapsed: boolean;
@@ -164,13 +176,19 @@ const renderer =
164176
);
165177
};
166178

179+
type AugmentedWidget<
180+
TWidgetFactory extends AnyWidgetFactory,
181+
TOverriddenKeys extends keyof Widget = 'init' | 'render' | 'dispose'
182+
> = Omit<ReturnType<TWidgetFactory>, TOverriddenKeys> &
183+
Pick<Required<Widget>, TOverriddenKeys>;
184+
167185
export type PanelWidget = <TWidgetFactory extends AnyWidgetFactory>(
168186
panelWidgetParams?: PanelWidgetParams<TWidgetFactory>
169187
) => (
170188
widgetFactory: TWidgetFactory
171189
) => (
172190
widgetParams: Parameters<TWidgetFactory>[0]
173-
) => ReturnType<TWidgetFactory>;
191+
) => AugmentedWidget<TWidgetFactory>;
174192

175193
/**
176194
* The panel widget wraps other widgets in a consistent panel design.
@@ -265,32 +283,37 @@ const panel: PanelWidget = (panelWidgetParams) => {
265283
},
266284
});
267285

268-
renderPanel({
269-
options: {},
270-
hidden: true,
271-
collapsible,
272-
collapsed: false,
273-
});
274-
275286
const widget = widgetFactory({
276287
...widgetParams,
277288
container: bodyContainerNode,
278289
});
279290

280291
// TypeScript somehow loses track of the ...widget type, since it's
281-
// not directly returned. Eventually the "as ReturnType<typeof widgetFactory>"
292+
// not directly returned. Eventually the "as AugmentedWidget<typeof widgetFactory>"
282293
// will not be needed anymore.
283294
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
284295
return {
285296
...widget,
286-
dispose(...args) {
287-
render(null, containerNode);
297+
init(...args) {
298+
const [renderOptions] = args;
288299

289-
if (typeof widget.dispose === 'function') {
290-
return widget.dispose.call(this, ...args);
291-
}
300+
const options = {
301+
...(widget.getWidgetRenderState
302+
? widget.getWidgetRenderState(renderOptions)
303+
: {}),
304+
...renderOptions,
305+
};
292306

293-
return undefined;
307+
renderPanel({
308+
options,
309+
hidden: true,
310+
collapsible,
311+
collapsed: false,
312+
});
313+
314+
if (typeof widget.init === 'function') {
315+
widget.init.call(this, ...args);
316+
}
294317
},
295318
render(...args) {
296319
const [renderOptions] = args;
@@ -313,7 +336,16 @@ const panel: PanelWidget = (panelWidgetParams) => {
313336
widget.render.call(this, ...args);
314337
}
315338
},
316-
} as ReturnType<typeof widgetFactory>;
339+
dispose(...args) {
340+
render(null, containerNode);
341+
342+
if (typeof widget.dispose === 'function') {
343+
return widget.dispose.call(this, ...args);
344+
}
345+
346+
return undefined;
347+
},
348+
} as AugmentedWidget<typeof widgetFactory>;
317349
};
318350
};
319351

0 commit comments

Comments
 (0)
Please sign in to comment.