diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 49b9251218..a84ba764db 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -8,7 +8,7 @@ plugins: rules: max-len: [error, 120] no-new: warn - quotes: [error, single] + quotes: [error, single, {"avoidEscape": true}] no-var: error curly: error no-floating-decimal: error diff --git a/packages/material-design-lite/index.js b/packages/material-design-lite/index.js index 270a16da45..7ad5f3411d 100644 --- a/packages/material-design-lite/index.js +++ b/packages/material-design-lite/index.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import BaseComponent from 'mdl-base-component'; +import BaseComponent from 'mdl-base'; import Checkbox from 'mdl-checkbox'; import Radio from 'mdl-radio'; import Ripple from 'mdl-ripple'; diff --git a/packages/material-design-lite/package.json b/packages/material-design-lite/package.json index 2921d79c27..b34204094b 100644 --- a/packages/material-design-lite/package.json +++ b/packages/material-design-lite/package.json @@ -6,7 +6,7 @@ "dependencies": { "mdl-animation": "^1.0.0", "mdl-auto-init": "^1.0.0", - "mdl-base-component": "^1.0.0", + "mdl-base": "^1.0.0", "mdl-checkbox": "^1.0.0", "mdl-radio": "^1.0.0", "mdl-ripple": "^1.0.0" diff --git a/packages/mdl-base-component/index.js b/packages/mdl-base-component/index.js deleted file mode 100644 index 1b50d49c2c..0000000000 --- a/packages/mdl-base-component/index.js +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Copyright 2016 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export {MDLBaseAdapter, ref} from './adapter'; - -/** - * Base class for all MDL components. - * @export - */ -export default class MaterialComponent { - /** - * Initialize component from a DOM node. - * @param {Element} root The element being upgraded. - */ - constructor(root) { - this.refreshFrame_ = 0; - this.root_ = root; - this.init_(); - } - - // eslint-disable-next-line valid-jsdoc - /** - * CSS classes used in this component. - * - * @return {Object} The CSS classes used in this component. - */ - static get cssClasses() { - // Empty in base class. Throw error if not correctly overriden. - throw new Error('Should have at least ROOT key with the style class names,' + - ' e.g. mdl-button'); - } - - /** - * Number constants used in this component. - * - * @return {Object} The numbers used in this component. - */ - static get numbers() { - // Empty in base class. - return {}; - } - - /** - * String constants used in this component. - * - * @return {Object} The strings used in this component. - */ - static get strings() { - // Empty in base class. - return {}; - } - - /** - * Initialize component by running common tasks. - * - * @protected - */ - init_() { - // Add CSS marker that component upgrade is finished. - // Useful, but beware flashes of unstyled content when relying on this. - this.root_.dataset.mdlUpgraded = ''; - } - - /** - * Optional function that can be overriden if additional work needs to be done on refresh. - * @protected - */ - refresh_() { - // Can be overridden in sub-component. - } - - /** - * Attach all listeners to the DOM. - * - * @export - */ - addEventListeners() { - // Empty in base class. - } - - /** - * Remove all listeners from the DOM. - * - * @export - */ - removeEventListeners() { - // Empty in base class. - } - - /** - * Run a visual refresh on the component, in case it's gone out of sync. - * Ensures a refresh on the browser render loop. - * @export - */ - refresh() { - if (this.refreshFrame_) { - cancelAnimationFrame(this.refreshFrame_); - } - this.refreshFrame_ = requestAnimationFrame(() => { - this.refresh_(); - this.refreshFrame_ = 0; - }); - } - - /** - * Kills a component, removing all event listeners and deleting the node from - * the DOM. - * - * @export - */ - kill() { - this.removeEventListeners(); - } -} diff --git a/packages/mdl-base-component/package.json b/packages/mdl-base-component/package.json deleted file mode 100644 index 5a838691d1..0000000000 --- a/packages/mdl-base-component/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "mdl-base-component", - "version": "1.0.0", - "license": "MIT", - "main": "index.js", - "dependencies": { - "mdl-animation": "^1.0.0" - } -} diff --git a/packages/mdl-base/README.md b/packages/mdl-base/README.md new file mode 100644 index 0000000000..afbc31192a --- /dev/null +++ b/packages/mdl-base/README.md @@ -0,0 +1,241 @@ +# mdl-base + +> NOTE: Please do not use or rely on `adapter.js`. It is deprecated and in the process of being +> removed. + +MDL base contains core foundation and component classes that serve as the base classes for all of MDL's foundation classes and components (respectively). + +Most of the time, you shouldn't need to depend on `mdl-base` directly. It is useful however if you'd like to write custom components that follow MDL's pattern and elegantly integrate with the MDL ecosystem. + +## Installation + +First install the module: + +> NOTE: Installation via the npm registry will be available after alpha. + +Then include it in your code in one of the following ways: + +#### ES2015+ + +```javascript +import MDLComponent, {MDLFoundation} from 'mdl-base'; +``` +#### CommonJS + +```javascript +const MDLComponent = require('mdl-base').default; +const MDLFoundation = require('mdl-base').MDLFoundation; +``` + +#### AMD + +```javascript +require(['path/to/mdl-base'], function(mdlBase) { + const MDLComponent = mdlBase.default; + const MDLFoundation = mdlBase.MDLFoundation; +}); +``` + +#### Vanilla + +```javascript +const MDLComponent = mdl.Base.default; +const MDLFoundation = mdl.Base.MDLFoundation; +``` + +## Usage + +mdl-base exposes two classes: `MDLComponent` (the default export) which all components extend from, and `MDLFoundation`, which all foundation classes extend from. To learn more about foundation classes vs. components, check out our [developer guide](https://github.com/google/material-design-lite/blob/master/docs/DEVELOPER.md) (_WIP_). + +### MDLFoundation + +MDLFoundation provides the basic mechanisms for implementing a foundation classes. Subclasses are expected to: + +- Provide implementations of the proper static getters where necessary. +- Provide `init()` and `destroy()` lifecycle methods + +```javascript +import {MDLFoundation} from 'mdl-base'; + +export default class MyFoundation extends MDLFoundation { + static get cssClasses() { + return { + ROOT: 'my-component', + MESSAGE: 'my-component__message', + BUTTON: 'my-component__button', + TOGGLED: 'my-component--toggled' + }; + } + + static get defaultAdapter() { + return { + toggleClass: (/* className: string */) => {}, + registerBtnClickHandler: (/* handler: Function */) => {}, + deregisterBtnClickHandler: (/* handler: Function */) => {} + }; + } + + constructor(adapter) { + super(Object.assign(MyFoundation.defaultAdapter, adapter)); + const {TOGGLED} = MyFoundation.cssClasses; + this.clickHandler_ = () => this.adapter_.toggleClass(TOGGLED); + } + + init() { + this.adapter_.registerBtnClickHandler(this.clickHandler_); + } + + destroy() { + this.adapter_.deregisterBtnClickHandler(this.clickHandler_); + } +} +``` + +#### Static Getters + +The static getters specify constants that can be used within the foundation class, its component, and by 3rd-party code. _It's important to remember to always put constants into these getters_. This will ensure your component can interop in as many environments as possible, including those where CSS classes need to be overwritten by the host library (e.g., Closure Stylesheets), or strings need to be modified (for i18n, for example). + +Note that you do not have to explicitly provide getters for constants if your component has none. + +The getters which should be provided are specified below: + +| getter | description | +| --- | --- | +| cssClasses | returns an object where each key identifies a css class that some code will rely on. | +| strings | returns an object where each key identifies a string constant, e.g. `ARIA_ROLE` | +| numbers | returns an object where each key identifies a numeric constant, e.g. `TRANSITION_DELAY_MS` | +| defaultAdapter | returns an object specifying the shape of the adapter. Can be used as sensible defaults for an adapter as well as a way to specify your adapter's "schema" | + +#### Lifecycle Methods + +Each foundation class has two lifecycle methods: `init()` and `destroy()`, which are described below: + +| method | time of invocation | use case | +| --- | --- | --- | +| init() | called by a host class when a component is ready to be initialized | add event listeners, query for info via adapters, etc. | +| destroy() | called by a host class when a component is no longer in use | remove event listeners, reset any transient state, etc. | + +> Please note: _the lifecycle methods are **not** a safe place to perform DOM reads/writes that would invalidate layout or cause a repaint_. If this needs to be done within these methods, it should be put into a `requestAnimationFrame()` call so that it's synchronized with the browser's refresh cycle and does not [cause jank](http://www.html5rocks.com/en/tutorials/speed/rendering/). + +### MDLComponent + +MDLComponent provides the basic mechanisms for implementing component classes. + +```javascript +import MyComponentFoundation from './foundation'; + +export default class MyComponent extends MDLComponent { + static buildDom() { + const {ROOT, MESSAGE, BUTTON} = MyComponentFoundation.cssClasses; + const root = document.createElement('div'); + root.classList.add(ROOT); + + const message = document.createElement('p'); + message.classList.add(MESSAGE); + root.appendChild(message); + + const button = document.createElement('button'); + button.type = 'button'; + button.classList.add(BUTTON); + root.appendChild(button); + + return root; + } + + static attachTo(root) { + return new MyComponent(root); + } + + getDefaultFoundation() { + const btn = this.root.querySelector(`.${MyComponentFoundation.cssClasses.BUTTON}`); + return new MyComponentFoundation({ + toggleClass: className => { + if (this.root.classList.contains(className)) { + this.root.classList.remove(className); + return; + } + this.root.classList.add(className); + }, + registerBtnClickHandler: handler => btn.addEventListener('click', handler), + deregisterBtnClickHandler: handler => btn.removeEventListener('click', handler) + }); + } +} +``` + +#### Properties + +`MDLComponent` provides the following "private" properties to subclasses: + +| property | description | +| --- | --- | +| `root_` | The root element passed into the constructor as the first argument. | +| `foundation_` | The foundation class for this component. This is either passed in as an optional second argument to the constructor, or assigned the result of calling `getDefaultFoundation()` | + +#### Methods + +`MDLComponent` provides the following methods to subclasses: + +| method | description | +| --- | --- | +| `getDefaultFoundation()` | Returns an instance of a foundation class properly configured for the component. Called when no foundation instance is given within the constructor. Subclasses **must** implement this method. | +| `initialSyncWithDOM()` | Called within the constructor. Subclasses may override this method if they wish to perform initial synchronization of state with the host DOM element. For example, a slider may want to check if its host element contains a pre-set value, and adjust its internal state accordingly. Note that the same caveats apply to this method as to foundation class lifecycle methods. Defaults to a no-op. | +| `destroy()` | Subclasses may override this method if they wish to perform any additional cleanup work when a component is destroyed. For example, a component may want to deregister a window resize listener. | + +#### Static Methods + +In addition to methods inherited, subclasses should implement the following two static methods within their code: + +| method | description | +| --- | --- | +| `buildDom(...any) => HTMLElement` | Subclasses may implement this as a convenience method to construct the proper DOM for a component. Users could then rely on this as an alternative to having to construct the DOM themselves. However, it should exist purely for convenience and _never_ be used as a dependency for the component itself. | +| `attachTo(root) => ` | Subclasses must implement this as a convenience method to instantiate and return an instance of the class using the root element provided. This will be used within `mdl-auto-init`, and in the future its presence may be enforced via a custom lint rule.| + +#### Foundation Lifecycle handling + +`MDLComponent` calls its foundation's `init()` function within its _constructor_, and its foundation's `destroy()` function within its own _destroy()_ function. Therefore it's important to remember to _always call super() when overriding destroy()_. Not doing so can lead to leaked resources. + +#### Best Practice: Keep your adapters simple + +If you find your adapters getting too complex, you should consider refactoring the complex parts out into their own implementations. + +```javascript +import MyComponentFoundation from './foundation'; +import {toggleClass} from './util'; + +class MyComponent { + // ... + getDefaultFoundation() { + return new MyComponentFoundation({ + toggleClass: className => util.toggleClass(this.root_, className), + // ... + }); + } +} +``` + +Where `./util` could look like: + +```javascript +export function toggleClass(element, className) { + if (root.classList.contains(className)) { + root.classList.remove(className); + return; + } + root.classList.add(className); +} +``` + +This not only reduces the complexity of your component class, but allows for the functionality of complex adapters to be adequately tested: + +```javascript +test('toggleClass() removes a class when present on an element', t => { + const root = document.createElement('div'); + root.classList.add('foo'); + + util.toggleClass(root, 'foo'); + + t.false(root.classList.contains('foo')); + t.end(); +}); +``` diff --git a/packages/mdl-base-component/adapter.js b/packages/mdl-base/adapter.js similarity index 95% rename from packages/mdl-base-component/adapter.js rename to packages/mdl-base/adapter.js index 9801bec405..e98889c38c 100644 --- a/packages/mdl-base-component/adapter.js +++ b/packages/mdl-base/adapter.js @@ -1,3 +1,5 @@ +// TODO: Remove this file once mdl-radio and mdl-ripple have been converted. + /** * Copyright 2016 Google Inc. All Rights Reserved. * diff --git a/packages/mdl-base/component.js b/packages/mdl-base/component.js new file mode 100644 index 0000000000..33c8332077 --- /dev/null +++ b/packages/mdl-base/component.js @@ -0,0 +1,61 @@ +/** + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import MDLFoundation from './foundation'; + +export default class MDLComponent { + static buildDom() { + // Classes which extend MDLBase should provide a buildDom() method which returns a node containing the basic + // DOM needed for rendering the component. Clients can then opt to use buildDom() as a convenience method + // rather than having to write the DOM themselves. + return document.createElement('div'); + } + + static attachTo(root) { + // Subclasses which extend MDLBase should provide an attachTo() method that takes a root element and + // returns an instantiated component with its root set to that element. Also note that in the cases of + // subclasses, an explicit foundation class will not have to be passed in; it will simply be initialized + // from getDefaultFoundation(). + return new MDLComponent(root, new MDLFoundation()); + } + + constructor(root, foundation) { + this.root_ = root; + this.foundation_ = foundation === undefined ? this.getDefaultFoundation() : foundation; + this.foundation_.init(); + this.initialSyncWithDOM(); + } + + getDefaultFoundation() { + // Subclasses must override this method to return a properly configured foundation class for the + // component. + throw new Error('Subclasses must override getDefaultFoundation to return a properly configured ' + + 'foundation class'); + } + + initialSyncWithDOM() { + // Subclasses should override this method if they need to perform work to synchronize with a host DOM + // object. An example of this would be a form control wrapper that needs to synchronize its internal state + // to some property or attribute of the host DOM. Please note: this is *not* the place to perform DOM + // reads/writes that would cause layout / paint, as this is called synchronously from within the constructor. + } + + destroy() { + // Subclasses may implement this method to release any resources / deregister any listeners they have + // attached. An example of this might be deregistering a resize event from the window object. + this.foundation_.destroy(); + } +} diff --git a/packages/mdl-base/foundation.js b/packages/mdl-base/foundation.js new file mode 100644 index 0000000000..395b01e1ed --- /dev/null +++ b/packages/mdl-base/foundation.js @@ -0,0 +1,54 @@ +/** + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export default class MDLFoundation { + static get cssClasses() { + // Classes extending MDLBaseFoundation should implement this method to return an object which exports every + // CSS class the foundation class needs as a property. e.g. {ACTIVE: 'mdl-component--active'} + return {}; + } + + static get strings() { + // Classes extending MDLBaseFoundation should implement this method to return an object which exports all + // semantic strings as constants. e.g. {ARIA_ROLE: 'tablist'} + return {}; + } + + static get numbers() { + // Classes extending MDLBaseFoundation should implement this method to return an object which exports all + // of its semantic numbers as constants. e.g. {ANIMATION_DELAY_MS: 350} + return {}; + } + + static get defaultAdapter() { + // Classes extending MDLBaseFoundation may choose to implement this getter in order to provide a convenient + // way of viewing the necessary methods of an adapter. In the future, this could also be used for adapter + // validation. + return {}; + } + + constructor(adapter = {}) { + this.adapter_ = adapter; + } + + init() { + // Subclasses should override this method to perform initialization routines (registering events, etc.) + } + + destroy() { + // Subclasses should override this method to perform de-initialization routines (de-registering events, etc.) + } +} diff --git a/packages/mdl-base/index.js b/packages/mdl-base/index.js new file mode 100644 index 0000000000..b46318d987 --- /dev/null +++ b/packages/mdl-base/index.js @@ -0,0 +1,20 @@ +/** + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import MDLComponent from './component'; +export {default as MDLFoundation} from './foundation'; +export {MDLBaseAdapter as MDLBaseAdapterLegacy, ref as refLegacy} from './adapter'; +export default MDLComponent; diff --git a/packages/mdl-base/package.json b/packages/mdl-base/package.json new file mode 100644 index 0000000000..291ba4886e --- /dev/null +++ b/packages/mdl-base/package.json @@ -0,0 +1,7 @@ +{ + "name": "mdl-base", + "description": "The set of base classes for Material Design Lite", + "version": "1.0.0", + "license": "Apache-2.0", + "main": "index.js" +} diff --git a/packages/mdl-checkbox/index.js b/packages/mdl-checkbox/index.js index 199f6168a0..d7c7a8c541 100644 --- a/packages/mdl-checkbox/index.js +++ b/packages/mdl-checkbox/index.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import MDLBaseComponent from 'mdl-base-component'; +import MDLComponent from 'mdl-base'; import MDLCheckboxMixin from './mixin'; import {cssClasses, strings, numbers} from './constants'; @@ -24,7 +24,7 @@ import {cssClasses, strings, numbers} from './constants'; * @final * @extends MDLBaseComponent */ -export default class MDLCheckbox extends MDLBaseComponent { +export default class MDLCheckbox extends MDLComponent { static attachTo(root) { return new MDLCheckbox(root); } diff --git a/packages/mdl-checkbox/package.json b/packages/mdl-checkbox/package.json index f1b3021ddf..8deff943ca 100644 --- a/packages/mdl-checkbox/package.json +++ b/packages/mdl-checkbox/package.json @@ -4,6 +4,6 @@ "license": "MIT", "dependencies": { "mdl-animation": "^1.0.0", - "mdl-base-component": "^1.0.0" + "mdl-base": "^1.0.0" } } diff --git a/packages/mdl-radio/index.js b/packages/mdl-radio/index.js index 61f542a7a2..4383c516a1 100644 --- a/packages/mdl-radio/index.js +++ b/packages/mdl-radio/index.js @@ -14,14 +14,12 @@ * limitations under the License. */ -import MDLBaseComponent, { - MDLBaseAdapter -} from 'mdl-base-component'; +import MDLComponent, {MDLBaseAdapterLegacy as MDLBaseAdapter} from 'mdl-base'; import MDLRadioMixin, { Identifier } from './mixin'; -export default class MDLRadio extends MDLBaseComponent { +export default class MDLRadio extends MDLComponent { static attachTo(root) { return new MDLRadio(root); } diff --git a/packages/mdl-radio/package.json b/packages/mdl-radio/package.json index b299e2aa1b..ae92bd74d7 100644 --- a/packages/mdl-radio/package.json +++ b/packages/mdl-radio/package.json @@ -3,6 +3,6 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "mdl-base-component": "^1.0.0" + "mdl-base": "^1.0.0" } } diff --git a/packages/mdl-ripple/index.js b/packages/mdl-ripple/index.js index ca7f065ffb..ff7b174a2a 100644 --- a/packages/mdl-ripple/index.js +++ b/packages/mdl-ripple/index.js @@ -14,9 +14,10 @@ * limitations under the License. */ -import MDLBaseComponent, { - MDLBaseAdapter, ref -} from 'mdl-base-component'; +import MDLComponent, { + MDLBaseAdapterLegacy as MDLBaseAdapter, + refLegacy as ref +} from 'mdl-base'; import MDLRippleMixin, { Class, Identifier, @@ -24,7 +25,7 @@ import MDLRippleMixin, { getNormalizedEventCoords } from './mixin'; -export default class MDLRipple extends MDLBaseComponent { +export default class MDLRipple extends MDLComponent { /** * Convenience helper to build required DOM. */ @@ -91,6 +92,11 @@ export default class MDLRipple extends MDLBaseComponent { this.addEventListeners(); } + getDefaultFoundation() { + // TODO(traviskaufman): Implement once ripple is ported over from mixin pattern. + return {init: () => {}}; + } + addEventListeners() { const surface = this.elements_[Identifier.SURFACE]; diff --git a/packages/mdl-ripple/package.json b/packages/mdl-ripple/package.json index d3feb9245b..0c8b7c0804 100644 --- a/packages/mdl-ripple/package.json +++ b/packages/mdl-ripple/package.json @@ -3,6 +3,6 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "mdl-base-component": "^1.0.0" + "mdl-base": "^1.0.0" } } diff --git a/test/unit/mdl-auto-init/mdl-auto-init.test.js b/test/unit/mdl-auto-init/mdl-auto-init.test.js index 658755f187..aacbed7dc9 100644 --- a/test/unit/mdl-auto-init/mdl-auto-init.test.js +++ b/test/unit/mdl-auto-init/mdl-auto-init.test.js @@ -95,8 +95,8 @@ test('#register warns when registered key is being overridden', t => { const warn = td.func('warn'); const {contains} = td.matchers; - mdlAutoInit.register(FakeComponent.name, () => ({overridden: true}), warn); + mdlAutoInit.register('FakeComponent', () => ({overridden: true}), warn); - t.true(() => td.verify(warn(contains('(mdl-auto-init) Overriding registration')))); + t.doesNotThrow(() => td.verify(warn(contains('(mdl-auto-init) Overriding registration')))); t.end(); }); diff --git a/test/unit/mdl-base/component.test.js b/test/unit/mdl-base/component.test.js new file mode 100644 index 0000000000..1a76e07588 --- /dev/null +++ b/test/unit/mdl-base/component.test.js @@ -0,0 +1,113 @@ +/** + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import test from 'tape'; +import td from 'testdouble'; +import MDLComponent from '../../../packages/mdl-base'; + +class FakeComponent extends MDLComponent { + get root() { + return this.root_; + } + + get foundation() { + return this.foundation_; + } + + getDefaultFoundation() { + return td.object({ + isDefaultFoundation: true, + init: () => {} + }); + } + + initialSyncWithDOM() { + this.synced = true; + } +} + +test('provides a static buildDom() method that returns an empty div by default', t => { + const dom = MDLComponent.buildDom(); + t.equal(dom.tagName.toLowerCase(), 'div'); + t.equal(dom.innerHTML, ''); + t.end(); +}); + +test('provides a static attachTo() method that returns a basic instance with the specified root', t => { + const root = document.createElement('div'); + const b = MDLComponent.attachTo(root); + t.true(b instanceof MDLComponent); + t.end(); +}); + +test('takes a root node constructor param and assigns it to the "root_" property', t => { + const root = document.createElement('div'); + const f = new FakeComponent(root); + t.equal(f.root, root); + t.end(); +}); + +test('takes an optional foundation constructor param and assigns it to the "foundation_" property', t => { + const root = document.createElement('div'); + const foundation = {init: () => {}}; + const f = new FakeComponent(root, foundation); + t.equal(f.foundation, foundation); + t.end(); +}); + +test('assigns the result of "getDefaultFoundation()" to "foundation_" by default', t => { + const root = document.createElement('div'); + const f = new FakeComponent(root); + t.true(f.foundation.isDefaultFoundation); + t.end(); +}); + +test("calls the foundation's init() method within the constructor", t => { + const root = document.createElement('div'); + const foundation = td.object({init: () => {}}); + // Testing side effects of constructor + // eslint-disable-next-line no-new + new FakeComponent(root, foundation); + t.doesNotThrow(() => td.verify(foundation.init())); + t.end(); +}); + +test('throws an error if getDefaultFoundation() is not overridden', t => { + const root = document.createElement('div'); + t.throws(() => new MDLComponent(root)); + t.end(); +}); + +test('calls initialSyncWithDOM() when initialized', t => { + const root = document.createElement('div'); + const f = new FakeComponent(root); + t.true(f.synced); + t.end(); +}); + +test('provides a default initialSyncWithDOM() no-op if none provided by subclass', t => { + t.doesNotThrow(MDLComponent.prototype.initialSyncWithDOM.bind({})); + t.end(); +}); + +test("provides a default destroy() method which calls the foundation's destroy() method", t => { + const root = document.createElement('div'); + const foundation = td.object({init: () => {}, destroy: () => {}}); + const f = new FakeComponent(root, foundation); + f.destroy(); + t.doesNotThrow(() => td.verify(foundation.destroy())); + t.end(); +}); diff --git a/test/unit/mdl-base/foundation.test.js b/test/unit/mdl-base/foundation.test.js new file mode 100644 index 0000000000..9481d2ac4b --- /dev/null +++ b/test/unit/mdl-base/foundation.test.js @@ -0,0 +1,69 @@ +/** + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import test from 'tape'; +import {MDLFoundation} from '../../../packages/mdl-base'; + +class FakeFoundation extends MDLFoundation { + get adapter() { + return this.adapter_; + } +} + +test('cssClasses getter returns an empty object', t => { + t.deepEqual(MDLFoundation.cssClasses, {}); + t.end(); +}); + +test('strings getter returns an empty object', t => { + t.deepEqual(MDLFoundation.strings, {}); + t.end(); +}); + +test('numbers getter returns an empty object', t => { + t.deepEqual(MDLFoundation.numbers, {}); + t.end(); +}); + +test('defaultAdapter getter returns an empty object', t => { + t.deepEqual(MDLFoundation.defaultAdapter, {}); + t.end(); +}); + +test('takes an adapter object in its constructor, assigns it to "adapter_"', t => { + const adapter = {adapter: true}; + const f = new FakeFoundation(adapter); + t.deepEqual(f.adapter, adapter); + t.end(); +}); + +test('assigns adapter to an empty object when none given', t => { + const f = new FakeFoundation(); + t.deepEqual(f.adapter, {}); + t.end(); +}); + +test('provides an init() lifecycle method, which defaults to a no-op', t => { + const f = new FakeFoundation(); + t.doesNotThrow(() => f.init()); + t.end(); +}); + +test('provides a destroy() lifecycle method, which defaults to a no-op', t => { + const f = new FakeFoundation(); + t.doesNotThrow(() => f.destroy()); + t.end(); +}); diff --git a/webpack.config.js b/webpack.config.js index b3a6561bc9..c32f54e115 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -29,7 +29,7 @@ module.exports = [{ name: 'js-components', entry: { autoInit: [path.resolve('./packages/mdl-auto-init/index.js')], - BaseComponent: [path.resolve('./packages/mdl-base-component/index.js')], + Base: [path.resolve('./packages/mdl-base/index.js')], Checkbox: [path.resolve('./packages/mdl-checkbox/index.js')], Radio: [path.resolve('./packages/mdl-radio/index.js')], Ripple: [path.resolve('./packages/mdl-ripple/index.js')]