Skip to content

Commit 54209ab

Browse files
authored
frint-data: Collections made immutable (#361)
* tests. * frint-data: mutations in Collections via custom methods only. * frint-data: update tests. * frint-data: docs updated. * frint-data: typo.
1 parent 48f17fb commit 54209ab

File tree

4 files changed

+177
-43
lines changed

4 files changed

+177
-43
lines changed

packages/frint-data/README.md

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@ const Todo = createModel({
9494
// a group of Todo models can be put in a Todos collection
9595
const Todos = createCollection({
9696
model: Todo,
97+
98+
addTodo(todo) {
99+
return this.push(todo);
100+
},
101+
102+
extractLast() {
103+
return this.pop();
104+
},
97105
});
98106
```
99107

@@ -108,7 +116,7 @@ const todo = new Todo({
108116

109117
// collection
110118
const todos = new Todos();
111-
todos.push(todo);
119+
todos.addTodo(todo);
112120
```
113121

114122
### Model usage
@@ -129,19 +137,20 @@ console.log(todo.title); // `First task [updated]`
129137
### Collection usage
130138

131139
```js
132-
// lets push the model to collection
133-
todos.push(todo);
140+
// lets add the model to collection
141+
todos.addTodo(todo);
134142
console.log(todos.length); // `1`
135143

136-
todos.push(new Todo({
144+
todos.addTodo(new Todo({
137145
title: 'My second task',
138146
completed: false
139147
}));
140148
console.log(todos.length); // `2`
141149

142150
// let's take the last model out of the collection
143-
const lastTodo = todos.pop();
151+
const lastTodo = todos.extractLast();
144152
console.log(lastTodo); // `My second task`
153+
console.log(todos.length); // `1`
145154
```
146155

147156
### Observing Models and Collections
@@ -342,6 +351,27 @@ const Todos = createCollection({
342351

343352
Collection instances also come with built-in methods like `map`, `filter`, `reduce` just like `Array`. See more in API Reference.
344353

354+
### Immutable collections
355+
356+
Collections are immutable by default. If you want to use built-in methods that mutate the collection, then you have to do them by defining custom methods first:
357+
358+
```js
359+
const Todos = createCollection({
360+
model: Todo,
361+
362+
addTodo(todo) {
363+
// `push` and other mutating methods are only available inside custom methods
364+
return this.push(todo);
365+
},
366+
});
367+
368+
const todos = new Todos();
369+
todos.addTodo(new Todo({ title: 'First task' })); // works
370+
371+
// this will NOT work
372+
todos.push(new Todo({ title: 'Another task' }));
373+
```
374+
345375
## Embedding
346376

347377
Models can embed other Models and Collections, and this can go as many levels deep as the data structure demands.

packages/frint-data/src/createCollection.js

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ export default function createCollection(options = {}) {
5555
}
5656
});
5757

58+
const mutableMethods = {};
59+
5860
/**
5961
* Built-in methods
6062
*/
@@ -68,7 +70,7 @@ export default function createCollection(options = {}) {
6870
};
6971
makeMethodReactive(this, 'at');
7072

71-
this.push = function (model) {
73+
mutableMethods.push = function (model) {
7274
if (!isModel(model)) {
7375
throw new CollectionError('not a valid Model instance is being pushed');
7476
}
@@ -101,7 +103,7 @@ export default function createCollection(options = {}) {
101103

102104
return result;
103105
};
104-
makeMethodReactive(this, 'push');
106+
makeMethodReactive(mutableMethods, 'push');
105107

106108
// native array methods
107109
[
@@ -140,7 +142,7 @@ export default function createCollection(options = {}) {
140142
makeMethodReactive(this, lodashFuncName);
141143
});
142144

143-
this.pop = function () {
145+
mutableMethods.pop = function () {
144146
const model = models.pop();
145147

146148
this._trigger('change');
@@ -150,7 +152,7 @@ export default function createCollection(options = {}) {
150152
return model;
151153
};
152154

153-
this.shift = function () {
155+
mutableMethods.shift = function () {
154156
const model = models.shift();
155157

156158
this._trigger('change');
@@ -160,7 +162,7 @@ export default function createCollection(options = {}) {
160162
return model;
161163
};
162164

163-
this.unshift = function (model) {
165+
mutableMethods.unshift = function (model) {
164166
if (!isModel(model)) {
165167
throw new CollectionError('not a valid Model instance is being pushed');
166168
}
@@ -187,13 +189,13 @@ export default function createCollection(options = {}) {
187189
return result;
188190
};
189191

190-
this.remove = function (model) {
192+
mutableMethods.remove = function (model) {
191193
const index = this.findIndex(model);
192194

193195
this.removeFrom(index);
194196
};
195197

196-
this.removeFrom = function (index) {
198+
mutableMethods.removeFrom = function (index) {
197199
const model = models[index];
198200

199201
if (!model) {
@@ -224,29 +226,48 @@ export default function createCollection(options = {}) {
224226
// listen$()
225227
addListenerMethod(this, 'collection');
226228

229+
// combined context
230+
const combinedContext = {
231+
_on: this._on,
232+
_off: this._off,
233+
_trigger: this._trigger,
234+
};
235+
236+
Object.keys(this).forEach((k) => {
237+
combinedContext[k] = this[k];
238+
});
239+
240+
Object.keys(mutableMethods).forEach((k) => {
241+
combinedContext[k] = mutableMethods[k];
242+
});
243+
244+
Object.keys(combinedContext).forEach((k) => {
245+
combinedContext[k] = combinedContext[k].bind(combinedContext);
246+
});
247+
227248
// methods
228249
each(methods, (methodFunc, methodName) => {
229250
if (typeof this[methodName] !== 'undefined') {
230251
throw new MethodError(`conflicting method name: ${methodName}`);
231252
}
232253

233-
this[methodName] = methodFunc.bind(this);
254+
this[methodName] = methodFunc.bind(combinedContext);
234255
});
235256

236257
// initialize
237258
givenModels.forEach((v) => {
238259
if (isModel(v)) {
239-
this.push(v);
260+
combinedContext.push(v);
240261

241262
return;
242263
}
243264

244265
const model = new Model(v);
245-
this.push(model);
266+
combinedContext.push(model);
246267
});
247268

248269
if (typeof options.initialize === 'function') {
249-
options.initialize.bind(this)();
270+
options.initialize.bind(combinedContext)();
250271
}
251272
}
252273
}

0 commit comments

Comments
 (0)