compago-controller
Advanced tools
Comparing version 1.0.0 to 2.0.0
{ | ||
"name": "compago-controller", | ||
"version": "1.0.0", | ||
"version": "2.0.0", | ||
"description": "A component of the Compago framework handling interactions between the application state and the DOM.", | ||
"main": "dist/node/index.js", | ||
"browser": "src/index.js", | ||
"module": "src/index.js", | ||
"keywords": [ | ||
@@ -20,8 +21,5 @@ "controller", | ||
"scripts": { | ||
"build-node": "babel src/index.js --out-file dist/node/index.js", | ||
"lint": "eslint src/index.js tests/unit.test.js", | ||
"clean": "rimraf dist && mkdirp dist dist/node", | ||
"build": "npm run clean && npm run build-node", | ||
"report-coverage": "cat ./coverage/lcov.info | codecov", | ||
"test": "babel-node ./node_modules/istanbul/lib/cli cover node_modules/mocha/bin/_mocha -- -R spec tests/unit.test.js" | ||
"test": "jest > nul" | ||
}, | ||
@@ -54,4 +52,30 @@ "author": "Maga D. Zandaqo <denelxan@gmail.com> (http://maga.name)", | ||
"babel": { | ||
"presets": [ | ||
"node6" | ||
"env": { | ||
"test": { | ||
"presets": [ | ||
"node7" | ||
] | ||
} | ||
} | ||
}, | ||
"jest": { | ||
"collectCoverage": true, | ||
"collectCoverageFrom": [ | ||
"**/src/**", | ||
"!**/node_modules/**", | ||
"!**/tests/**" | ||
], | ||
"coverageDirectory": "<rootDir>/coverage", | ||
"coverageReporters": [ | ||
"lcov" | ||
], | ||
"transformIgnorePatterns": [ | ||
"<rootDir>/node_modules/(?!compago-)\\.*" | ||
], | ||
"moduleNameMapper": { | ||
"compago-listener": "<rootDir>/node_modules/compago-listener/src/index.js", | ||
"compago-region": "<rootDir>/node_modules/compago-region/src/index.js" | ||
}, | ||
"roots": [ | ||
"<rootDir>/tests" | ||
] | ||
@@ -64,15 +88,11 @@ }, | ||
"devDependencies": { | ||
"babel-cli": "^6.16.0", | ||
"babel-preset-node6": "^11.0.0", | ||
"codecov.io": "^0.1.6", | ||
"eslint": "^3.7.1", | ||
"eslint-config-airbnb-base": "^8.0.0", | ||
"eslint-plugin-import": "^2.0.1", | ||
"expect": "^1.20.2", | ||
"eslint": "^3.18.0", | ||
"eslint-config-airbnb-base": "^11.1.1", | ||
"eslint-plugin-import": "^2.2.0", | ||
"istanbul": "^1.1.0-alpha.1", | ||
"mkdirp": "^0.5.1", | ||
"mocha": "^3.1.2", | ||
"mock-browser": "^0.92.12", | ||
"rimraf": "^2.5.4" | ||
"babel-jest": "^19.0.0", | ||
"babel-preset-node7": "1.5.0", | ||
"jest": "^19.0.2" | ||
} | ||
} |
@@ -20,12 +20,12 @@ /* eslint-env browser */ | ||
* @param {Object} [options.handlers] the DOM event handlers for the controller | ||
* @param {Object} [options.ui] a hash of names and respective selectors to use as shortcuts | ||
* for selecting UI elements inside the controller | ||
* @param {Object} [options.model] the data model used by the controller | ||
* @param {Object} [options.view] the view or template function used in rendering the controller | ||
* @param {Object} [options.renderEvents] the model events that cause the controller to re-render | ||
* @param {string} [options.renderEvents] the model events that cause the controller to re-render | ||
* @param {Array} [options.renderAttributes] the attributes of the controller's element | ||
* that cause it to re-render | ||
* @param {Object} [options.regions] a hash of regions of the controller | ||
*/ | ||
constructor(options = _opt) { | ||
const { el, tagName, attributes, handlers, ui, | ||
model, view, renderEvents, regions } = options; | ||
const { el, tagName, attributes, handlers, model, | ||
view, renderEvents, renderAttributes, regions } = options; | ||
Object.assign(this, Listener); | ||
@@ -38,6 +38,5 @@ this.tagName = tagName || 'div'; | ||
this._setEventHandlers(); | ||
this.uiSelectors = ui; | ||
this.ui = undefined; | ||
this.regionSelectors = regions; | ||
this.regions = undefined; | ||
this._observer = undefined; | ||
this.model = model; | ||
@@ -48,2 +47,5 @@ this.view = view; | ||
} | ||
if (renderAttributes) { | ||
this._observeAttributes(renderAttributes); | ||
} | ||
} | ||
@@ -55,4 +57,3 @@ | ||
* By default, invokes `this.view` supplying the controller's element and model if present, | ||
* establishes links (if `this.ui` is specified), prepares the controller's regions, | ||
* and returns the controller's DOM element. | ||
* prepares the controller's regions, and returns the controller's DOM element. | ||
* | ||
@@ -62,4 +63,3 @@ * @returns {HTMLElement} the DOM element of the controller | ||
render() { | ||
if (this.view) this.view(this.el, this.model); | ||
this._linkUI(); | ||
if (this.view) this.view(this.el, this); | ||
this._ensureRegions(); | ||
@@ -172,3 +172,6 @@ return this.el; | ||
this._disposeRegions(); | ||
this.ui = undefined; | ||
if (this._observer) { | ||
this._observer.disconnect(); | ||
this._observer = undefined; | ||
} | ||
const parent = this.el.parentNode; | ||
@@ -211,3 +214,3 @@ if (parent) parent.removeChild(this.el); | ||
if (prevent) event.preventDefault(); | ||
const field = bond !== true ? bond : target.dataset.bond; | ||
const field = bond !== true ? bond : target.getAttribute('data-bond'); | ||
this.model.set({ [field]: typeof parse === 'function' ? parse(target[value]) : target[value] }, { nested }); | ||
@@ -268,7 +271,5 @@ } | ||
} else { | ||
for (let el = event.target; el && el !== this.el; el = el.parentElement) { | ||
if (Controller._matches.call(el, selector)) { | ||
cb.call(this, event, el, data); | ||
break; | ||
} | ||
const el = event.target.closest(selector); | ||
if (el && this.el.contains(el)) { | ||
cb.call(this, event, el, data); | ||
} | ||
@@ -314,17 +315,2 @@ } | ||
/** | ||
* Establishes links to controller's UI elements. | ||
* | ||
* @returns {void} | ||
*/ | ||
_linkUI() { | ||
if (!this.uiSelectors) return; | ||
this.ui = {}; | ||
const names = Object.keys(this.uiSelectors); | ||
for (let i = 0; i < names.length; i++) { | ||
const name = names[i]; | ||
this.ui[name] = this.el.querySelector(this.uiSelectors[name]); | ||
} | ||
} | ||
/** | ||
* Disposes all regions of the controller. | ||
@@ -346,3 +332,17 @@ * | ||
/** | ||
* Sets up a Mutation Observer to watch for changes in attributes of the controller. | ||
* | ||
* @param {Array} attributeFilter the list attributes to watch | ||
* @returns {void} | ||
*/ | ||
_observeAttributes(attributeFilter) { | ||
const observer = new MutationObserver(() => { | ||
this.render(); | ||
}); | ||
observer.observe(this.el, { attributes: true, attributeFilter }); | ||
this._observer = observer; | ||
} | ||
/** | ||
* | ||
* @param {Function} callback | ||
@@ -366,5 +366,2 @@ * @param {number} wait | ||
/** The reference to the native implementation of `matchesSelector` method. */ | ||
Controller._matches = (typeof Element !== 'undefined') ? Element.prototype.matches || Element.prototype.msMatchesSelector : undefined; | ||
export default Controller; |
@@ -1,4 +0,4 @@ | ||
/* eslint-env node, mocha, browser */ | ||
import expect from 'expect'; | ||
import { mocks } from 'mock-browser'; | ||
/* eslint-env jest, browser */ | ||
/* globals jest, expect */ | ||
import Region from 'compago-region'; | ||
@@ -9,5 +9,12 @@ import Listener from 'compago-listener'; | ||
const mb = new mocks.MockBrowser(); | ||
GLOBAL.document = mb.getDocument(); | ||
Controller._matches = mb.getWindow().Element.prototype.matches; | ||
window.Element.prototype.closest = function (selector) { | ||
let el = this; | ||
while (el) { | ||
if (el.matches(selector)) { | ||
return el; | ||
} | ||
el = el.parentElement; | ||
} | ||
return null; | ||
}; | ||
@@ -17,2 +24,3 @@ describe('Controller', () => { | ||
let el; | ||
beforeEach(() => { | ||
@@ -23,3 +31,3 @@ v = new Controller(); | ||
v.el.appendChild(el); | ||
v.someMethod = expect.createSpy(); | ||
v.someMethod = jest.fn(); | ||
v.handlers = v._prepareHandlers({ | ||
@@ -92,3 +100,3 @@ click: 'someMethod', | ||
model = { | ||
set: expect.createSpy(), | ||
set: jest.fn(), | ||
}; | ||
@@ -99,3 +107,3 @@ v.model = model; | ||
v.el.appendChild(input); | ||
event = { type: 'input', target: input, preventDefault: expect.createSpy() }; | ||
event = { type: 'input', target: input, preventDefault: jest.fn() }; | ||
}); | ||
@@ -109,3 +117,3 @@ | ||
expect(model.set).toHaveBeenCalled(); | ||
expect(model.set.calls[0].arguments).toEqual([{ name: '' }, { nested: false }]); | ||
expect(model.set.mock.calls[0]).toEqual([{ name: '' }, { nested: false }]); | ||
}); | ||
@@ -119,3 +127,3 @@ | ||
expect(model.set).toHaveBeenCalled(); | ||
expect(model.set.calls[0].arguments).toEqual([{ 'name.first': '' }, { nested: true }]); | ||
expect(model.set.mock.calls[0]).toEqual([{ 'name.first': '' }, { nested: true }]); | ||
}); | ||
@@ -129,3 +137,3 @@ | ||
expect(model.set).toHaveBeenCalled(); | ||
expect(model.set.calls[0].arguments).toEqual([{ name: '' }, { nested: false }]); | ||
expect(model.set.mock.calls[0]).toEqual([{ name: '' }, { nested: false }]); | ||
expect(event.preventDefault).toHaveBeenCalled(); | ||
@@ -142,12 +150,12 @@ }); | ||
expect(model.set).toHaveBeenCalled(); | ||
expect(model.set.calls[0].arguments).toEqual([{ id: 1 }, { nested: false }]); | ||
expect(model.set.mock.calls[0]).toEqual([{ id: 1 }, { nested: false }]); | ||
}); | ||
it('debounces handlers if `debounce` is set', () => { | ||
expect.spyOn(Controller, '_handleDebounce').andCallThrough(); | ||
const spy = jest.spyOn(Controller, '_handleDebounce'); | ||
v.handlers = v._prepareHandlers({ | ||
'input #name': { bond: 'name', debounce: 1000 }, | ||
}); | ||
expect(Controller._handleDebounce).toHaveBeenCalled(); | ||
expect.restoreSpies(); | ||
expect(spy).toHaveBeenCalled(); | ||
spy.mockRestore(); | ||
}); | ||
@@ -172,4 +180,4 @@ }); | ||
beforeEach(() => { | ||
v.otherMethod = expect.createSpy(); | ||
v.yetAnother = expect.createSpy(); | ||
v.otherMethod = jest.fn(); | ||
v.yetAnother = jest.fn(); | ||
}); | ||
@@ -192,7 +200,7 @@ | ||
v._handle(event); | ||
expect(v.someMethod.calls.length).toBe(1); | ||
expect(v.otherMethod.calls.length).toBe(1); | ||
expect(v.yetAnother.calls.length).toBe(0); | ||
expect(v.someMethod.calls[0].arguments).toEqual([event, submit, undefined]); | ||
expect(v.otherMethod.calls[0].arguments).toEqual([event, undefined, undefined]); | ||
expect(v.someMethod.mock.calls.length).toBe(1); | ||
expect(v.otherMethod.mock.calls.length).toBe(1); | ||
expect(v.yetAnother.mock.calls.length).toBe(0); | ||
expect(v.someMethod.mock.calls[0]).toEqual([event, submit, undefined]); | ||
expect(v.otherMethod.mock.calls[0]).toEqual([event, undefined, undefined]); | ||
}); | ||
@@ -202,5 +210,5 @@ | ||
expect(v._handle({ type: 'nonExistantEvent' })).toBe(undefined); | ||
expect(v.someMethod.calls.length).toBe(0); | ||
expect(v.otherMethod.calls.length).toBe(0); | ||
expect(v.yetAnother.calls.length).toBe(0); | ||
expect(v.someMethod.mock.calls.length).toBe(0); | ||
expect(v.otherMethod.mock.calls.length).toBe(0); | ||
expect(v.yetAnother.mock.calls.length).toBe(0); | ||
}); | ||
@@ -211,4 +219,4 @@ }); | ||
beforeEach(() => { | ||
v.el.addEventListener = expect.createSpy(); | ||
v.el.removeEventListener = expect.createSpy(); | ||
v.el.addEventListener = jest.fn(); | ||
v.el.removeEventListener = jest.fn(); | ||
}); | ||
@@ -218,4 +226,4 @@ | ||
v._setEventHandlers(); | ||
expect(v.el.addEventListener.calls.length).toBe(1); | ||
expect(v.el.addEventListener.calls[0].arguments).toEqual(['click', v._handle]); | ||
expect(v.el.addEventListener.mock.calls.length).toBe(1); | ||
expect(v.el.addEventListener.mock.calls[0]).toEqual(['click', v._handle]); | ||
}); | ||
@@ -226,4 +234,4 @@ | ||
v._setEventHandlers(true); | ||
expect(v.el.removeEventListener.calls.length).toBe(1); | ||
expect(v.el.removeEventListener.calls[0].arguments).toEqual(['click', v._handle]); | ||
expect(v.el.removeEventListener.mock.calls.length).toBe(1); | ||
expect(v.el.removeEventListener.mock.calls[0]).toEqual(['click', v._handle]); | ||
}); | ||
@@ -245,4 +253,4 @@ }); | ||
v.undelegate(); | ||
v.el.addEventListener = expect.createSpy(); | ||
v.el.removeEventListener = expect.createSpy(); | ||
v.el.addEventListener = jest.fn(); | ||
v.el.removeEventListener = jest.fn(); | ||
}); | ||
@@ -253,5 +261,5 @@ | ||
expect(v.handlers.get('mouseover')[0]).toEqual([v.someMethod, '#submit']); | ||
expect(v.el.addEventListener.calls.length).toBe(1); | ||
expect(v.el.removeEventListener.calls.length).toBe(0); | ||
expect(v.el.addEventListener.calls[0].arguments).toEqual(['mouseover', v._handle]); | ||
expect(v.el.addEventListener.mock.calls.length).toBe(1); | ||
expect(v.el.removeEventListener.mock.calls.length).toBe(0); | ||
expect(v.el.addEventListener.mock.calls[0]).toEqual(['mouseover', v._handle]); | ||
}); | ||
@@ -264,5 +272,5 @@ | ||
expect(v.handlers.get('mouseover')[1]).toEqual(v.someMethod); | ||
expect(v.el.addEventListener.calls.length).toBe(1); | ||
expect(v.el.removeEventListener.calls.length).toBe(0); | ||
expect(v.el.addEventListener.calls[0].arguments).toEqual(['mouseover', v._handle]); | ||
expect(v.el.addEventListener.mock.calls.length).toBe(1); | ||
expect(v.el.removeEventListener.mock.calls.length).toBe(0); | ||
expect(v.el.addEventListener.mock.calls[0]).toEqual(['mouseover', v._handle]); | ||
}); | ||
@@ -273,3 +281,3 @@ | ||
expect(v.handlers.get('mouseover')).toBe(undefined); | ||
expect(v.el.addEventListener.calls.length).toBe(0); | ||
expect(v.el.addEventListener.mock.calls.length).toBe(0); | ||
}); | ||
@@ -279,4 +287,4 @@ | ||
v.delegate(); | ||
expect(v.el.addEventListener.calls.length).toBe(1); | ||
expect(v.el.removeEventListener.calls.length).toBe(1); | ||
expect(v.el.addEventListener.mock.calls.length).toBe(1); | ||
expect(v.el.removeEventListener.mock.calls.length).toBe(1); | ||
}); | ||
@@ -288,4 +296,4 @@ }); | ||
v.undelegate(); | ||
v.el.addEventListener = expect.createSpy(); | ||
v.el.removeEventListener = expect.createSpy(); | ||
v.el.addEventListener = jest.fn(); | ||
v.el.removeEventListener = jest.fn(); | ||
}); | ||
@@ -296,4 +304,4 @@ | ||
v.undelegate(); | ||
expect(v.el.addEventListener.calls.length).toBe(1); | ||
expect(v.el.removeEventListener.calls.length).toBe(2); | ||
expect(v.el.addEventListener.mock.calls.length).toBe(1); | ||
expect(v.el.removeEventListener.mock.calls.length).toBe(2); | ||
}); | ||
@@ -313,4 +321,4 @@ | ||
expect(v.handlers.get('mouseover')).toBe(undefined); | ||
expect(v.el.removeEventListener.calls.length).toBe(1); | ||
expect(v.el.removeEventListener.calls[0].arguments).toEqual(['mouseover', v._handle]); | ||
expect(v.el.removeEventListener.mock.calls.length).toBe(1); | ||
expect(v.el.removeEventListener.mock.calls[0]).toEqual(['mouseover', v._handle]); | ||
}); | ||
@@ -321,35 +329,15 @@ }); | ||
it('renders and returns the controller element', () => { | ||
v.view = expect.createSpy(); | ||
v.view = jest.fn(); | ||
v.model = {}; | ||
const result = v.render(); | ||
expect(result).toBe(v.el); | ||
expect(v.view.calls.length).toBe(1); | ||
expect(v.view.calls[0].arguments).toEqual([v.el, v.model]); | ||
expect(v.view.mock.calls.length).toBe(1); | ||
expect(v.view.mock.calls[0]).toEqual([v.el, v]); | ||
}); | ||
}); | ||
describe('_linkUI', () => { | ||
it('establishes links to the UI elements of the controller', () => { | ||
v.uiSelectors = { | ||
header: '#header', | ||
footer: '#footer', | ||
}; | ||
const header = document.createElement('div'); | ||
header.setAttribute('id', 'header'); | ||
v.el.appendChild(header); | ||
v._linkUI(); | ||
expect(v.ui.header).toBe(header); | ||
expect(v.ui.footer).toEqual(null); | ||
expect(v.uiSelectors.header).toBe('#header'); | ||
v._linkUI(); | ||
expect(v.ui.header).toBe(header); | ||
expect(v.uiSelectors.header).toBe('#header'); | ||
}); | ||
}); | ||
describe('_disposeRegions', () => { | ||
it('disposes all regions of the layout', () => { | ||
const region = new Region(el); | ||
region.dispose = expect.createSpy(); | ||
region.dispose = jest.fn(); | ||
v.regions = { region }; | ||
@@ -378,3 +366,3 @@ v._disposeRegions(); | ||
it('disposes of the regions of the controller', () => { | ||
v._disposeRegions = expect.createSpy(); | ||
v._disposeRegions = jest.fn(); | ||
v.dispose(); | ||
@@ -386,3 +374,3 @@ expect(v._disposeRegions).toHaveBeenCalled(); | ||
const model = { | ||
dispose: expect.createSpy(), | ||
dispose: jest.fn(), | ||
}; | ||
@@ -392,3 +380,3 @@ | ||
v.dispose({ save: true }); | ||
expect(model.dispose).toNotHaveBeenCalled(); | ||
expect(model.dispose).not.toHaveBeenCalled(); | ||
expect(v.model).toBe(undefined); | ||
@@ -407,12 +395,16 @@ | ||
v.otherMethod = expect.createSpy(); | ||
v.otherMethod = jest.fn(); | ||
v.on(v, 'dispose', v.otherMethod); | ||
v.dispose({ silent: true }); | ||
expect(v.otherMethod).toNotHaveBeenCalled(); | ||
expect(v.otherMethod).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
describe('_handleDebounce', (done) => { | ||
it('creates a closure to debounce event handlers for a given time', () => { | ||
const callback = expect.createSpy(); | ||
xdescribe('_observeAttributes', () => { | ||
// todo wait for MutationObserver support in jsdom | ||
}); | ||
describe('_handleDebounce', () => { | ||
it('creates a closure to debounce event handlers for a given time', (done) => { | ||
const callback = jest.fn(); | ||
const debounce = Controller._handleDebounce(callback, 500); | ||
@@ -423,3 +415,3 @@ expect(typeof debounce).toBe('function'); | ||
setTimeout(() => { | ||
expect(callback.calls.length).toBe(1); | ||
expect(callback.mock.calls.length).toBe(1); | ||
done(); | ||
@@ -426,0 +418,0 @@ }, 2000); |
Sorry, the diff of this file is not supported yet
8
28200
7
682