|
1 YUI.add('view', function (Y, NAME) { |
|
2 |
|
3 /** |
|
4 Represents a logical piece of an application's user interface, and provides a |
|
5 lightweight, overridable API for rendering content and handling delegated DOM |
|
6 events on a container element. |
|
7 |
|
8 @module app |
|
9 @submodule view |
|
10 @since 3.4.0 |
|
11 **/ |
|
12 |
|
13 /** |
|
14 Represents a logical piece of an application's user interface, and provides a |
|
15 lightweight, overridable API for rendering content and handling delegated DOM |
|
16 events on a container element. |
|
17 |
|
18 The View class imposes little structure and provides only minimal functionality |
|
19 of its own: it's basically just an overridable API interface that helps you |
|
20 implement custom views. |
|
21 |
|
22 As of YUI 3.5.0, View allows ad-hoc attributes to be specified at instantiation |
|
23 time, so you don't need to subclass `Y.View` to add custom attributes. Just pass |
|
24 them to the constructor: |
|
25 |
|
26 var view = new Y.View({foo: 'bar'}); |
|
27 view.get('foo'); // => "bar" |
|
28 |
|
29 @class View |
|
30 @constructor |
|
31 @extends Base |
|
32 @since 3.4.0 |
|
33 **/ |
|
34 |
|
35 function View() { |
|
36 View.superclass.constructor.apply(this, arguments); |
|
37 } |
|
38 |
|
39 Y.View = Y.extend(View, Y.Base, { |
|
40 // -- Public Properties ---------------------------------------------------- |
|
41 |
|
42 /** |
|
43 Template for this view's container. |
|
44 |
|
45 @property containerTemplate |
|
46 @type String |
|
47 @default "<div/>" |
|
48 @since 3.5.0 |
|
49 **/ |
|
50 containerTemplate: '<div/>', |
|
51 |
|
52 /** |
|
53 Hash of CSS selectors mapped to events to delegate to elements matching |
|
54 those selectors. |
|
55 |
|
56 CSS selectors are relative to the `container` element. Events are attached |
|
57 to the container, and delegation is used so that subscribers are only |
|
58 notified of events that occur on elements inside the container that match |
|
59 the specified selectors. This allows the container's contents to be re- |
|
60 rendered as needed without losing event subscriptions. |
|
61 |
|
62 Event handlers can be specified either as functions or as strings that map |
|
63 to function names on this view instance or its prototype. |
|
64 |
|
65 The `this` object in event handlers will refer to this view instance. If |
|
66 you'd prefer `this` to be something else, use `Y.bind()` to bind a custom |
|
67 `this` object. |
|
68 |
|
69 @example |
|
70 |
|
71 var view = new Y.View({ |
|
72 events: { |
|
73 // Call `this.toggle()` whenever the element with the id |
|
74 // "toggle-button" is clicked. |
|
75 '#toggle-button': {click: 'toggle'}, |
|
76 |
|
77 // Call `this.hoverOn()` when the mouse moves over any element |
|
78 // with the "hoverable" class, and `this.hoverOff()` when the |
|
79 // mouse moves out of any element with the "hoverable" class. |
|
80 '.hoverable': { |
|
81 mouseover: 'hoverOn', |
|
82 mouseout : 'hoverOff' |
|
83 } |
|
84 } |
|
85 }); |
|
86 |
|
87 @property events |
|
88 @type Object |
|
89 @default {} |
|
90 **/ |
|
91 events: {}, |
|
92 |
|
93 /** |
|
94 Template for this view's contents. |
|
95 |
|
96 This is a convenience property that has no default behavior of its own. |
|
97 It's only provided as a convention to allow you to store whatever you |
|
98 consider to be a template, whether that's an HTML string, a `Y.Node` |
|
99 instance, a Mustache template, or anything else your little heart |
|
100 desires. |
|
101 |
|
102 How this template gets used is entirely up to you and your custom |
|
103 `render()` method. |
|
104 |
|
105 @property template |
|
106 @type mixed |
|
107 @default '' |
|
108 **/ |
|
109 template: '', |
|
110 |
|
111 // -- Protected Properties ------------------------------------------------- |
|
112 |
|
113 /** |
|
114 This tells `Y.Base` that it should create ad-hoc attributes for config |
|
115 properties passed to View's constructor. This makes it possible to |
|
116 instantiate a view and set a bunch of attributes without having to subclass |
|
117 `Y.View` and declare all those attributes first. |
|
118 |
|
119 @property _allowAdHocAttrs |
|
120 @type Boolean |
|
121 @default true |
|
122 @protected |
|
123 @since 3.5.0 |
|
124 **/ |
|
125 _allowAdHocAttrs: true, |
|
126 |
|
127 // -- Lifecycle Methods ---------------------------------------------------- |
|
128 initializer: function (config) { |
|
129 config || (config = {}); |
|
130 |
|
131 // Set instance properties specified in the config. |
|
132 config.containerTemplate && |
|
133 (this.containerTemplate = config.containerTemplate); |
|
134 |
|
135 config.template && (this.template = config.template); |
|
136 |
|
137 // Merge events from the config into events in `this.events`. |
|
138 this.events = config.events ? Y.merge(this.events, config.events) : |
|
139 this.events; |
|
140 |
|
141 // When the container node changes (or when it's set for the first |
|
142 // time), we'll attach events to it, but not until then. This allows the |
|
143 // container to be created lazily the first time it's accessed rather |
|
144 // than always on init. |
|
145 this.after('containerChange', this._afterContainerChange); |
|
146 }, |
|
147 |
|
148 /** |
|
149 Destroys this View, detaching any DOM events and optionally also destroying |
|
150 its container node. |
|
151 |
|
152 By default, the container node will not be destroyed. Pass an _options_ |
|
153 object with a truthy `remove` property to destroy the container as well. |
|
154 |
|
155 @method destroy |
|
156 @param {Object} [options] Options. |
|
157 @param {Boolean} [options.remove=false] If `true`, this View's container |
|
158 will be removed from the DOM and destroyed as well. |
|
159 @chainable |
|
160 */ |
|
161 destroy: function (options) { |
|
162 // We also accept `delete` as a synonym for `remove`. |
|
163 if (options && (options.remove || options['delete'])) { |
|
164 // Attaching an event handler here because the `destroy` event is |
|
165 // preventable. If we destroyed the container before calling the |
|
166 // superclass's `destroy()` method and the event was prevented, the |
|
167 // class would end up in a broken state. |
|
168 this.onceAfter('destroy', function () { |
|
169 this._destroyContainer(); |
|
170 }); |
|
171 } |
|
172 |
|
173 return View.superclass.destroy.call(this); |
|
174 }, |
|
175 |
|
176 destructor: function () { |
|
177 this.detachEvents(); |
|
178 delete this._container; |
|
179 }, |
|
180 |
|
181 // -- Public Methods ------------------------------------------------------- |
|
182 |
|
183 /** |
|
184 Attaches delegated event handlers to this view's container element. This |
|
185 method is called internally to subscribe to events configured in the |
|
186 `events` attribute when the view is initialized. |
|
187 |
|
188 You may override this method to customize the event attaching logic. |
|
189 |
|
190 @method attachEvents |
|
191 @param {Object} [events] Hash of events to attach. See the docs for the |
|
192 `events` attribute for details on the format. If not specified, this |
|
193 view's `events` property will be used. |
|
194 @chainable |
|
195 @see detachEvents |
|
196 **/ |
|
197 attachEvents: function (events) { |
|
198 var container = this.get('container'), |
|
199 owns = Y.Object.owns, |
|
200 handler, handlers, name, selector; |
|
201 |
|
202 this.detachEvents(); |
|
203 |
|
204 events || (events = this.events); |
|
205 |
|
206 for (selector in events) { |
|
207 if (!owns(events, selector)) { continue; } |
|
208 |
|
209 handlers = events[selector]; |
|
210 |
|
211 for (name in handlers) { |
|
212 if (!owns(handlers, name)) { continue; } |
|
213 |
|
214 handler = handlers[name]; |
|
215 |
|
216 // TODO: Make this more robust by using lazy-binding: |
|
217 // `handler = Y.bind(handler, this);` |
|
218 if (typeof handler === 'string') { |
|
219 handler = this[handler]; |
|
220 } |
|
221 |
|
222 if (!handler) { |
|
223 Y.log('Missing handler for ' + selector + ' ' + name + ' event.', 'warn', 'View'); |
|
224 continue; |
|
225 } |
|
226 |
|
227 this._attachedViewEvents.push( |
|
228 container.delegate(name, handler, selector, this)); |
|
229 } |
|
230 } |
|
231 |
|
232 return this; |
|
233 }, |
|
234 |
|
235 /** |
|
236 Creates and returns a container node for this view. |
|
237 |
|
238 By default, the container is created from the HTML template specified in the |
|
239 `containerTemplate` property, and is _not_ added to the DOM automatically. |
|
240 |
|
241 You may override this method to customize how the container node is created |
|
242 (such as by rendering it from a custom template format). Your method must |
|
243 return a `Y.Node` instance. |
|
244 |
|
245 @method create |
|
246 @param {HTMLElement|Node|String} [container] Selector string, `Y.Node` |
|
247 instance, or DOM element to use at the container node. |
|
248 @return {Node} Node instance of the created container node. |
|
249 **/ |
|
250 create: function (container) { |
|
251 return container ? Y.one(container) : |
|
252 Y.Node.create(this.containerTemplate); |
|
253 }, |
|
254 |
|
255 /** |
|
256 Detaches DOM events that have previously been attached to the container by |
|
257 `attachEvents()`. |
|
258 |
|
259 @method detachEvents |
|
260 @chainable |
|
261 @see attachEvents |
|
262 **/ |
|
263 detachEvents: function () { |
|
264 Y.Array.each(this._attachedViewEvents, function (handle) { |
|
265 if (handle) { |
|
266 handle.detach(); |
|
267 } |
|
268 }); |
|
269 |
|
270 this._attachedViewEvents = []; |
|
271 return this; |
|
272 }, |
|
273 |
|
274 /** |
|
275 Removes this view's container element from the DOM (if it's in the DOM), |
|
276 but doesn't destroy it or any event listeners attached to it. |
|
277 |
|
278 @method remove |
|
279 @chainable |
|
280 **/ |
|
281 remove: function () { |
|
282 var container = this.get('container'); |
|
283 container && container.remove(); |
|
284 return this; |
|
285 }, |
|
286 |
|
287 /** |
|
288 Renders this view. |
|
289 |
|
290 This method is a noop by default. Override it to provide a custom |
|
291 implementation that renders this view's content and appends it to the |
|
292 container element. Ideally your `render` method should also return `this` as |
|
293 the end to allow chaining, but that's up to you. |
|
294 |
|
295 Since there's no default renderer, you're free to render your view however |
|
296 you see fit, whether that means manipulating the DOM directly, dumping |
|
297 strings into `innerHTML`, or using a template language of some kind. |
|
298 |
|
299 For basic templating needs, `Y.Node.create()` and `Y.Lang.sub()` may |
|
300 suffice, but there are no restrictions on what tools or techniques you can |
|
301 use to render your view. All you need to do is append something to the |
|
302 container element at some point, and optionally append the container |
|
303 to the DOM if it's not there already. |
|
304 |
|
305 @method render |
|
306 @chainable |
|
307 **/ |
|
308 render: function () { |
|
309 return this; |
|
310 }, |
|
311 |
|
312 // -- Protected Methods ---------------------------------------------------- |
|
313 |
|
314 /** |
|
315 Removes the `container` from the DOM and purges all its event listeners. |
|
316 |
|
317 @method _destroyContainer |
|
318 @protected |
|
319 **/ |
|
320 _destroyContainer: function () { |
|
321 var container = this.get('container'); |
|
322 container && container.remove(true); |
|
323 }, |
|
324 |
|
325 /** |
|
326 Getter for the `container` attribute. |
|
327 |
|
328 @method _getContainer |
|
329 @param {Node|null} value Current attribute value. |
|
330 @return {Node} Container node. |
|
331 @protected |
|
332 @since 3.5.0 |
|
333 **/ |
|
334 _getContainer: function (value) { |
|
335 // This wackiness is necessary to enable fully lazy creation of the |
|
336 // container node both when no container is specified and when one is |
|
337 // specified via a valueFn. |
|
338 |
|
339 if (!this._container) { |
|
340 if (value) { |
|
341 // Attach events to the container when it's specified via a |
|
342 // valueFn, which won't fire the containerChange event. |
|
343 this._container = value; |
|
344 this.attachEvents(); |
|
345 } else { |
|
346 // Create a default container and set that as the new attribute |
|
347 // value. The `this._container` property prevents infinite |
|
348 // recursion. |
|
349 value = this._container = this.create(); |
|
350 this._set('container', value); |
|
351 } |
|
352 } |
|
353 |
|
354 return value; |
|
355 }, |
|
356 |
|
357 // -- Protected Event Handlers --------------------------------------------- |
|
358 |
|
359 /** |
|
360 Handles `containerChange` events. Detaches event handlers from the old |
|
361 container (if any) and attaches them to the new container. |
|
362 |
|
363 Right now the `container` attr is initOnly so this event should only ever |
|
364 fire the first time the container is created, but in the future (once Y.App |
|
365 can handle it) we may allow runtime container changes. |
|
366 |
|
367 @method _afterContainerChange |
|
368 @protected |
|
369 @since 3.5.0 |
|
370 **/ |
|
371 _afterContainerChange: function () { |
|
372 this.attachEvents(this.events); |
|
373 } |
|
374 }, { |
|
375 NAME: 'view', |
|
376 |
|
377 ATTRS: { |
|
378 /** |
|
379 Container node into which this view's content will be rendered. |
|
380 |
|
381 The container node serves as the host for all DOM events attached by the |
|
382 view. Delegation is used to handle events on children of the container, |
|
383 allowing the container's contents to be re-rendered at any time without |
|
384 losing event subscriptions. |
|
385 |
|
386 The default container is a `<div>` Node, but you can override this in |
|
387 a subclass, or by passing in a custom `container` config value at |
|
388 instantiation time. If you override the default container in a subclass |
|
389 using `ATTRS`, you must use the `valueFn` property. The view's constructor |
|
390 will ignore any assignments using `value`. |
|
391 |
|
392 When `container` is overridden by a subclass or passed as a config |
|
393 option at instantiation time, you can provide it as a selector string, a |
|
394 DOM element, a `Y.Node` instance, or (if you are subclassing and modifying |
|
395 the attribute), a `valueFn` function that returns a `Y.Node` instance. |
|
396 The value will be converted into a `Y.Node` instance if it isn't one |
|
397 already. |
|
398 |
|
399 The container is not added to the page automatically. This allows you to |
|
400 have full control over how and when your view is actually rendered to |
|
401 the page. |
|
402 |
|
403 @attribute container |
|
404 @type HTMLElement|Node|String |
|
405 @default Y.Node.create(this.containerTemplate) |
|
406 @writeOnce |
|
407 **/ |
|
408 container: { |
|
409 getter : '_getContainer', |
|
410 setter : Y.one, |
|
411 writeOnce: true |
|
412 } |
|
413 }, |
|
414 |
|
415 /** |
|
416 Properties that shouldn't be turned into ad-hoc attributes when passed to |
|
417 View's constructor. |
|
418 |
|
419 @property _NON_ATTRS_CFG |
|
420 @type Array |
|
421 @static |
|
422 @protected |
|
423 @since 3.5.0 |
|
424 **/ |
|
425 _NON_ATTRS_CFG: [ |
|
426 'containerTemplate', |
|
427 'events', |
|
428 'template' |
|
429 ] |
|
430 }); |
|
431 |
|
432 |
|
433 |
|
434 }, '@VERSION@', {"requires": ["base-build", "node-event-delegate"]}); |