Skip to content

Commit

Permalink
Allow TeleportController to have any number of child nodes of any kind
Browse files Browse the repository at this point in the history
  • Loading branch information
laymonage authored and lb- committed Mar 9, 2024
1 parent a21b1cf commit 2003e14
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 25 deletions.
98 changes: 84 additions & 14 deletions client/src/controllers/TeleportController.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ describe('TeleportController', () => {

await Promise.resolve();

expect(document.getElementById('target-container').innerHTML).toEqual(
'<div id="content">Some content</div>',
);
expect(
document.getElementById('target-container').innerHTML.trim(),
).toEqual('<div id="content">Some content</div>');
});

it('should allow for a default target container within the root element of a shadow DOM', async () => {
Expand Down Expand Up @@ -136,9 +136,9 @@ describe('TeleportController', () => {

await Promise.resolve();

expect(document.getElementById('target-container').innerHTML).toEqual(
'<div id="content">Some content</div>',
);
expect(
document.getElementById('target-container').innerHTML.trim(),
).toEqual('<div id="content">Some content</div>');
});

it('should not clear the target container if the reset value is unset (false)', async () => {
Expand All @@ -160,12 +160,84 @@ describe('TeleportController', () => {

await Promise.resolve();

const contents = document.getElementById('target-container').innerHTML;
expect(contents).toContain('<p>I should still be here</p>');
expect(contents).toContain('<div id="content">Some content</div>');
});

it('should allow the template to contain multiple children', async () => {
document.body.innerHTML += `
<div id="target-container"></div>
`;

const template = document.querySelector('template');
template.setAttribute(
'data-w-teleport-target-value',
'#target-container',
);

const otherTemplateContent = document.createElement('div');
otherTemplateContent.innerHTML = 'Other content';
otherTemplateContent.id = 'other-content';
template.content.appendChild(otherTemplateContent);

expect(document.getElementById('target-container').innerHTML).toEqual('');

application.start();

await Promise.resolve();

const container = document.getElementById('target-container');
const content = container.querySelector('#content');
const otherContent = container.querySelector('#other-content');
expect(content).not.toBeNull();
expect(otherContent).not.toBeNull();
expect(content.innerHTML.trim()).toEqual('Some content');
expect(otherContent.innerHTML.trim()).toEqual('Other content');
});

it('should not throw an error if the template content is empty', async () => {
document.body.innerHTML += `
<div id="target-container"><p>I should still be here</p></div>
`;

const template = document.querySelector('template');
template.setAttribute(
'data-w-teleport-target-value',
'#target-container',
);

expect(document.getElementById('target-container').innerHTML).toEqual(
'<p>I should still be here</p>',
);

const errors = [];

document.getElementById('template').innerHTML = '';
application.handleError = (error, message) => {
errors.push({ error, message });
};

await Promise.resolve(application.start());

expect(errors).toEqual([]);

expect(document.getElementById('target-container').innerHTML).toEqual(
'<p>I should still be here</p><div id="content">Some content</div>',
'<p>I should still be here</p>',
);
});

it('should throw an error if the template content is empty', async () => {
it('should allow erasing the target container by using an empty template with reset value set to true', async () => {
document.body.innerHTML += `
<div id="target-container"><p>I should not be here</p></div>
`;

const template = document.querySelector('template');
template.setAttribute(
'data-w-teleport-target-value',
'#target-container',
);
template.setAttribute('data-w-teleport-reset-value', 'true');
const errors = [];

document.getElementById('template').innerHTML = '';
Expand All @@ -176,12 +248,10 @@ describe('TeleportController', () => {

await Promise.resolve(application.start());

expect(errors).toEqual([
{
error: new Error('Invalid template content.'),
message: 'Error connecting controller',
},
]);
expect(errors).toEqual([]);

const contents = document.getElementById('target-container').innerHTML;
expect(contents).toEqual('');
});

it('should throw an error if a valid target container cannot be resolved', async () => {
Expand Down
20 changes: 9 additions & 11 deletions client/src/controllers/TeleportController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class TeleportController extends Controller<HTMLTemplateElement> {
const complete = () => {
if (completed) return;
if (this.resetValue) target.innerHTML = '';
target.append(this.templateElement);
target.append(...this.templateFragment.childNodes);
this.dispatch('appended', { cancelable: false, detail: { target } });
completed = true;
if (this.keepValue) return;
Expand Down Expand Up @@ -88,22 +88,20 @@ export class TeleportController extends Controller<HTMLTemplateElement> {
}

/**
* Resolve a valid HTMLElement from the controlled element's children.
* Returns a fresh copy of the DocumentFragment from the controlled element.
*/
get templateElement() {
const templateElement =
this.element.content.firstElementChild?.cloneNode(true);

if (!(templateElement instanceof HTMLElement)) {
throw new Error('Invalid template content.');
}
get templateFragment() {
const content = this.element.content;
// https://developer.mozilla.org/en-US/docs/Web/API/Node/cloneNode (returns the same type)
// https://github.com/microsoft/TypeScript/issues/283 (TypeScript will return as Node, incorrectly)
const templateFragment = content.cloneNode(true) as typeof content;

// HACK:
// cloneNode doesn't run scripts, so we need to create new script elements
// and copy the attributes and innerHTML over. This is necessary when we're
// teleporting a template that contains legacy init code, e.g. initDateChooser.
// Only do this for inline scripts, as that's what we're expecting.
templateElement
templateFragment
.querySelectorAll('script:not([src], [type])')
.forEach((script) => {
const newScript = document.createElement('script');
Expand All @@ -114,6 +112,6 @@ export class TeleportController extends Controller<HTMLTemplateElement> {
script.replaceWith(newScript);
});

return templateElement;
return templateFragment;
}
}

0 comments on commit 2003e14

Please sign in to comment.