Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds events class and specs #1194

Merged
merged 2 commits into from
Mar 9, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions app/assets/javascripts/app/components/accordion.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import 'classlist.js';
import Events from '../utils/events';

class Accordion {
class Accordion extends Events {
constructor(el) {
super();

this.el = el;
this.controls = [].slice.call(el.querySelectorAll('[aria-controls]'));
this.content = el.querySelector('.accordion-content');
Expand Down Expand Up @@ -57,15 +60,6 @@ class Accordion {
}
}

on(event, callback) {
this.el.addEventListener(event, callback);
}

emitEvent(target = null, eventType) {
const emittable = new Event(eventType);
(target || this.el).dispatchEvent(emittable);
}

setExpanded(bool) {
this.headerControl.setAttribute('aria-expanded', bool);
}
Expand All @@ -75,14 +69,14 @@ class Accordion {
this.content.classList.remove('display-none');
this.content.classList.remove('animate-out');
this.content.classList.add('animate-in');
this.emitEvent(this.el, 'accordion.show');
this.emit('accordion.show');
}

close() {
this.setExpanded(false);
this.content.classList.remove('animate-in');
this.content.classList.add('animate-out');
this.emitEvent(this.el, 'accordion.hide');
this.emit('accordion.hide');
this.headerControl.focus();
}
}
Expand Down
40 changes: 40 additions & 0 deletions app/assets/javascripts/app/utils/events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const noOp = function() {};

class Events {
constructor() {
this.handlers = {};
}

on(eventName, handler = noOp, context = null) {
if (!eventName) return;

const handlersForEvent = this.handlers[eventName] || [];

if (!handlersForEvent.filter(obj => obj.handler === handler).length) {
handlersForEvent.push({ handler, context });
}

this.handlers[eventName] = handlersForEvent;
}

off(eventName, handler) {
if (!eventName) {
Object.keys(this.handlers).forEach((name) => {
this.handlers[name].length = 0;
});
} else if (!handler || typeof handler !== 'function') {
this.handlers[eventName].length = 0;
} else {
const handlers = this.handlers[eventName] || [];
this.handlers[eventName] = handlers.filter(obj => obj.handler !== handler);
}
}

emit(eventName, ...rest) {
const handlers = this.handlers[eventName] || [];

handlers.forEach(({ handler, context }) => handler.apply(context, rest));
}
}

export default Events;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"field-kit": "^2.1.0",
"hint.css": "^2.3.2",
"normalize.css": "^4.2.0",
"sinon": "^1.17.7",
"zxcvbn": "^4.3.0"
},
"devDependencies": {
Expand Down
89 changes: 89 additions & 0 deletions spec/javascripts/app/utils/events_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
const Events = require('../../../../app/assets/javascripts/app/utils/events').default;
/*eslint-disable */
const sinon = require('sinon');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sinon looks cool, do you think we should start moving in this direction for javascript tests? I tend to rely on feature specs but I know that's probably not the right way if we continue to add more javascript.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think we definitely want to be unit testing our javascript where possible. Self contained classes that don't have explicit dependencies on the DOM or browser APIs are great candidates for that.

/*eslint-enable */

describe('Events', () => {
const myEvent = 'super.event';
const dummyHandler = sinon.stub();
let events;

beforeEach(() => {
events = new Events();
});

it('maintains a map of handler objects', () => {
expect(events.handlers).not.to.be.undefined();
expect(events.handlers).to.be.an('object');
});

describe('#on', () => {
it('does nothing if an event name is not supplied', () => {
const initialHandlerLen = Object.keys(events.handlers).length;
events.on();

expect(Object.keys(events.handlers).length).to.equal(initialHandlerLen);
});

it('adds handler object to event key when name and function supplied', () => {
events.on(myEvent, dummyHandler);

const clickHandlers = events.handlers[myEvent];

expect(clickHandlers.length).to.equal(1);
expect(clickHandlers[0]).to.be.an('object');
expect(clickHandlers[0].handler).to.equal(dummyHandler);
});

it('stores the context of the handler when supplied', () => {
class FakeClass { contructor() { this.thing = 'fake'; } }

const fake = new FakeClass();

events.on(myEvent, dummyHandler, fake);

expect(events.handlers[myEvent][0].context instanceof FakeClass).to.be.true();
});
});

describe('#off', () => {
it('deletes all handlers if no event name is supplied', () => {
events.on(myEvent, dummyHandler);
events.off();

expect(events.handlers[myEvent].length).to.equal(0);
});

it('deletes all events registered for a given even when one is supplied', () => {
const otherEvent = 'other.event';

events.on(myEvent, dummyHandler);
events.on(otherEvent, function() {});

events.off(myEvent);

expect(events.handlers[otherEvent].length).to.equal(1);
});

it('deletes the specific handler supplied', () => {
const anotherStub = sinon.stub();

events.on(myEvent, dummyHandler);
events.on(myEvent, anotherStub);

events.off(myEvent, dummyHandler);

expect(events.handlers[myEvent].length).to.equal(1);
expect(events.handlers[myEvent][0].handler).to.equal(anotherStub);
});
});

describe('#emit', () => {
it('calls each handler registered to an event', () => {
events.on(myEvent, dummyHandler);
events.emit(myEvent);

expect(dummyHandler.calledOnce).to.be.true();
});
});
});