forked from cordjs/core
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathWidget.coffee
More file actions
1781 lines (1467 loc) · 64.9 KB
/
Widget.coffee
File metadata and controls
1781 lines (1467 loc) · 64.9 KB
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
define [
'cord!Collection'
'cord!Context'
'cord!css/helper'
'cord!errors'
'cord!helpers/TimeoutStubHelper'
'cord!isBrowser'
'cord!Model'
'cord!Module'
'cord!StructureTemplate'
'cord!templateLoader'
'cord!Utils'
'cord!utils/DomInfo'
'cord!utils/Future'
'dustjs-helpers'
'monologue' + (if CORD_IS_BROWSER then '' else '.js')
'postal'
'underscore'
], (Collection, Context, cssHelper, errors, TimeoutStubHelper, isBrowser, Model, Module, StructureTemplate,
templateLoader, Utils, DomInfo, Future,
dust, Monologue, postal, _) ->
dust.onLoad = (tmplPath, callback) ->
templateLoader.loadTemplate tmplPath, ->
callback null, ''
class Widget extends Module
@include Monologue.prototype
# widget repository
widgetRepo = null
# service container
container = null
# widget context
ctx: null
# child widgets
children: null
childByName: null
childById: null
behaviourClass: null
behaviour: null
cssClass: null
rootTag: 'div'
# internals
_renderStarted: false
_childWidgetCounter: 0
_structTemplate: null
_isExtended: false
# should not be used directly, use getBaseContext() for lazy loading
_baseContext: null
_modelBindings: null
_placeholdersClasses: null
# promise needed to prevent setParams() to be applied while template rendering is performed
# it just holds renderTemplate() method return value
_renderPromise: null
# promise to load widget completely (with all styles and behaviours, including children)
_widgetReadyPromise: null
# indicates that browserInit was already called. Initially should be true
# and reset in certain places via _resetWidgetReady() method
_browserInitialized: true
# promise that resolves when the widget is actually shown in the DOM
_shownPromise: null
_shown: false
# temporary helper data container for inline-block processing
_inlinesRuntimeInfo: null
# list of placeholders render information including deep placeholders of immediate-included widgets
# deep infomation is need by replacePlaceholders() to include child widgets and inlines correctly
_placeholdersRenderInfo: null
# Object of subscibed push binding (by parent widget context's param name)
_subscibedPushBindings: null
# Behaviuor's event handler duplication prevention temporary map. Used in the widget's behaviuor class.
_eventCursors: null
@_initParamRules: ->
###
Prepares rules for handling incoming params of the widget.
Converts params static attribute of the class into _paramRules array which defines behaviour of setParams()
method of the widget.
###
handleStringCallback = (rule, methodName) =>
if @prototype[methodName]
callback = @prototype[methodName]
if _.isFunction callback
rule.callback = callback
else
throw new Error("Callback #{ methodName } is not a function")
else
throw new Error("#{ methodName } doesn't exist")
@_paramRules = {}
for param, info of @params
rule = {}
if _.isFunction info
rule.type = ':callback'
rule.callback = info
else if _.isString info
if info.charAt(0) == ':'
if info == ':ctx'
rule.type = ':setSame'
else if info.substr(0, 5) == ':ctx.'
rule.type = ':set'
rule.ctxName = info.trim().substr(5)
else if info == ':ignore'
rule.type = ':ignore'
else
throw "Invalid special string value for param '#{ param }': #{ info }!"
else
rule.type = ':callback'
handleStringCallback(rule, info)
else if _.isObject info
if info.callback?
rule.type = ':callback'
if _.isString info.callback
handleStringCallback(rule, info.callback)
else if _.isFunction info.callback
rule.callback = info.callback
else if info.set
rule.type = ':set'
rule.ctxName = info.set
else
rule.type = ':setSame'
splittedParams = param.trim().split(/\s*,\s*/)
rule.id = splittedParams.join ','
if splittedParams.length > 1
rule.multiArgs = true
rule.params = splittedParams
for name in splittedParams
@_paramRules[name] ?= []
@_paramRules[name].push rule
@_parseChildEvents: ->
###
Converts child widget subscriptions form @childEvents into optimized three-level map:
childName -> topic -> callbacks.
This map is used later to bind child events when children are attached to the widgets of this type.
This conversion is performed only once for the whole widget class.
###
@_childEventSubscriptions = {}
if @childEvents
for eventDef, callback of @childEvents
[childName, topic] = eventDef.split(' ')
if _.isString(callback)
if @::[callback]
callback = @::[callback]
else
throw new Error("Child event callback name '#{callback}' is not a member of #{@__name}!")
if not _.isFunction(callback)
throw new Error("Invalid child widget callback definition: #{@__name}::[#{childName}, #{topic}]!")
@_childEventSubscriptions[childName] ?= {}
@_childEventSubscriptions[childName][topic] ?= []
@_childEventSubscriptions[childName][topic].push(callback)
@childEvents = undefined
@_initCss: (restoreMode) ->
###
Start to load CSS-files immediately when the first instance of the widget is instantiated on dynamically in the
browser.
@browser-only
###
@_cssPromise =
if not restoreMode
Future.require('cord!css/browserManager').then (cssManager) =>
promises = (cssManager.load(cssFile) for cssFile in @::getCssFiles())
Future.sequence(promises)
else
Future.resolved()
getPath: ->
@constructor.path
getDir: ->
@constructor.relativeDirPath
getBundle: ->
@constructor.bundle
@_initialized: false
@_init: (restoreMode) ->
###
Initializes some class-wide propreties and actions that must be done once for the widget class.
@param Boolean restoreMode indicates that widget is re-creating on the browser after passing from the server
###
if @params? or @initialCtx? # may be initialCtx is not necessary here
@_initParamRules()
@_parseChildEvents()
@_initCss(restoreMode) if isBrowser
@_rawStructPromise = undefined
@_initialized = this
constructor: (params) ->
###
Constructor
Accepted params:
* context (Object) - inject widget's context explicitly (should re used only to restore widget's state on node-browser
transfer
* repo (WidgetRepo) - inject widget repository (should be always set except in compileMode
* compileMode (boolean) - turn on/off special compile mode of the widget (default - false)
* extended (boolean) - mark widget as part of extend tree (default - false)
* restoreMode(boolean) - hint pointing that it's a recreation of the widget while passing from server to browser
helpful to make few optimizations
@param (optional)Object params custom params, accepted by widget
###
@constructor._init(params.restoreMode) if @constructor._initialized != @constructor # detects widget inheritance
@_modelBindings = {}
@_subscibedPushBindings = {}
@_eventCursors = {}
compileMode = false
if params?
if params.context?
if params.context instanceof Context
@ctx = params.context
else
@ctx = new Context(params.context)
@ctx.setOwnerWidget(this)
@setRepo params.repo if params.repo?
@setServiceContainer params.serviceContainer if params.serviceContainer?
compileMode = params.compileMode if params.compileMode?
@_isExtended = params.extended if params.extended?
if params.modelBindings?
@_modelBindings = params.modelBindings
@_initModelsEvents() if isBrowser
@_postalSubscriptions = []
@_tmpSubscriptions = []
@_placeholdersClasses = {}
@resetChildren()
if not @ctx?
if compileMode
id = 'rwdt-' + _.uniqueId()
else
id = (if isBrowser then 'b' else 'n') + 'wdt-' + _.uniqueId()
@ctx = new Context(id, Utils.cloneLevel2(@constructor.initialCtx))
@ctx.setOwnerWidget(this)
if isBrowser
@_browserInitialized = true
@_widgetReadyPromise = Future.single(@debug('_widgetReadyPromise.resolved')).resolve()
@_shownPromise = Future.single('Widget::_shownPromise ' + @constructor.__name)
@_renderPromise = Future.resolved()
@_callbacks = []
@_promises = []
clean: ->
###
Kind of destructor.
Deletes all event-subscriptions associated with the widget and do this recursively for all child widgets.
This have to be called when performing full re-render of some part of the widget tree to avoid double
subscriptions left from the disappeared widgets.
###
@_sentenced = true
@cleanChildren()
@_cleanBehaviour()
@cleanSubscriptions()
@cleanTmpSubscriptions()
@cleanModelSubscriptions()
@_modelBindings = null
@_subscibedPushBindings = null
@clearCallbacks()
@off() #clean monologue subscriptions
@_cleanPromises()
if @_shownPromise?
@_shownPromise.clear()
@_shownPromise = null
@ctx.clearDeferredTimeouts()
if @_widgetReadyPromise
if not @_browserInitialized and not @_widgetReadyPromise.completed()
@_widgetReadyPromise.reject(new errors.WidgetDropped("Widget #{@constructor.__name} is cleaned!"))
@_widgetReadyPromise.clear()
else
@_widgetReadyPromise.clear()
@_widgetReadyPromise = Future.rejected(new errors.WidgetDropped('widget is cleaned!'))
@_widgetReadyPromise.clear()
_cleanBehaviour: ->
###
Correctly cleans behaviour. DRY
###
if @behaviour?
@behaviour.clean()
@behaviour = null
if @_browserInitDebugTimeout?
clearTimeout(@_browserInitDebugTimeout)
@_browserInitDebugTimeout = null
getCallback: (callback) ->
###
Register callback and clear it in case of object destruction or clearCallbacks invocation
Need to be used, when reference to the widget object (@) is used inside a callback, for instance:
api.get Url, Params, @getCallback (result) =>
@ctx.set 'apiResult', result
###
makeSafeCallback = (callback) ->
result = ->
if !result.cleared
callback.apply(this, arguments)
result.cleared = false
result
safeCallback = makeSafeCallback(callback)
@_callbacks.push safeCallback
safeCallback
clearCallbacks: ->
###
Clear registered callbacks
###
callback.cleared = true for callback in @_callbacks
@_callbacks = []
createPromise: (initialCounter = 0, name = '') ->
promise = new Future initialCounter, name
@_promises.push promise
promise
addPromise: (promise) ->
@_promises.push promise
promise
_cleanPromises: ->
promise.clear() for promise in @_promises
@_promises = []
addSubscription: (subscription, callback = null) ->
###
Register event subscription associated with the widget.
Use this only for push bindings. todo: rename this method
All such subscritiptions need to be registered to be able to clean them up later (see @cleanChildren())
###
if callback and _.isString subscription
subscription = postal.subscribe
topic: subscription
callback: callback
@_postalSubscriptions.push subscription
subscription
cleanSubscriptions: ->
subscription.unsubscribe() for subscription in @_postalSubscriptions
@_postalSubscriptions = []
addTmpSubscription: (subscription) ->
@_tmpSubscriptions.push(subscription)
cleanTmpSubscriptions: ->
subscription.unsubscribe() for subscription in @_tmpSubscriptions
@_tmpSubscriptions = []
cleanModelSubscriptions: ->
for name, mb of @_modelBindings
mb.subscription?.unsubscribe()
setRepo: (repo) ->
###
Inject widget repository to create child widgets in same repository while rendering the same page.
The approach is one repository per request/page rendering.
@param WidgetRepo repo the repository
###
@widgetRepo = repo
setServiceContainer: (serviceContainer) ->
@container = serviceContainer
getServiceContainer: ->
@container
_registerModelBinding: (name, value) ->
###
Handles situation when widget's incoming param is model of collection.
@param String name param name
@param Any value param value which should be checked to be model or collection and handled accordingly
###
if value != undefined
if isBrowser and @_modelBindings[name]?
mb = @_modelBindings[name]
if value != mb.model
mb.subscription.unsubscribe() if mb.subscription?
delete @_modelBindings[name]
if value instanceof Model or value instanceof Collection
@_modelBindings[name] ?= {}
@_modelBindings[name].model = value
@_bindModelParamEvents(name) if isBrowser
_initModelsEvents: ->
###
Subscribes to model events for all model-params came to widget.
@browser-only
###
for name of @_modelBindings
@_bindModelParamEvents(name)
# # make context to wait until widget's behaviour readiness before triggering events
# @ctx.setEventKeeper(@ready())
# @ready().done => @ctx.setEventKeeper(null)
_bindModelParamEvents: (name) ->
###
Subscribes the widget to the model events if param with the given name is model
@browser-only
@param String name widget's param name
###
mb = @_modelBindings[name]
rules = @constructor._paramRules
if not mb.subscription? and rules[name]?
if mb.model instanceof Model
mb.subscription = mb.model.on 'change', (changed) =>
for rule in rules[name]
switch rule.type
when ':setSame' then @ctx.set(name, changed)
when ':set' then @ctx.set(rule.ctxName, changed)
when ':callback'
if rule.multiArgs
(params = {})[name] = changed
args = (params[multiName] for multiName in rule.params)
rule.callback.apply(this, args)
else
rule.callback.call(this, changed)
else if mb.model instanceof Collection
mb.subscription = mb.model.on 'change', (changed) =>
for rule in rules[name]
switch rule.type
when ':setSame' then @ctx.set(name, mb.model.toArray())
when ':set' then @ctx.set(rule.ctxName, mb.model.toArray())
when ':callback'
if rule.multiArgs
(params = {})[name] = mb.model
args = (params[multiName] for multiName in rule.params)
rule.callback.apply(this, args)
else
rule.callback.call(this, mb.model)
setParamsSafe: (params) ->
###
Main "reactor" to the widget's API params change from outside.
Changes widget's context variables according to the rules, defined in "params" static configuration of the widget.
Rules are applied only to defined input params.
Prevents applying params to the context during widget template rendering and defers actual context modification
to the moment after rendering. If more than one setParams() is called during template rendering then only last
call is performed, all others are rejected.
@param Object params changed params
@return Future
###
if @_renderPromise.completed()
if @_sentenced
Future.rejected(new errors.WidgetParamsRace("#{ @debug 'setParamsSafe' } is called for sentenced widget!"))
else
Future.try => @setParams(params)
else
if not @_lastSetParams?
@_renderPromise.finally =>
@_nextSetParamsCallback()
else
@_lastSetParams.reject(new errors.WidgetParamsRace("#{@debug('setParamsSafe') } overlapped with new call!"))
@_lastSetParams = Future.single()
@_nextSetParamsCallback = =>
if @_sentenced
x = new errors.WidgetParamsRace("#{ @debug('setParamsSafe') } is called for sentenced widget!")
@_lastSetParams.reject(x)
else
Future.try =>
@setParams(params)
.link(@_lastSetParams)
@_nextSetParamsCallback = null
@_lastSetParams = null
@_lastSetParams
setParams: (params) ->
###
Actual synchronous "applyer" of params for the setParams() call.
@see setParamsSafe()
@param Map[String -> Any] params incoming params
@synchronous
@throws validation errors
###
_console.log "#{ @debug 'setParams' } -> ", params if global.config.debug.widget
if @constructor.params? or @constructor.initialCtx?
rules = @constructor._paramRules
processedRules = {}
specialParams = ['match', 'history', 'shim', 'trigger', 'params']
for name, value of params
if rules[name]?
for rule in rules[name]
if rule.hasValidation and not rule.validate(value)
throw new Error("Validation of param '#{ name }' of widget #{ @debug() } is not passed!")
else
switch rule.type
when ':setSame' then @ctx.set(name, value)
when ':set' then @ctx.set(rule.ctxName, value)
when ':callback'
if rule.multiArgs
if not processedRules[rule.id]
args = []
for multiName in rule.params
value = params[multiName]
@_registerModelBinding(multiName, value)
args.push(value)
rule.callback.apply(this, args)
processedRules[rule.id] = true
else
@_registerModelBinding(name, value)
rule.callback.call(this, value)
when ':ignore'
else
throw new Error("Invalid param rule type: '#{ rule.type }'")
else if specialParams.indexOf(name) == -1
throw new Error("Widget #{ @getPath() } is not accepting param with name #{ name }!")
else
for key in params
_console.warn "#{ @debug() } doesn't accept any params, '#{ key }' given!"
_handleOnShow: ->
###
Executes onShow-callback if it is defined for the widget and delays widget rendering if ':block' is returned.
@return Future
###
if @onShow?
result = Future.single(@debug('_handleOnShow'))
if @onShow(-> result.resolve()) != ':block'
result.resolve()
result
else
Future.resolved()
show: (params, domInfo) ->
###
Main method to call if you want to show rendered widget template
@param Object params params to pass to the widget processor
@param DomInfo domInfo DOM creating and inserting promise container
@public
@final
@return Future(String)
###
@setParamsSafe(params).then =>
_console.log "#{ @debug 'show' } -> params:", params, " context:", @ctx if global.config.debug.widget
@_handleOnShow()
.then =>
@renderTemplate(domInfo)
getTemplatePath: ->
"#{ @getDir() }/#{ @constructor.dirName }.html"
cleanChildren: ->
if @children.length
if @_structTemplate? and not @_structTemplate.isEmpty()
@_structTemplate.unassignWidget(widget) for widget in @children
# widget.drop will mutate @children indirectly so it's better to work with clone
widget.drop() for widget in _.clone(@children)
@resetChildren()
sentenceChildrenToDeath: ->
child.sentenceToDeath() for child in @children
sentenceToDeath: ->
if not @_sentenced
@cleanSubscriptions()
@cleanModelSubscriptions()
@_sentenced = true
if not @_browserInitialized and not @_widgetReadyPromise.completed()
@_widgetReadyPromise.reject(new errors.WidgetSentenced("Widget #{@constructor.__name} is sentenced!"))
@sentenceChildrenToDeath()
isSentenced: ->
@_sentenced
getStructTemplate: ->
###
Loads (if neccessary) and returns in Future structure teamplate of the widget or :empty if it has no one.
@return Future(StructureTemplate | :empty)
###
if @_structTemplate?
Future.resolved(@_structTemplate)
else
if not @constructor._rawStructPromise
tmplStructureFile = "bundles/#{ @getTemplatePath() }.struct"
@constructor._rawStructPromise = Future.require(tmplStructureFile)
@constructor._rawStructPromise.map (struct) =>
if struct.widgets? and Object.keys(struct.widgets).length > 1
@_structTemplate = new StructureTemplate(struct, this)
else
@_structTemplate = StructureTemplate.emptyTemplate()
@_structTemplate
inject: (params, transition) ->
###
Injects the widget into the extend-tree and reorganizes the tree.
Recursively walks through it's extend-widgets until matching widget is found in the current extend-tree.
If the matching extend-widget is found then new widgets are 'attached' to it's placeholders.
If the matching extend-widget is not eventually found then the page is reloaded to fully rebuild the DOM.
@browser-only
@param Object params
@param PageTransition transition
@return Future[Widget] common base widget found in extend-tree
###
_console.log "#{ @debug 'inject' }", params if global.config.debug.widget
@widgetRepo.registerNewExtendWidget(this)
@setParamsSafe(params).then =>
@getStructTemplate().zip(@_handleOnShow())
.then (tmpl) =>
@_resetWidgetReady()
@_behaviourContextBorderVersion = null
@_placeholdersRenderInfo = []
@_deferredBlockCounter = 0
extendWidgetInfo = if not tmpl.isEmpty() then tmpl.struct.extend else null
if extendWidgetInfo?
extendWidget = @widgetRepo.findAndCutMatchingExtendWidget(tmpl.struct.widgets[extendWidgetInfo.widget].path)
if extendWidget?
readyPromise = new Future(@debug('_injectRender:readyPromise'))
@_inlinesRuntimeInfo = []
@registerChild extendWidget
extendWidget.cleanSubscriptions() # clean up supscriptions to the old parent's context change
@resolveParamRefs(extendWidget, extendWidgetInfo.params).then (params) ->
extendWidget.setParamsSafe(params)
.link(readyPromise)
tmpl.assignWidget extendWidgetInfo.widget, extendWidget
Future.require('jquery')
.zip(tmpl.replacePlaceholders(extendWidgetInfo.widget, extendWidget.ctx[':placeholders'], transition))
.then ($) =>
# if there are inlines owned by this widget
if @_inlinesRuntimeInfo.length
$el = $()
# collect all placeholder roots with all inlines to pass to the behaviour
$el = $el.add(domRoot) for domRoot in @_inlinesRuntimeInfo
else
$el = undefined
@browserInit(extendWidget, $el)
.link(readyPromise)
.done => @markShown()
readyPromise
.then =>
@_inlinesRuntimeInfo = null
extendWidget
# if not extendsWidget? (if it's a new widget in extend tree)
else
tmpl.getWidget(extendWidgetInfo.widget).then (extendWidget) =>
@registerChild extendWidget
@resolveParamRefs(extendWidget, extendWidgetInfo.params).then (params) =>
extendWidget.inject(params, transition)
.then (commonBaseWidget) =>
@browserInit(extendWidget).done => @markShown()
commonBaseWidget
else
location.reload()
renderTemplate: (domInfo) ->
###
Decides wether to call extended template parsing of self-template parsing and calls it.
@param DomInfo domInfo DOM creating and inserting promise container
@return Future(String)
###
_console.log @debug('renderTemplate') if global.config.debug.widget
@_resetWidgetReady() # allowing to call browserInit() after template re-render is reasonable
@_behaviourContextBorderVersion = null
@_placeholdersRenderInfo = []
@_deferredBlockCounter = 0
@_renderPromise = @getStructTemplate().flatMap (tmpl) =>
if tmpl.isExtended()
@_renderExtendedTemplate(tmpl, domInfo)
else
@_renderSelfTemplate(domInfo)
_renderSelfTemplate: (domInfo) ->
###
Usual way of rendering template via dust.
@param DomInfo domInfo DOM creating and inserting promise container
@return Future(String)
###
_console.log @debug('_renderSelfTemplate') if global.config.debug.widget
tmplPath = @getPath()
templateLoader.loadWidgetTemplate(tmplPath).flatMap =>
@markRenderStarted()
@cleanChildren()
@_saveContextVersionForBehaviourSubscriptions()
@_domInfo = domInfo
result = Future.call(dust.render, tmplPath, @getBaseContext().push(@ctx))
@markRenderFinished()
result
resolveParamRefs: (widget, params) ->
###
Waits until child widget param values referenced using '^' sing to deferred context values are ready.
By the way subscribes child widget to the pushing of those changed context values from the parent (this) widget.
Completes returned promise with fully resolved map of child widget's params.
@param Widget widget the target child widget
@param Map[String -> Any] params map of it's params with values with unresolved references to the parent's context
@return Future[String -> Any] resolved params
###
params = _.clone(params) # this is necessary to avoid corruption of original structure template params
# removing special params
delete params.placeholder
delete params.type
delete params.class
delete params.name
delete params.timeout
result = new Future(@debug('resolveParamRefs'))
bindings = {}
# waiting for parent's necessary context-variables availability before rendering widget...
for name, value of params
if name != 'name' and name != 'type'
if typeof value is 'string' and value.charAt(0) == '^'
value = value.slice(1) # cut leading ^
bindings[value] = name
# if context value is deferred, than waiting asynchronously...
if @ctx.isDeferred(value)
result.fork()
do (name, value) =>
@subscribeValueChange params, name, value, =>
@widgetRepo.subscribePushBinding(@ctx.id, value, widget, name, @ctx.getVersion()) if isBrowser
result.resolve()
# otherwise just getting it's value synchronously
else
# param with name "params" is a special case and we should expand the value as key-value pairs
# of widget's params
if name == 'params'
if _.isObject @ctx[value]
for subName, subValue of @ctx[value]
params[subName] = subValue
@widgetRepo.subscribePushBinding(@ctx.id, value, widget, 'params', @ctx.getVersion()) if isBrowser
else
# todo: warning?
else
params[name] = @ctx[value]
@widgetRepo.subscribePushBinding(@ctx.id, value, widget, name, @ctx.getVersion()) if isBrowser
if Object.keys(bindings).length != 0
@childBindings[widget.ctx.id] = bindings
result.map -> params
_renderExtendedTemplate: (tmpl, domInfo) ->
###
Render template if it uses #extend plugin to extend another widget
@param StructureTemplate tmpl structure template object
@param DomInfo domInfo DOM creating and inserting promise container
@return Future(String)
###
extendWidgetInfo = tmpl.struct.extend
tmpl.getWidget(extendWidgetInfo.widget).then (extendWidget) =>
extendWidget._isExtended = true if @_isExtended
@registerChild extendWidget, extendWidgetInfo.name
@resolveParamRefs(extendWidget, extendWidgetInfo.params).then (params) ->
extendWidget.show(params, domInfo)
renderInline: (inlineName, domInfo) ->
###
Renders widget's inline-block by name
@param String inlineName name of the inline to render
@param DomInfo domInfo DOM creating and inserting promise container
@return Future(String)
###
_console.log "#{ @constructor.__name }::renderInline(#{ inlineName })" if global.config.debug.widget
if @ctx[':inlines'][inlineName]?
tmplPath = "#{ @getDir() }/#{ @ctx[':inlines'][inlineName].template }.html"
templateLoader.loadToDust(tmplPath).then =>
@_saveContextVersionForBehaviourSubscriptions()
@_domInfo = DomInfo.merge(@_domInfo, domInfo)
Future.call(dust.render, tmplPath, @getBaseContext().push(@ctx))
else
Future.rejected(new Error("Trying to render unknown inline (name = #{ inlineName })!"))
renderRootTag: (content) ->
###
Builds and returns correct html-code of the widget's root tag with the given rendered contents.
@param String content rendered template of the widget
@return String
###
classString = @_buildClassString()
classAttr = if classString.length then ' class="' + classString + '"' else ''
"<#{ @rootTag } id=\"#{ @ctx.id }\"#{ classAttr }>#{ content }</#{ @rootTag }>"
renderPlaceholderTag: (name, content) ->
###
Wraps content with appropriate placeholder root tag and returns resulting HTML
@param String name name of the placeholder
@param String content html-contents of the placeholder
@return String
###
classParam = ""
if @_placeholdersClasses[name]
classParam = "class=\"#{ @_placeholdersClasses[name] }\""
"<div id=\"#{ @_getPlaceholderDomId(name) }\" #{ classParam }>#{ content }</div>"
renderInlineTag: (name, content) ->
###
Builds and returns correct html-code of the widget's inline root tag with the given name and rendered contents.
@param String name inline name
@param String content rendered template of the widget
@return Future[String]
###
info = @ctx[':inlines'][name]
@getStructTemplate().then (struct) =>
classString =
# widget's classes should be injected to it's inlines only if the widget doesn't have it's own DOM root
# (i.e. extended widgets)
if struct.isExtended()
@_buildClassString(info.class)
else
info.class
classAttr = if classString.length then ' class="' + classString + '"' else ''
"<#{ info.tag } id=\"#{ info.id }\"#{ classAttr }>#{ content }</#{ info.tag }>"
replaceModifierClass: (cls) ->
###
Sets the new modifier class(es) (replacing the old if threre was) and immediately updates widget's root element
with new "class" attribute in DOM.
@param String cls space-separeted list of new modifier classes
@browser-only
###
@setModifierClass(cls)
require ['jquery'], ($) =>
$('#'+@ctx.id).attr('class', @_buildClassString())
_buildClassString: (dynamicClass) ->
classList = []
classList.push(@cssClass) if @cssClass
classList.push(@ctx._modifierClass) if @ctx._modifierClass
classList.push(dynamicClass) if dynamicClass
classList = classList.concat(@ctx.__cord_dyn_classes__) if @ctx.__cord_dyn_classes__?
classList.join(' ')
setModifierClass: (cls) ->
###
Save modifier classes came from the template in the state of the widget.
Need it to be able to restore when widget is re-rendered and the root tag is recreated.
@param String class space-separeted list of css class-names
###
@ctx._modifierClass = cls
addDynClass: (cls) ->
###
Adds the specified CSS class for the root element(s) of the widget.
This class is considered dynamically dependent from the current state of the widget.
The list of such classes is preserved separately from the static `cssClass` field in a special context value and
can be modified runtime via (add|remove|toggle)Class() methods of the widget's behaviour class.
This method should be used only before first widget render (typically in the onShow() method). All later
modifications should be done only in behaviour.
@param {String} cls Single CSS class name to be added
###
if cls
@ctx.__cord_dyn_classes__ ?= []
@ctx.__cord_dyn_classes__.push(cls) if @ctx.__cord_dyn_classes__.indexOf(cls) == -1
_saveContextVersionForBehaviourSubscriptions: ->
if not @_behaviourContextBorderVersion?
@_behaviourContextBorderVersion = @ctx.getVersion()
@ctx.stashEvents()
setSubscribedPushBinding: (pushBindings) ->
###
Set widget's push bindings of parent widget context
@param Object push binding by parent context param's name
###
@_subscibedPushBindings = pushBindings
_renderPlaceholder: (name, domInfo) ->
###
Render contents of the placeholder with the given name
@param String name name of the placeholder to render
@param Function(String, Array) callback result-callback with resulting HTML and special helper structure with
information about contents (needed by replacePlaceholders() method)
###
placeholderOut = []
renderInfo = []
promise = new Future("#{ @debug('_renderPlaceholder') }")
self = this
i = 0
placeholderOrder = {}
phs = @ctx[':placeholders'] ? []
ph = phs[name] ? []
for info in ph
do (info) =>
promise.fork()
widgetId = info.widget
widget = @widgetRepo.getById(widgetId)
widget.setModifierClass(info.class) if info.type != 'inline'
timeoutTemplateOwner = info.timeoutTemplateOwner
delete info.timeoutTemplateOwner
processWidget = (out) ->
###
DRY for regular widget result fixing
###
placeholderOut[placeholderOrder[widgetId]] = widget.renderRootTag(out)