Skip to content

Commit

Permalink
allow defining the element used when hydrating svelte components (#221)
Browse files Browse the repository at this point in the history
* allow defining the mounted element for used when hydrating svelte components

* make sure component mount element tags match

* fix up tests after adding default element div hydrate option

* revert bumping of match[0] index as it comes before the added element name match

Co-authored-by: Douglas Ward <[email protected]>
  • Loading branch information
douglasward and Douglas Ward authored Dec 1, 2021
1 parent 205f039 commit e8435c2
Show file tree
Hide file tree
Showing 7 changed files with 46 additions and 34 deletions.
10 changes: 5 additions & 5 deletions src/partialHydration/__tests__/inlineSvelteComponent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ test('#escapeHtml', () => {
});

test('#inlinePreprocessedSvelteComponent', () => {
const options = 'loading=lazy';
const options = '{"loading":"lazy"}';
expect(
inlinePreprocessedSvelteComponent({
name: 'Home',
Expand All @@ -18,10 +18,10 @@ test('#inlinePreprocessedSvelteComponent', () => {
options,
}),
).toEqual(
`<div class="ejs-component" data-ejs-component="Home" data-ejs-props={JSON.stringify([object Object])} data-ejs-options={JSON.stringify(${options})} />`,
`<div class="ejs-component" data-ejs-component="Home" data-ejs-props={JSON.stringify([object Object])} data-ejs-options={JSON.stringify({"loading":"lazy","element":"div"})} />`,
);
expect(inlinePreprocessedSvelteComponent({})).toEqual(
`<div class="ejs-component" data-ejs-component="" data-ejs-props={JSON.stringify([object Object])} data-ejs-options={JSON.stringify({"loading":"lazy"})} />`,
`<div class="ejs-component" data-ejs-component="" data-ejs-props={JSON.stringify([object Object])} data-ejs-options={JSON.stringify({"loading":"lazy","element":"div"})} />`,
);
});

Expand All @@ -38,9 +38,9 @@ test('#inlineSvelteComponent', () => {
options,
}),
).toEqual(
`<div class="ejs-component" data-ejs-component="Home" data-ejs-props="{&quot;welcomeText&quot;:&quot;Hello World&quot;}" data-ejs-options="{&quot;loading&quot;:&quot;lazy&quot;}"></div>`,
`<div class="ejs-component" data-ejs-component="Home" data-ejs-props="{&quot;welcomeText&quot;:&quot;Hello World&quot;}" data-ejs-options="{&quot;loading&quot;:&quot;lazy&quot;,&quot;element&quot;:&quot;div&quot;}"></div>`,
);
expect(inlineSvelteComponent({})).toEqual(
`<div class="ejs-component" data-ejs-component="" data-ejs-props="{}" data-ejs-options="{&quot;loading&quot;:&quot;lazy&quot;}"></div>`,
`<div class="ejs-component" data-ejs-component="" data-ejs-props="{}" data-ejs-options="{&quot;loading&quot;:&quot;lazy&quot;,&quot;element&quot;:&quot;div&quot;}"></div>`,
);
});
22 changes: 11 additions & 11 deletions src/partialHydration/__tests__/partialHydration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,55 +9,55 @@ describe('#partialHydration', () => {
})
).code,
).toEqual(
`<div class="ejs-component" data-ejs-component="DatePicker" data-ejs-props={JSON.stringify({ a: "b" })} data-ejs-options={JSON.stringify({"loading":"lazy"})} />`,
`<div class="ejs-component" data-ejs-component="DatePicker" data-ejs-props={JSON.stringify({ a: "b" })} data-ejs-options={JSON.stringify({"loading":"lazy","element":"div"})} />`,
);
});

it('explicit lazy', async () => {
expect(
(
await partialHydration.markup({
content: '<DatePicker hydrate-client={{ a: "c" }} hydrate-options={{ loading: "lazy" }}/>',
content: '<DatePicker hydrate-client={{ a: "c" }} hydrate-options={{ "loading": "lazy" }}/>',
})
).code,
).toEqual(
`<div class="ejs-component" data-ejs-component="DatePicker" data-ejs-props={JSON.stringify({ a: "c" })} data-ejs-options={JSON.stringify({ loading: "lazy" })} />`,
`<div class="ejs-component" data-ejs-component="DatePicker" data-ejs-props={JSON.stringify({ a: "c" })} data-ejs-options={JSON.stringify({"loading":"lazy","element":"div"})} />`,
);
});

it('explicit timeout', async () => {
expect(
(
await partialHydration.markup({
content: '<DatePicker hydrate-client={{ a: "c" }} hydrate-options={{ timeout: 2000 }}/>',
content: '<DatePicker hydrate-client={{ a: "c" }} hydrate-options={{ "timeout": 2000 }}/>',
})
).code,
).toEqual(
`<div class="ejs-component" data-ejs-component="DatePicker" data-ejs-props={JSON.stringify({ a: "c" })} data-ejs-options={JSON.stringify({ timeout: 2000 })} />`,
`<div class="ejs-component" data-ejs-component="DatePicker" data-ejs-props={JSON.stringify({ a: "c" })} data-ejs-options={JSON.stringify({"loading":"lazy","element":"div","timeout":2000})} />`,
);
});

it('eager', async () => {
expect(
(
await partialHydration.markup({
content: '<DatePicker hydrate-client={{ a: "b" }} hydrate-options={{ loading: "eager" }} />',
content: '<DatePicker hydrate-client={{ a: "b" }} hydrate-options={{ "loading": "eager" }} />',
})
).code,
).toEqual(
`<div class="ejs-component" data-ejs-component="DatePicker" data-ejs-props={JSON.stringify({ a: "b" })} data-ejs-options={JSON.stringify({ loading: "eager" })} />`,
`<div class="ejs-component" data-ejs-component="DatePicker" data-ejs-props={JSON.stringify({ a: "b" })} data-ejs-options={JSON.stringify({"loading":"eager","element":"div"})} />`,
);
});
it('eager, root margin, threshold', async () => {
expect(
(
await partialHydration.markup({
content:
'<DatePicker hydrate-client={{ a: "b" }} hydrate-options={{ loading: "eager", rootMargin: "500px", threshold: 0 }} />',
'<DatePicker hydrate-client={{ a: "b" }} hydrate-options={{ "loading": "eager", "rootMargin": "500px", "threshold": 0 }} />',
})
).code,
).toEqual(
`<div class="ejs-component" data-ejs-component="DatePicker" data-ejs-props={JSON.stringify({ a: "b" })} data-ejs-options={JSON.stringify({ loading: "eager", rootMargin: "500px", threshold: 0 })} />`,
`<div class="ejs-component" data-ejs-component="DatePicker" data-ejs-props={JSON.stringify({ a: "b" })} data-ejs-options={JSON.stringify({"loading":"eager","element":"div","rootMargin":"500px","threshold":0})} />`,
);
});
it('open string', async () => {
Expand Down Expand Up @@ -102,11 +102,11 @@ describe('#partialHydration', () => {
expect(
(
await partialHydration.markup({
content: `<Clock hydrate-client={{}} hydrate-options={{ loading: 'eager', preload: true }} /><Block hydrate-client={{}} hydrate-options={{ loading: 'lazy' }} /><Alock hydrate-client={{}} hydrate-options={{ loading: 'lazy' }} />`,
content: `<Clock hydrate-client={{}} hydrate-options={{ "loading": "eager", "preload": true }} /><Block hydrate-client={{}} hydrate-options={{ "loading": "lazy" }} /><Alock hydrate-client={{}} hydrate-options={{ "loading": "lazy" }} />`,
})
).code,
).toEqual(
`<div class="ejs-component" data-ejs-component="Clock" data-ejs-props={JSON.stringify({})} data-ejs-options={JSON.stringify({ loading: 'eager', preload: true })} /><div class="ejs-component" data-ejs-component="Block" data-ejs-props={JSON.stringify({})} data-ejs-options={JSON.stringify({ loading: 'lazy' })} /><div class="ejs-component" data-ejs-component="Alock" data-ejs-props={JSON.stringify({})} data-ejs-options={JSON.stringify({ loading: 'lazy' })} />`,
`<div class="ejs-component" data-ejs-component="Clock" data-ejs-props={JSON.stringify({})} data-ejs-options={JSON.stringify({"loading":"eager","element":"div","preload":true})} /><div class="ejs-component" data-ejs-component="Block" data-ejs-props={JSON.stringify({})} data-ejs-options={JSON.stringify({"loading":"lazy","element":"div"})} /><div class="ejs-component" data-ejs-component="Alock" data-ejs-props={JSON.stringify({})} data-ejs-options={JSON.stringify({"loading":"lazy","element":"div"})} />`,
);
});
});
19 changes: 13 additions & 6 deletions src/partialHydration/inlineSvelteComponent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
const defaultHydrationOptions = {
import { HydrateOptions } from '../utils/types';

const defaultHydrationOptions: HydrateOptions = {
loading: 'lazy',
element: 'div',
};

export function escapeHtml(text: string): string {
Expand All @@ -22,26 +25,29 @@ export function inlinePreprocessedSvelteComponent({
props = {},
options = '',
}: InputParamsInlinePreprocessedSvelteComponent): string {
const hydrationOptions = options.length > 0 ? options : JSON.stringify(defaultHydrationOptions);
const hydrationOptions =
options.length > 0 ? { ...defaultHydrationOptions, ...JSON.parse(options) } : defaultHydrationOptions;
const hydrationOptionsString = JSON.stringify(hydrationOptions);

const replacementAttrs = {
class: '"ejs-component"',
'data-ejs-component': `"${name}"`,
'data-ejs-props': `{JSON.stringify(${props})}`,
'data-ejs-options': `{JSON.stringify(${hydrationOptions})}`,
'data-ejs-options': `{JSON.stringify(${hydrationOptionsString})}`,
};
const replacementAttrsString = Object.entries(replacementAttrs).reduce(
(out, [key, value]) => `${out} ${key}=${value}`,
'',
);
return `<div${replacementAttrsString} />`;
return `<${hydrationOptions.element}${replacementAttrsString} />`;
}

type InputParamsInlineSvelteComponent = {
name?: string;
props?: any;
options?: {
loading?: string; // todo: enum, can't get it working: 'lazy', 'eager', 'none'
element?: string; // default: 'div'
};
};

Expand All @@ -50,7 +56,8 @@ export function inlineSvelteComponent({
props = {},
options = {},
}: InputParamsInlineSvelteComponent): string {
const hydrationOptions = Object.keys(options).length > 0 ? options : defaultHydrationOptions;
const hydrationOptions =
Object.keys(options).length > 0 ? { ...defaultHydrationOptions, ...options } : defaultHydrationOptions;

const replacementAttrs = {
class: '"ejs-component"',
Expand All @@ -63,5 +70,5 @@ export function inlineSvelteComponent({
'',
);

return `<div${replacementAttrsString}></div>`;
return `<${hydrationOptions.element}${replacementAttrsString}></${hydrationOptions.element}>`;
}
12 changes: 6 additions & 6 deletions src/partialHydration/mountComponentsInHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,23 @@ export default function mountComponentsInHtml({ page, html, hydrateOptions }): s
let outputHtml = html;
// sometimes svelte adds a class to our inlining.
const matches = outputHtml.matchAll(
/<div class="ejs-component[^]*?" data-ejs-component="([A-Za-z]+)" data-ejs-props="({[^]*?})" data-ejs-options="({[^]*?})"><\/div>/gim,
/<(\S+) class="ejs-component[^]*?" data-ejs-component="([A-Za-z]+)" data-ejs-props="({[^]*?})" data-ejs-options="({[^]*?})"><\/\1>/gim,
);

for (const match of matches) {
const hydrateComponentName = match[1];
const hydrateComponentName = match[2];
let hydrateComponentProps;
let hydrateComponentOptions;

try {
hydrateComponentProps = JSON.parse(replaceSpecialCharacters(match[2]));
hydrateComponentProps = JSON.parse(replaceSpecialCharacters(match[3]));
} catch (e) {
throw new Error(`Failed to JSON.parse props for ${hydrateComponentName} ${match[2]}`);
throw new Error(`Failed to JSON.parse props for ${hydrateComponentName} ${match[3]}`);
}
try {
hydrateComponentOptions = JSON.parse(replaceSpecialCharacters(match[3]));
hydrateComponentOptions = JSON.parse(replaceSpecialCharacters(match[4]));
} catch (e) {
throw new Error(`Failed to JSON.parse props for ${hydrateComponentName} ${match[3]}`);
throw new Error(`Failed to JSON.parse props for ${hydrateComponentName} ${match[4]}`);
}

if (hydrateOptions) {
Expand Down
10 changes: 5 additions & 5 deletions src/utils/__tests__/svelteComponent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ describe('#svelteComponent', () => {
render: () => ({
head: '<head>',
css: { code: '<old>' },
html: '<div class="svelte-datepicker"><div class="ejs-component" data-ejs-component="Datepicker" data-ejs-props="{ "a": "b" }" data-ejs-options="{ "loading": "lazy" }"></div></div>',
html: '<div class="svelte-datepicker"><div class="ejs-component" data-ejs-component="Datepicker" data-ejs-props="{ "a": "b" }" data-ejs-options="{ "loading": "lazy", "element": "div" }"></div></div>',
}),
_css: ['<css>', '<css2>'],
}),
Expand All @@ -110,7 +110,7 @@ describe('#svelteComponent', () => {
);

expect(componentProps.page.componentsToHydrate[0]).toMatchObject({
hydrateOptions: { loading: 'lazy' },
hydrateOptions: { loading: 'lazy', element: 'div' },
id: 'SwrzsrVDCd',
name: 'datepickerSwrzsrVDCd',
prepared: {},
Expand All @@ -125,7 +125,7 @@ describe('#svelteComponent', () => {
render: () => ({
head: '<head>',
css: { code: '<old>' },
html: '<div class="svelte-datepicker"><div class="ejs-component" data-ejs-component="Datepicker" data-ejs-props="{ "a": "b" }" data-ejs-options="{ "loading": "lazy" }"></div></div>',
html: '<div class="svelte-datepicker"><div class="ejs-component" data-ejs-component="Datepicker" data-ejs-props="{ "a": "b" }" data-ejs-options="{ "loading": "lazy", "element": "div" }"></div></div>',
}),
_css: ['<css>', '<css2>'],
_cssMap: ['<cssmap>', '<cssmap2>'],
Expand Down Expand Up @@ -189,7 +189,7 @@ describe('#svelteComponent', () => {
);
expect(props.page.svelteCss).toEqual([{ css: ['<css>', '<css2>'], cssMap: ['<cssmap>', '<cssmap2>'] }]);
expect(props.page.componentsToHydrate[0]).toMatchObject({
hydrateOptions: { loading: 'lazy' },
hydrateOptions: { loading: 'lazy', element: 'div' },
id: 'SwrzsrVDCd',
name: 'datepickerSwrzsrVDCd',
prepared: {},
Expand All @@ -204,7 +204,7 @@ describe('#svelteComponent', () => {
render: () => ({
head: '<head>',
css: { code: '<old>' },
html: '<div class="svelte-datepicker"><div class="ejs-component" data-ejs-component="Datepicker" data-ejs-props="{ "a": "b" }" data-ejs-options="{ "loading": "lazy" }"></div></div>',
html: '<div class="svelte-datepicker"><div class="ejs-component" data-ejs-component="Datepicker" data-ejs-props="{ "a": "b" }" data-ejs-options="{ "loading": "lazy", "element": "div" }"></div></div>',
}),
_css: ['<css>', '<css2>'],
_cssMap: ['<cssmap>', '<cssmap2>'],
Expand Down
6 changes: 5 additions & 1 deletion src/utils/svelteComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ const svelteComponent =
id,
});

return `<div class="${cleanComponentName.toLowerCase()}-component" id="${uniqueComponentName}">${innerHtml}</div>`;
return `<${
hydrateOptions.element
} class="${cleanComponentName.toLowerCase()}-component" id="${uniqueComponentName}">${innerHtml}</${
hydrateOptions.element
}>`;
} catch (e) {
// console.log(e);
page.errors.push(e);
Expand Down
1 change: 1 addition & 0 deletions src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ export type HydrateOptions = {
noPrefetch?: boolean;
threshold?: number;
rootMargin?: string;
element?: string;
};

export interface ComponentPayload {
Expand Down

0 comments on commit e8435c2

Please sign in to comment.