-
Notifications
You must be signed in to change notification settings - Fork 26
/
lib.coffee
1158 lines (912 loc) · 46.7 KB
/
lib.coffee
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
# TODO: Deduplicate between blaze component and common component packages.
createMatcher = (propertyOrMatcherOrFunction, checkMixins) ->
if _.isString propertyOrMatcherOrFunction
property = propertyOrMatcherOrFunction
propertyOrMatcherOrFunction = (child, parent) =>
# If child is parent, we might get into an infinite loop if this is
# called from getFirstWith, so in that case we do not use getFirstWith.
if checkMixins and child isnt parent and child.getFirstWith
!!child.getFirstWith null, property
else
property of child
else if not _.isFunction propertyOrMatcherOrFunction
assert _.isObject propertyOrMatcherOrFunction
matcher = propertyOrMatcherOrFunction
propertyOrMatcherOrFunction = (child, parent) =>
for property, value of matcher
# If child is parent, we might get into an infinite loop if this is
# called from getFirstWith, so in that case we do not use getFirstWith.
if checkMixins and child isnt parent and child.getFirstWith
childWithProperty = child.getFirstWith null, property
else
childWithProperty = child if property of child
return false unless childWithProperty
if _.isFunction childWithProperty[property]
return false unless childWithProperty[property]() is value
else
return false unless childWithProperty[property] is value
true
propertyOrMatcherOrFunction
getTemplateInstance = (view, skipBlockHelpers) ->
while view and not view._templateInstance
if skipBlockHelpers
view = view.parentView
else
view = view.originalParentView or view.parentView
view?._templateInstance
# More or less the same as aldeed:template-extension's template.get('component') just specialized.
# It allows us to not have a dependency on template-extension package and that we can work with Iron
# Router which has its own DynamicTemplate class which is not patched by template-extension and thus
# does not have .get() method.
templateInstanceToComponent = (templateInstanceFunc, skipBlockHelpers) ->
templateInstance = templateInstanceFunc?()
# Iron Router uses its own DynamicTemplate which is not a proper template instance, but it is
# passed in as such, so we want to find the real one before we start searching for the component.
templateInstance = getTemplateInstance templateInstance?.view, skipBlockHelpers
while templateInstance
return templateInstance.component if 'component' of templateInstance
if skipBlockHelpers
templateInstance = getTemplateInstance templateInstance.view.parentView, skipBlockHelpers
else
templateInstance = getTemplateInstance (templateInstance.view.originalParentView or templateInstance.view.parentView), skipBlockHelpers
null
getTemplateInstanceFunction = (view, skipBlockHelpers) ->
templateInstance = getTemplateInstance view, skipBlockHelpers
->
templateInstance
class ComponentsNamespaceReference
constructor: (@namespace, @templateInstance) ->
# We extend the original dot operator to support {{> Foo.Bar}}. This goes through a getTemplateHelper path, but
# we want to redirect it to the getTemplate path. So we mark it in getTemplateHelper and then here call getTemplate.
originalDot = Spacebars.dot
Spacebars.dot = (value, args...) ->
if value instanceof ComponentsNamespaceReference
return Blaze._getTemplate "#{value.namespace}.#{args.join '.'}", value.templateInstance
originalDot value, args...
originalInclude = Spacebars.include
Spacebars.include = (templateOrFunction, args...) ->
# If ComponentsNamespaceReference gets all the way to the Spacebars.include it means that we are in the situation
# where there is both namespace and component with the same name, and user is including a component. But namespace
# reference is created instead (because we do not know in advance that there is no Spacebars.dot call around lookup
# call). So we dereference the reference and try to resolve a template. Of course, a component might not really exist.
if templateOrFunction instanceof ComponentsNamespaceReference
templateOrFunction = Blaze._getTemplate templateOrFunction.namespace, templateOrFunction.templateInstance
originalInclude templateOrFunction, args...
# We override the original lookup method with a similar one, which supports components as well.
#
# Now the order of the lookup will be, in order:
# a helper of the current template
# a property of the current component (not the BlazeComponent.currentComponent() though, but @component())
# a helper of the current component's base template (not the BlazeComponent.currentComponent() though, but @component())
# the name of a component
# the name of a template
# global helper
# a property of the data context
#
# Returns a function, a non-function value, or null. If a function is found, it is bound appropriately.
#
# NOTE: This function must not establish any reactive dependencies itself. If there is any reactivity
# in the value, lookup should return a function.
#
# TODO: Should we also lookup for a property of the component-level data context (and template-level data context)?
Blaze._getTemplateHelper = (template, name, templateInstance) ->
isKnownOldStyleHelper = false
if template.__helpers.has name
helper = template.__helpers.get name
if helper is Blaze._OLDSTYLE_HELPER
isKnownOldStyleHelper = true
else if helper?
return wrapHelper bindDataContext(helper), templateInstance
else
return null
# Old-style helper.
if name of template
# Only warn once per helper.
unless isKnownOldStyleHelper
template.__helpers.set name, Blaze._OLDSTYLE_HELPER
unless template._NOWARN_OLDSTYLE_HELPERS
Blaze._warn "Assigning helper with `" + template.viewName + "." + name + " = ...` is deprecated. Use `" + template.viewName + ".helpers(...)` instead."
if template[name]?
return wrapHelper bindDataContext(template[name]), templateInstance
else
return null
return null unless templateInstance
# Do not resolve component helpers if inside Template.dynamic. The reason is that Template.dynamic uses a data context
# value with name "template" internally. But when used inside a component the data context lookup is then resolved
# into a current component's template method and not the data context "template". To force the data context resolving
# Template.dynamic should use "this.template" in its templates, but it does not, so we have a special case here for it.
return null if template.viewName in ['Template.__dynamicWithDataContext', 'Template.__dynamic']
# Blaze.View::lookup should not introduce any reactive dependencies, but we can simply ignore reactivity here because
# template instance probably cannot change without reconstructing the component as well.
component = Tracker.nonreactive ->
# We want to skip any block helper. {{method}} should resolve to
# {{component.method}} and not to {{currentComponent.method}}.
templateInstanceToComponent templateInstance, true
# Component.
if component
# This will first search on the component and then continue with mixins.
if mixinOrComponent = component.getFirstWith null, name
return wrapHelper bindComponent(mixinOrComponent, mixinOrComponent[name]), templateInstance
# A special case to support {{> Foo.Bar}}. This goes through a getTemplateHelper path, but we want to redirect
# it to the getTemplate path. So we mark it and leave to Spacebars.dot to call getTemplate.
# TODO: We should provide a BaseComponent.getComponentsNamespace method instead of accessing components directly.
if name and name of BlazeComponent.components
return new ComponentsNamespaceReference name, templateInstance
# Maybe a preexisting template helper on the component's base template.
if component
# We know that component is really a component.
if (helper = component._componentInternals?.templateBase?.__helpers.get name)?
return wrapHelper bindDataContext(helper), templateInstance
null
share.inExpandAttributes = false
bindComponent = (component, helper) ->
if _.isFunction helper
(args...) ->
result = helper.apply component, args
# If we are expanding attributes and this is an object with dynamic attributes,
# then we want to bind all possible event handlers to the component as well.
if share.inExpandAttributes and _.isObject result
for name, value of result when share.EVENT_HANDLER_REGEX.test name
if _.isFunction value
result[name] = _.bind value, component
else if _.isArray value
result[name] = _.map value, (fun) ->
if _.isFunction fun
_.bind fun, component
else
fun
result
else
helper
bindDataContext = (helper) ->
if _.isFunction helper
->
data = Blaze.getData()
data ?= {}
helper.apply data, arguments
else
helper
wrapHelper = (f, templateFunc) ->
# XXX COMPAT WITH METEOR 1.0.3.2
return Blaze._wrapCatchingExceptions f, 'template helper' unless Blaze.Template._withTemplateInstanceFunc
return f unless _.isFunction f
->
self = @
args = arguments
Blaze.Template._withTemplateInstanceFunc templateFunc, ->
Blaze._wrapCatchingExceptions(f, 'template helper').apply self, args
if Blaze.Template._withTemplateInstanceFunc
withTemplateInstanceFunc = Blaze.Template._withTemplateInstanceFunc
else
# XXX COMPAT WITH METEOR 1.0.3.2.
withTemplateInstanceFunc = (templateInstance, f) ->
f()
getTemplateBase = (component) ->
# We do not allow template to be a reactive method.
Tracker.nonreactive ->
componentTemplate = component.template()
if _.isString componentTemplate
templateBase = Template[componentTemplate]
throw new Error "Template '#{componentTemplate}' cannot be found." unless templateBase
else if componentTemplate
templateBase = componentTemplate
else
throw new Error "Template for the component '#{component.componentName() or 'unnamed'}' not provided."
templateBase
callTemplateBaseHooks = (component, hookName) ->
# We want to call template base hooks only when we are calling this function on a component itself.
return unless component is component.component()
templateInstance = Tracker.nonreactive ->
component._componentInternals.templateInstance()
callbacks = component._componentInternals.templateBase._getCallbacks hookName
Template._withTemplateInstanceFunc(
->
templateInstance
,
->
for callback in callbacks
callback.call templateInstance
)
return
wrapViewAndTemplate = (currentView, f) ->
# For template content wrapped inside the block helper, we want to skip the block
# helper when searching for corresponding template. This means that Template.instance()
# will return the component's template, while BlazeComponent.currentComponent() will
# return the component inside.
templateInstance = getTemplateInstanceFunction currentView, true
# We set template instance to match the current view (mostly, only not when inside
# the block helper). The latter we use for BlazeComponent.currentComponent(), but
# it is good that both template instance and current view correspond to each other
# as much as possible.
withTemplateInstanceFunc templateInstance, ->
# We set view based on the current view so that inside event handlers
# BlazeComponent.currentData() (and Blaze.getData() and Template.currentData())
# returns data context of event target and not component/template. Moreover,
# inside event handlers BlazeComponent.currentComponent() returns the component
# of event target.
Blaze._withCurrentView currentView, ->
f()
addEvents = (view, component) ->
eventsList = component.events()
throw new Error "'events' method from the component '#{component.componentName() or 'unnamed'}' did not return a list of event maps." unless _.isArray eventsList
for events in eventsList
eventMap = {}
for spec, handler of events
do (spec, handler) ->
eventMap[spec] = (args...) ->
event = args[0]
currentView = Blaze.getView event.currentTarget
wrapViewAndTemplate currentView, ->
handler.apply component, args
# Make sure CoffeeScript does not return anything.
# Returning from event handlers is deprecated.
return
Blaze._addEventMap view, eventMap, view
return
originalGetTemplate = Blaze._getTemplate
Blaze._getTemplate = (name, templateInstance) ->
# Blaze.View::lookup should not introduce any reactive dependencies, so we are making sure it is so.
template = Tracker.nonreactive ->
if Blaze.currentView
parentComponent = BlazeComponent.currentComponent()
else
# We do not skip block helpers to assure that when block helpers are used,
# component tree integrates them nicely into a tree.
parentComponent = templateInstanceToComponent templateInstance, false
BlazeComponent.getComponent(name)?.renderComponent parentComponent
return template if template and (template instanceof Blaze.Template or _.isFunction template)
originalGetTemplate name
registerHooks = (template, hooks) ->
if template.onCreated
template.onCreated hooks.onCreated
template.onRendered hooks.onRendered
template.onDestroyed hooks.onDestroyed
else
# XXX COMPAT WITH METEOR 1.0.3.2.
template.created = hooks.onCreated
template.rendered = hooks.onRendered
template.destroyed = hooks.onDestroyed
registerFirstCreatedHook = (template, onCreated) ->
if template._callbacks
template._callbacks.created.unshift onCreated
else
# XXX COMPAT WITH METEOR 1.0.3.2.
oldCreated = template.created
template.created = ->
onCreated.call @
oldCreated?.call @
# We make Template.dynamic resolve to the component if component name is specified as a template name, and not
# to the non-component template which is probably used only for the content. We simply reuse Blaze._getTemplate.
# TODO: How to pass args?
# Maybe simply by using Spacebars nested expressions (https://github.com/meteor/meteor/pull/4101)?
# Template.dynamic template="..." data=(args ...)? But this exposes the fact that args are passed as data context.
# Maybe we should simply override Template.dynamic and add "args" argument?
# TODO: This can be removed once https://github.com/meteor/meteor/pull/4036 is merged in.
Template.__dynamicWithDataContext.__helpers.set 'chooseTemplate', (name) ->
Blaze._getTemplate name, =>
Template.instance()
argumentsConstructor = ->
# This class should never really be created.
assert false
# TODO: Find a way to pass arguments to the component without having to introduce one intermediary data context into the data context hierarchy.
# (In fact two data contexts, because we add one more when restoring the original one.)
Template.registerHelper 'args', ->
obj = {}
# We use custom constructor to know that it is not a real data context.
obj.constructor = argumentsConstructor
obj._arguments = arguments
obj
share.EVENT_HANDLER_REGEX = /^on[A-Z]/
share.isEventHandler = (fun) ->
_.isFunction(fun) and fun.eventHandler
# When event handlers are provided directly as args they are not passed through
# Spacebars.event by the template compiler, so we have to do it ourselves.
originalFlattenAttributes = HTML.flattenAttributes
HTML.flattenAttributes = (attrs) ->
if attrs = originalFlattenAttributes attrs
for name, value of attrs when share.EVENT_HANDLER_REGEX.test name
# Already processed by Spacebars.event.
continue if share.isEventHandler value
continue if _.isArray(value) and _.some value, share.isEventHandler
# When event handlers are provided directly as args,
# we require them to be just event handlers.
if _.isArray value
attrs[name] = _.map value, Spacebars.event
else
attrs[name] = Spacebars.event value
attrs
Spacebars.event = (eventHandler, args...) ->
throw new Error "Event handler not a function: #{eventHandler}" unless _.isFunction eventHandler
# Execute all arguments.
args = Spacebars.mustacheImpl ((xs...) -> xs), args...
fun = (event, eventArgs...) ->
currentView = Blaze.getView event.currentTarget
wrapViewAndTemplate currentView, ->
# We do not have to bind "this" because event handlers are resolved
# as template helpers and are already bound. We bind event handlers
# in dynamic attributes already as well.
eventHandler.apply null, [event].concat args, eventArgs
fun.eventHandler = true
fun
# When converting the component to the static HTML, remove all event handlers.
originalVisitTag = HTML.ToHTMLVisitor::visitTag
HTML.ToHTMLVisitor::visitTag = (tag) ->
if attrs = tag.attrs
attrs = HTML.flattenAttributes attrs
for name of attrs when share.EVENT_HANDLER_REGEX.test name
delete attrs[name]
tag.attrs = attrs
originalVisitTag.call @, tag
currentViewIfRendering = ->
view = Blaze.currentView
if view?._isInRender
view
else
null
contentAsFunc = (content) ->
# We do not check content for validity.
if !_.isFunction content
return ->
content
content
contentAsView = (content) ->
# We do not check content for validity.
if content instanceof Blaze.Template
content.constructView()
else if content instanceof Blaze.View
content
else
Blaze.View 'render', contentAsFunc content
HTMLJSExpander = Blaze._HTMLJSExpander.extend()
HTMLJSExpander.def
# Based on Blaze._HTMLJSExpander, but calls our expandView.
visitObject: (x) ->
if x instanceof Blaze.Template
x = x.constructView()
if x instanceof Blaze.View
return expandView x, @parentView
HTML.TransformingVisitor.prototype.visitObject.call @, x
# Based on Blaze._expand, but uses our HTMLJSExpander.
expand = (htmljs, parentView) ->
parentView = parentView or currentViewIfRendering()
(new HTMLJSExpander parentView: parentView).visit htmljs
# Based on Blaze._expandView, but with flushing.
expandView = (view, parentView) ->
Blaze._createView view, parentView, true
view._isInRender = true
htmljs = Blaze._withCurrentView view, ->
view._render()
view._isInRender = false
Tracker.flush()
result = expand htmljs, view
Tracker.flush()
if Tracker.active
Tracker.onInvalidate ->
Blaze._destroyView view
else
Blaze._destroyView view
Tracker.flush()
result
class BlazeComponent extends BaseComponent
# TODO: Figure out how to do at the BaseComponent level?
@getComponentForElement: (domElement) ->
return null unless domElement
# This uses the same check if the argument is a DOM element that Blaze._DOMRange.forElement does.
throw new Error "Expected DOM element." unless domElement.nodeType is Node.ELEMENT_NODE
# For DOM elements we want to return the component which matches the template
# with that DOM element and not the component closest in the component tree.
# So we skip the block helpers. (If DOM element is rendered by the block helper
# this will find that block helper template/component.)
templateInstance = getTemplateInstanceFunction Blaze.getView(domElement), true
templateInstanceToComponent templateInstance, true
childComponents: (nameOrComponent) ->
if (component = @component()) isnt @
component.childComponents nameOrComponent
else
super arguments...
# A version of childComponentsWith which knows about mixins.
# When checking for properties it checks mixins as well.
childComponentsWith: (propertyOrMatcherOrFunction) ->
if (component = @component()) isnt @
component.childComponentsWith propertyOrMatcherOrFunction
else
assert propertyOrMatcherOrFunction
propertyOrMatcherOrFunction = createMatcher propertyOrMatcherOrFunction, true
super propertyOrMatcherOrFunction
parentComponent: (parentComponent) ->
if (component = @component()) isnt @
component.parentComponent parentComponent
else
super arguments...
addChildComponent: (childComponent) ->
if (component = @component()) isnt @
component.addChildComponent childComponent
else
super arguments...
removeChildComponent: (childComponent) ->
if (component = @component()) isnt @
component.removeChildComponent childComponent
else
super arguments...
mixins: ->
[]
# When a component is used as a mixin, createMixins will call this method to set the parent
# component using this mixin. Extend this method if you want to do any action when parent is
# set, for example, add dependency mixins to the parent. Make sure you call super as well.
mixinParent: (mixinParent) ->
@_componentInternals ?= {}
# Setter.
if mixinParent
@_componentInternals.mixinParent = mixinParent
# To allow chaining.
return @
# Getter.
@_componentInternals.mixinParent or null
requireMixin: (nameOrMixin) ->
assert @_componentInternals?.mixins
Tracker.nonreactive =>
# Do not do anything if mixin is already required. This allows multiple mixins to call requireMixin
# in mixinParent method to add dependencies, but if dependencies are already there, nothing happens.
return if @getMixin nameOrMixin
if _.isString nameOrMixin
# It could be that the component is not a real instance of the BlazeComponent class,
# so it might not have a constructor pointing back to a BlazeComponent subclass.
if @constructor.getComponent
mixinInstanceComponent = @constructor.getComponent nameOrMixin
else
mixinInstanceComponent = BlazeComponent.getComponent nameOrMixin
throw new Error "Unknown mixin '#{nameOrMixin}'." unless mixinInstanceComponent
mixinInstance = new mixinInstanceComponent()
else if _.isFunction nameOrMixin
mixinInstance = new nameOrMixin()
else
mixinInstance = nameOrMixin
# We add mixin before we call mixinParent so that dependencies come after this mixin,
# and that we prevent possible infinite loops because of circular dependencies.
# TODO: For now we do not provide an official API to add dependencies before the mixin itself.
@_componentInternals.mixins.push mixinInstance
# We allow mixins to not be components, so methods are not necessary available.
# Set mixin parent.
if mixinInstance.mixinParent
mixinInstance.mixinParent @
# Maybe mixin has its own mixins as well.
mixinInstance.createMixins?()
if component = @component()
component._componentInternals ?= {}
component._componentInternals.templateInstance ?= new ReactiveField null, (a, b) -> a is b
# If a mixin is adding a dependency using requireMixin after its mixinParent class (for example, in onCreate)
# and this is this dependency mixin, the view might already be created or rendered and callbacks were
# already called, so we should call them manually here as well. But only if he view has not been destroyed
# already. For those mixins we do not call anything, there is little use for them now.
unless component._componentInternals.templateInstance()?.view.isDestroyed
mixinInstance.onCreated?() if not component._componentInternals.inOnCreated and component._componentInternals.templateInstance()?.view.isCreated
mixinInstance.onRendered?() if not component._componentInternals.inOnRendered and component._componentInternals.templateInstance()?.view.isRendered
# To allow chaining.
@
# Method to instantiate all mixins.
createMixins: ->
@_componentInternals ?= {}
# To allow calling it multiple times, but non-first calls are simply ignored.
return if @_componentInternals.mixins
@_componentInternals.mixins = []
for mixin in @mixins()
@requireMixin mixin
# To allow chaining.
@
getMixin: (nameOrMixin) ->
if _.isString nameOrMixin
# By passing @ as the first argument, we traverse only mixins.
@getFirstWith @, (child, parent) =>
# We do not require mixins to be components, but if they are, they can
# be referenced based on their component name.
mixinComponentName = child.componentName?() or null
return mixinComponentName and mixinComponentName is nameOrMixin
else
# By passing @ as the first argument, we traverse only mixins.
@getFirstWith @, (child, parent) =>
# nameOrMixin is a class.
return true if child.constructor is nameOrMixin
# nameOrMixin is an instance, or something else.
return true if child is nameOrMixin
false
# Calls the component (if afterComponentOrMixin is null) or the first next mixin
# after afterComponentOrMixin it finds, and returns the result.
callFirstWith: (afterComponentOrMixin, propertyName, args...) ->
assert _.isString propertyName
componentOrMixin = @getFirstWith afterComponentOrMixin, propertyName
# TODO: Should we throw an error here? Something like calling a function which does not exist?
return unless componentOrMixin
# We are not calling callFirstWith on the componentOrMixin because here we
# are already traversing mixins so we do not recurse once more.
if _.isFunction componentOrMixin[propertyName]
return componentOrMixin[propertyName] args...
else
return componentOrMixin[propertyName]
getFirstWith: (afterComponentOrMixin, propertyOrMatcherOrFunction) ->
assert @_componentInternals?.mixins
assert propertyOrMatcherOrFunction
# Here we are already traversing mixins so we do not recurse once more.
propertyOrMatcherOrFunction = createMatcher propertyOrMatcherOrFunction, false
# If afterComponentOrMixin is not provided, we start with the component.
if not afterComponentOrMixin
return @ if propertyOrMatcherOrFunction.call @, @, @
# And continue with mixins.
found = true
# If afterComponentOrMixin is the component, we start with mixins.
else if afterComponentOrMixin and afterComponentOrMixin is @
found = true
else
found = false
# TODO: Implement with a map between mixin -> position, so that we do not have to seek to find a mixin.
for mixin in @_componentInternals.mixins
return mixin if found and propertyOrMatcherOrFunction.call @, mixin, @
found = true if mixin is afterComponentOrMixin
null
# This class method more or less just creates an instance of a component and calls its renderComponent
# method. But because we want to allow passing arguments to the component in templates, we have some
# complicated code around to extract and pass those arguments. It is similar to how data context is
# passed to block helpers. In a data context visible only to the block helper template.
# TODO: This could be made less hacky. See https://github.com/meteor/meteor/issues/3913
@renderComponent: (parentComponent) ->
Tracker.nonreactive =>
componentClass = @
if Blaze.currentView
# We check data context in a non-reactive way, because we want just to peek into it
# and determine if data context contains component arguments or not. And while
# component arguments might change through time, the fact that they are there at
# all or not ("args" template helper was used or not) does not change through time.
# So we can check that non-reactively.
data = Template.currentData()
else
# There is no current view when there is no data context yet, thus also no arguments
# were provided through "args" template helper, so we just continue normally.
data = null
if data?.constructor isnt argumentsConstructor
# So that currentComponent in the constructor can return the component
# inside which this component has been constructed.
return wrapViewAndTemplate Blaze.currentView, =>
component = new componentClass()
return component.renderComponent parentComponent
# Arguments were provided through "args" template helper.
# We want to reactively depend on the data context for arguments, so we return a function
# instead of a template. Function will be run inside an autorun, a reactive context.
->
assert Tracker.active
# We cannot use Template.getData() inside a normal autorun because current view is not defined inside
# a normal autorun. But we do not really have to depend reactively on the current view, only on the
# data context of a known (the closest Blaze.With) view. So we get this view by ourselves.
currentWith = Blaze.getView 'with'
# By default dataVar in the Blaze.With view uses ReactiveVar with default equality function which
# sees all objects as different. So invalidations are triggered for every data context assignments
# even if data has not really changed. This is why wrap it into a ComputedField with EJSON.equals.
# Because it uses EJSON.equals it will invalidate our function only if really changes.
# See https://github.com/meteor/meteor/issues/4073
reactiveArguments = new ComputedField ->
data = currentWith.dataVar.get()
assert.equal data?.constructor, argumentsConstructor
data._arguments
,
EJSON.equals
# Here we register a reactive dependency on the ComputedField.
nonreactiveArguments = reactiveArguments()
Tracker.nonreactive ->
# Arguments were passed in as a data context. We want currentData in the constructor to return the
# original (parent) data context. Like we were not passing in arguments as a data context.
template = Blaze._withCurrentView Blaze.currentView.parentView.parentView, =>
# So that currentComponent in the constructor can return the component
# inside which this component has been constructed.
return wrapViewAndTemplate Blaze.currentView, =>
# Use arguments for the constructor.
component = new componentClass nonreactiveArguments...
return component.renderComponent parentComponent
# It has to be the first callback so that other have a correct data context.
registerFirstCreatedHook template, ->
# Arguments were passed in as a data context. Restore original (parent) data
# context. Same logic as in Blaze._InOuterTemplateScope.
@view.originalParentView = @view.parentView
@view.parentView = @view.parentView.parentView.parentView
template
renderComponent: (parentComponent) ->
# To make sure we do not introduce any reactive dependency. This is a conscious design decision.
# Reactivity should be changing data context, but components should be more stable, only changing
# when structure change in rendered DOM. You can change the component you are including (or pass
# different arguments) reactively though.
Tracker.nonreactive =>
component = @component()
# If mixins have not yet been created.
component.createMixins()
templateBase = getTemplateBase component
# Create a new component template based on the Blaze template. We want our own template
# because the same Blaze template could be reused between multiple components.
# TODO: Should we cache these templates based on (componentName, templateBase) pair? We could use two levels of ES2015 Maps, componentName -> templateBase -> template. What about component arguments changing?
template = new Blaze.Template "BlazeComponent.#{component.componentName() or 'unnamed'}", templateBase.renderFunction
# We lookup preexisting template helpers in Blaze._getTemplateHelper, if the component does not have
# a property with the same name. Preexisting event handlers and life-cycle hooks are taken care of
# in the related methods in the base class.
component._componentInternals ?= {}
component._componentInternals.templateBase = templateBase
registerHooks template,
onCreated: ->
# @ is a template instance.
if parentComponent
# component.parentComponent is reactive, so we use Tracker.nonreactive just to make sure we do not leak any reactivity here.
Tracker.nonreactive =>
# TODO: Should we support that the same component can be rendered multiple times in parallel? How could we do that? For different component parents or only the same one?
assert not component.parentComponent(), "Component '#{component.componentName() or 'unnamed'}' parent component '#{component.parentComponent()?.componentName() or 'unnamed'}' already set."
# We set the parent only when the component is created, not just constructed.
component.parentComponent parentComponent
parentComponent.addChildComponent component
@view._onViewRendered =>
# Attach events the first time template instance renders.
return unless @view.renderCount is 1
# We first add event handlers from the component, then mixins.
componentOrMixin = null
while componentOrMixin = @component.getFirstWith componentOrMixin, 'events'
addEvents @view, componentOrMixin
@component = component
# TODO: Should we support that the same component can be rendered multiple times in parallel? How could we do that? For different component parents or only the same one?
assert not Tracker.nonreactive => @component._componentInternals.templateInstance?()
@component._componentInternals.templateInstance ?= new ReactiveField @, (a, b) -> a is b
@component._componentInternals.templateInstance @
@component._componentInternals.isCreated ?= new ReactiveField true
@component._componentInternals.isCreated true
# Maybe we are re-rendering the component. So let's initialize variables just to be sure.
@component._componentInternals.isRendered ?= new ReactiveField false
@component._componentInternals.isRendered false
@component._componentInternals.isDestroyed ?= new ReactiveField false
@component._componentInternals.isDestroyed false
try
# We have to know if we should call onCreated on the mixin inside the requireMixin or not. We want to call
# it only once. If it requireMixin is called from onCreated of another mixin, then it will be added at the
# end and we will get it here at the end. So we should not call onCreated inside requireMixin because then
# onCreated would be called twice.
@component._componentInternals.inOnCreated = true
componentOrMixin = null
while componentOrMixin = @component.getFirstWith componentOrMixin, 'onCreated'
componentOrMixin.onCreated()
finally
delete @component._componentInternals.inOnCreated
onRendered: ->
# @ is a template instance.
@component._componentInternals.isRendered ?= new ReactiveField true
@component._componentInternals.isRendered true
Tracker.nonreactive =>
assert.equal @component._componentInternals.isCreated(), true
try
# Same as for onCreated above.
@component._componentInternals.inOnRendered = true
componentOrMixin = null
while componentOrMixin = @component.getFirstWith componentOrMixin, 'onRendered'
componentOrMixin.onRendered()
finally
delete @component._componentInternals.inOnRendered
onDestroyed: ->
@autorun (computation) =>
# @ is a template instance.
# We wait for all children components to be destroyed first.
# See https://github.com/meteor/meteor/issues/4166
return if @component.childComponents().length
computation.stop()
Tracker.nonreactive =>
assert.equal @component._componentInternals.isCreated(), true
@component._componentInternals.isCreated false
@component._componentInternals.isRendered ?= new ReactiveField false
@component._componentInternals.isRendered false
@component._componentInternals.isDestroyed ?= new ReactiveField true
@component._componentInternals.isDestroyed true
componentOrMixin = null
while componentOrMixin = @component.getFirstWith componentOrMixin, 'onDestroyed'
componentOrMixin.onDestroyed()
if parentComponent
# The component has been destroyed, clear up the parent.
component.parentComponent null
parentComponent.removeChildComponent component
# Remove the reference so that it is clear that template instance is not available anymore.
@component._componentInternals.templateInstance null
template
removeComponent: ->
Blaze.remove @component()._componentInternals.templateInstance().view if @isRendered()
@_renderComponentTo: (visitor, parentComponent, parentView, data) ->
component = Tracker.nonreactive =>
componentClass = @
parentView = parentView or currentViewIfRendering() or (parentComponent?.isRendered() and parentComponent._componentInternals.templateInstance().view) or null
wrapViewAndTemplate parentView, =>
new componentClass()
if arguments.length > 2
component._renderComponentTo visitor, parentComponent, parentView, data
else
component._renderComponentTo visitor, parentComponent, parentView
@renderComponentToHTML: (parentComponent, parentView, data) ->
if arguments.length > 2
@_renderComponentTo new HTML.ToHTMLVisitor(), parentComponent, parentView, data
else
@_renderComponentTo new HTML.ToHTMLVisitor(), parentComponent, parentView
_renderComponentTo: (visitor, parentComponent, parentView, data) ->
template = Tracker.nonreactive =>
parentView = parentView or currentViewIfRendering() or (parentComponent?.isRendered() and parentComponent._componentInternals.templateInstance().view) or null
wrapViewAndTemplate parentView, =>
@component().renderComponent parentComponent
if arguments.length > 2
expandedView = expandView Blaze._TemplateWith(data, contentAsFunc template), parentView
else
expandedView = expandView contentAsView(template), parentView
visitor.visit expandedView
renderComponentToHTML: (parentComponent, parentView, data) ->
if arguments.length > 2
@_renderComponentTo new HTML.ToHTMLVisitor(), parentComponent, parentView, data
else
@_renderComponentTo new HTML.ToHTMLVisitor(), parentComponent, parentView
template: ->
@callFirstWith(@, 'template') or @constructor.componentName()
onCreated: ->
callTemplateBaseHooks @, 'created'
onRendered: ->
callTemplateBaseHooks @, 'rendered'
onDestroyed: ->
callTemplateBaseHooks @, 'destroyed'
isCreated: ->
component = @component()
component._componentInternals ?= {}
component._componentInternals.isCreated ?= new ReactiveField false
component._componentInternals.isCreated()
isRendered: ->
component = @component()
component._componentInternals ?= {}
component._componentInternals.isRendered ?= new ReactiveField false
component._componentInternals.isRendered()
isDestroyed: ->
component = @component()
component._componentInternals ?= {}
component._componentInternals.isDestroyed ?= new ReactiveField false
component._componentInternals.isDestroyed()
insertDOMElement: (parent, node, before) ->
before ?= null
if parent and node and (node.parentNode isnt parent or node.nextSibling isnt before)
parent.insertBefore node, before
return
moveDOMElement: (parent, node, before) ->
before ?= null
if parent and node and (node.parentNode isnt parent or node.nextSibling isnt before)
parent.insertBefore node, before
return
removeDOMElement: (parent, node) ->
if parent and node and node.parentNode is parent
parent.removeChild node
return
events: ->
# In mixins there is no reason for a template instance to extend a Blaze template.
return [] unless @ is @component()
@_componentInternals ?= {}
view = Tracker.nonreactive =>
@_componentInternals.templateInstance().view
# We skip block helpers to match Blaze behavior.
templateInstance = getTemplateInstanceFunction view, true
for events in @_componentInternals.templateBase.__eventMaps
eventMap = {}
for spec, handler of events
do (spec, handler) ->
eventMap[spec] = (args...) ->