Skip to content

Commit

Permalink
Implement support for use in add-ons (#10)
Browse files Browse the repository at this point in the history
* Implement support for use within add-ons

* Decommission babel plugin in favor of checking dependencies

* Fix state leak

* Rebuild package lock

Co-authored-by: Ten Bitcomb <[email protected]>
  • Loading branch information
Ravenstine and Ten Bitcomb committed Jan 29, 2021
1 parent 75a14b0 commit 8b24456
Show file tree
Hide file tree
Showing 16 changed files with 2,251 additions and 2,982 deletions.
19 changes: 8 additions & 11 deletions addon/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
// eslint-disable-next-line no-unused-vars
import EmberCustomElement, { CURRENT_CUSTOM_ELEMENT, INITIALIZERS } from './lib/custom-element';
import EmberCustomElement, {
CURRENT_CUSTOM_ELEMENT,
CUSTOM_ELEMENT_OPTIONS,
INITIALIZERS
} from './lib/custom-element';
import {
getCustomElements,
addCustomElement,
Expand All @@ -9,7 +13,7 @@ import {
isApp
} from './lib/common';
import { isGlimmerComponent } from './lib/glimmer-compat';
import { getOwner, setOwner } from '@ember/application';
import { setOwner } from '@ember/application';

export { default as EmberOutletElement } from './lib/outlet-element';
export { default as EmberCustomElement } from './lib/custom-element';
Expand Down Expand Up @@ -114,13 +118,7 @@ export function customElement() {
}

// Overwrite the original config on the element
const initialize = function() {
const ENV = getOwner(this).resolveRegistration('config:environment') || {};
const { defaultOptions = {} } = ENV.emberCustomElements || {};
this.options = Object.assign({}, defaultOptions, customElementOptions);
}
const initializers = [initialize];
INITIALIZERS.set(element, initializers);
CUSTOM_ELEMENT_OPTIONS.set(element, customElementOptions);

addCustomElement(decoratedClass, element);

Expand Down Expand Up @@ -204,8 +202,7 @@ export function setupCustomElementFor(instance, registrationName) {
setOwner(this, instance);
this.parsedName = parsedName;
};
const initializers = INITIALIZERS.get(customElement) || [];
initializers.unshift(initialize);
INITIALIZERS.set(customElement, initialize);
}
}

Expand Down
37 changes: 27 additions & 10 deletions addon/instance-initializers/ember-custom-elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,12 @@ export function initialize(instance) {
const entityNames = instance.__registry__.fallback.resolver.knownForType(type);
for (const entityName in entityNames) {
const parsedName = instance.__registry__.fallback.resolver.parseName(entityName);
const moduleName = instance.__registry__.fallback.resolver.findModuleName(parsedName);
const _module = instance.__registry__.fallback.resolver._moduleRegistry._entries[moduleName];
const code = _module.callback.toString();
const {
emberCustomElements = {}
} = instance.resolveRegistration('config:environment');
// Only evaluate the component module if its code contains our sigil.
const _moduleName = instance.__registry__.fallback.resolver.findModuleName(parsedName);
const _module = instance.__registry__.fallback.resolver._moduleRegistry._entries[_moduleName];
// Only evaluate the component module if it is using our decorator.
// This optimization is ignored in testing so that components can be
// dynamically created and registered.
const shouldEvalModule =
emberCustomElements.deoptimizeModuleEval ||
/\n\s*"~~EMBER~CUSTOM~ELEMENT~~";\s*\n/.test(code);
const shouldEvalModule = determineIfShouldEvalModule(instance, _module);
if (!shouldEvalModule) continue;
const componentClass = instance.resolveRegistration(entityName);
const customElements = getCustomElements(componentClass);
Expand All @@ -63,3 +57,26 @@ export function initialize(instance) {
export default {
initialize
};

const DECORATOR_REGEX = /customElement\s*\){0,1}\s*\(/;

function determineIfShouldEvalModule(instance, _module) {
const {
emberCustomElements = {}
} = instance.resolveRegistration('config:environment');
if (emberCustomElements.deoptimizeModuleEval) return true;
function _moduleShouldEval(_module) {
for (const moduleName of _module.deps) {
// Check if ember-custom-elements is a dependency of the module
if (moduleName === 'ember-custom-elements') {
const code = (_module.callback || function() {}).toString();
// Test if a function named "customElement" is called within the module
if (DECORATOR_REGEX.test(code)) return true;
}
const dep = instance.__registry__.fallback.resolver._moduleRegistry._entries[moduleName];
if (dep && _moduleShouldEval(dep)) return true;
}
return false;
}
return _moduleShouldEval(_module);
}
41 changes: 27 additions & 14 deletions addon/lib/custom-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import OutletElement, { OUTLET_VIEWS } from './outlet-element';
import BlockContent from './block-content';

export const CURRENT_CUSTOM_ELEMENT = { element: null };
export const CUSTOM_ELEMENT_OPTIONS = new WeakMap();
export const INITIALIZERS = new WeakMap();

const APPS = new WeakMap();
Expand Down Expand Up @@ -44,20 +45,22 @@ export default class EmberCustomElement extends HTMLElement {
* @returns {String|null}
*/
get outlet() {
return this.options.outletName || 'main';
const options = getOptions(this);
return options.outletName || 'main';
}
/**
* 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;
const options = getOptions(this);
return options.preserveOutletContent;
}

/**
Expand All @@ -75,8 +78,8 @@ export default class EmberCustomElement extends HTMLElement {

await getInitializationPromise();

const initializers = INITIALIZERS.get(this.constructor) || [];
for (const initializer of initializers) initializer.call(this);
const initializer = INITIALIZERS.get(this.constructor);
initializer.call(this);

const { type } = this.parsedName;
if (type === 'component') return connectComponent.call(this);
Expand Down Expand Up @@ -126,10 +129,11 @@ function updateComponentArgs() {
try {
const view = COMPONENT_VIEWS.get(this);
if (!view) return;
const options = getOptions(this);
const attrs = { ...view.attrs };
set(view, 'attrs', attrs);
for (const attr of changes) {
const attrName = this.options.camelizeArgs ? camelize(attr) : attr;
const attrName = options.camelizeArgs ? camelize(attr) : attr;
attrs[attrName] = this.getAttribute(attr);
notifyPropertyChange(view, `attrs.${attrName}`);
}
Expand Down Expand Up @@ -158,14 +162,15 @@ async function connectComponent() {
// Capture block content and replace
const blockContent = BlockContent.from(this.childNodes);
BLOCK_CONTENTS.set(this, blockContent);
const useShadowRoot = !!this.options.useShadowRoot;
const options = getOptions(this);
const useShadowRoot = !!options.useShadowRoot;
if (useShadowRoot) this.attachShadow({mode: 'open'});
const target = this.shadowRoot ? this.shadowRoot : this;
if (target === this) this.innerHTML = '';
// Setup attributes and attribute observer
const attrs = {};
for (const attr of this.getAttributeNames()) {
const attrName = this.options.camelizeArgs ? camelize(attr) : attr;
const attrName = options.camelizeArgs ? camelize(attr) : attr;
attrs[attrName] = this.getAttribute(attr);
}
const observedAttributes = this.constructor.observedAttributes;
Expand All @@ -174,7 +179,7 @@ async function connectComponent() {
// to be tracked if they become present later and set to be observed.
// eslint-disable-next-line no-prototype-builtins
for (const attr of observedAttributes) if (!attrs.hasOwnProperty(attr)) {
const attrName = this.options.camelizeArgs ? camelize(attr) : attr;
const attrName = options.camelizeArgs ? camelize(attr) : attr;
attrs[attrName] = null;
}
} else if (observedAttributes !== false) {
Expand Down Expand Up @@ -216,27 +221,28 @@ async function connectComponent() {
* @private
*/
async function connectRoute() {
const useShadowRoot = this.options.useShadowRoot;
const options = getOptions(this);
const useShadowRoot = options.useShadowRoot;
if (useShadowRoot) {
this.attachShadow({ mode: 'open' });
}
OutletElement.prototype.connectedCallback.call(this);
}
/**
* Sets up an application to be rendered in the element.
*
*
* Here, we are actually booting the app into a detached
* element and then relying on `connectRoute` to render
* the application route for the app instance.
*
*
* There are a few advantages to this. This allows the
* rendered content to be less "deep", meaning that we
* don't need two useless elements, which the app
* instance is expecting, to be present in the DOM. The
* second advantage is that this prevents problems
* rendering apps within other apps in a way that doesn't
* require the use of a shadowRoot.
*
*
* @private
*/
async function connectApplication() {
Expand All @@ -255,5 +261,12 @@ async function connectApplication() {
connectRoute.call(this);
}

function getOptions(element) {
const customElementOptions = CUSTOM_ELEMENT_OPTIONS.get(element.constructor);
const ENV = getOwner(element).resolveRegistration('config:environment') || {};
const { defaultOptions = {} } = ENV.emberCustomElements || {};
return Object.assign({}, defaultOptions, customElementOptions);
}

EmberCustomElement.prototype.updateOutletState = OutletElement.prototype.updateOutletState;
EmberCustomElement.prototype.scheduleUpdateOutletState = OutletElement.prototype.scheduleUpdateOutletState;
12 changes: 0 additions & 12 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const { hasPlugin, addPlugin } = require('ember-cli-babel-plugin-helpers');
const plugin = require.resolve('./lib/babel-plugin-ember-custom-elements');
// eslint-disable-next-line node/no-unpublished-require
const { precompile } = require('ember-source/dist/ember-template-compiler');
const replace = require('broccoli-string-replace');
Expand All @@ -10,16 +8,6 @@ const BASE_TEMPLATE_STRING = '<ComponentName @attributeName={{this.valueName}}>{

module.exports = {
name: require('./package').name,
included(parent) {
// eslint-disable-next-line prefer-rest-params
this._super.included.apply(this, arguments);

const target = parent;

if (!hasPlugin(target, plugin)) {
addPlugin(target, plugin);
}
},
treeForAddon(tree) {
let outputTree = replace(tree, {
files: ['lib/template-compiler.js'],
Expand Down
47 changes: 0 additions & 47 deletions lib/babel-plugin-ember-custom-elements.js

This file was deleted.

Loading

0 comments on commit 8b24456

Please sign in to comment.