forked from gaperton/Type-R
-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathindex.html
More file actions
2129 lines (1979 loc) · 159 KB
/
index.html
File metadata and controls
2129 lines (1979 loc) · 159 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
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Type-R v4.0 API Reference</title>
<link rel="icon" href="docs/images/logo-dark.png" />
<link href="docs/lib/stylesheets/screen.css" rel="stylesheet" type="text/css" media="screen" />
<link href="docs/lib/stylesheets/print.css" rel="stylesheet" type="text/css" media="print" />
<link href="docs/lib/stylesheets/default.css" rel="stylesheet" type="text/css" />
<style>
.logo-section img {
vertical-align: middle;
margin: 15px;
height: 48px;
}
.logo-section .logo-text {
vertical-align: middle;
color: white;
display: inline-block;
}
.logo-section .logo-caption {
font-size: 28px;
}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script src="docs/lib/javascripts/all.js" type="text/javascript"></script>
<script>
$(function() {
var langs = [];
langs.push("javascript");
setupLanguages( langs );
});
</script>
</head>
<body class="index">
<a href="#" id="nav-button">
<span>
NAV
<img src="docs/images/navbar.png" />
</span>
</a>
<div class="tocify-wrapper">
<div class="logo-section">
<img src="docs/images/logo.png" />
<div class="logo-text">
<div class="logo-caption">Type-R v4.0</div>
<div>serializable type system</div>
</div>
</div>
<!--<div class="lang-selector">
<a href="#" data-language-name="javascript">javascript</a>
</div>-->
<div class="search">
<input type="text" class="search" id="input-search" placeholder="Search">
</div>
<ul class="search-results"></ul>
<div id="toc">
</div>
<ul class="toc-footer">
<li><a href="https://github.com/VoliJS/Type-R">GitHub repository</a></li>
<li><a href="https://github.com/VoliJS/Type-R/issues">Report the bug</a></li>
<li><a href="https://groups.google.com/forum/#!forum/volicon-open-source">Ask the question</a></li>
</ul>
</div>
<div class="page-wrapper">
<div class="content">
<p><img src="docs/images/overview.png" alt="overview"></p>
<h1 id="type-r-overview">Type-R Overview</h1>
<p>Type-R is a serializable type system for JS and TS. Data structures you describe with Type-R models are automatically and with zero effort:</p>
<ul>
<li>mapped to JSON and, optionally, REST API;</li>
<li>protected from improper updates at run-time;</li>
<li>deeply observable.</li>
</ul>
<h2 id="features">Features</h2>
<p>Mapping of complex JS types to JSON (such as Date, classes, objects trees with cross-references) is automatic with Type-R which eliminates a possibility of programmer's error and improves productivity. Less code to write means less things to unit test, less bugs to fix, and less code to read and understand when making changes.</p>
<p>Type-R models safeguard both frontend and backend from errors in JSON. Programmer's mistake on a frontend can't affect the JSON sent to the server. Wrong JSON received from the server will be validated, sanitized, and can't cause catastrophic failures on the frontend. Type-R guarantee that the data structures will retain the declared shape and it immediately reports improper assignments to the console.</p>
<p>There are virtually no point in unit-testing Type-R models as they are mostly declarative definitions. They are able to check the structural integrity themselves, and Type-R can be instructed to throw exceptions instead of console logging. It makes the unit tests of the data layer unnecessary, and greately reduces an effort when writing an integration test.</p>
<h2 id="react-integration">React integration</h2>
<p>Data structures defined with Type-R are deeply observable by default. They can be used to manage the state of React applications right out of box utilizing "unidirectional data flow" with no additional tooling. Type-R data structures support two-way data binding and attribute-level validation rules, making the a complex forms UI a trivial task. Normally, you don't change your UI code to add validation, just add the validation check to model's attributes.</p>
<h2 id="example">Example</h2>
<p>The main Type-R building block is the <code>Model</code> class with attributes types declaration which behaves as a regular JS class. Models and collections of models can be nested indefinitely to define data structures of arbitrary complexity.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">import</span> { define, Record, Collection } <span class="hljs-keyword">from</span> <span class="hljs-string">'@type-r/models'</span>
<span class="hljs-keyword">import</span> { restfulIO } <span class="hljs-keyword">from</span> <span class="hljs-string">'@type-r/endpoints'</span>
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Record</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span> : <span class="hljs-string">''</span>,
<span class="hljs-attr">email</span> : <span class="hljs-string">''</span>
}
}
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Message</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Record</span> </span>{
<span class="hljs-keyword">static</span> endpoint = restfulIO( <span class="hljs-string">'/api/messages'</span>, {
<span class="hljs-comment">// REST I/O is simulated when the mock data is present, that's how you start.</span>
mockData : [ { <span class="hljs-attr">id</span> : <span class="hljs-number">0</span>, <span class="hljs-attr">createdAt</span> : <span class="hljs-string">"1999-07-25T03:33:29.687Z"</span>, <span class="hljs-attr">author</span> : {}, <span class="hljs-attr">to</span> : [] }]
} );
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">createdAt</span> : <span class="hljs-built_in">Date</span>,
<span class="hljs-attr">author</span> : User, <span class="hljs-comment">// aggregated User record.</span>
to : Collection.of( User ), <span class="hljs-comment">// aggregated collection of users</span>
subject : <span class="hljs-string">''</span>,
<span class="hljs-attr">body</span> : <span class="hljs-string">''</span>
}
}
<span class="hljs-keyword">const</span> messages = Collection.of( Message ).create();
<span class="hljs-keyword">await</span> messages.fetch({ <span class="hljs-attr">params</span> : { <span class="hljs-attr">page</span> : <span class="hljs-number">0</span> }});
<span class="hljs-keyword">const</span> msg = messages.first();
msg.author.name = <span class="hljs-string">'Alan Poe'</span>;
msg.subject = <span class="hljs-string">'Nevermore'</span>;
<span class="hljs-keyword">await</span> msg.save();
</code></pre>
<h2 id="api-reference-and-docs"><a href="https://volijs.github.io/Type-R/">API reference and docs</a></h2>
<h2 id="installation-and-requirements">Installation and requirements</h2>
<aside class="success">IE10+, Edge, Safari, Chrome, and Firefox are supported</aside>
<aside class="warning">IE9 and Opera may work but has not been tested. IE8 won't work.</aside>
<p>Install Type-R models and built-in set of I/O endpoints (restfulIO, localStorageIO, and memoryIO):</p>
<p><code>npm install @type-r/models @type-r/endpoints</code></p>
<p>Install React bindings:</p>
<p><code>npm install @type-r/react</code></p>
<p>Install extended data types (Email, URL, IP, Integer, Microsoft date, UNIX Timestamp date):</p>
<p><code>npm install @type-r/ext-types</code></p>
<h2 id="repository-structure">Repository structure</h2>
<ul>
<li><code>models</code> - Type-R framework core.</li>
<li><code>endpoints</code> - Type-R endpoints enabling models and collections I/O API.</li>
<li><code>react</code> - Type-R React bindings.</li>
<li><p><code>ext-types</code> - Extended data types.</p>
</li>
<li><p><code>globals</code> - <code>@type-r/globals</code> providing backward API compatibility for Type-R v2 apps.</p>
</li>
<li><p><code>mixture</code> - Events, Mixins, and log router. Used by <code>@type-r/models</code>.</p>
</li>
<li><code>tests</code> - private package containing all the unit tests.</li>
<li><code>examples/*</code> - example <code>@type-r/react</code> apps.</li>
</ul>
<h1 id="defining-the-model">Defining the model</h1>
<h2 id="overview">Overview</h2>
<p>The <code>Model</code> class is a main building block of Type-R representing serializable and observable object. Models are mapped to objects in JSON according to the types in attributes definition. Model asserts attribute types on assignment and guarantee that these types will be preserved at run time, continuously checking the client-server protocol and guarding it from errors on both ends.</p>
<p>There are four sorts of model attributes:</p>
<ul>
<li><strong>Primitive</strong> types (Number, String, Boolen) mapped to JSON directly.</li>
<li><strong>Immutable</strong> types (Date, Array, Object, or custom immutable class).</li>
<li><strong>Aggregated</strong> models and collections represented as nested objects and arrays of objects in JSON.</li>
<li><strong>References</strong> to models and collections. References can be either:<ul>
<li><strong>serializable</strong> to JSON as an ids of the referenced models, used to model one-to-many and many-to-many relashinships in JSON;</li>
<li><strong>observable</strong>, which is a non-persistent run-time only reference, used to model temporary application state.</li>
</ul>
</li>
</ul>
<p>Model is an observable state container efficiently detecting changes in all of its attributes, including the deep changes in aggregated and observable reference attributes. Type-R models follow BackboneJS change events model which makes it a straightforward task to integrate with virtually any view layer.</p>
<p>Model attribute definitions have extended metadata to control all aspects of model behavior on the particular attribute's level, making it easy to define reusable attribute types with custom serialization, validation, and reactions on changes.</p>
<p>Type-R models are almost as easy to use as plain JS objects, and much easier when more complex data types and serialization scenarios are involved. </p>
<pre><code class="highlight javascript"><span class="hljs-comment">// We have `/api/users` endpoint on the server. Lets describe what it is with a model.</span>
<span class="hljs-keyword">import</span> { define, Model, Collection, value, type } <span class="hljs-keyword">from</span> <span class="hljs-string">'@type-r/models'</span>
<span class="hljs-keyword">import</span> { restfulIO } <span class="hljs-keyword">from</span> <span class="hljs-string">'@type-r/endpoints'</span>
<span class="hljs-keyword">import</span> { Role } <span class="hljs-keyword">from</span> <span class="hljs-string">'./roles'</span> <span class="hljs-comment">// <- That's another model definition.</span>
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-comment">// Tell this model that it has a REST endpoint on the server to enable I/O API.</span>
<span class="hljs-keyword">static</span> endpoint = restfulIO( <span class="hljs-string">'/api/users'</span> );
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span> : <span class="hljs-string">''</span>, <span class="hljs-comment">// type is inferred from the default value as String</span>
email : <span class="hljs-string">''</span>, <span class="hljs-comment">// String</span>
isActive : <span class="hljs-literal">false</span>, <span class="hljs-comment">// Boolean</span>
<span class="hljs-comment">// nested array of objects in JSON, collection of Role models in JS</span>
roles : Collection.of( Role )
<span class="hljs-comment">// Add metadata to a Number attribute.</span>
<span class="hljs-comment">// Receive it from the server, but don't send it back on save.</span>
failedLoginCount : value( <span class="hljs-number">0</span> ).toJSON( <span class="hljs-literal">false</span> ), <span class="hljs-comment">// Number</span>
<span class="hljs-comment">// ISO UTC date string in JSON, `Date` in JS. Read it as Date, but don't save it.</span>
createdAt : type( <span class="hljs-built_in">Date</span> ).toJSON( <span class="hljs-literal">false</span> ), <span class="hljs-comment">// Date</span>
}
}
<span class="hljs-comment">// Somewhere in other code...</span>
<span class="hljs-comment">// Fetch the users list from the server...</span>
<span class="hljs-keyword">const</span> users = <span class="hljs-keyword">await</span> Collection.of( User ).create().fetch();
<span class="hljs-comment">// Subscribe for the changes...</span>
users.onChanges( <span class="hljs-function"><span class="hljs-params">()</span> =></span> <span class="hljs-built_in">console</span>.log( <span class="hljs-string">'changes!'</span> ) );
<span class="hljs-comment">// ...and make the first user active.</span>
<span class="hljs-keyword">const</span> firstUser = users.first();
firstUser.isActive = <span class="hljs-literal">true</span>; <span class="hljs-comment">// Here we'll got the 'changes!' log message</span>
<span class="hljs-keyword">await</span> firstUser.save();
</code></pre>
<p>Model is defined by extending the <code>Model</code> class with attributes definition and applying the <code>@define</code> decorator.</p>
<h3 id="decorator-define"><code>decorator</code> @define</h3>
<p>Class decorator which must preceede the <code>Model</code> subclass declaration. <code>@define</code> is a mixin which will read attribute definitions from the Model's <code>static attributes</code> and generate class properties accessors accordingly.</p>
<p><code>@define</code> assembles attribute's update pipeline for each attribute individually depending on its type and employs a number of JIT-friendly optimizations. As a result, Type-R models handle updates about 10 times faster than frameworks like BackboneJS in all popular browsers making a collections of 10K objects a practical choice on a frontend.</p>
<h3 id="static-attributes"><code>static</code> attributes</h3>
<p>Attributes must be defined in <code>static attributes</code>. In a majority of cases, an attribute definition is a constructor function or the default value.</p>
<p>If the function is used as attribute definition, it's assumed to be am attributes constructor and designates attribute type. If it's not a function, it's treated as an attribute's default value and type is being determened from this value type. The following attribute definitions are equivalent:</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span> : <span class="hljs-built_in">String</span>, <span class="hljs-comment">// Same as ''</span>
email : <span class="hljs-string">''</span> <span class="hljs-comment">// Same as String</span>
}
}
</code></pre>
<p>To assign <code>null</code> as a default value both attribute type and value need to be specified, as the type cannot be inferred from <code>null</code>. It's done through the attribute's metadata like this:</p>
<pre><code>nullStringAttr : type( <span class="hljs-built_in">String</span> ).value( <span class="hljs-literal">null</span> )
</code></pre><h3 id="static-idattribute-attrname-"><code>static</code> idAttribute = 'attrName'</h3>
<p>A model's unique identifier is stored under the pre-defined <code>id</code> attribute.
If you're directly communicating with a backend (CouchDB, MongoDB) that uses a different unique key, you may set a Model's <code>idAttribute</code> to transparently map from that key to id.</p>
<p>Model's <code>id</code> property will still be linked to Model's id, no matter which value <code>idAttribute</code> has.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Meal</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> idAttribute = <span class="hljs-string">"_id"</span>;
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">_id</span> : <span class="hljs-built_in">Number</span>,
<span class="hljs-attr">name</span> : <span class="hljs-string">''</span>
}
}
<span class="hljs-keyword">const</span> cake = <span class="hljs-keyword">new</span> Meal({ <span class="hljs-attr">_id</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">name</span>: <span class="hljs-string">"Cake"</span> });
alert(<span class="hljs-string">"Cake id: "</span> + cake.id);
</code></pre>
<h3 id="static-endpoint"><code>static</code> endpoint</h3>
<p>Enable model's I/O API by specifying an I/O endpoint. There are the list of endpoints in <code>@type-r/enpoints</code> package to work with browsers local storage, REST, and mock data.</p>
<h2 id="primitive-attributes">Primitive attributes</h2>
<p>Primitive attribute types are directly mapped to their values in JSON.
Assigned value is being converted to the declared attribute type at run time. I.e. if an email is declared to be a string, it's guaranteed that it will always remain a string.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span> : <span class="hljs-string">''</span>,
<span class="hljs-attr">email</span> : <span class="hljs-built_in">String</span>, <span class="hljs-comment">// ''</span>
isActive : <span class="hljs-built_in">Boolean</span>, <span class="hljs-comment">// false</span>
failedLoginCount : <span class="hljs-built_in">Number</span> <span class="hljs-comment">// 0</span>
}
}
</code></pre>
<h3 id="attribute-number"><code>attribute</code> : Number</h3>
<p>JS <code>number</code> primitive type. Assigned value (except <code>null</code>) is automatically converted to <code>number</code> with a constructor call <code>Number( value )</code>.</p>
<p>If something other than <code>null</code>, number, or a proper string representation of number is being assigned, the result of the convertion is <code>NaN</code> and the warning
will be displayed in the console. Models with <code>NaN</code> in their <code>Number</code> attributes will fail the validation check.</p>
<h3 id="attribute-boolean"><code>attribute</code> : Boolean</h3>
<p>JS <code>boolean</code> primitive type. Assigned value (except <code>null</code>) is automatically converted to <code>true</code> or <code>false</code> with a constructor call <code>Boolean( value )</code>.</p>
<p>This attribute type is always valid.</p>
<h3 id="attribute-string"><code>attribute</code> : String</h3>
<p>JS <code>string</code> primitive type. Assigned value (except <code>null</code>) is automatically converted to <code>string</code> with a constructor call <code>String( value )</code>.</p>
<p>This attribute type is always valid.</p>
<h2 id="immutable-attributes">Immutable attributes</h2>
<h3 id="attribute-date"><code>attribute</code> : Date</h3>
<p>JS <code>Date</code> type represented as ISO UTC date string in JSON. If assigned value is not a <code>Date</code> or <code>null</code>, it is automatically converted to <code>Date</code> with a constructor call <code>new Date( value )</code>.</p>
<p>If something other than the integer timestamp or the proper string representation of date is being assigned, the result of the convertion is <code>Invalid Date</code> and the warning
will be displayed in the console. Models with <code>Invalid Date</code> in their <code>Date</code> attributes will fail the validation check.</p>
<p>Note that the changes to a <code>Date</code> attribute are not observable; dates are treated as immutables and need to be replaced for the model to notice the change.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span> : <span class="hljs-string">''</span>,
<span class="hljs-attr">email</span> : <span class="hljs-built_in">String</span>, <span class="hljs-comment">// ''</span>
createdAt : <span class="hljs-built_in">Date</span>
}
}
</code></pre>
<h3 id="attribute-array"><code>attribute</code> : Array</h3>
<p>Immutable JS Array mapped to JSON as is. Type-R assumes that an Array attribute contains a raw JSON with no complex data types in it. </p>
<p><code>Array</code> type is primarily used to represent a list of primitives. It's recommended to use aggregated collections of models for the array of objects in JSON.</p>
<p>If an assigned value is not an <code>Array</code>, the assignment will be ignored and a warning will be displayed in the console.
Array attributes are always valid.</p>
<p>Note that the changes to an <code>Array</code> attribute are not observable; arrays need to be replaced with an updated copy for the model to notice the change. Type-R uses <code>Linked</code> class proxying popular array methods from <code>@linked/value</code> package to simplify manipulations with immutable arrays.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span> : <span class="hljs-built_in">String</span>,
<span class="hljs-attr">roles</span> : [ <span class="hljs-string">'admin'</span> ]
}
}
user.$.roles.push( <span class="hljs-string">'user'</span> );
</code></pre>
<h3 id="attribute-object"><code>attribute</code> : Object</h3>
<p>Immutable JS Object mapped to JSON as is. Type-R assumes that an Object attribute contains a raw JSON with no complex data types in it. </p>
<p>Plain JSON object type primarily used to represent dynamic hashmaps of primitives. It's recommended to use aggregated models for the complex nested objects in JSON.</p>
<p>If an assigned value is not a plain object, the assignment will be ignored and the warning will be displayed in the console. Object attributes are always valid.</p>
<p>Changes in Object attribute are not observable, object needs to be copied for the Type-R to notice the change. Type-R uses <code>Linked</code> class from <code>@linked/value</code> package to simplify manipulations with immutable objects.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span> : <span class="hljs-built_in">String</span>,
<span class="hljs-attr">roles</span> : { <span class="hljs-attr">admin</span> : <span class="hljs-literal">true</span> }
}
}
user.$.roles.at( <span class="hljs-string">'user'</span> ).set( <span class="hljs-literal">false</span> );
</code></pre>
<h3 id="attribute-function"><code>attribute</code> : Function</h3>
<p>Function as an attribute value. Please note that functions are not serializable.</p>
<p>Function attributes are initialized with an empty function by default. If something other than function will be assigned, it will be ignored with a error in a console.</p>
<h3 id="attribute-classconstructor"><code>attribute</code> : ClassConstructor</h3>
<p>If the class constructor used as an attribute type and it's not a model or collection subclass, it is considered to be an <strong>immutable attribute</strong>. Type-R has following assumptions on immutable attributes class:</p>
<ul>
<li>It has <code>toJSON()</code></li>
<li>Its constructor can take JSON as a single argument.</li>
</ul>
<p>Changes in immutable attributes <em>are not observable</em>, object needs to be replaced with its updated copy for the model to notice the change. The class itself doesn't need to be immutable, though, as Type-R makes no other assumptions.</p>
<h2 id="aggregated-models">Aggregated models</h2>
<p>Aggregated models are the part of the model represented in JSON as nested objects. Aggregated models <strong>will</strong> be copied, destroyed, and validated together with the parent.</p>
<p>Model has an exclusive ownership rights on its aggregated attributes. Aggregated models can't be assigned to another model's attribute unless the source attribute is cleared or the target attribute is a reference.</p>
<p>Aggregated models are deeply observable. A change of the aggregated model's attributute will trigger the <code>change</code> event on its parent.</p>
<h3 id="attribute-modelclass"><code>attribute</code> : ModelClass</h3>
<p>Model attribute containing another model. Describes an attribute represented in JSON as an object.</p>
<ul>
<li>Attribute <strong>is</strong> serializable as <code>{ attr1 : value1, attr2 : value2, ... }</code></li>
<li>Changes of enclosed model's attributes <strong>will not</strong> trigger change of the model.</li>
</ul>
<pre><code class="highlight javascript"><span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">users</span> : Collection.of( User ),
<span class="hljs-attr">selectedUser</span> : memberOf( <span class="hljs-string">'users'</span> )
}
</code></pre>
<h3 id="attribute-collection-of-modelclass-"><code>attribute</code> : Collection.of( ModelClass )</h3>
<p>Collection containing models. The most popular collection type describing JSON array of objects.</p>
<ul>
<li>Collection <strong>is</strong> serializable as <code>[ { ...user1 }, { ...user2 }, ... ]</code></li>
<li>All changes to enclosed model's attributes are treated as a change of the collection.</li>
</ul>
<pre><code class="highlight javascript"><span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">users</span> : Collection.of( User )
}
</code></pre>
<h2 id="serializable-model-references">Serializable model references</h2>
<p>Model attribute with reference to existing models or collections. Referenced objects <strong>will not</strong> be copied, destroyed, or validated as a part of the model.</p>
<p>References can be either deeply observable <strong>or</strong> serializable.</p>
<p>Serializable id-references is a Type-R way to describe many-to-one and many-to-many relashionship in JSON. Models must have an id to have serializable references. Serializable id-references are not observable.</p>
<p>Id references represented as model ids in JSON and appears as regular models at run time. Ids are being resolved to actual model instances with lookup in the base collection <strong>on first attribute access</strong>, which allows the definition of a complex serializable object graphs consisting of multiple collections of cross-referenced models fetched asynchronously.</p>
<h3 id="basecollection-parameter">baseCollection parameter</h3>
<p><code>baseCollection</code> argument could be:</p>
<ul>
<li>a direct reference to the singleton collection object</li>
<li>a function returning the collection which is called in a context of the model</li>
<li>a symbolic path, which is a string with a dot-separated path resolved relative to the model's <code>this</code>.</li>
</ul>
<h3 id="attribute-modelclass-memberof-basecollection-"><code>attribute</code> : ModelClass.memberOf( baseCollection )</h3>
<p>Model attribute holding serializable id-reference to a model from the base collection. Used to describe one-to-may relashioship with a model attribute represented in JSON as a model id.</p>
<ul>
<li>Attribute <strong>is</strong> serializable as <code>model.id</code></li>
<li>Changes of enclosed model's attributes <strong>will not</strong> trigger the change of the attribute.</li>
</ul>
<p>Attribute can be assigned with either the model from the base collection or the model id. If there are no such a model in the base collection <strong>on the moment of first attribute access</strong>, the attribute value will be <code>null</code>.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">static</span> attributes = {
<span class="hljs-comment">// Nested collection of users.</span>
users : Collection.of( User ),
<span class="hljs-comment">// Model from `users` serializable as `user.id`</span>
selectedUser : memberOf( <span class="hljs-string">'users'</span> )
}
</code></pre>
<h3 id="attribute-collection-subsetof-basecollection-"><code>attribute</code> : Collection.subsetOf( baseCollection )</h3>
<p>Collection of id-references to models from base collection. Used to describe many-to-many relationship with a collection of models represented in JSON as an array of model ids. The subset collection itself <strong>will be</strong> be copied, destroyed, and validated as a part of the owner model, but not the models in it.</p>
<ul>
<li>Collection <strong>is</strong> serializable as <code>[ user1.id, user2.id, ... ]</code>.</li>
<li>Changes of enclosed model's attributes <strong>will not</strong> trigger change of the collection.</li>
</ul>
<p>If some models are missing in the base collection <strong>on the moment of first attribute access</strong>, such a models will be removed from a subset collection.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">static</span> attributes = {
<span class="hljs-comment">// Nested collection of users.</span>
users : Collection.of( User ),
<span class="hljs-comment">// Collection with a subset of `users` serializable as an array of `user.id`</span>
selectedUsers : Collection.subsetOf( <span class="hljs-string">'users'</span> ) <span class="hljs-comment">// 'users' == function(){ return this.users }</span>
}
</code></pre>
<h2 id="observable-references">Observable references</h2>
<p>Non-serializable run time reference to models or collections. Used to describe a temporary observable application state.</p>
<h3 id="attribute-refto-modelorcollection-"><code>attribute</code> : refTo( ModelOrCollection )</h3>
<p>Model attribute holding a reference to a model or collection.</p>
<ul>
<li>Attribute <strong>is not</strong> serializable.</li>
<li>Changes of enclosed model's attributes <strong>will</strong> trigger change of the model.</li>
</ul>
<pre><code class="highlight javascript"><span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">users</span> : refTo( Collection.of( User ) ),
<span class="hljs-attr">selectedUser</span> : refTo( User )
}
</code></pre>
<h3 id="attribute-collection-ofrefsto-user-"><code>attribute</code> : Collection.ofRefsTo( User )</h3>
<p>Collection of references to models. The collection itself <strong>will be</strong> be copied, destroyed, and validated as a part of the model, but not the models in it.</p>
<ul>
<li>Collection <strong>is not</strong> serializable.</li>
<li>Changes of enclosed model's attributes <strong>will</strong> trigger change of the collection.</li>
</ul>
<pre><code class="highlight javascript"><span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">users</span> : Collection.of( User ),
<span class="hljs-attr">selectedUsers</span> : Collection.ofRefsTo( User )
}
</code></pre>
<h2 id="attribute-metadata">Attribute metadata</h2>
<h3 id="attribute-type-type-"><code>attribute</code> : type(Type)</h3>
<p>In Type-R, every aspect of a model behavior can be customized on the attribute level through the attaching metadata to the attribute definitions. Since the attribute definition is a regular JavaScript, an attribute definition with metadata can be shared and reused across the different models and projects. Such an object is called <em>attribute metatype</em>.</p>
<p>Metadata is attached through a chain of calls after the <code>type( Ctor )</code> call. Attribute's default value is the most common example of such a metadata.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">import</span> { define, type, Model }
<span class="hljs-keyword">const</span> AString = type( <span class="hljs-built_in">String</span> ).value( <span class="hljs-string">"a"</span> );
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Dummy</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">a</span> : AString,
<span class="hljs-attr">b</span> : type( <span class="hljs-built_in">String</span> ).value( <span class="hljs-string">"b"</span> )
}
}
</code></pre>
<h3 id="attribute-type-constructor-value-defaultvalue-"><code>attribute</code> : type(Constructor).value(defaultValue)</h3>
<p>Declare an attribute with type Constructor having the custom <code>defaultValue</code>. Normally, all attributes are initialized with a default constructor call.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Person</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">phone</span> : type( <span class="hljs-built_in">String</span> ).value( <span class="hljs-literal">null</span> ) <span class="hljs-comment">// String attribute which is null by default.</span>
...
}
}
</code></pre>
<h3 id="attribute-value-defaultvalue-"><code>attribute</code> : value( defaultValue )</h3>
<p>Similar to <code>type( T ).value( x )</code>, but infers the type from the default value. So, for instance, <code>type( String )</code> is equivalent to <code>value("")</code>.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">import</span> { define, type, Model }
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Dummy</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">a</span> : value( <span class="hljs-string">"a"</span> )
}
}
</code></pre>
<h3 id="metatype-type-type-check-predicate-errormsg-"><code>metatype</code> type( Type ).check( predicate, errorMsg? )</h3>
<p>Attribute-level validator.</p>
<ul>
<li><code>predicate : value => boolean</code> is the function taking attribute's value and returning <code>true</code> whenever the value is valid.</li>
<li>optional <code>errorMsg</code> is the error message which will be passed in case if the validation fail.</li>
</ul>
<p>If <code>errorMsg</code> is omitted, error message will be taken from <code>predicate.error</code>. It makes possible to define reusable validation functions.</p>
<pre><code class="highlight javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isAge</span>(<span class="hljs-params"> years </span>)</span>{
<span class="hljs-keyword">return</span> years >= <span class="hljs-number">0</span> && years < <span class="hljs-number">200</span>;
}
isAge.error = <span class="hljs-string">"Age must be between 0 and 200"</span>;
</code></pre>
<p>Attribute may have any number of checks attached which are being executed in a sequence. Validation stops when first check in sequence fails.
It can be used to define reusable attribute types as demonstrated below:</p>
<pre><code class="highlight javascript"><span class="hljs-comment">// Define new attribute metatypes encapsulating validation checks.</span>
<span class="hljs-keyword">const</span> Age = type( <span class="hljs-built_in">Number</span> )
.check( <span class="hljs-function"><span class="hljs-params">x</span> =></span> x == <span class="hljs-literal">null</span> || x >= <span class="hljs-number">0</span>, <span class="hljs-string">'I guess you are a bit older'</span> )
.check( <span class="hljs-function"><span class="hljs-params">x</span> =></span> x == <span class="hljs-literal">null</span> || x < <span class="hljs-number">200</span>, <span class="hljs-string">'No way man can be that old'</span> );
<span class="hljs-keyword">const</span> Word = type( <span class="hljs-built_in">String</span> ).check( <span class="hljs-function"><span class="hljs-params">x</span> =></span> indexOf( <span class="hljs-string">' '</span> ) < <span class="hljs-number">0</span>, <span class="hljs-string">'No spaces allowed'</span> );
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Person</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">firstName</span> : Word,
<span class="hljs-attr">lastName</span> : Word,
<span class="hljs-attr">age</span> : Age
}
}
</code></pre>
<h3 id="metatype-type-type-required"><code>metatype</code> type( Type ).required</h3>
<p>The special case of attribute-level check cutting out empty values. Attribute value must be truthy to pass, <code>"Required"</code> is used as validation error.</p>
<p><code>isRequired</code> is the first validator to check, no matter in which order validators were attached.</p>
<h3 id="attribute-type-type-get-hook-"><code>attribute</code> : type(Type).get(<code>hook</code>)</h3>
<p>Attach get hook to the model's attribute. <code>hook</code> is the function of signature <code>( value, attr ) => value</code> which is used to transform the attribute's value right <em>before it will be read</em>. Hook is executed in the context of the model.</p>
<h3 id="attribute-type-type-set-hook-"><code>attribute</code> : type(Type).set(<code>hook</code>)</h3>
<p>Attach the set hook to the model's attribute. <code>hook</code> is the function of signature <code>( value, attr ) => value</code> which is used to transform the attribute's value <em>before it will be assigned</em>. Hook is executed in the context of the model.</p>
<p>If set hook will return <code>undefined</code>, it will cancel attribute update.</p>
<h3 id="metatype-type-type-tojson-false-"><code>metatype</code> type( Type ).toJSON( false )</h3>
<p>Do <em>not</em> serialize the specific attribute.</p>
<h3 id="metatype-type-type-tojson-value-name-options-json-"><code>metatype</code> type( Type ).toJSON( ( value, name, options ) => json )</h3>
<p>Override the default serialization for the specific model's attribute.</p>
<p>Attribute is not serialized when the function return <code>undefined</code>.</p>
<h3 id="metatype-type-type-parse-json-name-value-"><code>metatype</code> type( Type ).parse( ( json, name ) => value )</h3>
<p>Transform the data before it will be assigned to the model's attribute.</p>
<p>Invoked when the <code>{ parse : true }</code> option is set.</p>
<pre><code class="highlight javascript"><span class="hljs-comment">// Define custom boolean attribute type which is serialized as 0 or 1.</span>
<span class="hljs-keyword">const</span> MyWeirdBool = type( <span class="hljs-built_in">Boolean</span> )
.parse( <span class="hljs-function"><span class="hljs-params">x</span> =></span> x === <span class="hljs-number">1</span> )
.toJSON( <span class="hljs-function"><span class="hljs-params">x</span> =></span> x ? <span class="hljs-number">1</span> : <span class="hljs-number">0</span> );
</code></pre>
<h3 id="metatype-type-type-watcher-watcher-"><code>metatype</code> type( Type ).watcher( watcher )</h3>
<p>Attach custom reaction on attribute change. <code>watcher</code> can either be the model's method name or the function <code>( newValue, attr ) => void</code>. Watcher is executed in the context of the model.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span> : type( <span class="hljs-built_in">String</span> ).watcher( <span class="hljs-string">'onNameChange'</span> ),
<span class="hljs-attr">isAdmin</span> : <span class="hljs-built_in">Boolean</span>,
}
onNameChange(){
<span class="hljs-comment">// Cruel. But we need it for the purpose of the example.</span>
<span class="hljs-keyword">this</span>.isAdmin = <span class="hljs-keyword">this</span>.name.indexOf( <span class="hljs-string">'Admin'</span> ) >= <span class="hljs-number">0</span>;
}
}
</code></pre>
<h3 id="metatype-type-modelorcollection-changeevents-false-"><code>metatype</code> type( ModelOrCollection ).changeEvents( false )</h3>
<p>Turn off observable changes for the attribute.</p>
<p>Model automatically listens to change events of all nested models and collections triggering appropriate change events for its attributes. This declaration turns it off for the specific attribute.</p>
<h3 id="metatype-type-type-events-eventname-handler-"><code>metatype</code> type( Type ).events({ eventName : handler, ... })</h3>
<p>Automatically manage custom event subscription for the attribute. <code>handler</code> is either the method name or the handler function. <code>Type</code> needs to be a <code>Messenger</code> subclass from <code>@type-r/mixture</code> or include it as a mixin.</p>
<p>Both <code>Model</code> and <code>Collection</code> includes <code>Messenger</code> as a mixin.</p>
<h3 id="metatype-type-type-endpoint-endpoint-"><code>metatype</code> type( Type ).endpoint( <code>endpoint</code> )</h3>
<p>Override or define an I/O endpoint for the specific model's attribute.</p>
<h2 id="custom-attribute-metatypes">Custom attribute metatypes</h2>
<p>"Attribute metatype" is the Type-R attribute type descriptor with metadata attached.
Metatype is created by assigning the result of <code>type( T )</code> expression to some variable.</p>
<p>The following attribute types are available from <code>@type-r/ext-types</code> package.</p>
<h3 id="attribute-microsoftdate"><code>attribute</code> : MicrosoftDate</h3>
<p><code>Date</code> attribute represented in JSON as Microsoft date (represented in JSON as string <code>/Date(timestamp)</code>)</p>
<h3 id="attribute-timestamp"><code>attribute</code> : Timestamp</h3>
<p><code>Date</code> attribute represented in JSON as UNIX timestamp (the result of <code>date.getTime()</code>).</p>
<h3 id="attribute-integer"><code>attribute</code> : Integer</h3>
<p><code>Number</code> attribute converting value to integer on assignment. Can be called as function.</p>
<h3 id="attribute-email"><code>attribute</code> : Email</h3>
<p><code>String</code> attribute with email validation check.</p>
<h3 id="attribute-ipaddress"><code>attribute</code> : IPAddress</h3>
<p><code>String</code> attribute with IP address validation check.</p>
<h3 id="attribute-url"><code>attribute</code> : Url</h3>
<p><code>String</code> attribute with URL validation check.</p>
<h1 id="model-api">Model API</h1>
<h2 id="create-and-dispose">Create and dispose</h2>
<h3 id="new-model-attrs-options-">new Model( attrs?, options?)</h3>
<p>Create the model. If no <code>attrs</code> is supplied, initialize it with defaults taken from the attributes definition.</p>
<p>When no default value is explicitly provided for an attribute, it's initialized as <code>new AttributeType()</code> (just <code>AttributeType()</code> for primitives). When the default value is provided and it's not compatible with the attribute type, the value is converted to the proper type with <code>new Type( defaultValue )</code> call.</p>
<p>If <code>{parse: true}</code> option is set the <code>attrs</code> is assumed to be the JSON. In this case, <code>model.parse( attr )</code> and attribute's <code>parse</code> hooks will be called to give you an option to transform the JSON.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Book</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">title</span> : <span class="hljs-string">''</span>,
<span class="hljs-attr">author</span> : <span class="hljs-string">''</span>
}
}
<span class="hljs-keyword">const</span> book = <span class="hljs-keyword">new</span> Book({
<span class="hljs-attr">title</span>: <span class="hljs-string">"One Thousand and One Nights"</span>,
<span class="hljs-attr">author</span>: <span class="hljs-string">"Scheherazade"</span>
});
</code></pre>
<h3 id="modelclass-from-attrs-options-">ModelClass.from(attrs, options?)</h3>
<p>Create <code>RecordClass</code> from attributes. Similar to direct model creation, but supports additional option for strict data validation.
If <code>{ strict : true }</code> option is passed the model validation will be performed immediately and an exception will be thrown in case of an error.</p>
<p>Type-R always perform type checks on assignments, convert types, and reject improper updates reporting it as error. It won't, however, execute custom validation
rules on every updates as validation is evaluated lazily. <code>strict</code> option will invoke custom validators and will throw on every error or warning instead of reporting them and continue.</p>
<pre><code class="highlight javascript"><span class="hljs-comment">// Fetch model with a given id.</span>
<span class="hljs-keyword">const</span> book = <span class="hljs-keyword">await</span> Book.from({ <span class="hljs-attr">id</span> : <span class="hljs-number">5</span> }).fetch();
<span class="hljs-comment">// Validate the body of an incoming HTTP request.</span>
<span class="hljs-comment">// Throw an exception if validation fails.</span>
<span class="hljs-keyword">const</span> body = MyRequestBody.from( ctx.request.body, { <span class="hljs-attr">parse</span> : <span class="hljs-literal">true</span>, <span class="hljs-attr">strict</span> : <span class="hljs-literal">true</span> });
</code></pre>
<h3 id="static-modelclass-create-attrs-options-"><code>static</code> ModelClass.create( attrs, options )</h3>
<p>Static factory function used internally by Type-R to create instances of the model.</p>
<p>May be redefined in the abstract Model base class to make it serializable type.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Widget</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">type</span> : <span class="hljs-built_in">String</span>
}
<span class="hljs-keyword">static</span> create( attrs, options ){
<span class="hljs-keyword">switch</span>( attrs.type ){
<span class="hljs-keyword">case</span> <span class="hljs-string">"typeA"</span> : <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> TypeA( attrs, options );
<span class="hljs-keyword">case</span> <span class="hljs-string">"typeB"</span> : <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> TypeB( attrs, options );
}
}
}
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TypeA</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Widget</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">type</span> : <span class="hljs-string">"typeA"</span>,
...
}
}
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TypeB</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Widget</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">type</span> : <span class="hljs-string">"typeB"</span>,
...
}
}
</code></pre>
<h3 id="model-clone-">model.clone()</h3>
<p>Create the deep copy of the aggregation tree, recursively cloning all aggregated models and collections. References to shared members will be copied, but not shared members themselves.</p>
<h3 id="callback-model-initialize-attrs-options-"><code>callback</code> model.initialize(attrs?, options?)</h3>
<p>Called at the end of the <code>Model</code> constructor when all attributes are assigned and the model's inner state is properly initialized. Takes the same arguments as
a constructor.</p>
<h3 id="model-dispose-">model.dispose()</h3>
<p>Recursively dispose the model and its aggregated members. "Dispose" means that elements of the aggregation tree will unsubscribe from all event sources. It's crucial to prevent memory leaks in SPA.</p>
<p>The whole aggregation tree will be recursively disposed, shared members won't.</p>
<h2 id="read-and-update">Read and update</h2>
<h3 id="model-cid">model.cid</h3>
<p>Read-only client-side model's identifier. Generated upon creation of the model and is unique for every model's instance. Cloned models will have different <code>cid</code>.</p>
<h3 id="model-id">model.id</h3>
<p>Predefined model's attribute, the <code>id</code> is an arbitrary string (integer id or UUID). <code>id</code> is typically generated by the server. It is used in JSON for id-references.</p>
<p>Records can be retrieved by <code>id</code> from collections, and there can be just one instance of the model with the same <code>id</code> in the particular collection.</p>
<h3 id="model-attrname">model.attrName</h3>
<p>Model's attributes can be directly accessed with their names as a regular class properties.</p>
<p>If the value is not compatible with attribute's type from the declaration on assignment, it is converted with <code>Type( value )</code> call for primitive types, and with <code>new Type( value )</code> for other types.</p>
<p>There is an important exception in type convertion logic for models and collections. Instead of applying a contructor, Type-R will try to update existing model and collection instances in place calling their <code>set()</code> method instead. This logic keeps the model and collection references stable and safe to pass around.</p>
<p>Model triggers events on changes:</p>
<ul>
<li><code>change:attrName</code> <em>( model, value )</em>.</li>
<li><code>change</code> <em>( model )</em>.</li>
</ul>
<aside class="warning">Please note, that you *have to declare all attributes* in `static attributes` declaration.</aside>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Book</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">title</span> : <span class="hljs-built_in">String</span>,
<span class="hljs-attr">author</span> : <span class="hljs-built_in">String</span>
price : <span class="hljs-built_in">Number</span>,
<span class="hljs-attr">publishedAt</span> : <span class="hljs-built_in">Date</span>,
<span class="hljs-attr">available</span> : <span class="hljs-built_in">Boolean</span>
}
}
<span class="hljs-keyword">const</span> myBook = <span class="hljs-keyword">new</span> Book({ <span class="hljs-attr">title</span> : <span class="hljs-string">"State management with Type-R"</span> });
myBook.author = <span class="hljs-string">'Vlad'</span>; <span class="hljs-comment">// That works.</span>
myBook.price = <span class="hljs-string">'Too much'</span>; <span class="hljs-comment">// Converted with Number( 'Too much' ), resulting in NaN.</span>
myBook.price = <span class="hljs-string">'123'</span>; <span class="hljs-comment">// = Number( '123' ).</span>
myBook.publishedAt = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(); <span class="hljs-comment">// Type is compatible, no conversion.</span>
myBook.publishedAt = <span class="hljs-string">'1678-10-15 12:00'</span>; <span class="hljs-comment">// new Date( '1678-10-15 12:00' )</span>
myBook.available = some && weird || condition; <span class="hljs-comment">// Will always be Boolean. Or null.</span>
</code></pre>
<h3 id="model-set-attrname-value-options-options-">model.set({ attrName : value, ... }, options? : <code>options</code>)</h3>
<p>Bulk assign model's attributes using the same logic as attribute's assignment.</p>
<p>Model will trigger <code>change:attrName</code> <em>( model, value )</em> event per changed attribute and a single <code>change</code> <em>( model )</em> event at the end.</p>
<h3 id="model-transaction-fun-">model.transaction(fun)</h3>
<p>Execute the all changes made to the model in <code>fun</code> as single transaction triggering the single <code>change</code> event at the end.</p>
<p>All model updates occurs in the scope of transactions. Transaction is the sequence of changes which results in a single <code>change</code> event.
Transaction can be opened either manually or implicitly with calling <code>set()</code> or assigning an attribute.
Any additional changes made to the model in <code>change:attr</code> event handler will be executed in the scope of the original transaction, and won't trigger additional <code>change</code> events.</p>
<pre><code class="highlight javascript">some.model.transaction( <span class="hljs-function"><span class="hljs-params">model</span> =></span> {
model.a = <span class="hljs-number">1</span>; <span class="hljs-comment">// `change:a` event is triggered.</span>
model.b = <span class="hljs-number">2</span>; <span class="hljs-comment">// `change:b` event is triggered.</span>
}); <span class="hljs-comment">// `change` event is triggered.</span>
</code></pre>
<p>Manual transactions with attribute assignments are superior to <code>model.set()</code> in terms of both performance and flexibility.</p>
<h3 id="model-assignfrom-otherrecord-">model.assignFrom(otherRecord)</h3>
<p>Makes an existing <code>model</code> to be the full clone of <code>otherRecord</code>, recursively assigning all attributes.
In contracts to <code>model.clone()</code>, the model is updated in place.</p>
<pre><code class="highlight javascript"><span class="hljs-comment">// Another way of doing the bestSeller.clone()</span>
<span class="hljs-keyword">const</span> book = <span class="hljs-keyword">new</span> Book();
book.assignFrom(bestSeller);
</code></pre>
<h2 id="validation">Validation</h2>
<h3 id="overview">Overview</h3>
<p>Type-R supports validation API allowing developer to attach custom validation rules to attributes, models, and collections. Type-R validation mechanics based on following principles:</p>
<ul>
<li>Validation happens transparently on the first access to the validation error. There's no special API to trigger the validation.</li>
<li>Validation is performed recursively on the aggregated models. If a model at the bottom of the model tree is not valid all its owners are not valid as well.</li>
<li>Validation results are cached across the models and collections, thus consequent validation error reads are cheap. Only changed models and collections will be validated again when necessary.</li>
</ul>
<h3 id="model-isvalid-attr-">model.isValid( attr? )</h3>
<p>When called without arguments, returns <code>true</code> if the model is valid having the same effect as <code>!model.getValidationError()</code>.</p>
<p>When attr name is specified, returns <code>true</code> if the particular attribute is valid having the same effect as <code>!model.getValidationError( attrName )</code></p>
<h3 id="model-getvalidationerror-attrname-">model.getValidationError( attrName? )</h3>
<p>Return the validation error object for the model or the given attribute, or return <code>null</code> if there's no error.</p>
<p>When called without arguments and when the attribute is another model or collection the <code>ValidationError</code> object is returned which is an internal Type-R validation cache. It has the following shape:</p>
<pre><code class="highlight javascript">{
<span class="hljs-attr">error</span> : <span class="hljs-comment">/* as returned from collection.validate() */</span>,
<span class="hljs-comment">// Members validation errors.</span>
nested : {
<span class="hljs-comment">// key is an attrName for the model, and model.cid for the collcation</span>
key : validationError,
...
}
}
</code></pre>
<h3 id="callback-model-validate-"><code>callback</code> model.validate()</h3>
<p>Override this method to define model-level validation rules. Whatever is returned from <code>validate()</code> is treated as validation error.</p>
<aside class="notice">Do not call this method directly, that's not the way how validation works.</aside>
<h3 id="model-eachvalidationerror-iteratee-error-key-obj-void-">model.eachValidationError( iteratee : ( error, key, obj ) => void )</h3>
<p>Recursively traverse validation errors in all aggregated models.</p>
<p><code>iteratee</code> is a function taking following arguments:</p>
<ul>
<li><code>error</code> is the value of the error as specified at <code>type( T ).check( validator, error )</code> or returned by <code>validate()</code> callback.</li>
<li><code>obj</code> is the reference to the current model or collection having an error.</li>
<li><code>key</code> is:<ul>
<li>an attribute name for a model.</li>
<li>model.id for collection.</li>
<li><code>null</code> for the object-level validation error returned by <code>validate()</code>.</li>
</ul>
</li>
</ul>
<h2 id="i-o">I/O</h2>
<h3 id="model-isnew-">model.isNew()</h3>
<p>Has this model been saved to the server yet? If the model does not yet have an <code>id</code>, it is considered to be new.</p>
<h3 id="async-model-fetch-options-"><code>async</code> model.fetch( options? )</h3>
<p>Asynchronously fetch the model using <code>endpoint.read()</code> method. Returns an abortable ES6 promise.</p>
<p>An endpoint must be defined for the model in order to use that method.</p>
<h3 id="async-model-save-options-"><code>async</code> model.save( options? )</h3>
<p>Asynchronously save the model using <code>endpoint.create()</code> (if there are no id) or <code>endpoint.update()</code> (if id is present) method. Returns an abortable ES6 promise.</p>
<p>An endpoint must be defined for the model in order to use that method.</p>
<h3 id="async-model-destroy-options-"><code>async</code> model.destroy( options? )</h3>
<p>Asynchronously destroy the model using <code>endpoint.destroy()</code> method. Returns an abortable ES6 promise. The model is removed from the aggregating collection upon the completion of the I/O request.</p>
<p>An endpoint must be defined for the model in order to use that method.</p>
<h3 id="model-haspendingio-">model.hasPendingIO()</h3>
<p>Returns an promise if there's any I/O pending with the object, or <code>null</code> otherwise. Can be used to check for active I/O in progress.</p>
<h3 id="model-getendpoint-">model.getEndpoint()</h3>
<p>Returns an model's IO endpoint. Normally, this is an endpoint which is defined in object's <code>static endpoint = ...</code> declaration, but it might be overridden by the parent's model using <code>type( Type ).endpoint( ... )</code> attribute declaration.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> endpoint = restfulIO( <span class="hljs-string">'/api/users'</span> );
...
}
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserRole</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> endpoint = restfulIO( <span class="hljs-string">'/api/roles'</span> );
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-comment">// Use the relative path '/api/roles/:id/users'</span>
users : type( User.Collection ).endpoint( restfulIO( <span class="hljs-string">'./users'</span> ) ),
...
}
}
</code></pre>
<h3 id="model-tojson-options-">model.toJSON( options? )</h3>
<p>Serialize model or collection to JSON. Used internally by <code>save()</code> I/O method (<code>options.ioMethod === 'save'</code> when called from within <code>save()</code>). Can be overridden to customize serialization.</p>
<p>Produces the JSON for the given model or collection and its aggregated members. Aggregation tree is serialized as nested JSON. Model corresponds to an object in JSON, while the collection is represented as an array of objects.</p>
<p>If you override <code>toJSON()</code>, it usually means that you must override <code>parse()</code> as well, and vice versa.</p>
<aside class="notice">
Serialization can be controlled on per-attribute level with <b>type( Type ).toJSON()</b> declaration.
</aside>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Comment</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">body</span> : <span class="hljs-string">''</span>
}
}
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BlogPost</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">title</span> : <span class="hljs-string">''</span>,
<span class="hljs-attr">body</span> : <span class="hljs-string">''</span>,
<span class="hljs-attr">comments</span> : Comment.Collection
}
}
<span class="hljs-keyword">const</span> post = <span class="hljs-keyword">new</span> BlogPost({
<span class="hljs-attr">title</span>: <span class="hljs-string">"Type-R is cool!"</span>,
<span class="hljs-attr">comments</span> : [ { <span class="hljs-attr">body</span> : <span class="hljs-string">"Agree"</span> }]
});
<span class="hljs-keyword">const</span> rawJSON = post.toJSON()
<span class="hljs-comment">// { title : "Type-R is cool!", body : "", comments : [{ body : "Agree" }] }</span>
</code></pre>
<h3 id="option-parse-true-"><code>option</code> { parse : true }</h3>
<p><code>obj.set()</code> and constructor's option to force parsing of the raw JSON. Is used internally by I/O methods to parse the data received from the server.</p>
<pre><code class="highlight javascript"><span class="hljs-comment">// Another way of doing the bestSeller.clone()</span>
<span class="hljs-comment">// Amazingly, this is guaranteed to work by default.</span>
<span class="hljs-keyword">const</span> book = <span class="hljs-keyword">new</span> Book();
book.set( bestSeller.toJSON(), { <span class="hljs-attr">parse</span> : <span class="hljs-literal">true</span> } );
</code></pre>
<h3 id="callback-model-parse-json-options-"><code>callback</code> model.parse( json, options? )</h3>
<p>Optional hook called to transform the JSON when it's passes to the model or collection with <code>set( json, { parse : true })</code> call. Used internally by I/O methods (<code>options.ioMethod</code> is either "save" or "fetch" when called from I/O method).</p>
<p>If you override <code>toJSON()</code>, it usually means that you must override <code>parse()</code> as well, and vice versa.</p>
<aside class="notice">
Parsing can be controlled on per-attribute level with <b>type( Type ).parse()</b> declaration.
</aside>
<h2 id="change-events">Change events</h2>
<p>Type-R implements <em>deeply observable changes</em> on the object graph constructed of models and collection.</p>
<p>All of the model and collection updates happens in a scope of the transaction followed by the change event. Every model or collection update operation opens <em>implicit</em> transaction. Several update operations can be groped to the single <em>explicit</em> transaction if executed in the scope of the <code>obj.transaction()</code> or <code>col.updateEach()</code> call.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Author</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span> : <span class="hljs-string">''</span>
}
}
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Book</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span> : <span class="hljs-string">''</span>,
<span class="hljs-attr">datePublished</span> : <span class="hljs-built_in">Date</span>,
<span class="hljs-attr">author</span> : Author
}
}
<span class="hljs-keyword">const</span> book = <span class="hljs-keyword">new</span> Book();
book.on( <span class="hljs-string">'change'</span>, () => <span class="hljs-built_in">console</span>.log( <span class="hljs-string">'Book is changed'</span>) );
<span class="hljs-comment">// Implicit transaction, prints to the console</span>
book.author.name = <span class="hljs-string">'John Smith'</span>;
</code></pre>
<h3 id="events-mixin-methods-7-">Events mixin methods (7)</h3>
<p>Model implements <a href="#events-mixin">Events</a> mixin.</p>
<h3 id="event-change-model-"><code>event</code> "change" ( model )</h3>
<p>Triggered by the model at the end of the attributes update transaction in case if there were any changes applied.</p>
<h3 id="event-change-attrname-model-value-"><code>event</code> "change:attrName" ( model, value )</h3>
<p>Triggered by the model during the attributes update transaction for every changed attribute.</p>
<h3 id="model-changed">model.changed</h3>
<p>The <code>changed</code> property is the internal hash containing all the attributes that have changed during its last transaction.
Please do not update <code>changed</code> directly since its state is internally maintained by <code>set()</code>.
A copy of <code>changed</code> can be acquired from <code>changedAttributes()</code>.</p>
<h3 id="model-changedattributes-attrs-">model.changedAttributes( attrs? )</h3>
<p>Retrieve a hash of only the model's attributes that have changed during the last transaction,
or false if there are none. Optionally, an external attributes hash can be passed in,
returning the attributes in that hash which differ from the model.
This can be used to figure out which portions of a view should be updated,
or what calls need to be made to sync the changes to the server.</p>
<h3 id="model-previous-attr-">model.previous( attr )</h3>
<p>During a "change" event, this method can be used to get the previous value of a changed attribute.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Person</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span></span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span>: <span class="hljs-string">''</span>
}
}
<span class="hljs-keyword">const</span> bill = <span class="hljs-keyword">new</span> Person({
<span class="hljs-attr">name</span>: <span class="hljs-string">"Bill Smith"</span>
});
bill.on(<span class="hljs-string">"change:name"</span>, ( model, name ) => {
alert( <span class="hljs-string">`Changed name from <span class="hljs-subst">${ bill.previous(<span class="hljs-string">'name'</span>) }</span> to <span class="hljs-subst">${ name }</span>`</span>);
});
bill.name = <span class="hljs-string">"Bill Jones"</span>;
</code></pre>
<h3 id="model-previousattributes-">model.previousAttributes()</h3>
<p>Return a copy of the model's previous attributes. Useful for getting a diff between versions of a model, or getting back to a valid state after an error occurs.</p>
<h2 id="other">Other</h2>
<h3 id="model-getowner-">model.getOwner()</h3>
<p>If the model is an nested in an aggregated attribute return the owner model or <code>null</code> otherwise.
If the model is a member of an <code>Collection.of( ModelType )</code>, the collection will be bypassed and the owner of the collection will be returned.</p>
<h3 id="model-collection">model.collection</h3>
<p>If the model is a member of a some <code>Collection.of( ModelType )</code> return this collection or <code>null</code> otherwise.</p>
<h1 id="collection-api">Collection API</h1>
<h2 id="overview">Overview</h2>
<p>Collection is an ordered sets of models represented in JSON as an array of objects. In run time, it's encapsulates JS Array of models (<code>collection.models</code>) and an object hashmap by the model's <code>id</code>/<code>cid</code> for a fast O(1) look-ups. Collections expose ES6 Array and BackboneJS Collection API.</p>
<p>Collections are deeply observable. They emit the full set of Backbone Collection events (add, remove, change, etc) having the same semantic plus "changes" event triggered once when the processing of other change events is finished.</p>
<p>In most cases you don't need to define collection class manually, although you may. Every <code>Model</code> class has an implicitly defined <code>Collection</code>.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Book</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">title</span> : <span class="hljs-built_in">String</span>
author : Author
}
}
<span class="hljs-comment">// Access an implicitly defined collection.</span>
<span class="hljs-keyword">const</span> books = Collection.of( Book ).create();
</code></pre>
<h2 id="create-and-dispose">Create and dispose</h2>
<h3 id="callback-collection-initialize-models-options-"><code>callback</code> collection.initialize( models?, options? )</h3>
<p>Initialization function which is called at the end of the constructor.</p>
<h3 id="constructor-collection-of-modelclass-"><code>constructor</code> Collection.of( ModelClass )</h3>
<p>The most common collection type is an <strong>aggregating serializable collection</strong>. By default, collection aggregates its elements which are treated as an integral part of the collection (serialized, cloned, disposed, and validated recursively). An aggregation means the <em>single owner</em>, as the single object cannot be an integral part of two distinct things. The collection will take ownership on its models and will put an error in the console if it can't.</p>
<p>When creating a Collection, you may choose to pass in the initial array of models.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Role</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span> : <span class="hljs-built_in">String</span>
}
}
<span class="hljs-keyword">const</span> roles = <span class="hljs-keyword">new</span> Role.Collection( json, { <span class="hljs-attr">parse</span> : <span class="hljs-literal">true</span> } );
</code></pre>
<h3 id="constructor-collection-ofrefsto-modelclass-"><code>constructor</code> Collection.ofRefsTo( ModelClass )</h3>
<p>Collection of model references is a <strong>non-aggregating non-serializable collection</strong>. <code>Collection.Refs</code> doesn't aggregate its elements, which means that containing models are not considered as an integral part of the enclosing collection and not being validated, cloned, disposed, and serialized recursively.</p>
<p>It is useful for a local non-persistent application state.</p>
<h3 id="new-collectionclass-models-options-">new CollectionClass( models?, options? )</h3>
<h3 id="collectionclass-create-models-options-">CollectionClass.create( models?, options? )</h3>
<h3 id="collection-clone-">collection.clone()</h3>
<p>Clone the collection. An aggregating collection will be recursively cloned, non-aggregated collections will be shallow cloned.</p>
<h3 id="collectionclass-from-models-options-">CollectionClass.from( models, options? )</h3>
<p>Create <code>CollectionClass</code> from the array of models. Similar to direct collection creation, but supports additional option for strict data validation.
If <code>{ strict : true }</code> option is passed the validation will be performed and an exception will be thrown in case of an error.</p>
<p>Please note, that Type-R always performs type checks on assignments, convert types, and reject improper updates reporting it as an error. It won't, however, execute custom validation
rules on every update as they are evaluated lazily. <code>strict</code> option will invoke custom validators and will throw on every error or warning instead of reporting them and continue.</p>
<pre><code class="highlight javascript"><span class="hljs-comment">// Validate the body of an incoming HTTP request.</span>
<span class="hljs-comment">// Throw an exception if validation fails.</span>
<span class="hljs-keyword">const</span> body = MyRequestBody.from( ctx.request.body, { <span class="hljs-attr">parse</span> : <span class="hljs-literal">true</span>, <span class="hljs-attr">strict</span> : <span class="hljs-literal">true</span> });
</code></pre>
<h3 id="collection-dispose-">collection.dispose()</h3>
<p>Dispose of the collection. An aggregating collection will recursively dispose of its models.</p>
<p>All changes in the models cause change events in the collections they are contained in.</p>
<p>Subset collections is an exception; they don't observe changes of its elements by default.</p>
<h3 id="collection-createsubset-models-options-">collection.createSubset( models?, options? )</h3>
<p>Creates a collection which is an instance of <code>Collection.subsetOf( sourceCollection )</code> attribute type.
Takes the same arguments as the collection's constructor.</p>
<p>Subset collections are serializable as an array of model ids. Create the collection which is a subset of a source collection.</p>
<aside class="notice">
Records in the collection must have an `id` attribute populated to work properly with subsets.
</aside>
<p>The subset of other collections are <strong>non-aggregating serializable collection</strong>. Subset-of collection is serialized as an array of model ids and used to model many-to-many relationships. The collection object itself is recursively created and cloned, however, its models are not aggregated by the collection thus they are not recursively cloned, validated, or disposed. <code>CollectionClass</code> argument may be omitted unless you need the model's attribute to be an instance of the particular collection class.</p>
<aside class="notice">
<b>subsetOf</b> collections are not deeply observable.
</aside>
<aside class="notice">
Since its an attribute <i>metatype</i> (combination of type and attribute metadata), it's not a real constructor and cannot be used with <b>new</b>. Use <b>collection.createSubset()</b> method to create subset-of collection instances.
</aside>
<p>Must have a reference to the master collection which is used to resolve model ids to models. <code>masterRef</code> may be:</p>
<ul>
<li>direct reference to a singleton collection.</li>
<li>function, returning the reference to the collection.</li>
<li>symbolic dot-separated path to the master collection resolved relative to the model's <code>this</code>. You may use <code>owner</code> and <code>store</code> macro in path:<ul>
<li><code>owner</code> is the reference to the model's owner. <code>owner.some.path</code> works as <code>() => this.getOwner().some.path</code>.</li>
<li><code>store</code> is the reference to the closes store. <code>store.some.path</code> works as <code>() => this.getStore().some.path</code>.</li>
</ul>
</li>
</ul>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Role</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span> : <span class="hljs-built_in">String</span>,
...
}
}
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span> : <span class="hljs-built_in">String</span>,
<span class="hljs-attr">roles</span> : subsetOf( <span class="hljs-string">'owner.roles'</span>, Role.Collection )
}
}
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UsersDirectory</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Store</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">roles</span> : Role.Collection,
<span class="hljs-attr">users</span> : User.Collection <span class="hljs-comment">// `~roles` references will be resolved against this.roles</span>
}
}
</code></pre>
<h2 id="read-and-update">Read and update</h2>
<p>Common options used by Backbone API methods:</p>
<ul>
<li><code>{ sort : false }</code> - do not sort the collection.</li>
<li><code>{ parse : true }</code> - parse raw JSON (used to set collection with data from the server).</li>
</ul>
<h3 id="collection-assignfrom-othercollection-">collection.assignFrom( otherCollection )</h3>
<p>Synchronize the state of the collection and its aggregation tree with other collection of the same type. Updates existing objects in place. Model in the collection is considered to be "existing" if it has the same <code>id</code>.</p>
<p>Equivalent to <code>collection.set( otherCollection.models, { merge : true } )</code> and triggers similar events on change.</p>
<h3 id="collection-models">collection.models</h3>
<p>Raw access to the JavaScript array of models inside of the collection. Usually, you'll want to use <code>get</code>, <code>at</code>, or the other methods to access model objects, but occasionally a direct reference to the array is desired.</p>
<h3 id="collection-get-id-">collection.get( id )</h3>
<p>Get a model from a collection, specified by an <code>id</code>, a <code>cid</code>, or by passing in a model.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">const</span> book = library.get(<span class="hljs-number">110</span>);
</code></pre>
<h3 id="collection-at-index-">collection.at( index )</h3>
<p>Get a model from a collection, specified by index. Useful if your collection is sorted, and if your collection isn't sorted, at will still retrieve models in insertion order. When passed a negative index, it will retrieve the model from the back of the collection.</p>
<h3 id="collection-add-models-options-">collection.add( models, options? )</h3>
<p>Add a model (or an array of models) to the collection. If this is the <code>Model.Collection</code>, you may also pass raw attributes objects, and have them be vivified as instances of the <code>Model</code>. Returns the added (or preexisting, if duplicate) models.</p>
<p>Pass <code>{at: index}</code> to splice the model into the collection at the specified index. If you're adding models to the collection that are already in the collection, they'll be ignored, unless you pass <code>{merge: true}</code>, in which case their attributes will be merged into the corresponding models.</p>
<ol>
<li>Trigger the one event per model:<ul>
<li><code>add</code>(model, collection, options) for each model added.</li>
<li><code>change</code>(model, options) for each model changed (if the <code>{merge: true}</code> option is passed).</li>
</ul>
</li>
<li>Trigger the single event:<ul>
<li><code>update</code>(collection, options) if any models were added.</li>
<li><code>sort</code>(collection, options) if an order of models was changed.</li>
</ul>
</li>
<li>Trigger <code>changes</code> event in case if any changes were made to the collection and objects inside.</li>
</ol>
<h3 id="collection-remove-models-options-">collection.remove( models, options? )</h3>
<p>Remove a model (or an array of models) from the collection, and return them. Each model can be a model instance, an id string or a JS object, any value acceptable as the id argument of collection.get.</p>
<ol>
<li>Trigger <code>remove</code>(model, collection, options) for each model removed.</li>
<li>If any models were removed, trigger:<ul>
<li><code>update</code>(collection, options)</li>
<li><code>changes</code>(collection, options).</li>
</ul>
</li>
</ol>
<h3 id="collection-set-models-options-">collection.set( models, options? )</h3>
<p>The set method performs a "smart" update of the collection with the passed list of models. If a model in the list isn't yet in the collection it will be added; if the model is already in the collection its attributes will be merged; and if the collection contains any models that aren't present in the list, they'll be removed. All of the appropriate "add", "remove", and "change" events are fired as this happens. Returns the touched models in the collection. If you'd like to customize the behavior, you can disable it with options: <code>{remove: false}</code>, or <code>{merge: false}</code>.</p>
<h4 id="events">Events</h4>
<ol>
<li>Trigger the one event per model:<ul>
<li><code>add</code>(model, collection, options) for each model added.</li>
<li><code>remove</code>(model, collection, options) for each model removed.</li>
<li><code>change</code>(model, options) for each model changed.</li>
</ul>
</li>
<li>Trigger the single event:<ul>
<li><code>update</code>(collection, options) if any models were added.</li>
<li><code>sort</code>(collection, options) if an order of models was changed.</li>
</ul>
</li>
<li>Trigger <code>changes</code> event in case if any changes were made to the collection and objects inside.</li>
</ol>
<pre><code class="highlight javascript"><span class="hljs-keyword">const</span> vanHalen = <span class="hljs-keyword">new</span> Man.Collection([ eddie, alex, stone, roth ]);
vanHalen.set([ eddie, alex, stone, hagar ]);