-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
marionette.view.js
233 lines (181 loc) · 7.68 KB
/
marionette.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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
/* jshint maxlen: 114, nonew: false */
// Marionette.View
// ---------------
// The core view class that other Marionette views extend from.
Marionette.View = Backbone.View.extend({
constructor: function(options) {
_.bindAll(this, 'render');
// this exposes view options to the view initializer
// this is a backfill since backbone removed the assignment
// of this.options
// at some point however this may be removed
this.options = _.extend({}, _.result(this, 'options'), _.isFunction(options) ? options.call(this) : options);
// parses out the @ui DSL for events
this.events = this.normalizeUIKeys(_.result(this, 'events'));
if (_.isObject(this.behaviors)) {
new Marionette.Behaviors(this);
}
Backbone.View.apply(this, arguments);
Marionette.MonitorDOMRefresh(this);
this.listenTo(this, 'show', this.onShowCalled);
},
// Get the template for this view
// instance. You can set a `template` attribute in the view
// definition or pass a `template: "whatever"` parameter in
// to the constructor options.
getTemplate: function() {
return this.getOption('template');
},
// Mix in template helper methods. Looks for a
// `templateHelpers` attribute, which can either be an
// object literal, or a function that returns an object
// literal. All methods and attributes from this object
// are copies to the object passed in.
mixinTemplateHelpers: function(target) {
target = target || {};
var templateHelpers = this.getOption('templateHelpers');
if (_.isFunction(templateHelpers)) {
templateHelpers = templateHelpers.call(this);
}
return _.extend(target, templateHelpers);
},
normalizeUIKeys: function(hash) {
var ui = _.result(this, 'ui');
var uiBindings = _.result(this, '_uiBindings');
return Marionette.normalizeUIKeys(hash, uiBindings || ui);
},
// Configure `triggers` to forward DOM events to view
// events. `triggers: {"click .foo": "do:foo"}`
configureTriggers: function() {
if (!this.triggers) { return; }
var triggerEvents = {};
// Allow `triggers` to be configured as a function
var triggers = this.normalizeUIKeys(_.result(this, 'triggers'));
// Configure the triggers, prevent default
// action and stop propagation of DOM events
_.each(triggers, function(value, key) {
var hasOptions = _.isObject(value);
var eventName = hasOptions ? value.event : value;
// build the event handler function for the DOM event
triggerEvents[key] = function(e) {
// stop the event in its tracks
if (e) {
var prevent = e.preventDefault;
var stop = e.stopPropagation;
var shouldPrevent = hasOptions ? value.preventDefault : prevent;
var shouldStop = hasOptions ? value.stopPropagation : stop;
if (shouldPrevent && prevent) { prevent.apply(e); }
if (shouldStop && stop) { stop.apply(e); }
}
// build the args for the event
var args = {
view: this,
model: this.model,
collection: this.collection
};
// trigger the event
this.triggerMethod(eventName, args);
};
}, this);
return triggerEvents;
},
// Overriding Backbone.View's delegateEvents to handle
// the `triggers`, `modelEvents`, and `collectionEvents` configuration
delegateEvents: function(events) {
this._delegateDOMEvents(events);
this.bindEntityEvents(this.model, this.getOption('modelEvents'));
this.bindEntityEvents(this.collection, this.getOption('collectionEvents'));
},
// internal method to delegate DOM events and triggers
_delegateDOMEvents: function(events) {
events = events || this.events;
if (_.isFunction(events)) { events = events.call(this); }
// normalize ui keys
events = this.normalizeUIKeys(events);
var combinedEvents = {};
// look up if this view has behavior events
var behaviorEvents = _.result(this, 'behaviorEvents') || {};
var triggers = this.configureTriggers();
// behavior events will be overriden by view events and or triggers
_.extend(combinedEvents, behaviorEvents, events, triggers);
Backbone.View.prototype.delegateEvents.call(this, combinedEvents);
},
// Overriding Backbone.View's undelegateEvents to handle unbinding
// the `triggers`, `modelEvents`, and `collectionEvents` config
undelegateEvents: function() {
var args = Array.prototype.slice.call(arguments);
Backbone.View.prototype.undelegateEvents.apply(this, args);
this.unbindEntityEvents(this.model, this.getOption('modelEvents'));
this.unbindEntityEvents(this.collection, this.getOption('collectionEvents'));
},
// Internal method, handles the `show` event.
onShowCalled: function() {},
// Internal helper method to verify whether the view hasn't been destroyed
_ensureViewIsIntact: function() {
if (this.isDestroyed) {
var err = new Error('Cannot use a view thats already been destroyed.');
err.name = 'ViewDestroyedError';
throw err;
}
},
// Default `destroy` implementation, for removing a view from the
// DOM and unbinding it. Regions will call this method
// for you. You can specify an `onDestroy` method in your view to
// add custom code that is called after the view is destroyed.
destroy: function() {
if (this.isDestroyed) { return; }
var args = Array.prototype.slice.call(arguments);
this.triggerMethod.apply(this, ['before:destroy'].concat(args));
// mark as destroyed before doing the actual destroy, to
// prevent infinite loops within "destroy" event handlers
// that are trying to destroy other views
this.isDestroyed = true;
this.triggerMethod.apply(this, ['destroy'].concat(args));
// unbind UI elements
this.unbindUIElements();
// remove the view from the DOM
this.remove();
},
// This method binds the elements specified in the "ui" hash inside the view's code with
// the associated jQuery selectors.
bindUIElements: function() {
if (!this.ui) { return; }
// store the ui hash in _uiBindings so they can be reset later
// and so re-rendering the view will be able to find the bindings
if (!this._uiBindings) {
this._uiBindings = this.ui;
}
// get the bindings result, as a function or otherwise
var bindings = _.result(this, '_uiBindings');
// empty the ui so we don't have anything to start with
this.ui = {};
// bind each of the selectors
_.each(_.keys(bindings), function(key) {
var selector = bindings[key];
this.ui[key] = this.$(selector);
}, this);
},
// This method unbinds the elements specified in the "ui" hash
unbindUIElements: function() {
if (!this.ui || !this._uiBindings) { return; }
// delete all of the existing ui bindings
_.each(this.ui, function($el, name) {
delete this.ui[name];
}, this);
// reset the ui element to the original bindings configuration
this.ui = this._uiBindings;
delete this._uiBindings;
},
// import the `triggerMethod` to trigger events with corresponding
// methods if the method exists
triggerMethod: Marionette.triggerMethod,
// Imports the "normalizeMethods" to transform hashes of
// events=>function references/names to a hash of events=>function references
normalizeMethods: Marionette.normalizeMethods,
// Proxy `getOption` to enable getting options from this or this.options by name.
getOption: Marionette.proxyGetOption,
// Proxy `unbindEntityEvents` to enable binding view's events from another entity.
bindEntityEvents: Marionette.proxyBindEntityEvents,
// Proxy `unbindEntityEvents` to enable unbinding view's events from another entity.
unbindEntityEvents: Marionette.proxyUnbindEntityEvents
});