Skip to content

Commit 0989189

Browse files
committed
feat: update to native classes
1 parent f046f32 commit 0989189

File tree

18 files changed

+538
-176
lines changed

18 files changed

+538
-176
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
/testem.log
2121
/yarn-error.log
2222

23+
# macOS
24+
.DS_Store
25+
**/.DS_Store
26+
2327
# ember-try
2428
/.node_modules.ember-try/
2529
/bower.json.ember-try

README.md

Lines changed: 107 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,28 @@ you are saving on `localStorage` which you then use to update your UI through ev
1414
* Ember CLI v4.4 or above
1515
* Node.js v14 or above
1616

17+
## Modern Implementation
18+
19+
As of version 3.0.0, the `master-tab-factory` service has been updated to use native ES6 class syntax for better performance, security, and maintainability:
20+
21+
* **Native class syntax** - Uses modern `class` syntax instead of `Service.extend()`
22+
* **Secure implementation** - Replaces insecure `eval()` calls with safe boolean parsing
23+
* **Reactive `isMasterTab` property** - Uses `@tracked` decorator for automatic template updates when master tab status changes
24+
* **Full backward compatibility** - All existing APIs remain unchanged and work with both classic and native class patterns
25+
26+
The examples in this README use **native class syntax** (Ember Octane style), which is the recommended approach for modern Ember applications. The service works seamlessly with both classic and native class implementations in your application.
27+
28+
### Automatic Reactivity
29+
30+
The `isMasterTab` property is now decorated with `@tracked`, which means templates automatically re-render when the master tab status changes:
31+
32+
```hbs
33+
{{!-- This will automatically update when master tab status changes --}}
34+
<p>Is this the master tab? <strong>{{#if this.masterTab.isMasterTab}}Yes{{else}}No{{/if}}</strong></p>
35+
```
36+
37+
When a tab loses or gains master status (e.g., when the master tab closes), any template or computed property referencing `this.masterTab.isMasterTab` will automatically update without requiring manual property observation or `set()` calls. This provides a seamless reactive experience with Ember's auto-tracking system.
38+
1739
## Notes
1840

1941
* The service ensures that only one master tab exists at any one time.
@@ -37,48 +59,58 @@ You can clone this repository and have a look at the dummy app to see it in acti
3759

3860
```js
3961
// services/server-time-run.js
40-
import Ember from 'ember';
41-
42-
export default Ember.Service.extend({
43-
masterTab: Ember.inject.service(),
44-
currentTime: null,
45-
init() {
46-
this._super(...arguments);
47-
window.addEventListener('storage', e => { // only slave tabs will receive this event
62+
import Service from '@ember/service';
63+
import { service } from '@ember/service';
64+
import { tracked } from '@glimmer/tracking';
65+
import { later } from '@ember/runloop';
66+
67+
export default class ServerTimeRunService extends Service {
68+
@service masterTab;
69+
@tracked currentTime = null;
70+
71+
constructor() {
72+
super(...arguments);
73+
window.addEventListener('storage', (e) => {
74+
// only slave tabs will receive this event
4875
if (e.key === 'current-time-run') {
49-
this.set('currentTime', e.newValue);
76+
this.currentTime = e.newValue;
5077
}
5178
});
5279
this._updateTime();
53-
},
80+
}
81+
5482
_updateTime() {
55-
Ember.run.later(() => {
83+
later(() => {
5684
this.updateTime();
5785
this._updateTime();
5886
}, 900);
59-
},
87+
}
88+
6089
updateTime(force = false) {
61-
this.get('masterTab')
90+
this.masterTab
6291
.run(() => {
63-
Ember.$.getJSON('/api/current-time').then(data => { // will only run on the master tab
64-
const currentTime = data.currentTime;
65-
this.set('currentTime', currentTime);
66-
localStorage['current-time-run'] = currentTime;
67-
});
92+
fetch('/api/current-time')
93+
.then((response) => response.json())
94+
.then((data) => {
95+
// will only run on the master tab
96+
const currentTime = data.currentTime;
97+
this.currentTime = currentTime;
98+
localStorage['current-time-run'] = currentTime;
99+
});
68100
}, { force })
69101
.else(() => {
70102
// Master tab is handling it.
71103
});
72104
}
73-
});
105+
}
74106
```
75107
*Notes*:
76108
- `else()` is optional.
77109
- `run()` takes a second optional `boolean` parameter. If `true` it will
78110
make the function run irregardless of this being the master tab or not
79111
for that call on that tab and the function passed to `else()` will not
80112
run. Considering the previous example, this would be useful if a
81-
controller calls `this.get('serverTimeRun').updateTime(true)` directly
113+
controller calls `this.serverTimeRun.updateTime(true)` directly
82114
on any tab.
83115

84116
**`lock(lockName, func1, options = {}).wait(func2)`**
@@ -93,35 +125,49 @@ export default Ember.Service.extend({
93125

94126
```js
95127
// services/server-time-lock.js
96-
import Ember from 'ember';
128+
import Service from '@ember/service';
129+
import { service } from '@ember/service';
130+
import { tracked } from '@glimmer/tracking';
131+
import { later } from '@ember/runloop';
132+
133+
export default class ServerTimeLockService extends Service {
134+
@service masterTab;
135+
@tracked currentTime = null;
97136

98-
export default Ember.Service.extend({
99-
masterTab: Ember.inject.service(),
100-
currentTime: null,
101-
init() {
102-
this._super(...arguments);
137+
constructor() {
138+
super(...arguments);
103139
this._updateTime();
104-
},
140+
}
141+
105142
_updateTime() {
106-
Ember.run.later(() => {
143+
later(() => {
107144
this.updateTime();
108145
this._updateTime();
109146
}, 900);
110-
},
147+
}
148+
111149
updateTime(force = false) {
112-
this.get('masterTab')
113-
.lock('server-time', () => {
114-
return Ember.$.getJSON('/api/current-time').then(data => { // will only run on the master tab
115-
const currentTime = data.currentTime;
116-
this.set('currentTime', currentTime);
117-
return currentTime; // will be passed to slave tabs
118-
});
119-
}, { force })
120-
.wait(currentTime => { // will only run on slave tabs; currentTime is the result from the master tab
121-
this.set('currentTime', currentTime);
150+
this.masterTab
151+
.lock(
152+
'server-time',
153+
() => {
154+
return fetch('/api/current-time')
155+
.then((response) => response.json())
156+
.then((data) => {
157+
// will only run on the master tab
158+
const currentTime = data.currentTime;
159+
this.currentTime = currentTime;
160+
return currentTime; // will be passed to slave tabs
161+
});
162+
},
163+
{ force }
164+
)
165+
.wait((currentTime) => {
166+
// will only run on slave tabs; currentTime is the result from the master tab
167+
this.currentTime = currentTime;
122168
});
123169
}
124-
});
170+
}
125171
```
126172
*Notes*:
127173
- `wait()` is optional. It can take a second callback which runs if the
@@ -141,7 +187,7 @@ export default Ember.Service.extend({
141187
that call on that tab. It sets a lock and the callbacks passed to `wait()`
142188
will not run. If the master tab encounters a lock during this, it will instead
143189
run the `wait()` callbacks. Considering the previous example, this would
144-
be useful if a controller calls `this.get('serverTimeLock').updateTime(true)`
190+
be useful if a controller calls `this.serverTimeLock.updateTime(true)`
145191
directly on any tab.
146192
- The service will save to `localStorage` whatever the promise returns.
147193
This value will be passed to the appropriate callback given to `wait()`.
@@ -156,40 +202,44 @@ So, following the theme of the previous examples, you could also work with `Even
156202
(or `WebSocket`, etc.) like this:
157203
```js
158204
// services/server-time-sse.js
159-
import Ember from 'ember';
160-
161-
export default Ember.Service.extend({
162-
masterTab: Ember.inject.service(),
163-
currentTime: null,
164-
init() {
165-
this._super(...arguments);
166-
if (this.get('masterTab.isMasterTab')) {
205+
import Service from '@ember/service';
206+
import { service } from '@ember/service';
207+
import { tracked } from '@glimmer/tracking';
208+
209+
export default class ServerTimeSseService extends Service {
210+
@service masterTab;
211+
@tracked currentTime = null;
212+
213+
constructor() {
214+
super(...arguments);
215+
if (this.masterTab.isMasterTab) {
167216
this.setup();
168217
}
169-
this.get('masterTab').on('isMasterTab', isMaster => {
218+
this.masterTab.on('isMasterTab', (isMaster) => {
170219
if (isMaster) {
171220
this.setup();
172221
}
173222
});
174-
window.addEventListener('storage', e => {
223+
window.addEventListener('storage', (e) => {
175224
if (e.key === 'current-time-sse') {
176-
this.set('currentTime', e.newValue);
225+
this.currentTime = e.newValue;
177226
}
178227
});
179-
},
228+
}
229+
180230
setup() {
181231
const sse = new EventSource('/sse');
182-
sse.onmessage = e => {
183-
this.set('currentTime', e.data);
232+
sse.onmessage = (e) => {
233+
this.currentTime = e.data;
184234
window.localStorage['current-time-sse'] = e.data;
185235
};
186-
this.get('masterTab').on('isMasterTab', isMaster => {
236+
this.masterTab.on('isMasterTab', (isMaster) => {
187237
if (!isMaster) {
188238
sse.close();
189239
}
190240
});
191241
}
192-
});
242+
}
193243
```
194244
*Notes*:
195245
- The event is only raised after the application has been initialized. Therefore,

addon/consts.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ export const namespace = 'ember-master-tab:';
44

55
export const tabIdKey = `${namespace}tab-id`;
66
export const shouldInvalidateMasterTabKey = `${namespace}should-invalidate-master-tab`;
7-
export const tabId = v4();
7+
export const tabId = v4();

addon/initializers/master-tab.js

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
import MasterTabService from '../services/master-tab-factory';
22

33
export function initialize(application) {
4-
if (!application.testing) {
5-
const masterTab = MasterTabService.create();
6-
application.unregister('service:master-tab-factory');
7-
application.register('service:master-tab', masterTab, { instantiate: false });
8-
application.deferReadiness();
9-
masterTab.setup().then(() => {
10-
masterTab.initialized = true;
11-
application.advanceReadiness();
12-
});
13-
}
4+
if (!application.testing) {
5+
const masterTab = MasterTabService.create();
6+
application.unregister('service:master-tab-factory');
7+
application.register('service:master-tab', masterTab, {
8+
instantiate: false,
9+
});
10+
application.deferReadiness();
11+
masterTab.setup().then(() => {
12+
masterTab.initialized = true;
13+
application.advanceReadiness();
14+
});
15+
}
1416
}
1517

1618
export default {
1719
name: 'master-tab',
18-
initialize
20+
initialize,
1921
};

0 commit comments

Comments
 (0)