-
Notifications
You must be signed in to change notification settings - Fork 8
/
ampersand-collection-view.js
155 lines (144 loc) · 5.41 KB
/
ampersand-collection-view.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
/*$AMPERSAND_VERSION*/
var assign = require('lodash/assign');
var invokeMap = require('lodash/invokeMap');
var pick = require('lodash/pick');
var find = require('lodash/find');
var difference = require('lodash/difference');
var bind = require('lodash/bind');
var Events = require('ampersand-events');
var ampExtend = require('ampersand-class-extend');
// options
var options = ['collection', 'el', 'viewOptions', 'view', 'emptyView', 'filter', 'reverse', 'parent'];
function CollectionView(spec) {
if (!spec) {
throw new ReferenceError('Collection view missing required parameters: collection, el');
}
if (!spec.collection) {
throw new ReferenceError('Collection view requires a collection');
}
if (!spec.el && !this.insertSelf) {
throw new ReferenceError('Collection view requires an el');
}
assign(this, pick(spec, options));
this.views = [];
this.listenTo(this.collection, 'add', this._addViewForModel);
this.listenTo(this.collection, 'remove', this._removeViewForModel);
this.listenTo(this.collection, 'sort', this._rerenderAll);
this.listenTo(this.collection, 'refresh reset', this._reset);
}
assign(CollectionView.prototype, Events, {
// for view contract compliance
render: function () {
this._renderAll();
return this;
},
remove: function () {
invokeMap(this.views, 'remove');
this.stopListening();
},
_getViewByModel: function (model) {
return find(this.views, function (view) {
return model === view.model;
});
},
_createViewForModel: function (model, renderOpts) {
var defaultViewOptions = {model: model, collection: this.collection, parent: this};
var view = new this.view(assign(defaultViewOptions, this.viewOptions));
this.views.push(view);
view.renderedByParentView = true;
view.render(renderOpts);
return view;
},
_getOrCreateByModel: function (model, renderOpts) {
return this._getViewByModel(model) || this._createViewForModel(model, renderOpts);
},
_addViewForModel: function (model, collection, options) {
var matches = this.filter ? this.filter(model) : true;
if (!matches) {
return;
}
if (this.renderedEmptyView) {
this._removeView(this.renderedEmptyView);
delete this.renderedEmptyView;
}
var view = this._getOrCreateByModel(model, {containerEl: this.el});
if (options && options.rerender) {
this._insertView(view);
} else {
this._insertViewAtIndex(view);
}
},
_insertViewAtIndex: function (view) {
if (!view.insertSelf) {
var pos = this.collection.indexOf(view.model);
pos = this.reverse ? pos - 1 : pos + 1;
var modelToInsertBefore = this.collection.at(pos);
var viewToInsertBefore = this._getViewByModel(modelToInsertBefore);
// FIX IE bug (https://developer.mozilla.org/en-US/docs/Web/API/Node.insertBefore)
// "In Internet Explorer an undefined value as referenceElement will throw errors, while in rest of the modern browsers, this works fine."
if (viewToInsertBefore) {
this.el.insertBefore(view.el, viewToInsertBefore && viewToInsertBefore.el);
} else {
this.el.appendChild(view.el);
}
}
},
_insertView: function (view) {
if (!view.insertSelf) {
if (this.reverse && this.el.firstChild) {
this.el.insertBefore(view.el, this.el.firstChild);
} else {
this.el.appendChild(view.el);
}
}
},
_removeViewForModel: function (model) {
var view = this._getViewByModel(model);
if (!view) {
return;
}
var index = this.views.indexOf(view);
if (index !== -1) {
// remove it if we found it calling animateRemove
// to give user option of gracefully destroying.
view = this.views.splice(index, 1)[0];
this._removeView(view);
this._renderEmptyView();
}
},
_removeView: function (view) {
if (view.animateRemove) {
view.animateRemove();
} else {
view.remove();
}
},
_renderAll: function () {
this.collection.each(bind(this._addViewForModel, this));
this._renderEmptyView();
},
_rerenderAll: function (collection, options) {
options = options || {};
this.collection.each(bind(function (model) {
this._addViewForModel(model, this, assign(options, {rerender: true}));
}, this));
},
_renderEmptyView: function() {
if (this.views.length === 0 && this.emptyView && !this.renderedEmptyView) {
this.renderedEmptyView = new this.emptyView({parent: this});
this.el.appendChild(this.renderedEmptyView.render().el);
}
},
_reset: function () {
var newViews = this.collection.map(bind(this._getOrCreateByModel, this));
//Remove existing views from the ui
var toRemove = difference(this.views, newViews);
toRemove.forEach(this._removeView, this);
//Rerender the full list with the new views
this.views = newViews;
this._rerenderAll();
this._renderEmptyView();
}
});
CollectionView.extend = ampExtend;
module.exports = CollectionView;