-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathHELP.chem
1521 lines (1180 loc) · 67.1 KB
/
HELP.chem
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
`! lic=unlicense
`! `,+ vim: et sts=-1 sw=2 ai
`! fd=0
`# Documentation guidelines
`* Place most important information near the beginning, include in-depth info in the end, don't be too formal about including list of all arguments, types, return values, etc. or following a stringent standard if this info is either obvious or too complicated to fit properly in small tables of `> and `= tokens.
Description template (sections):
`[
! +fn=func:arg } OR
! `, +fna=function ( [key] ) }
! +prop=events }
#+tag_... tag_...
* `@absolute\reference.to_self()`@
#
#-readOnly setOn...
Synopsis (manpage-style).
... description ...
= type of`, result
> function's
> arguments
?`[
examples
`]
Other notes:
* Note.
* Note.
# In-depth
Stuff.
`]
`! +pg=_7ibvkbid
`#setOnDecl
` `*May only be set upon declaration via `@Core::extend()`@ or `#mixIn.`*
`#settable
` `*May be set upon declaration via `@Core::extend()`@ or `#mixIn and/or read/written directly on run-time.`*
`#setOnDeclViaPush
` `*May only be set after `@Core::extend()`@ using `[MyClass.prop.push(...)`].`*
`#readOnly
` `*May only be read, not changed.`*
`#inMergeProps
This property is listed in `#_mergeProps by default so subclass defining it adds to its parent's object instead of overwriting it entirely (but identical keys are still overwritten).
`#inMergePropsA
This property is listed in `#_mergeProps by default so subclass defining it adds to its parent's values instead of overwriting the array entirely.
`#es6thiswarn
Warning: avoid using ES6 arrow functions as handlers due to their fixed `'this (`#es6this).
`#inBB
`## In Backbone...
`#unordered
Attention: JavaScript objects are unordered, as explained in the description of `#Ordered.
`#tag_Events Event system (`#evt):
`#tag_Extension Inheritance-related:
`#tag_Lifecycle Object's lifecycle (construct, attach, render, update):
`#tag_Nesting Children and nesting (`#chld):
`#tag_Options Dealing with options (`#opt):
`#tag_Utilities Utility methods (`#util):
`! +pg=start +pgo=-10
`# Introduction
`## Sqimitive.js - A Universal Primitive
Sqimitive.js is a minimalistic paradigm-agnostic building block for applications written in JavaScript (not necessary in-browser or DOM-related). It provides core infrastructure without much domain-specific logic that you can quickly develop thanks to its flexible API.
`* Declaration-time and run-time mix-ins and traditional OOP inheritance using events. `#evt `#events `#mixIn `#_mergeProps
`* Collections with Underscore (or other) integration and assisted management of children. `#util `#_childEvents `#autoOff
`* Transparently ordered collections. `#Ordered `#_repos
`* Attributes with normalization, on-change notifications and batching. `#opt `#change_OPT `#batch
`* Unserialization of data in arbitrary format. `#assignChildren `#assignResp
`* DOM integration for "views". `#jQuery `@Base.elEvents`@
`* Fractal Promise on steroids. `#Async `#Fetch
Sqimitive.js is under 20K (minified), sports documentation that is over 200 pages long (printed A4), needs no transpilers (plain pre-ES6 JavaScript, IE 11+) and depends only on a compatible utility library (NoDash `@no@`@ +16K, Underscore.js >=1.9.0 `@un:`@ or LoDash) and, optionally, on jQuery `@jq:`@ or Zepto (for `@Sqimitive\jQuery`@).
And it's in public domain (`@http://unlicense.org`@).
`#-jqex
`## Resources
`<img src="data:image/gif;base64,R0lGODlhKQAQAIAAAP///8s5OCH5BAAAAAAALAAAAAApABAAAAJOjI+py+0Po5wK2GsP3np7kHygIY5BiYUXaaZst55jhri0fOf2bLK0roP9eKqhsCMkyopKnCf5WtZiwKfPKJW4KNxr97v9OkUMlPlcPosKADs" alt="npm"`> `*npm install sqimitive`*
`* New to Sqimitive? Start with the overview: `#ov
`* Download for development: `@https://github.com/ProgerXP/Sqimitive/archive/master.zip`@
`* Download for production (minified): `@https://raw.githubusercontent.com/ProgerXP/Sqimitive/master/sqimitive.min.js`@
`* Check out the sample To-Do application: `@https://squizzle.me/js/sqimitive/demo/to-do.html`@
`* Report issues: `@https://github.com/ProgerXP/Sqimitive/issues`@
`* Get the code on GitHub: `@https://github.com/ProgerXP/Sqimitive`@
If you spot a typo - select the fragment and press `*Ctrl+Enter`*. Help us keep this memo free of vexatious mistakes! Powered by `@https://github.com/ProgerXP/Orphus`@.
`## Basic code conventions
`* `*Protected fields begin with underscore (`'_)`* - such fields (data properties and methods) are meant to be read or written only by the class they are defined in, or by one of its subclasses. It's bad practice to try to access them from an ancestor (not the descendant they're defined in) or, mind you, from an unrelated class - this is only justified, if ever, by really severe optimizations (such as tons of `@Base._opt`@ calls bypassing `#get()).
`* `*Naming style: underscores separating camelCase`* - when an identifier (`[oneId`]) needs to be combined with another (`[twoId`]) it becomes simply `[oneId_twoId`] instead of `[oneIdTwoId`], `[one_id_two_id`] or something else dictated by "pretty printing" identifiers. For example, `#Base fires events of form "`#change_OPT" so the event name used when `'attrName option (`@Base._opt`@) was changed is `'change_attrName.
`* `*Semicolon-free zone`* - JavaScript is one of those languages where operation separators are optional, much like in Lua. You just need to remember to put `[;`] before any operation beginning with `[(`] (which most of the time has to do with IIFE):
`[
foo = 123
;(function () { ... })()
// without ; above is equivalent to 123(function () { {... })()
`]
`* `*`'null is `'undefined`* - unless mentioned, these two values are used interchangeably: when testing, use `[value == null`] instead of `[===`]; when supplying, give any of the two. One case when `'undefined is different are event handler results (see `#::fire()).
`##deps Customizing dependencies
Sqimitive can be included using several methods as implemented by `[umh.js`]. Each method has its own way of overriding dependencies (which default to NoDash `@no@`@ and jQuery `@jq:`@). Alternatively, you can change the `'deps variable in the source code but you'll have to redo it on upgrade.
`### CommonJS/Node.js
In npm 8.3+, put this into your project's `[package.json`] before doing `[npm install sqimitive`] (`@https://github.com/npm/cli/issues/4909`@):
`[
"overrides": {
"sqimitive": {
"nodash": "npm:underscore",
"jquery": "npm:zepto"
}
}
`]
`### AMD/Require.js
Use `'map and/or `'path options (`@https://requirejs.org/docs/api.html#config`@):
`[
requirejs.config({
map: {
'sqimitive/main': {
'nodash/main': 'lodash'
},
'sqimitive/jquery': {
jquery: 'zepto'
}
},
paths: {
nodash: 'https://cdn.jsdelivr.net/npm/underscore@latest/underscore-umd-min.js'
}
})
`]
`### Plain `[<script>`]
Dependencies land in global `'window so simply fetch the needed script before including Sqimitive:
`[
<script src="https://cdn.jsdelivr.net/npm/underscore@latest/underscore-umd-min.js"></script>
<script src="https://raw.githubusercontent.com/ProgerXP/Sqimitive/master/sqimitive.min.js"></script>
`]
`! +pg=vsbb +pgo=10
`# Comparing with Backbone
Sqimitive has been inspired by Backbone.js (`@bb:`@) and was written as a solution to most annoying problems after working with Backbone for months in a production project. It avoids certain less used (read: project-specific) features while conceptually bringing the rest under one roof. In particular, Sqimitive:
`* Removes somewhat artificial separation for Model, Collection and View classes providing a single unified "primitive" (the "sqimitive"). This way Views can use options (`#opt - attributes with on-change notifications, `#change_OPT), Underscore.js helpers (`#util), object nesting (`#chld - just like Collections), jQuery/Zepto bindings (`[this.el`], `@jQuery.elEvents`@) and every other feature, shared by all, not imposed on any.
`* Greatly expands on the idea of events (`#evt), making inheritance as sensible as possible given JavaScript's idea of OOP and adding automatic event management for connected objects (e.g. elements of a collection - `#_childEvents).
`* Removes server-side communication layer (`@bb:Model-sync`@) while keeping routines to smart-update object states (`#assignChildren(), `#assignResp()). Instead of overriding parts of the library (as it's often done with Backbone) you implement them yourself just once given your unique project requirements.
While different, Sqimitive will appear very familiar to Backbone users and being paradigm-free, it will even let you work the way you used with Backbone or another framework - in aspects you wish. You can create your own Model, Collection and View classes to follow MVC, use traditional `'__super__-style inheritance, write your specific `'sync() routines and so on.
This section highlights most notable gaps present in Backbone and other frameworks. Strictly subjective!
`## OOP that makes sense
In Backbone and other JavaScript frameworks inheritance (overriding of parent properties and methods) is painful.
` `*With properties`*, if it's a complex object that you would rather extend (append to) rather than replace entirely - as it is the case with `@bb:Model-attributes`@, `@bb:Model-defaults`@, `@bb:View-events`@ (DOM bindings) and others - you have to add them in the constructor because if you define such a property in the child class you will `*entirely overwrite the inherited value`*. Moreover, if at some point the base class that did not have that property decided to declare it - all its subclasses overwrite it without notice... Unless you remember to update them, which is an unnecessary cognitive burden.
` `*With methods`*, you have to hardcode reference to the base class - that's not to mention the entire construct which is already ugly: `[My.Class.__super__.baseMethod.apply(this, arguments)`]. So if you later rename the base class or move this subclass under another parent, or rename the method, or just make a typo - the entire inheritance chain for this method is broken. You may or may not get an error, too.
This is the price for trying to push classic OOP into JavaScript prototype model.
Sqimitive solves both problems by using `#_mergeProps for listing which properties should be merged together on extension. For methods, there is a fantastic event-driven inheritance (`#evt) that makes JavaScript OO coding a breeze. Any method can be turned into an event without altering the base class, it can be temporary (`#once()) and works for "traditional" events too, like `#change notifications.
At the same time, the `'__super__ way of doing this is still available if you are feeling a bit masochistic.
Below is just one simple example. Look for more details at the mentioned sections.
`[
var MyBaseClass = Sqimitive.jQuery.extend({
complexProp: {
base: 123,
},
init: function () {
alert('I init()!')
},
})
// _mergeProps is a static property.
MyBaseClass._mergeProps.push('complexProp')
var MySubclass = MyBaseClass.extend({
complexProp: {
sub: 'foo',
},
events: {
init: function () {
alert('Now I init() too!')
},
},
})
var obj = new MySubclass
// 1. alert: I init()!
// 2. alert: Now I init() too!
alert(obj.complexProp.base)
// alert: 123
alert(obj.complexProp.sub)
// alert: foo
`]
`## Built-in child nesting
Backbone offers no nested views out of the box. There are plugins such as Babysitter (from Marionette.js, `@https://github.com/marionettejs/backbone.babysitter`@) but they don't provide a complete solution.
In Sqimitive, nested views are native. When used, they are automatically managed: elements and event listeners removed (`#autoOff()) when the corresponding view is removed, DOM events rebound (`@jQuery.elEvents`@) and root element reinserted (`@jQuery.attach()`@) when root element is moved (e.g. on `#render()), it is possible to filter (`#util) nested views using Underscore methods (just like filtering contents of `@bb:Collection`@s in Backbone), receive notifications about newly `#nested/`#unnested/changed views, maintain sort order (`#Ordered, `#_repos) and so on.
No more need for `'delegateEvents(), `[off(null, null, this)`] or `[return this`] (when overriding `'render() thanks to `#evtpf).
See more details and examples in the Views overview section (`#vw).
`## Change-driven behaviour
In Backbone and other frameworks, you have to track changes to your data and view states. Backbone eases this task by providing standard events like `[change:attr`] event for Models. However, this approach must be brought further to let us take full advantage of it.
In Sqimitive, you get a special object property - `@Base._opt`@ ("options") - that is a pool of trackable states, similar to Backbone's `@bb:Model-attributes`@. Items in this pool are read with `#get() and written with `#set(). When a new and different value is written a bunch of events occurs; within their listeners you can cancel or normalize the change (`#normalize_OPT()) or perform an action (`#change_OPT() and `#change()). Parent sqimitives can optionally forward their children events to themselves (`#_childEvents).
This can be used to create event observation spots too. Suppose that you have an object that's loading some data from the server. Since it's asynchronous you don't know when (or if) it will finish. Usually you would either create a callback property and call it or use a custom event (like jQuery and Backbone do) and trigger it once loading is done. In Sqimitive, you simply introduce some `[_opt.loading`], set it to `'false initially and once done - change to `'true. Observers listen to the `'change_loading event having the full set of event handling routines at their disposal (`#on, `#once, `#fuse, `#off, `#autoOff, `#logEvents, etc.) instead of introducing `'isLoading(), `'abortLoading(), `'onLoading(), etc.
Thanks to the new option, this starts working the other way too: an external object can `[set('loading', true)`] to trigger data refresh on the object. Better still - the loading object itself can set this option to trigger its own loading routine - naturally, avoiding code duplication.
Moreover, since options are not accessed directly you can always listen for their access with `'get or add custom behaviour with `'set, disregarding the fact they are originally methods (`#get(), `#set()). Lack of direct access to `#opt'ions improves stability of the code base.
Given this mechanism you can significantly lower your `'render() and custom event rates while being much more aware of what has triggered the change at the same time, performing incremental (lightweight) updates. And since Models and Views are the same thing in Sqimitive these principles work universally for any kind of object you might have.
`[
var MyView = Sqimitive.jQuery.extend({
_opt: {
loading: false,
people: [], // array of names.
},
// We will keep jqXHR object here to be able to abort the request.
_loadingXHR: null,
events: {
init: function () {
// Wait 300 msec and then start fetching server data.
_.delay(_.bind(this.set, this, 'loading', true), 300)
},
// Loading was either cancelled or started.
change_loading: function (value) {
// Update class name of our DOM element.
this.el.toggleClass('loading', value)
// Abort old request, if any.
this._loadingXHR && this._loadingXHR.abort()
if (value) {
// Fetch the data, pass to _load().
this._loadingXHR = $.getJSON('api/get/some', _.bind(this._load, this))
}
},
// _opt.people changed - update names.
change_people: '_updatePeople',
render: function () {
// Retrieve all _opt to pass them to the template.
var vars = this.get()
// Overwrite with new HTML.
this.el.html(_.template($('#MyViewTemplate').text(), vars))
// Add peoples' names.
this._updatePeople()
},
},
_updatePeople: function () {
var el = this.$('.people').empty()
_.each(this.get('people'), function (name) {
$('<em>').text(name).appendTo(el)
})
},
_load: function (resp) {
// Do something with resp, then finally:
this.set('loading', false)
},
})
`]
`## Events do occur on events - no cheating
If you have used Backbone's `'add, `'remove and `'change events then you probably know that they do not occur... always. For example, if you `'reset() a Collection it won't fire `'add for new Models, `'remove for gone or `'change for existing Models that were updated.
This smells like an optimization - after all, firing hundreds of events when resetting a huge Collection might be slow. However, such cases are very rare and yet the existence of `'reset() makes it problematic to track the updates because often you will end up with your own routine bound to `'reset as an event that will figure the difference between old Collection contents and new one.
This also happens with attribute normalization on Models that for some reason doesn't occur when assigning an API response.
Sqimitive fires events when things change, period. In case you do have a performance-heavy object you can always implement your own, less "noisy" and more optimized update routines. However, 90% of the time you will rely on `#nest, `#unnested, `#change and others to keep you notified.
`## Cloned instance properties
In JavaScript, when you create a prototype with properties having non-scalar values (like objects or arrays) what you actually create are `*shared instance properties`*. If at run-time you modify such a property (but not reassign it) this operation will affect all objects where that property was defined, unless its value was overwritten with a new object.
Backbone inherits this behaviour which most of the time leads to very confusing results (from the human's perspective). Consider this snippet (`@http://jsfiddle.net/Proger/vwqk67h8/`@):
`[
var MyView = Backbone.View.extend({foo: []})
var first = new MyView
var second = new MyView
first.foo.push(123)
alert(second.foo[0])
// alert: 123
`]
The only way around is to assign such properties in the constructor, which you should first override using the crazy `[My.Class.__super__.baseMethod.apply(this, arguments)`] construct. This gives your code -50 points in the ability to save kittens on this planet!
Sqimitive eliminates this problem by automatically deep cloning (`#deepClone) all complex values upon new object instantiation. Of course, sometimes this is not desired - for this you can always assign them in the constructor just like before or list them in `#_shareProps to prevent automatic cloning.
`## Backbonization
The following tricks can be used to make Sqimitive's API appear more like Backbone's. It will not make it 100% identical so don't use it in production as is - it's intended to give you some pointers.
`### General
In Backbone, `#_cid is named `@bb:Model-cid`@, `@jQuery.el`@ is `@bb:View-$el`@ with the `@bb:View-el`@ counterpart (native DOM node), `#fire() is named `@bb:Events-trigger`@(), `@bb:Events-listenTo`@() and `@bb:Events-stopListening`@() are `#autoOff() flavours, `@bb:Model-toJSON`@() is basically `#get(), `@bb:Model-attributes`@ is the same as `@Base._opt`@, `@bb:Model-defaults`@ are declaration-time `'_opt.
`[
var BackboneBase = Sqimitive.Base.extend('BackboneBase', {
cid: null, // alias to _cid; do not write to.
events: {
'-init': function () {
this.cid = this._cid
},
init: function () {
this._childClass = this._childClass || this.model // used below.
},
},
trigger: function (event, arg_1) {
return this.fire(event, _.rest(arguments))
},
listenTo: function (sqim, event, func) {
this.autoOff(sqim, _.object([[event, func]]))
return this
},
stopListening: function (sqim, event, func) {
if (!arguments.length) {
return this.autoOff()
} else if (func || (sqim && event)) {
throw new Error('Unsupported stopListening() call.')
} else {
return this.off(event || sqim)
}
},
})
`]
`### Collection
Collections have `@bb:Collection-model`@ (a class reference) which simply specifies `#_childClass, `@bb:Collection-reset`@() is alike to `#assignChildren (`'Collection's `@bb:Collection-set`@() is entirely different though), `@bb:Collection-push`@(), `@bb:Collection-pop`@(), `@bb:Collection-shift`@(), `@bb:Collection-unshift`@() are shortcuts for `#nest() and the company of `#util (Sqimitive by default is not `#Ordered), `@bb:Model-clone()`@ is another shortcut for non-`#_owning lists. Models are keyed by their `@bb:Model-id`@ attribute value (or `@bb:Model-idAttribute`@) which are like `#_defaultKey().
`[
var BackboneCollection = BackboneBase.extend('BackboneCollection', {
model: null, // alias to _childClass; do not write to.
el: null,
idAttribute: 'id',
events: {
'=assignChildren': function (sup, resp, options) {
options = _.extend(options || {}, {eqFunc: this.idAttribute})
return sup(this, [resp, options])
},
},
_defaultKey: function (model) {
return model.get(this.idAttribute)
},
reset: function (models, options) {
if (!arguments.length) {
this.each(this.unlist, this)
} else {
this.assignChildren(models, {
eqFunc: this.idAttribute || 'id',
keepMissing: !('remove' in options) || !options.remove,
})
}
return this
},
push: function (model) {
this.nest(model)
},
unshift: function (model) {
this.nest(model)
},
pop: function () {
var model = this.last()
model && model.remove()
return model
},
shift: function () {
var model = this.first()
model && model.remove()
return model
},
toJSON: function () {
return this.invoke('get')
},
clone: function () {
if (this._owning) {
throw new Error('clone() will clear the original collection.')
}
var copy = new this.constructor(this.get())
this.each(function (child, key) { copy.nest(key, child) })
return copy
},
})
`]
`### Model
Models' `@bb:Model-escape`@() and `@bb:Model-has`@() are shortcuts for `#get(). `@bb:Model-id`@ option ("attribute") is readable/writable as `[get('id')`]/`[set('id', ...)`] and also readable directly as `[obj.id`] (write attempts won't be caught nor will they change the "real" `'id).
`[
var BackboneModel = BackboneBase.extend('BackboneModel', {
attributes: {}, // alias to _opt; do not write to.
defaults: {}, // alias to declaration-time _opt; do not write to.
id: null, // alias to get('id'); do not write to.
_opt: {
id: null,
},
events: {
'-init': function () {
this.defaults = Sqimitive.Core.deepClone(this._opt)
},
init: function () {
this.attributes = this._opt
},
change_id: function (id) { this.id = id },
},
toJSON: function () {
return this.get()
},
escape: function (opt) {
return _.escape(this.get(opt))
},
has: function (opt) {
return this.get(opt) != null // undefined or null.
},
})
`]
`### View
Views have `@bb:View-setElement`@(), `@bb:View-delegateEvents`@() and `@bb:View-undelegateEvents`@() that are similar to `@jQuery.attach()`@.
`[
var BackboneView = BackboneBase.extend('BackboneView', {
el: null, // el must remain a wrapped jQuery node in Sqimitive.
$el: null, // alias to el; do not write to.
events: {
init: function () {
this.$el = this.el
},
},
setElement: function (el) {
this.undelegateEvents() // unnecessary since attach() does this already.
this.el = this.$el = el
return this.attach() // only binds events if el has no parent.
},
delegateEvents: function (events) {
this.elEvents = events
return this.attach() // only binds events if el has no parent.
},
undelegateEvents: function () {
this.el.off('.sqim-' + this._cid)
},
})
`]
`### Babysitting Models
One of the most common problems when developing a complex client-side app is keeping track of multiple Models or of a Collection connected to a particular View. When a new Model appears you need to create and display a new nested View; when it's removed - its View should go away; when Model attributes change the View should be updated (and this case is often but not always handled by that Model's specific View).
On top of that, when parent View acquires another Collection it should properly detach itself from the previously assigned Collection, attach to the new object and repopulate itself.
Things get even more complex with asynchronous operations - sometimes the user gets ahead of his network packets and you don't want the user interface to tangle up.
Sqimitive addresses all of these challenges:
`* Attach/detach event handlers to the related Collection with `#autoOff() or `[off(collection)`] (`#off).
`* Keep that Collection as an option (`@Base._opt`@) so once it changes you receive `[change_collection(newCol, oldCol)`] event (`#change_OPT).
`* Make sure only proper Collections are assigned by checking the value-to-be-set in `[normalize_collection(newCol)`] (`#normalize_OPT).
`* Listen to new Models with `#on on `@Base.nestEx`@ (`[on('+nestEx')`]) and to gone Models on `#unnested.
`* Listen to changed Models via `#_childEvents: set it to `[['change']`] and listen to `[on('.change')`].
`* Automatically manage nested Views' connections to DOM with `'attachPath (`@jQuery._opt`@) and `@jQuery.attach()`@. Quite often there's no need for `#render() at all.
Sample code below demonstrates this in practice. It's a good idea to make a class handling this basic logic and reuse it throughout your project.
`[
var ParentView = Sqimitive.jQuery.extend({
_childClass: ChildView,
_opt: {
collection: null, // Collection.Foo.
},
normalize_collection: function (newCol) {
if (!(newCol instanceof Collection.Foo)) {
throw new Error('Bad collection type.')
} else {
return newCol
}
},
change_collection: function (newCol, oldCol) {
// Unbind self from the old collection, if any.
oldCol && oldCol.off(this)
// Clear existing nested Views, if any (this form only works if _owning).
this.invoke('remove')
// Set up the link to the new collection.
if (newCol) {
this.autoOff(newCol, {
nestEx: function (options) {
options.changed && this._modelAdded(options.child)
},
unnested: '_modelRemoved',
// Requires that the collection declares _childEvents: ['change'].
'.change': '_modelChanged',
})
// Populate with the existing models.
newCol.each(this._modelAdded, this)
}
},
_modelAdded: function (model) {
// This is the place where a new View gets created, nested and linked to
// the model. First argument to nest() is view's parent key by which it
// can be retrieved later. Model's ID is usually unique so use it if you
// have one. If this key already existed then that View will be removed
// and replaced with the new View.
var view = this.nest(model.get('id'), new this._childClass({
model: model,
attachPath: '.models',
}))
// Append view.el to this.el.find('.models') and bind its DOM event
// listeners. This won't render() the View but it might listen to
// 'attach' and call render() automatically.
view.attach()
// ...If it doesn't auto-render - no big deal:
view.render()
},
_modelRemoved: function (model) {
var view = this.nested(model.get('id'))
if (view) {
// Removes view's DOM element from this.el and unnests view from this
// children. This essentially unbinds view's DOM listeners and its
// Sqimitive listeners on the parent (this).
view.remove()
} else {
console.warn('Removed a Model with no nested View.')
}
},
_modelChanged: function (model) {
// Update something when model's options change...
},
})
`]
The above code can be used like this:
`[
var col = new Collection.Foo
col.nest(new Model.Foo({id: 1}))
new ParentView({collection: col, attachPath: 'body'})
// Gets created with one nested ChildView (existing Model in col).
// ParentView's el is appended to <body>.
col.nest(new Model.Foo({id: 2}))
// New ChildView nested. Now col.length and ParentView's length are 2.
col.nested(2).remove()
// Removed the just nested View after retrieving it by its key ("id" here).
col.nested(1).set('smth', 'foo')
// The '.change' event got fired on ParentView.
`]
`! +pg=ov +pgo=-3
`# Anatomy of a Sqimitive
Objects in Sqimitive are homogeneous and mostly deal with other "sqimitives" only - the basic building blocks provided by the library, the "primitive".
Each sqimitive has three fundamental features that in different combinations cover an extremely wide range of tasks: `*`#opt options`*, `*`#chld children`* and `*`#evt events`*. Below is a quick high-level overview to demonstrate the main idea; consult the API documentation on `#Core, `#Base and others with hundreds of examples if you want to get all out of Sqimitive.
`##opt Options
` `*Options`* are `@bb:Model-attributes`@ in Backbone's terms - set of key/value pairs defined under `@Base._opt`@ which trigger events on `#change or access, can be normalized and can be virtual (i.e. you can write your accessor that won't correspond to a "physical", listed option). They are solely accessed via `#set() and `#get() methods to create a sort-of public object interface.
Sample code below defines a class with two options - `'isCompleted (boolean) and `'caption (string). When one of them is changed, the associated DOM node is updated (see the sample To-Do application for more: `@https://squizzle.me/js/sqimitive/demo/to-do.html`@).
`[
var MyToDoItem = Sqimitive.jQuery.extend({
_opt: {
isCompleted: true,
caption: 'Do me at home!',
},
events: {
// When task becomes complete or incomplete its DOM element gets that
// class added or removed on the fly.
change_isCompleted: function (newValue) {
this.el.toggleClass('done', newValue)
},
change_caption: 'render',
},
// HTML template for this node's contents as used below.
_tpl: '<h3><%- caption %></h3>',
normalize_isCompleted: function (value) {
// Turn whatever is given as a new value for isCompleted into a boolean.
// If the result is identical to the current value - 'change' is not
// fired.
return !!value
},
// Trim whitespace around the caption.
normalize_caption: function (value) {
return _.trim(value)
},
render: function () {
// Retrieve the map of all options/values and pass them as variables to
// the template. Note that template() implementation differs in NoDash,
// Underscore and LoDash.
var vars = this.get() // = {isCompleted: false, caption: 'foo'}
this.el.html(_.template(this._tpl, vars))
},
})
`]
`##chld Children
` `*Children`* are zero or more "sqimitives" `#nested into zero or more parent "sqimitives". Their events may be forwarded to parent - but only while they are still part of that parent (`#_childEvents); upon removal they are automatically unbound (`#autoOff). When a child is added or removed its parent, if any, gets notified (`#unnested). Also, most of Underscore.js is available as methods to easily filter or transform children into native arrays or objects (`#util).
Parent sqimitives can be of two types: `*`#_owning`* (by default) and `*non-owning`*. First represent a typical tree where each child has exactly one `#_parent and which you can traverse the tree in either direction starting from any node. If you `#nest a child into another parent it's automatically removed from the former owner. Second type is more of a list where you can only traverse from the outside because a child doesn't know in what other sqimitives it might be listed in, if at all, and no automatic removal is done when a child becomes part of another parent.
Sample code below defines a to-do list that is meant for storing `[MyToDoItem`]s from the above example. Note that in Backbone you would have at least two classes: one `@bb:Collection`@ for storing the list of to-do items and one `@bb:View`@ for displaying that collection... Or, to be brutally honest, you would create four classes: a `@bb:Model`@ holding data of a single to-do item, a `@bb:View`@ displaying it, a `@bb:Collection`@ holding to-do items as a whole and another `@bb:View`@ holding the Collection holding the Models - and you still have to link each Model to its View and keep track of their events and DOM elements.
In Sqimitive you can still do all that but it's the author's opinion that such pure concepts are only good for academics and very large projects - most of the time you would rather have something more utilitarian and dirty, if you will. Sqimitive allows you a choice since everything is ultimately a primitive and can be "purified" to the point you need.
Note that the hierarchy of sqimitives (defined by `#nest) doesn't necessary reflect the hierarchy in DOM (of `@jQuery.el`@) - `#_children can have their elements under their `#_parent's `'el or elsewhere, or not have DOM elements at all (as "models" do).
`[
var MyToDoList = Sqimitive.jQuery.extend({
// Add extra protection against accidental foreign class being added as a
// child.
_childClass: MyToDoItem,
// Leading dash means "listen before" - see next section about events.
_childEvents: ['-change', 'change'],
events: {
// To avoid collisions between children-generated and self events those
// forwarded from children get prepended with a period. If you have
// another parent that is forwarding its child's children events then
// another period appears - e.g. '..change'. Think of this as of regular
// '../../path' notation where each period means "one level above".
'.-change': function (sqim, optName, newValue, currentValue) {
// Outputs something like "To-do item's caption ... from foo to bar".
console.log('To-do item\'s ' + optName + ' is about to be changed' +
' from ' + currentValue + ' to ' + newValue)
},
'.change': function (sqim, optName, newValue, currentValue) {
console.log(optName + ' has changed to ' + newValue)
},
},
postInit: function () {
var itemOptions = {isCompleted: false, caption: 'Dummy item'}
var sqim = this.nest(new MyToDoItem(itemOptions))
sqim.set('caption', 'fire them!')
// Because of forwarded events two new messages have appeared in the
// console.
// Can also assign an explicit name (if omitted _cid is used).
this.nest('childName', new this._childClass)
// Can retrieve the object like this:
var sqim = this.nested('childName')
sqim.unnest()
},
// Use Underscore to retireve only children with isCompleted being false.
getIncomplete: function () {
// picker() gets inherited from Sqimitive.Core and is simply a function
// calling a method on the given object with given parameters. In other
// words, equivalent to ES6's: (o) => o.get('isCompleted') LoDash has a
// similar method called rearg().
return this.reject(MyToDoList.picker('get', 'isCompleted'))
},
})
`]
`##evt Events
` `*Events`* are Squimitive's Swiss Army Knife to deal with everything from inheritance (OOP style - `#extend and `#mixIn) and prototyping (native JavaScript style) to dynamic property morphing and dispatching notifications in an Observer-like fashion. When defined upon class declaration handlers are "`#fuse'd" into the class (reducing their performance overhead), otherwise they work as regular event listeners that can be manipulated on run-time using `#on, `#off and others (for instance, they are automatically removed once a `#nested sqimitive is `#unnested).
When you try to listen to an event and there is a method of the same name, Sqimitive turns that method into an event slot and the method itself becomes its first listener. This way every method is potentially an event that you can manipulate on run-time as well as upon declaration (`#events), working with nearly conventional OOP as found in languages like C and PHP while still utilizing the power of dynamic object manipulation as it's meant with JavaScript.
Likewise, if there is no method when you define an event - Sqimitive creates it so that calling it actually triggers the event (`#firer). This way you can always invoke a method without knowing if it's a real function or an event trampoline, bridging the gap between the two.
An example to demonstrate how methods become events "on demand" (`#logEvents):
`[
var MyBase = Sqimitive.Base.extend({
// render() is essentially a function, regular "method".
render: function () {
this.el.text('Hello!')
},
})
// What we're doing is calling a function. It's not an event and won't be
// caught by logEvents().
;(new MyBase).render()
var MyChild = MyBase.extend({
events: {
render: function () {
this.el.append('...I extend...')
},
},
})
// Now we are in fact firing 'render' which is an event with two listeners:
// one from MyBase (called first) and another from MyChild. 'render' is fired
// whenever we call render() in MyChild and descendants, and for them
// logEvents() logs the call.
;(new MyChild).render()
// POTENTIALLY WRONG:
var MyChile = MyChild.extend({
render: function () {
alert('Boom!')
},
})
// Now we're back to event-less render() - a mere function. Note that two
// former 'render' handlers are still present so if we attach a new listener
// to 'render' current render() ("Boom") will be prepended to the list of
// handlers as a 3rd handler and MyChile.render() itself will be replaced by
// firer('render'). It's a bad practice to supersede an "evented" function
// like this and usually indicates a mistake (forgetting that a method of the
// same name exists in some parent). Consequently, logEvents() here won't
// track anything. More on this below, in #evtconc.
;(new MyChile).render()
`]
Compare Sqimitive-style inheritance (`#evtpf) and the traditional inheritance using `'__super__ (which still works in Sqimitive):
`[
var MyBase = Sqimitive.Base.extend({
effect: function (arg) {
console.log('MyBase.effect(' + arg + ')')
return this
},
})
// Traditional JavaScript-OOP inheritance as supported by Backbone.
var JsOopSubclassing = Sqimitive.Base.extend({
// This way you override the inherited method, entirely.
effect: function (arg) {
return 'foo'
},
// Calling the inherited implementation...
effect: function (arg) {
console.log('pre-actions')
// We have to hardcode current class and method names, plus the call is
// quite long.
var result = JsOopSubclassing.__super__.effect.apply(this, arguments)
console.log('post-actions')
return result
},
})
// Event-oriented Sqimitive inheritance.
var SqimitiveSubclassing = Sqimitive.Base.extend({
events: {
// This is how you override the entire method in Sqimitive.
'=effect': function (sup, arg) {
return 'foo'
},
// ...and this is how you call the inherited implementation.
'=effect': function (sup, arg) {
console.log('pre-actions')
// No hardcoded class reference, concise calling format.
var result = sup(this, arguments)
console.log('post-actions')
return result
},
// However, such a full override is rarely needed - most often you only
// need to do something after the original method executes, keeping its
// return value. This one is identical to the above in effect but without
// logging 'pre-actions'.
effect: function (arg) {
console.log('post-actions')
},
// Sometimes we need to do just 'pre-actions' - this is how.
'-effect': function (arg) {
console.log('pre-actions')
},
// Yet at other times we need to call the original code and obtain and/or
// change its return value.
'+effect': function (result, arg) {
console.log('post-actions')
if (result === this) {
// Return something other than the original code returned.
return new That(arg)
}
// Returning undefined or not returning at all retains the current
// result. These are identical:
//return undefined
//return
},
},
})
`]
Finally, to demonstrate the usage of dynamic event binding and method overriding.
`[
var DynamicEvents = Sqimitive.Base.extend({
events: {
slotA: function () {
console.log('slotA')
return 'slotA'
},
},
slotB: function () {
return 'slotB'
},
// Just a property that isn't a function.
notASlot: 123,
listeners: function () {
// When slotA is fired, it outputs "slotA" and "post-effect" to the
// console and returns 'slotA'. Exactly the same would be with slotB even
// though it was't explicitly declared as an event - it becomes one as
// soon as the first handler is attached.
this.on('slotA', function () {
console.log('post-effect')
})
// Nobody said we can't create events out of thin air without defining
// them anywhere first. Note that since it's an event handler and not a
// class method it cannot return any value (it would be ignored). This
// way no disruption is caused if the class suddenly declares a method of
// the same name (this handler will be called after it).
this.on('slotC', function () {
console.log('post-effect')
return 'ignored'
})
this.slotC() // both are
this.fire('slotC') // equivalent.
// Of course, events can have prefixes seen in the previous sample.
this.on('+slotC', function (result) {
console.log('post-effect')
return 'new result'
},
this.on('-slotC', function () {
console.log('pre-effect')
return 'ignored'
})
// You can do a full override as well - and the beauty is that you can
// off() it any time later and original method(s) (now "inside" sup) will
// be put back in place.
this.on('=slotC', function (sup) {
console.log('pre-effect')
var result = sup(this, arguments)
console.log('post-effect')
return result
})
// If you try to turn a non-method into an event nothing will break - you
// will add an event listener all right and fire('notASlot') will work
// but doing notASlot() won't fire the event - only access that property.
// Granted, it's confusing to have an event which works differently from
// this.notASlot() so better avoid it.
this.on('notASlot', function () {
alert('Boo!')
})
alert(this.notASlot) // alerts 123.
this.fire('notASlot') // alerts Boo!
},
dynamic: function () {
var handler = function () { };
var context = new SomeObject;
// Unless "fused" (on class declaration time), each event handler gets a
// unique ID that can be used to unbind it later (very fast). Contrary to
// the common approach, Sqimitive offers no event namespaces (such as
// my.handler) used to unbind group of events - by-context lookup is
// available and it covers most of such use-cases (see below).
var id = this.on('event', handler)
this.off(id)
// You are free to use dots and colons in event names for your needs.
this.on('com.myapi.proc:group', handler)
// Slower but removes all bindings to the given context object among all
// events of this object in one go.
this.on('withContext', handler, context)
this.off(context)
// You can also clear all listeners to a particular event.
this.on('wipeEvent', handler)
this.off('wipeEvent')
},
})
`]
`##evtconc Events vs methods on extension
As demonstrated, method calls are often event triggers in disguise and so both `[obj.method()`] and `[obj.fire('method')`] work exactly the same way. However, there is a catch: when `#extend'ing a class or an object (`#mixIn) you can either add methods within the `#events property or list them as regular properties - and the latter is wrong. Compare: