diff --git a/packages/mdc-auto-init/index.js b/packages/mdc-auto-init/index.ts similarity index 52% rename from packages/mdc-auto-init/index.js rename to packages/mdc-auto-init/index.ts index a76766fcac6..18bcba6a74b 100644 --- a/packages/mdc-auto-init/index.js +++ b/packages/mdc-auto-init/index.ts @@ -21,16 +21,30 @@ * THE SOFTWARE. */ -const registry = Object.create(null); +// tslint:disable:only-arrow-functions -const CONSOLE_WARN = console.warn.bind(console); +import {MDCComponent, MDCFoundation} from '@material/base/index'; -function _emit(evtType, evtData, shouldBubble = false) { +interface ComponentClass { + // tslint:disable-next-line:no-any a component can pass in anything it needs to the constructor + new(root: Element, foundation?: F, ...args: any[]): MDCComponent; + attachTo(root: Element): MDCComponent; +} + +interface Registry { + [key: string]: ComponentClass; +} + +const registry: Registry = {}; + +const CONSOLE_WARN = console.warn.bind(console); // tslint:disable-line:no-console + +function _emit(evtType: string, evtData: T, shouldBubble = false) { let evt; if (typeof CustomEvent === 'function') { - evt = new CustomEvent(evtType, { - detail: evtData, + evt = new CustomEvent(evtType, { bubbles: shouldBubble, + detail: evtData, }); } else { evt = document.createEvent('CustomEvent'); @@ -40,36 +54,39 @@ function _emit(evtType, evtData, shouldBubble = false) { document.dispatchEvent(evt); } +/* istanbul ignore next: optional argument is not a branch statement */ /** - * Auto-initializes all mdc components on a page. + * Auto-initializes all MDC components on a page. */ -export default function mdcAutoInit(root = document, warn = CONSOLE_WARN) { +export function mdcAutoInit(root = document, warn = CONSOLE_WARN) { const components = []; - const nodes = root.querySelectorAll('[data-mdc-auto-init]'); - for (let i = 0, node; (node = nodes[i]); i++) { - const ctorName = node.dataset.mdcAutoInit; + const nodes: Element[] = [].slice.call(root.querySelectorAll('[data-mdc-auto-init]')); + + for (const node of nodes) { + const ctorName = node.getAttribute('data-mdc-auto-init'); if (!ctorName) { throw new Error('(mdc-auto-init) Constructor name must be given.'); } - const Ctor = registry[ctorName]; - if (typeof Ctor !== 'function') { + const Constructor = registry[ctorName]; // tslint:disable-line:variable-name + if (typeof Constructor !== 'function') { throw new Error( `(mdc-auto-init) Could not find constructor in registry for ${ctorName}`); } - if (node[ctorName]) { + if (Object.getOwnPropertyDescriptor(node, ctorName)) { warn(`(mdc-auto-init) Component already initialized for ${node}. Skipping...`); continue; } // TODO: Should we make an eslint rule for an attachTo() static method? - const component = Ctor.attachTo(node); + // See https://github.com/Microsoft/TypeScript/issues/14600 for discussion of static interface support in TS + const component = Constructor.attachTo(node); Object.defineProperty(node, ctorName, { + configurable: true, + enumerable: false, value: component, writable: false, - enumerable: false, - configurable: true, }); components.push(component); } @@ -78,22 +95,27 @@ export default function mdcAutoInit(root = document, warn = CONSOLE_WARN) { return components; } -mdcAutoInit.register = function(componentName, Ctor, warn = CONSOLE_WARN) { - if (typeof Ctor !== 'function') { - throw new Error(`(mdc-auto-init) Invalid Ctor value ${Ctor}. Expected function`); +// Constructor is PascalCased because it is a direct reference to a class, rather than an instance of a class. +// tslint:disable-next-line:variable-name +mdcAutoInit.register = function(componentName: string, Constructor: ComponentClass, warn = CONSOLE_WARN) { + if (typeof Constructor !== 'function') { + throw new Error(`(mdc-auto-init) Invalid Ctor value ${Constructor}. Expected function`); } if (registry[componentName]) { warn( - `(mdc-auto-init) Overriding registration for ${componentName} with ${Ctor}. ` + + `(mdc-auto-init) Overriding registration for ${componentName} with ${Constructor}. ` + `Was: ${registry[componentName]}`); } - registry[componentName] = Ctor; + registry[componentName] = Constructor; }; -mdcAutoInit.deregister = function(componentName) { +mdcAutoInit.deregister = function(componentName: string) { delete registry[componentName]; }; mdcAutoInit.deregisterAll = function() { - Object.keys(registry).forEach(this.deregister, this); + const keys = Object.keys(registry) as string[]; + keys.forEach(this.deregister, this); }; + +export default mdcAutoInit; diff --git a/packages/mdc-auto-init/package.json b/packages/mdc-auto-init/package.json index d6074449fc8..2c09174a18e 100644 --- a/packages/mdc-auto-init/package.json +++ b/packages/mdc-auto-init/package.json @@ -7,5 +7,8 @@ "repository": { "type": "git", "url": "https://github.com/material-components/material-components-web.git" + }, + "dependencies": { + "@material/base": "^0.41.0" } } diff --git a/scripts/webpack/js-bundle-factory.js b/scripts/webpack/js-bundle-factory.js index f5992945d81..b937085f35f 100644 --- a/scripts/webpack/js-bundle-factory.js +++ b/scripts/webpack/js-bundle-factory.js @@ -153,7 +153,7 @@ class JsBundleFactory { bundleName: 'main-js-a-la-carte', chunks: { animation: getAbsolutePath('/packages/mdc-animation/index.ts'), - autoInit: getAbsolutePath('/packages/mdc-auto-init/index.js'), + autoInit: getAbsolutePath('/packages/mdc-auto-init/index.ts'), base: getAbsolutePath('/packages/mdc-base/index.ts'), checkbox: getAbsolutePath('/packages/mdc-checkbox/index.ts'), chips: getAbsolutePath('/packages/mdc-chips/index.ts'), diff --git a/test/unit/mdc-auto-init/mdc-auto-init.test.js b/test/unit/mdc-auto-init/mdc-auto-init.test.js index c531ab266ba..9347f078a4b 100644 --- a/test/unit/mdc-auto-init/mdc-auto-init.test.js +++ b/test/unit/mdc-auto-init/mdc-auto-init.test.js @@ -36,6 +36,12 @@ class FakeComponent { } } +class InvalidComponent { + constructor(node) { + this.node = node; + } +} + const createFixture = () => bel`

Fake Element

@@ -48,6 +54,12 @@ const setupTest = () => { return createFixture(); }; +const setupInvalidTest = () => { + mdcAutoInit.deregisterAll(); + mdcAutoInit.register('InvalidComponent', InvalidComponent); + return createFixture(); +}; + suite('MDCAutoInit'); test('calls attachTo() on components registered for identifier on nodes w/ data-mdc-auto-init attr', () => { @@ -57,6 +69,11 @@ test('calls attachTo() on components registered for identifier on nodes w/ data- assert.isOk(root.querySelector('.mdc-fake').FakeComponent instanceof FakeComponent); }); +test('throws when attachTo() is missing', () => { + const root = setupInvalidTest(); + assert.throws(() => mdcAutoInit(root)); +}); + test('passes the node where "data-mdc-auto-init" was found to attachTo()', () => { const root = setupTest(); mdcAutoInit(root);