Skip to content

Commit 8407a21

Browse files
jlengstorfcassidooerezrokah
authored
feat: add support for unregistering event handlers (#283)
* feat: add support for unregistering event handlers Refactor callback management to use `Map` so that we can unregister handlers without changing the current behavior. Co-authored-by: Cassidy Williams <[email protected]> * fix: use a Set instead of a Map * test: add on/off events tests * docs: update readme with off method example Co-authored-by: Cassidy Williams <[email protected]> Co-authored-by: erezrokah <[email protected]>
1 parent 3646afe commit 8407a21

File tree

3 files changed

+185
-3
lines changed

3 files changed

+185
-3
lines changed

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ netlifyIdentity.on('error', err => console.error('Error', err));
6666
netlifyIdentity.on('open', () => console.log('Widget opened'));
6767
netlifyIdentity.on('close', () => console.log('Widget closed'));
6868

69+
// Unbind from events
70+
netlifyIdentity.off('login'); // to unbind all registered handlers
71+
netlifyIdentity.off('login', handler); // to unbind a single handler
72+
6973
// Close the modal
7074
netlifyIdentity.close();
7175

@@ -114,6 +118,10 @@ netlifyIdentity.on('error', err => console.error('Error', err));
114118
netlifyIdentity.on('open', () => console.log('Widget opened'));
115119
netlifyIdentity.on('close', () => console.log('Widget closed'));
116120

121+
// Unbind from events
122+
netlifyIdentity.off('login'); // to unbind all registered handlers
123+
netlifyIdentity.off('login', handler); // to unbind a single handler
124+
117125
// Close the modal
118126
netlifyIdentity.close();
119127

src/netlify-identity.js

+13-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import modalCSS from "./components/modal.css";
99

1010
const callbacks = {};
1111
function trigger(callback) {
12-
(callbacks[callback] || []).forEach(cb => {
12+
const cbMap = callbacks[callback] || new Set();
13+
Array.from(cbMap.values()).forEach(cb => {
1314
cb.apply(cb, Array.prototype.slice.call(arguments, 1));
1415
});
1516
}
@@ -22,8 +23,17 @@ const validActions = {
2223

2324
const netlifyIdentity = {
2425
on: (event, cb) => {
25-
callbacks[event] = callbacks[event] || [];
26-
callbacks[event].push(cb);
26+
callbacks[event] = callbacks[event] || new Set();
27+
callbacks[event].add(cb);
28+
},
29+
off: (event, cb) => {
30+
if (callbacks[event]) {
31+
if (cb) {
32+
callbacks[event].delete(cb);
33+
} else {
34+
callbacks[event].clear();
35+
}
36+
}
2737
},
2838
open: action => {
2939
action = action || "login";

src/netlify-identity.test.js

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
jest.mock("./components/modal.css", () => "");
2+
3+
describe("netlifyIdentity", () => {
4+
beforeEach(() => {
5+
jest.resetModules();
6+
jest.mock("./state/store", () => {
7+
const { observable } = require("mobx");
8+
const store = observable({
9+
user: null,
10+
recovered_user: null,
11+
message: null,
12+
settings: null,
13+
gotrue: null,
14+
error: null,
15+
siteURL: null,
16+
remember: true,
17+
saving: false,
18+
invite_token: null,
19+
email_change_token: null,
20+
namePlaceholder: null,
21+
modal: {
22+
page: "login",
23+
isOpen: false,
24+
logo: true
25+
},
26+
locale: "en"
27+
});
28+
return store;
29+
});
30+
});
31+
32+
describe("on", () => {
33+
it("should invoke login callback when user is set to an object", () => {
34+
const store = require("./state/store");
35+
const { default: netlifyIdentity } = require("./netlify-identity");
36+
37+
const loginCallback = jest.fn();
38+
netlifyIdentity.on("login", loginCallback);
39+
40+
store.user = {
41+
name: "user"
42+
};
43+
44+
expect(loginCallback).toHaveBeenCalledTimes(1);
45+
expect(loginCallback).toHaveBeenCalledWith({ name: "user" });
46+
});
47+
48+
it("should invoke logout callback when user is set to null", () => {
49+
const store = require("./state/store");
50+
store.user = {
51+
name: "user"
52+
};
53+
54+
const { default: netlifyIdentity } = require("./netlify-identity");
55+
56+
const logoutCallback = jest.fn();
57+
netlifyIdentity.on("logout", logoutCallback);
58+
59+
store.user = null;
60+
61+
expect(logoutCallback).toHaveBeenCalledTimes(1);
62+
});
63+
64+
it("should not invoke login callback when user is set to null", () => {
65+
const store = require("./state/store");
66+
store.user = {
67+
name: "user"
68+
};
69+
70+
const { default: netlifyIdentity } = require("./netlify-identity");
71+
72+
const loginCallback = jest.fn();
73+
netlifyIdentity.on("login", loginCallback);
74+
75+
store.user = null;
76+
77+
expect(loginCallback).toHaveBeenCalledTimes(0);
78+
});
79+
80+
it("should not invoke logout callback when user is set to an object", () => {
81+
const store = require("./state/store");
82+
store.user = null;
83+
84+
const { default: netlifyIdentity } = require("./netlify-identity");
85+
86+
const loginCallback = jest.fn();
87+
netlifyIdentity.on("logout", loginCallback);
88+
89+
store.user = {
90+
name: "user"
91+
};
92+
93+
expect(loginCallback).toHaveBeenCalledTimes(0);
94+
});
95+
});
96+
97+
describe("off", () => {
98+
it("should not throw when an unregistered callback is removed", () => {
99+
const { default: netlifyIdentity } = require("./netlify-identity");
100+
101+
expect(() => netlifyIdentity.off("login", () => undefined)).not.toThrow();
102+
});
103+
104+
it("should remove all callbacks when called with only first argument", () => {
105+
const store = require("./state/store");
106+
const { default: netlifyIdentity } = require("./netlify-identity");
107+
108+
const loginCallback1 = jest.fn();
109+
const loginCallback2 = jest.fn();
110+
111+
netlifyIdentity.on("login", loginCallback1);
112+
netlifyIdentity.on("login", loginCallback2);
113+
114+
store.user = {
115+
name: "user"
116+
};
117+
118+
expect(loginCallback1).toHaveBeenCalledTimes(1);
119+
expect(loginCallback2).toHaveBeenCalledTimes(1);
120+
121+
loginCallback1.mockClear();
122+
loginCallback2.mockClear();
123+
124+
netlifyIdentity.off("login");
125+
126+
store.user = {
127+
name: "other user"
128+
};
129+
130+
expect(loginCallback1).toHaveBeenCalledTimes(0);
131+
expect(loginCallback2).toHaveBeenCalledTimes(0);
132+
});
133+
134+
it("should remove a specific callback when called with two arguments", () => {
135+
const store = require("./state/store");
136+
const { default: netlifyIdentity } = require("./netlify-identity");
137+
138+
const loginCallback1 = jest.fn();
139+
const loginCallback2 = jest.fn();
140+
141+
netlifyIdentity.on("login", loginCallback1);
142+
netlifyIdentity.on("login", loginCallback2);
143+
144+
store.user = {
145+
name: "user"
146+
};
147+
148+
expect(loginCallback1).toHaveBeenCalledTimes(1);
149+
expect(loginCallback2).toHaveBeenCalledTimes(1);
150+
151+
loginCallback1.mockClear();
152+
loginCallback2.mockClear();
153+
154+
netlifyIdentity.off("login", loginCallback1);
155+
156+
store.user = {
157+
name: "other user"
158+
};
159+
160+
expect(loginCallback1).toHaveBeenCalledTimes(0);
161+
expect(loginCallback2).toHaveBeenCalledTimes(1);
162+
});
163+
});
164+
});

0 commit comments

Comments
 (0)