Skip to content

Commit fce8caa

Browse files
Merge pull request #514 from universal-ember/improvements-to-portals
Improvements to portals (pattern for ember-wormhole migration, elemnt support, @Append support)
2 parents 180aae1 + 418d648 commit fce8caa

File tree

5 files changed

+155
-14
lines changed

5 files changed

+155
-14
lines changed

ember-primitives/src/components/portal.gts

Lines changed: 80 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
2+
import { assert } from "@ember/debug";
3+
14
import { modifier } from "ember-modifier";
25
import { cell } from "ember-resources";
36

@@ -13,9 +16,17 @@ export interface Signature {
1316
* The name of the PortalTarget to render in to.
1417
* This is the value of the `data-portal-name` attribute
1518
* of the element you wish to render in to.
19+
*
20+
* This can also be an Element which pairs nicely with query-utilities such as `wormhole`, or the platform-native `querySelector`
21+
*/
22+
to: (Targets | (string & {})) | Element;
23+
24+
/**
25+
* Set to true to append to the portal instead of replace
26+
*
27+
* Default: false
1628
*/
17-
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
18-
to: Targets | (string & {});
29+
append?: boolean;
1930
};
2031
Blocks: {
2132
/**
@@ -25,6 +36,39 @@ export interface Signature {
2536
};
2637
}
2738

39+
/**
40+
* Polyfill for ember-wormhole behavior
41+
*
42+
* Example usage:
43+
* ```gjs
44+
* import { wormhole, Portal } from 'ember-primitives/components/portal';
45+
*
46+
* <template>
47+
* <div id="the-portal"></div>
48+
*
49+
* <Portal @to={{wormhole "the-portal"}}>
50+
* content renders in the above div
51+
* </Portal>
52+
* </template>
53+
*
54+
* ```
55+
*/
56+
export function wormhole(query: string | null | undefined | Element) {
57+
assert(`Expected query/element to be truthy.`, query);
58+
59+
if (query instanceof Element) {
60+
return query;
61+
}
62+
63+
let found = document.getElementById(query);
64+
65+
found ??= document.querySelector(query);
66+
67+
assert(`Could not find element with id/selector ${query}`, found);
68+
69+
return found;
70+
}
71+
2872
const anchor = modifier(
2973
(element: Element, [to, update]: [string, ReturnType<typeof ElementValue>["set"]]) => {
3074
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
@@ -37,21 +81,43 @@ const anchor = modifier(
3781

3882
const ElementValue = () => cell<Element | ShadowRoot>();
3983

84+
function isElement(x: unknown): x is Element {
85+
return x instanceof Element;
86+
}
87+
4088
export const Portal: TOC<Signature> = <template>
41-
{{#let (ElementValue) as |target|}}
42-
{{! This div is always going to be empty,
89+
{{#if (isElement @to)}}
90+
{{#if @append}}
91+
{{#in-element @to insertBefore=null}}
92+
{{yield}}
93+
{{/in-element}}
94+
{{else}}
95+
{{#in-element @to}}
96+
{{yield}}
97+
{{/in-element}}
98+
{{/if}}
99+
{{else}}
100+
{{#let (ElementValue) as |target|}}
101+
{{! This div is always going to be empty,
43102
because it'll either find the portal and render content elsewhere,
44103
it it won't find the portal and won't render anything.
45-
}}
46-
{{! template-lint-disable no-inline-styles }}
47-
<div style="display:contents;" {{anchor @to target.set}}>
48-
{{#if target.current}}
49-
{{#in-element target.current}}
50-
{{yield}}
51-
{{/in-element}}
52-
{{/if}}
53-
</div>
54-
{{/let}}
104+
}}
105+
{{! template-lint-disable no-inline-styles }}
106+
<div style="display:contents;" {{anchor @to target.set}}>
107+
{{#if target.current}}
108+
{{#if @append}}
109+
{{#in-element target.current insertBefore=null}}
110+
{{yield}}
111+
{{/in-element}}
112+
{{else}}
113+
{{#in-element target.current}}
114+
{{yield}}
115+
{{/in-element}}
116+
{{/if}}
117+
{{/if}}
118+
</div>
119+
{{/let}}
120+
{{/if}}
55121
</template>;
56122

57123
export default Portal;

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test-app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
"prettier-plugin-ember-template-tag": "^2.0.5",
8888
"qunit": "^2.22.0",
8989
"qunit-dom": "^3.2.1",
90+
"qunit-theme-ember": "^1.0.0",
9091
"read-package-up": "^11.0.0",
9192
"stylelint": "^16.19.1",
9293
"stylelint-config-standard": "^38.0.0",

test-app/tests/portals/portal-rendering-test.gts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,73 @@ module('Rendering | <Portal>', function (hooks) {
3333

3434
assert.dom(`[data-portal-name="${PORTALS.popover}"]`).hasText('content');
3535
});
36+
37+
module('@append', function () {
38+
test('true appends', async function (assert) {
39+
await render(
40+
<template>
41+
<PortalTargets />
42+
<Portal @to={{PORTALS.popover}}>
43+
one
44+
</Portal>
45+
<Portal @to={{PORTALS.popover}} @append={{true}}>
46+
two
47+
</Portal>
48+
</template>
49+
);
50+
51+
assert.dom().hasText('one two');
52+
});
53+
54+
test('true appends, but a third without append will replace', async function (assert) {
55+
await render(
56+
<template>
57+
<PortalTargets />
58+
<Portal @to={{PORTALS.popover}}>
59+
one
60+
</Portal>
61+
<Portal @to={{PORTALS.popover}} @append={{true}}>
62+
two
63+
</Portal>
64+
<Portal @to={{PORTALS.popover}}>
65+
three
66+
</Portal>
67+
</template>
68+
);
69+
70+
assert.dom().hasText('three');
71+
});
72+
});
73+
74+
module('@to is an element', function () {
75+
test('element then Portal', async function (assert) {
76+
const element = document.createElement('output');
77+
78+
await render(
79+
<template>
80+
{{element}}
81+
<Portal @to={{element}}>
82+
content here
83+
</Portal>
84+
</template>
85+
);
86+
87+
assert.dom('output').hasText('content here');
88+
});
89+
90+
test('Portal then element', async function (assert) {
91+
const element = document.createElement('output');
92+
93+
await render(
94+
<template>
95+
<Portal @to={{element}}>
96+
content here
97+
</Portal>
98+
{{element}}
99+
</template>
100+
);
101+
102+
assert.dom('output').hasText('content here');
103+
});
104+
});
36105
});

test-app/tests/test-helper.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'qunit-theme-ember/qunit.css';
2+
13
import { currentRouteName, currentURL, getSettledState, setApplication } from '@ember/test-helpers';
24
import { getPendingWaiterState } from '@ember/test-waiters';
35
import * as QUnit from 'qunit';

0 commit comments

Comments
 (0)