Skip to content

Commit

Permalink
Add preserveOutletContent and fix minor bug
Browse files Browse the repository at this point in the history
  • Loading branch information
Ravenstine committed Jun 5, 2020
1 parent 25823f3 commit 58cd741
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 8 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Ember Web Components Changelog
==============================

### v0.2.0

- Added `preserveOutletContent` option, which can be used to keep outlet DOM contents from being cleared when navigating away from a route.
- Fixed a bug in the Outlet element where router event listeners were not being removed, causing the outlet to try and update even after the outlet view has been destroyed.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ At present, there are a few options you can pass when creating custom elements:
- **customElementClass**: In the extreme edge case that you need to redefine the behavior of the custom element class itself, you can `import { EmberCustomElement } from 'ember-custom-elements';`, extend it into a subclass, and pass that subclass to the `customElementClass` option. This is definitely an expert tool and, even if you think you need this, you probably don't need it. This is made available only for the desperate. The `EmberCustomElement` class should be considered a private entity.
- **camelizeArgs**: Element attributes must be kabob-case, but if `camelizeArgs` is set to true, these attributes will be exposed to your components in camelCase.
- **outletName**: (routes only) The name of an outlet you wish to render for a route. Defaults to 'main'. The section on [named outlets][#named-outlets] goes into further detail.
- **preserveOutletContent**: (routes only) When set to `true`, this prevents the DOM content inside the element from being cleared when transition away from the route is performed. This is `false` by default, but you may want to set this to `true` in the case where you need to keep the DOM content around for animation purposes.



Expand Down
1 change: 1 addition & 0 deletions addon/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { setOwner } from '@ember/application';
* @param {Array<String>} customElementOptions.observedAttributes - An array of attribute names specifying which custom element attributes should be observed. Observed attributes will update their value to the Ember/Glimmer component when said value changes.
* @param {Boolean=false} customElementOptions.camelizeArgs - Element attributes must be kabob-case, but if `camelizeArgs` is set to true, these attributes will be exposed to your components in camelCase.
* @param {String="main"} customElementOptions.outletName - The name of the outlet to render. This option only applies to Ember.Route.
* @param {Boolean="true"} customElementOptions.clearsOutletAfterTransition - When set to `false`, this prevents the DOM content inside the element from being cleared when transition away from the route is performed. This is `true` by default, but you may want to set this to `false` in the case where you need to keep the DOM content around for animation purposes.
*
* Basic usage:
* @example
Expand Down
15 changes: 14 additions & 1 deletion addon/lib/custom-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ export default class EmberCustomElement extends HTMLElement {
get outlet() {
return this.options.outletName;
}
/**
* If the referenced class is a route, and this is set to `true`, the DOM tree
* inside the element will not be cleared when the route is transitioned away
* until the element itself is destroyed.
*
* This only applies to routes. No behavior changes when applied to components
* or applications.
*
* @returns {Boolean=false}
*/
get preserveOutletContent() {
return this.options.preserveOutletContent;
}

constructor() {
super(...arguments);
Expand Down Expand Up @@ -180,7 +193,7 @@ export default class EmberCustomElement extends HTMLElement {
if (this._blockContent) this._blockContent.destroy();
if (this.attributesObserver) this.attributesObserver.disconnect();
const { type } = this.parsedName;
if (type === 'route') this.innerHTML = '';
if (type === 'route' && !this.preserveOutletContent) this.innerHTML = '';
}
}

Expand Down
17 changes: 11 additions & 6 deletions addon/lib/outlet-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { scheduleOnce } from '@ember/runloop';
*
* @argument {String} route - The dot-delimited name of a route.
* @argument {String='main'} name - The name of an outlet.
* @argument {String='true'} preserveContent - Prevents outlet contents from being cleared when transitioning out of the route or when the element is disconnected.
*/
export default class EmberWebOutlet extends HTMLElement {
get route() {
Expand All @@ -16,6 +17,10 @@ export default class EmberWebOutlet extends HTMLElement {
return this.getAttribute('name') || 'main';
}

get preserveOutletContent() {
return this.getAttribute('preserve-content') === 'true' || false;
}

constructor() {
super(...arguments);
this.initialize();
Expand All @@ -35,21 +40,21 @@ export default class EmberWebOutlet extends HTMLElement {
const view = OutletView.create();
view.appendTo(target);
this.view = view;
// router.on('willTransition', this.scheduleUpdateOutletState);
// router.on('didTransition', this.scheduleUpdateOutletState);
router.on('routeWillChange', this.scheduleUpdateOutletState);
router.on('routeDidChange', this.scheduleUpdateOutletState);
this.updateOutletState();
}

scheduleUpdateOutletState() {
scheduleUpdateOutletState(transition) {
if (transition.to.name !== this.route && this.preserveOutletContent) return;
scheduleOnce('render', this, 'updateOutletState')
}

/**
* Looks up the outlet on the top-level view and updates the state of our outlet view.
*/
updateOutletState() {
if (!this.isConnected) return;
const router = getOwner(this).lookup('router:main');
const outletState = lookupOutlet(router._toplevelView.ref.outletState, this.route, this.outlet) || {};
this.view.setOutletState(outletState);
Expand All @@ -58,8 +63,8 @@ export default class EmberWebOutlet extends HTMLElement {
disconnectedCallback() {
const owner = getOwner(this);
const router = owner.lookup('router:main');
router.off('willTransition', this.scheduleUpdateOutletState);
router.off('didTransition', this.scheduleUpdateOutletState);
router.off('routeWillChange', this.scheduleUpdateOutletState);
router.off('routeDidChange', this.scheduleUpdateOutletState);
this.destroyOutlet();
}

Expand All @@ -69,7 +74,7 @@ export default class EmberWebOutlet extends HTMLElement {
this.view = null;
}
const target = this.shadowRoot || this;
target.innerHTML = '';
if (this.preserveOutletContent !== 'true') target.innerHTML = '';
}
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ember-custom-elements",
"version": "0.1.0",
"version": "0.2.0",
"description": "Easily use custom elements to invoke your Ember components, routes, and applications.",
"keywords": [
"ember-addon",
Expand Down
60 changes: 60 additions & 0 deletions tests/integration/ember-custom-elements-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,66 @@ module('Integration | Component | ember-custom-elements', function(hooks) {
await settled();
assert.equal(element.shadowRoot.textContent.trim(), 'foo baz', 'transitions to second route');
});

test('it destroys DOM contents when navigating away', async function(assert) {
@customElement('foo-route')
class FooRoute extends Route {

}
setupRouteForTest(this.owner, FooRoute, 'foo-route');

@customElement('bar-route')
class BazRoute extends Route {

}
setupRouteForTest(this.owner, BazRoute, 'bar-route');

this.owner.register('template:application', hbs`<foo-route></foo-route><bar-route></bar-route>`);
this.owner.register('template:foo-route', hbs`<h2 data-test-foo>foo</h2>`);
this.owner.register('template:bar-route', hbs`<h2 data-test-bar>bar</h2>`);

setupTestRouter(this.owner, function() {
this.route('foo-route', { path: '/foo' });
this.route('bar-route', { path: '/bar' });
});

this.owner.lookup('router:main').transitionTo('/foo');
await settled();
this.owner.lookup('router:main').transitionTo('/bar');
await settled();
const element = find('foo-route');
assert.notOk(element.shadowRoot.querySelector('[data-test-foo]'), 'it destroys DOM contents');
});

test('it can preserve DOM contents when navigating away', async function(assert) {
@customElement('foo-route', { preserveOutletContent: true })
class FooRoute extends Route {

}
setupRouteForTest(this.owner, FooRoute, 'foo-route');

@customElement('bar-route')
class BazRoute extends Route {

}
setupRouteForTest(this.owner, BazRoute, 'bar-route');

this.owner.register('template:application', hbs`<foo-route></foo-route><bar-route></bar-route>`);
this.owner.register('template:foo-route', hbs`<h2 data-test-foo>foo</h2>`);
this.owner.register('template:bar-route', hbs`<h2 data-test-bar>bar</h2>`);

setupTestRouter(this.owner, function() {
this.route('foo-route', { path: '/foo' });
this.route('bar-route', { path: '/bar' });
});

this.owner.lookup('router:main').transitionTo('/foo');
await settled();
this.owner.lookup('router:main').transitionTo('/bar');
await settled();
const element = find('foo-route');
assert.ok(element.shadowRoot.querySelector('[data-test-foo]'), 'it preserves DOM contents');
});
});

module('unsupported', function() {
Expand Down

0 comments on commit 58cd741

Please sign in to comment.