-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
/
view.js
2015 lines (1570 loc) · 60.4 KB
/
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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// ==========================================================================
// Project: Ember - JavaScript Application Framework
// Copyright: ©2006-2011 Strobe Inc. and contributors.
// Portions ©2008-2011 Apple Inc. All rights reserved.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
require("ember-views/system/render_buffer");
var get = Ember.get, set = Ember.set, addObserver = Ember.addObserver;
var getPath = Ember.getPath, meta = Ember.meta, fmt = Ember.String.fmt;
var a_slice = [].slice;
var a_forEach = Ember.EnumerableUtils.forEach;
var childViewsProperty = Ember.computed(function() {
var childViews = get(this, '_childViews');
var ret = Ember.A();
a_forEach(childViews, function(view) {
if (view.isVirtual) {
ret.pushObjects(get(view, 'childViews'));
} else {
ret.push(view);
}
});
return ret;
}).property().cacheable();
var VIEW_PRESERVES_CONTEXT = Ember.VIEW_PRESERVES_CONTEXT;
Ember.warn("The way that the {{view}} helper affects templates is about to change. Previously, templates inside child views would use the new view as the context. Soon, views will preserve their parent context when rendering their template. You can opt-in early to the new behavior by setting `ENV.VIEW_PRESERVES_CONTEXT = true`. For more information, see https://gist.github.com/2494968. You should update your templates as soon as possible; this default will change soon, and the option will be eliminated entirely before the 1.0 release.", VIEW_PRESERVES_CONTEXT);
/**
@static
Global hash of shared templates. This will automatically be populated
by the build tools so that you can store your Handlebars templates in
separate files that get loaded into JavaScript at buildtime.
@type Hash
*/
Ember.TEMPLATES = {};
var invokeForState = {
preRender: {},
inBuffer: {},
hasElement: {},
inDOM: {},
destroyed: {}
};
/**
@class
`Ember.View` is the class in Ember responsible for encapsulating templates of HTML
content, combining templates with data to render as sections of a page's DOM, and
registering and responding to user-initiated events.
## HTML Tag
The default HTML tag name used for a view's DOM representation is `div`. This can be
customized by setting the `tagName` property. The following view class:
ParagraphView = Ember.View.extend({
tagName: 'em'
})
Would result in instances with the following HTML:
<em id="ember1" class="ember-view"></em>
## HTML `class` Attribute
The HTML `class` attribute of a view's tag can be set by providing a `classNames` property
that is set to an array of strings:
MyView = Ember.View.extend({
classNames: ['my-class', 'my-other-class']
})
Will result in view instances with an HTML representation of:
<div id="ember1" class="ember-view my-class my-other-class"></div>
`class` attribute values can also be set by providing a `classNameBindings` property
set to an array of properties names for the view. The return value of these properties
will be added as part of the value for the view's `class` attribute. These properties
can be computed properties:
MyView = Ember.View.extend({
classNameBindings: ['propertyA', 'propertyB'],
propertyA: 'from-a',
propertyB: function(){
if(someLogic){ return 'from-b'; }
}.property()
})
Will result in view instances with an HTML representation of:
<div id="ember1" class="ember-view from-a from-b"></div>
If the value of a class name binding returns a boolean the property name itself
will be used as the class name if the property is true. The class name will
not be added if the value is `false` or `undefined`.
MyView = Ember.View.extend({
classNameBindings: ['hovered'],
hovered: true
})
Will result in view instances with an HTML representation of:
<div id="ember1" class="ember-view hovered"></div>
When using boolean class name bindings you can supply a string value other than the
property name for use as the `class` HTML attribute by appending the preferred value after
a ":" character when defining the binding:
MyView = Ember.View.extend({
classNameBindings: ['awesome:so-very-cool'],
awesome: true
})
Will result in view instances with an HTML representation of:
<div id="ember1" class="ember-view so-very-cool"></div>
Boolean value class name bindings whose property names are in a camelCase-style
format will be converted to a dasherized format:
MyView = Ember.View.extend({
classNameBindings: ['isUrgent'],
isUrgent: true
})
Will result in view instances with an HTML representation of:
<div id="ember1" class="ember-view is-urgent"></div>
Class name bindings can also refer to object values that are found by
traversing a path relative to the view itself:
MyView = Ember.View.extend({
classNameBindings: ['messages.empty']
messages: Ember.Object.create({
empty: true
})
})
Will result in view instances with an HTML representation of:
<div id="ember1" class="ember-view empty"></div>
Updates to the the value of a class name binding will result in automatic update
of the HTML `class` attribute in the view's rendered HTML representation.
If the value becomes `false` or `undefined` the class name will be removed.
Both `classNames` and `classNameBindings` are concatenated properties.
See `Ember.Object` documentation for more information about concatenated properties.
## HTML Attributes
The HTML attribute section of a view's tag can be set by providing an `attributeBindings`
property set to an array of property names on the view. The return value of these properties
will be used as the value of the view's HTML associated attribute:
AnchorView = Ember.View.extend({
tagName: 'a',
attributeBindings: ['href'],
href: 'http://google.com'
})
Will result in view instances with an HTML representation of:
<a id="ember1" class="ember-view" href="http://google.com"></a>
If the return value of an `attributeBindings` monitored property is a boolean
the property will follow HTML's pattern of repeating the attribute's name as
its value:
MyTextInput = Ember.View.extend({
tagName: 'input',
attributeBindings: ['disabled'],
disabled: true
})
Will result in view instances with an HTML representation of:
<input id="ember1" class="ember-view" disabled="disabled" />
`attributeBindings` can refer to computed properties:
MyTextInput = Ember.View.extend({
tagName: 'input',
attributeBindings: ['disabled'],
disabled: function(){
if (someLogic) {
return true;
} else {
return false;
}
}.property()
})
Updates to the the property of an attribute binding will result in automatic update
of the HTML attribute in the view's rendered HTML representation.
`attributeBindings` is a concatenated property. See `Ember.Object` documentation
for more information about concatenated properties.
## Templates
The HTML contents of a view's rendered representation are determined by its template.
Templates can be any function that accepts an optional context parameter and returns
a string of HTML that will be inserted within the view's tag. Most
typically in Ember this function will be a compiled Ember.Handlebars template.
AView = Ember.View.extend({
template: Ember.Handlebars.compile('I am the template')
})
Will result in view instances with an HTML representation of:
<div id="ember1" class="ember-view">I am the template</div>
The default context of the compiled template will be the view instance itself:
AView = Ember.View.extend({
template: Ember.Handlebars.compile('Hello {{excitedGreeting}}')
})
aView = AView.create({
content: Ember.Object.create({
firstName: 'Barry'
})
excitedGreeting: function(){
return this.getPath("content.firstName") + "!!!"
}
})
Will result in an HTML representation of:
<div id="ember1" class="ember-view">Hello Barry!!!</div>
Within an Ember application is more common to define a Handlebars templates as
part of a page:
<script type='text/x-handlebars' data-template-name='some-template'>
Hello
</script>
And associate it by name using a view's `templateName` property:
AView = Ember.View.extend({
templateName: 'some-template'
})
Using a value for `templateName` that does not have a Handlebars template with a
matching `data-template-name` attribute will throw an error.
Assigning a value to both `template` and `templateName` properties will throw an error.
For views classes that may have a template later defined (e.g. as the block portion of a `{{view}}`
Handlebars helper call in another template or in a subclass), you can provide a `defaultTemplate`
property set to compiled template function. If a template is not later provided for the view
instance the `defaultTemplate` value will be used:
AView = Ember.View.extend({
defaultTemplate: Ember.Handlebars.compile('I was the default'),
template: null,
templateName: null
})
Will result in instances with an HTML representation of:
<div id="ember1" class="ember-view">I was the default</div>
If a `template` or `templateName` is provided it will take precedence over `defaultTemplate`:
AView = Ember.View.extend({
defaultTemplate: Ember.Handlebars.compile('I was the default')
})
aView = AView.create({
template: Ember.Handlebars.compile('I was the template, not default')
})
Will result in the following HTML representation when rendered:
<div id="ember1" class="ember-view">I was the template, not default</div>
## Layouts
Views can have a secondary template that wraps their main template. Like
primary templates, layouts can be any function that accepts an optional context
parameter and returns a string of HTML that will be inserted inside view's tag. Views whose HTML
element is self closing (e.g. `<input />`) cannot have a layout and this property will be ignored.
Most typically in Ember a layout will be a compiled Ember.Handlebars template.
A view's layout can be set directly with the `layout` property or reference an
existing Handlebars template by name with the `layoutName` property.
A template used as a layout must contain a single use of the Handlebars `{{yield}}`
helper. The HTML contents of a view's rendered `template` will be inserted at this location:
AViewWithLayout = Ember.View.extend({
layout: Ember.Handlebars.compile("<div class='my-decorative-class'>{{yield}}</div>")
template: Ember.Handlebars.compile("I got wrapped"),
})
Will result in view instances with an HTML representation of:
<div id="ember1" class="ember-view">
<div class="my-decorative-class">
I got wrapped
</div>
</div>
See `Handlebars.helpers.yield` for more information.
## Responding to Browser Events
Views can respond to user-initiated events in one of three ways: method implementation,
through an event manager, and through `{{action}}` helper use in their template or layout.
### Method Implementation
Views can respond to user-initiated events by implementing a method that matches the
event name. A `jQuery.Event` object will be passed as the argument to this method.
AView = Ember.View.extend({
click: function(event){
// will be called when when an instance's
// rendered element is clicked
}
})
### Event Managers
Views can define an object as their `eventManager` property. This object can then
implement methods that match the desired event names. Matching events that occur
on the view's rendered HTML or the rendered HTML of any of its DOM descendants
will trigger this method. A `jQuery.Event` object will be passed as the first
argument to the method and an `Ember.View` object as the second. The `Ember.View`
will be the view whose rendered HTML was interacted with. This may be the view with
the `eventManager` property or one of its descendent views.
AView = Ember.View.extend({
eventManager: Ember.Object.create({
doubleClick: function(event, view){
// will be called when when an instance's
// rendered element or any rendering
// of this views's descendent
// elements is clicked
}
})
})
An event defined for an event manager takes precedence over events of the same
name handled through methods on the view.
AView = Ember.View.extend({
mouseEnter: function(event){
// will never trigger.
},
eventManager: Ember.Object.create({
mouseEnter: function(event, view){
// takes presedence over AView#mouseEnter
}
})
})
Similarly a view's event manager will take precedence for events of any views
rendered as a descendent. A method name that matches an event name will not be called
if the view instance was rendered inside the HTML representation of a view that has
an `eventManager` property defined that handles events of the name. Events not handled
by the event manager will still trigger method calls on the descendent.
OuterView = Ember.View.extend({
template: Ember.Handlebars.compile("outer {{#view InnerView}}inner{{/view}} outer"),
eventManager: Ember.Object.create({
mouseEnter: function(event, view){
// view might be instance of either
// OutsideView or InnerView depending on
// where on the page the user interaction occured
}
})
})
InnerView = Ember.View.extend({
click: function(event){
// will be called if rendered inside
// an OuterView because OuterView's
// eventManager doesn't handle click events
},
mouseEnter: function(event){
// will never be called if rendered inside
// an OuterView.
}
})
### Handlebars `{{action}}` Helper
See `Handlebars.helpers.action`.
### Event Names
Possible events names for any of the responding approaches described above are:
Touch events: 'touchStart', 'touchMove', 'touchEnd', 'touchCancel'
Keyboard events: 'keyDown', 'keyUp', 'keyPress'
Mouse events: 'mouseDown', 'mouseUp', 'contextMenu', 'click', 'doubleClick', 'mouseMove',
'focusIn', 'focusOut', 'mouseEnter', 'mouseLeave'
Form events: 'submit', 'change', 'focusIn', 'focusOut', 'input'
HTML5 drag and drop events: 'dragStart', 'drag', 'dragEnter', 'dragLeave', 'drop', 'dragEnd'
## Handlebars `{{view}}` Helper
Other `Ember.View` instances can be included as part of a view's template by using the `{{view}}`
Handlebars helper. See `Handlebars.helpers.view` for additional information.
@extends Ember.Object
@extends Ember.Evented
*/
Ember.View = Ember.Object.extend(Ember.Evented,
/** @scope Ember.View.prototype */ {
/** @private */
concatenatedProperties: ['classNames', 'classNameBindings', 'attributeBindings'],
/**
@type Boolean
@default true
@constant
*/
isView: true,
// ..........................................................
// TEMPLATE SUPPORT
//
/**
The name of the template to lookup if no template is provided.
Ember.View will look for a template with this name in this view's
`templates` object. By default, this will be a global object
shared in `Ember.TEMPLATES`.
@type String
@default null
*/
templateName: null,
/**
The name of the layout to lookup if no layout is provided.
Ember.View will look for a template with this name in this view's
`templates` object. By default, this will be a global object
shared in `Ember.TEMPLATES`.
@type String
@default null
*/
layoutName: null,
/**
The hash in which to look for `templateName`.
@type Ember.Object
@default Ember.TEMPLATES
*/
templates: Ember.TEMPLATES,
/**
The template used to render the view. This should be a function that
accepts an optional context parameter and returns a string of HTML that
will be inserted into the DOM relative to its parent view.
In general, you should set the `templateName` property instead of setting
the template yourself.
@field
@type Function
*/
template: Ember.computed(function(key, value) {
if (value !== undefined) { return value; }
var templateName = get(this, 'templateName'),
template = this.templateForName(templateName, 'template');
return template || get(this, 'defaultTemplate');
}).property('templateName').cacheable(),
/**
The controller managing this view. If this property is set, it will be
made available for use by the template.
@type Object
*/
controller: Ember.computed(function(key, value) {
var parentView;
if (arguments.length === 2) {
return value;
} else {
parentView = get(this, 'parentView');
return parentView ? get(parentView, 'controller') : null;
}
}).property().cacheable(),
/**
A view may contain a layout. A layout is a regular template but
supersedes the `template` property during rendering. It is the
responsibility of the layout template to retrieve the `template`
property from the view (or alternatively, call `Handlebars.helpers.yield`,
`{{yield}}`) to render it in the correct location.
This is useful for a view that has a shared wrapper, but which delegates
the rendering of the contents of the wrapper to the `template` property
on a subclass.
@field
@type Function
*/
layout: Ember.computed(function(key, value) {
if (arguments.length === 2) { return value; }
var layoutName = get(this, 'layoutName'),
layout = this.templateForName(layoutName, 'layout');
return layout || get(this, 'defaultLayout');
}).property('layoutName').cacheable(),
templateForName: function(name, type) {
if (!name) { return; }
var templates = get(this, 'templates'),
template = get(templates, name);
if (!template) {
throw new Ember.Error(fmt('%@ - Unable to find %@ "%@".', [this, type, name]));
}
return template;
},
/**
The object from which templates should access properties.
This object will be passed to the template function each time the render
method is called, but it is up to the individual function to decide what
to do with it.
By default, this will be the view itself.
@type Object
*/
context: Ember.computed(function(key, value) {
if (arguments.length === 2) {
set(this, '_context', value);
return value;
} else {
return get(this, '_context');
}
}).cacheable(),
/**
@private
Private copy of the view's template context. This can be set directly
by Handlebars without triggering the observer that causes the view
to be re-rendered.
*/
_context: Ember.computed(function(key, value) {
var parentView, controller;
if (arguments.length === 2) {
return value;
}
if (VIEW_PRESERVES_CONTEXT) {
if (controller = get(this, 'controller')) {
return controller;
}
parentView = get(this, '_parentView');
if (parentView) {
return get(parentView, '_context');
}
}
return this;
}).cacheable(),
/**
If a value that affects template rendering changes, the view should be
re-rendered to reflect the new value.
@private
*/
_displayPropertyDidChange: Ember.observer(function() {
this.rerender();
}, 'context', 'controller'),
/**
If the view is currently inserted into the DOM of a parent view, this
property will point to the parent of the view.
@type Ember.View
@default null
*/
parentView: Ember.computed(function() {
var parent = get(this, '_parentView');
if (parent && parent.isVirtual) {
return get(parent, 'parentView');
} else {
return parent;
}
}).property('_parentView').volatile(),
_parentView: null,
// return the current view, not including virtual views
concreteView: Ember.computed(function() {
if (!this.isVirtual) { return this; }
else { return get(this, 'parentView'); }
}).property('_parentView').volatile(),
/**
If false, the view will appear hidden in DOM.
@type Boolean
@default null
*/
isVisible: true,
/**
Array of child views. You should never edit this array directly.
Instead, use appendChild and removeFromParent.
@private
@type Array
@default []
*/
childViews: childViewsProperty,
_childViews: [],
/**
When it's a virtual view, we need to notify the parent that their
childViews will change.
*/
_childViewsWillChange: Ember.beforeObserver(function() {
if (this.isVirtual) {
var parentView = get(this, 'parentView');
if (parentView) { Ember.propertyWillChange(parentView, 'childViews'); }
}
}, 'childViews'),
/**
When it's a virtual view, we need to notify the parent that their
childViews did change.
*/
_childViewsDidChange: Ember.observer(function() {
if (this.isVirtual) {
var parentView = get(this, 'parentView');
if (parentView) { Ember.propertyDidChange(parentView, 'childViews'); }
}
}, 'childViews'),
/**
Return the nearest ancestor that is an instance of the provided
class.
@param {Class} klass Subclass of Ember.View (or Ember.View itself)
@returns Ember.View
*/
nearestInstanceOf: function(klass) {
var view = get(this, 'parentView');
while (view) {
if(view instanceof klass) { return view; }
view = get(view, 'parentView');
}
},
/**
Return the nearest ancestor that has a given property.
@param {String} property A property name
@returns Ember.View
*/
nearestWithProperty: function(property) {
var view = get(this, 'parentView');
while (view) {
if (property in view) { return view; }
view = get(view, 'parentView');
}
},
/**
Return the nearest ancestor whose parent is an instance of
`klass`.
@param {Class} klass Subclass of Ember.View (or Ember.View itself)
@returns Ember.View
*/
nearestChildOf: function(klass) {
var view = get(this, 'parentView');
while (view) {
if(get(view, 'parentView') instanceof klass) { return view; }
view = get(view, 'parentView');
}
},
/**
Return the nearest ancestor that is an Ember.CollectionView
@returns Ember.CollectionView
*/
collectionView: Ember.computed(function() {
return this.nearestInstanceOf(Ember.CollectionView);
}).cacheable(),
/**
Return the nearest ancestor that is a direct child of
an Ember.CollectionView
@returns Ember.View
*/
itemView: Ember.computed(function() {
return this.nearestChildOf(Ember.CollectionView);
}).cacheable(),
/**
Return the nearest ancestor that has the property
`content`.
@returns Ember.View
*/
contentView: Ember.computed(function() {
return this.nearestWithProperty('content');
}).cacheable(),
/**
@private
When the parent view changes, recursively invalidate
collectionView, itemView, and contentView
*/
_parentViewDidChange: Ember.observer(function() {
if (this.isDestroying) { return; }
this.invokeRecursively(function(view) {
view.propertyDidChange('collectionView');
view.propertyDidChange('itemView');
view.propertyDidChange('contentView');
});
if (getPath(this, 'parentView.controller') && !get(this, 'controller')) {
this.notifyPropertyChange('controller');
}
}, '_parentView'),
_controllerDidChange: Ember.observer(function() {
if (this.isDestroying) { return; }
this.forEachChildView(function(view) {
view.propertyDidChange('controller');
});
}, 'controller'),
cloneKeywords: function() {
var templateData = get(this, 'templateData');
var keywords = templateData ? Ember.copy(templateData.keywords) : {};
keywords.view = get(this, 'concreteView');
keywords.controller = get(this, 'controller');
return keywords;
},
/**
Called on your view when it should push strings of HTML into a
Ember.RenderBuffer. Most users will want to override the `template`
or `templateName` properties instead of this method.
By default, Ember.View will look for a function in the `template`
property and invoke it with the value of `context`. The value of
`context` will be the view's controller unless you override it.
@param {Ember.RenderBuffer} buffer The render buffer
*/
render: function(buffer) {
// If this view has a layout, it is the responsibility of the
// the layout to render the view's template. Otherwise, render the template
// directly.
var template = get(this, 'layout') || get(this, 'template');
if (template) {
var context = get(this, '_context');
var keywords = this.cloneKeywords();
var data = {
view: this,
buffer: buffer,
isRenderData: true,
keywords: keywords
};
// Invoke the template with the provided template context, which
// is the view by default. A hash of data is also passed that provides
// the template with access to the view and render buffer.
Ember.assert('template must be a function. Did you mean to call Ember.Handlebars.compile("...") or specify templateName instead?', typeof template === 'function');
// The template should write directly to the render buffer instead
// of returning a string.
var output = template(context, { data: data });
// If the template returned a string instead of writing to the buffer,
// push the string onto the buffer.
if (output !== undefined) { buffer.push(output); }
}
},
invokeForState: function(name) {
var stateName = this.state, args, fn;
// try to find the function for the state in the cache
if (fn = invokeForState[stateName][name]) {
args = a_slice.call(arguments);
args[0] = this;
return fn.apply(this, args);
}
// otherwise, find and cache the function for this state
var parent = this, states = parent.states, state;
while (states) {
state = states[stateName];
while (state) {
fn = state[name];
if (fn) {
invokeForState[stateName][name] = fn;
args = a_slice.call(arguments, 1);
args.unshift(this);
return fn.apply(this, args);
}
state = state.parentState;
}
states = states.parent;
}
},
/**
Renders the view again. This will work regardless of whether the
view is already in the DOM or not. If the view is in the DOM, the
rendering process will be deferred to give bindings a chance
to synchronize.
If children were added during the rendering process using `appendChild`,
`rerender` will remove them, because they will be added again
if needed by the next `render`.
In general, if the display of your view changes, you should modify
the DOM element directly instead of manually calling `rerender`, which can
be slow.
*/
rerender: function() {
return this.invokeForState('rerender');
},
clearRenderedChildren: function() {
var lengthBefore = this.lengthBeforeRender,
lengthAfter = this.lengthAfterRender;
// If there were child views created during the last call to render(),
// remove them under the assumption that they will be re-created when
// we re-render.
// VIEW-TODO: Unit test this path.
var childViews = get(this, '_childViews');
for (var i=lengthAfter-1; i>=lengthBefore; i--) {
if (childViews[i]) { childViews[i].destroy(); }
}
},
/**
@private
Iterates over the view's `classNameBindings` array, inserts the value
of the specified property into the `classNames` array, then creates an
observer to update the view's element if the bound property ever changes
in the future.
*/
_applyClassNameBindings: function() {
var classBindings = get(this, 'classNameBindings'),
classNames = get(this, 'classNames'),
elem, newClass, dasherizedClass;
if (!classBindings) { return; }
// Loop through all of the configured bindings. These will be either
// property names ('isUrgent') or property paths relative to the view
// ('content.isUrgent')
a_forEach(classBindings, function(binding) {
// Variable in which the old class value is saved. The observer function
// closes over this variable, so it knows which string to remove when
// the property changes.
var oldClass, property;
// Set up an observer on the context. If the property changes, toggle the
// class name.
var observer = function() {
// Get the current value of the property
newClass = this._classStringForProperty(binding);
elem = this.$();
// If we had previously added a class to the element, remove it.
if (oldClass) {
elem.removeClass(oldClass);
// Also remove from classNames so that if the view gets rerendered,
// the class doesn't get added back to the DOM.
classNames.removeObject(oldClass);
}
// If necessary, add a new class. Make sure we keep track of it so
// it can be removed in the future.
if (newClass) {
elem.addClass(newClass);
oldClass = newClass;
} else {
oldClass = null;
}
};
// Get the class name for the property at its current value
dasherizedClass = this._classStringForProperty(binding);
if (dasherizedClass) {
// Ensure that it gets into the classNames array
// so it is displayed when we render.
classNames.push(dasherizedClass);
// Save a reference to the class name so we can remove it
// if the observer fires. Remember that this variable has
// been closed over by the observer.
oldClass = dasherizedClass;
}
// Extract just the property name from bindings like 'foo:bar'
property = binding.split(':')[0];
addObserver(this, property, observer);
}, this);
},
/**
Iterates through the view's attribute bindings, sets up observers for each,
then applies the current value of the attributes to the passed render buffer.
@param {Ember.RenderBuffer} buffer
*/
_applyAttributeBindings: function(buffer) {
var attributeBindings = get(this, 'attributeBindings'),
attributeValue, elem, type;
if (!attributeBindings) { return; }
a_forEach(attributeBindings, function(binding) {
var split = binding.split(':'),
property = split[0],
attributeName = split[1] || property;
// Create an observer to add/remove/change the attribute if the
// JavaScript property changes.
var observer = function() {
elem = this.$();
attributeValue = get(this, property);
Ember.View.applyAttributeBindings(elem, attributeName, attributeValue);
};
addObserver(this, property, observer);
// Determine the current value and add it to the render buffer
// if necessary.
attributeValue = get(this, property);
Ember.View.applyAttributeBindings(buffer, attributeName, attributeValue);
}, this);
},