From 96f38d339cd47e10270ec57de684ee1f83e08a56 Mon Sep 17 00:00:00 2001 From: Haydn Ewers Date: Tue, 25 Aug 2015 17:32:06 +0930 Subject: [PATCH] Start adding event handlers. --- spec/store/events/off-spec.js | 56 ++++++++++++++++++++ spec/store/events/on-spec.js | 99 +++++++++++++++++++++++++++++++++++ src/.eslintrc | 2 +- src/store.js | 70 +++++++++++++++++++++++++ 4 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 spec/store/events/off-spec.js create mode 100644 spec/store/events/on-spec.js diff --git a/spec/store/events/off-spec.js b/spec/store/events/off-spec.js new file mode 100644 index 0000000..38a56c7 --- /dev/null +++ b/spec/store/events/off-spec.js @@ -0,0 +1,56 @@ +var Store = require("../../../src/store"); + +describe("off", function() { + + var store, listener, context; + + beforeEach(function() { + store = new Store(); + listener = { + handler: function () {} + }; + context = {}; + spyOn(listener, "handler"); + store.define("products", {}); + }); + + it("must remove event handlers", function () { + store.on("added", "products", "1", listener.handler, context); + store.on("added", "products", listener.handler, context); + store.on("updated", "products", "1", listener.handler, context); + store.on("updated", "products", listener.handler, context); + store.on("removed", "products", "1", listener.handler, context); + store.on("removed", "products", listener.handler, context); + store.add({ + "type": "products", + "id": "1" + }); + store.add({ + "type": "products", + "id": "1" + }); + store.remove("products", "1"); + expect(listener.handler.calls.count()).toEqual(6); + store.off("added", "products", "1", listener.handler, context); + store.off("added", "products", listener.handler, context); + store.off("updated", "products", "1", listener.handler, context); + store.off("updated", "products", listener.handler, context); + store.off("removed", "products", "1", listener.handler, context); + store.off("removed", "products", listener.handler, context); + store.add({ + "type": "products", + "id": "1" + }); + store.add({ + "type": "products", + "id": "1" + }); + store.remove("products", "1"); + expect(listener.handler.calls.count()).toEqual(6); + }); + + it("must allow context to be optional"); + + it("must throw an error when an unknown event is passed"); + +}); diff --git a/spec/store/events/on-spec.js b/spec/store/events/on-spec.js new file mode 100644 index 0000000..74df3bd --- /dev/null +++ b/spec/store/events/on-spec.js @@ -0,0 +1,99 @@ +var Store = require("../../../src/store"); + +describe("on", function() { + + var store, listener, context; + + beforeEach(function() { + store = new Store(); + listener = { + handler: function () {} + }; + context = {}; + spyOn(listener, "handler"); + store.define("products", {}); + }); + + it("must fire an added event when the resource with the given type & id is added to the store", function () { + store.on("added", "products", "1", listener.handler, context); + store.add({ + "type": "products", + "id": "1" + }); + expect(listener.handler).toHaveBeenCalledWith(store.find("products", "1")); + expect(listener.handler.calls.mostRecent().object).toBe(context); + }); + + it("must fire an added event when the resource with the given type is added to the store", function () { + store.on("added", "products", listener.handler, context); + store.add({ + "type": "products", + "id": "1" + }); + expect(listener.handler).toHaveBeenCalledWith(store.find("products", "1")); + expect(listener.handler.calls.mostRecent().object).toBe(context); + }); + + it("must fire an updated event when the resource with the given type & id is updated in the store", function () { + store.on("updated", "products", "1", listener.handler, context); + store.add({ + "type": "products", + "id": "1" + }); + expect(listener.handler.calls.count()).toEqual(0); + store.add({ + "type": "products", + "id": "1" + }); + expect(listener.handler).toHaveBeenCalledWith(store.find("products", "1")); + expect(listener.handler.calls.mostRecent().object).toBe(context); + }); + + it("must fire an updated event when the resource with the given type is updated in the store", function () { + store.on("updated", "products", listener.handler, context); + store.add({ + "type": "products", + "id": "1" + }); + expect(listener.handler.calls.count()).toEqual(0); + store.add({ + "type": "products", + "id": "1" + }); + expect(listener.handler).toHaveBeenCalledWith(store.find("products", "1")); + expect(listener.handler.calls.mostRecent().object).toBe(context); + }); + + it("must fire an removed event when the resource with the given type & id is removed from the store", function () { + store.on("removed", "products", "1", listener.handler, context); + store.add({ + "type": "products", + "id": "1" + }); + expect(listener.handler.calls.count()).toEqual(0); + store.remove("products", "1"); + expect(listener.handler).toHaveBeenCalledWith(store.find("products", "1")); + expect(listener.handler.calls.mostRecent().object).toBe(context); + }); + + it("must fire an removed event when the resource with the given type is removed from the store", function () { + store.on("removed", "products", listener.handler, context); + store.add({ + "type": "products", + "id": "1" + }); + expect(listener.handler.calls.count()).toEqual(0); + store.remove("products", "1"); + expect(listener.handler).toHaveBeenCalledWith(store.find("products", "1")); + expect(listener.handler.calls.mostRecent().object).toBe(context); + }); + + it("must not fire an added event when automatically creating a resource as the result of a find", function () { + store.on("added", "products", "1", listener.handler, context); + store.find("products", "1"); + expect(listener.handler.calls.count()).toEqual(0); + }); + + it("must throw an error when an unknown event is passed"); + +}); diff --git a/src/.eslintrc b/src/.eslintrc index 0a87f12..598ee8f 100644 --- a/src/.eslintrc +++ b/src/.eslintrc @@ -15,11 +15,11 @@ "consistent-return": 2, "curly": 2, "default-case": 2, - "dot-notation": 2, "eqeqeq": 2, "guard-for-in": 2, "no-alert": 2, "no-caller": 2, + "no-console": 1, "no-div-regex": 2, "no-else-return": 0, "no-empty-label": 2, diff --git a/src/store.js b/src/store.js index d24e7d2..6d27968 100644 --- a/src/store.js +++ b/src/store.js @@ -89,7 +89,9 @@ export default class Store { } constructor() { + this._collectionListeners = { "added": {}, "updated": {}, "removed": {} }; this._data = {}; + this._resourceListeners = { "added": {}, "updated": {}, "removed": {} }; this._types = {}; } @@ -105,6 +107,7 @@ export default class Store { add(object) { if (object) { if (object.type && object.id) { + let event = this._data[object.type] && this._data[object.type][object.id] ? "updated" : "added"; let resource = this.find(object.type, object.id); let definition = this._types[object.type]; Object.keys(definition).forEach(fieldName => { @@ -112,6 +115,12 @@ export default class Store { this._addField(object, resource, definition, fieldName); } }); + if (this._resourceListeners[event][object.type] && this._resourceListeners[event][object.type][object.id]) { + this._resourceListeners[event][object.type][object.id].forEach(x => x[0].call(x[1], resource)); + } + if (this._collectionListeners[event][object.type]) { + this._collectionListeners[event][object.type].forEach(x => x[0].call(x[1], resource)); + } } else { throw new TypeError(`The data must have a type and id`); } @@ -185,6 +194,61 @@ export default class Store { } } + /** + * Unregister an event listener that was registered with on(). + * + * @since 0.4.0 + * @param {string} event - Name of the event. + * @param {string} type - Name of resource to originally passed to on(). + * @param {string} [id] - ID of the resource to originally passed to on(). + * @param {function} callback - Function originally passed to on(). + * @param {Object} [context] - Context originally passed to on(). + * @return {undefined} - Nothing. + */ + off(event, type, id, callback, context) { + if (id && ({}).toString.call(id) === '[object Function]') { + this.off.call(this, event, type, null, id, callback); + } else { + if (id) { + if (this._resourceListeners[event][type] && this._resourceListeners[event][type][id]) { + this._resourceListeners[event][type][id] = this._resourceListeners[event][type][id].filter(x => { + return !(x[0] === callback && x[1] === context); + }); + } + } else if (this._collectionListeners[event][type]) { + this._collectionListeners[event][type] = this._collectionListeners[event][type].filter(x => { + return !(x[0] === callback && x[1] === context); + }); + } + } + } + + /** + * Register an event listener: "added", "updated" or "removed". + * + * @since 0.4.0 + * @param {string} event - Name of the event. + * @param {string} type - Name of resource to watch. + * @param {string} [id] - ID of the resource to watch. + * @param {function} callback - Function to call when the event occurs. + * @param {Object} [context] - Context in which to call the callback. + * @return {undefined} - Nothing. + */ + on(event, type, id, callback, context) { + if (id && ({}).toString.call(id) === '[object Function]') { + this.on.call(this, event, type, null, id, callback); + } else { + if (id) { + this._resourceListeners[event][type] = this._resourceListeners[event][type] || {}; + this._resourceListeners[event][type][id] = this._resourceListeners[event][type][id] || []; + this._resourceListeners[event][type][id].push([ callback, context ]); + } else { + this._collectionListeners[event][type] = this._collectionListeners[event][type] || []; + this._collectionListeners[event][type].push([ callback, context ]); + } + } + } + /** * Add a JSON API response to the store. This method can be used to handle a * successful GET or POST response from the server. @@ -221,6 +285,12 @@ export default class Store { let resource = this._data[type][id]; if (resource) { this._remove(resource); + if (this._resourceListeners["removed"][type] && this._resourceListeners["removed"][type][id]) { + this._resourceListeners["removed"][type][id].forEach(x => x[0].call(x[1], resource)); + } + if (this._collectionListeners["removed"][type]) { + this._collectionListeners["removed"][type].forEach(x => x[0].call(x[1], resource)); + } } } else { Object.keys(this._data[type]).forEach(id => this.remove(type, id)); -- 2.51.2