-
Notifications
You must be signed in to change notification settings - Fork 28
/
index.html
1604 lines (1357 loc) · 62.4 KB
/
index.html
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 lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="ObjectModel : Strong Dynamically Typed Object Modeling for JavaScript">
<meta name="author" content="Sylvain Pollet-Villard">
<title>ObjectModel</title>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-74235687-2"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'UA-74235687-2');
</script>
<link rel="icon" type="image/png" href="docs/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="docs/favicon-16x16.png" sizes="16x16" />
<link rel="shortcut icon" href="docs/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="docs/style/main.compiled.css" />
<script defer src="docs/js/main.compiled.js"></script>
<script async type="module">
import * as globals from "./dist/object-model.js"
Object.assign(window, globals)
</script>
</head>
<body>
<button id="menu-button" class="lines-button arrow arrow-left" aria-label="Toggle Navigation">
<span class="lines"></span>
</button>
<nav id="menu">
<a class="title download-link" href="#download">
<h2>Download</h2>
</a>
<hr>
<a class="title github-link" target="_blank" rel="noopener"
href="https://github.com/sylvainpolletvillard/ObjectModel">
<h2>View on Github</h2>
</a>
<hr>
<div>
<a href="#introduction" class="title">Introduction</a>
<ul>
<li><a href="#introduction">What is it</a></li>
<li><a href="#video-demo">Video demo</a></li>
<li><a href="#features">Features</a></li>
</ul>
</div>
<hr>
<div>
<a href="#doc-basic-model" class="title">Documentation</a>
<ul>
<li><a href="#doc-basic-model">Basic models</a></li>
<li><a href="#doc-object-model">Object models</a></li>
<li><a href="#doc-model">Model constructor</a></li>
<li><a href="#doc-es6-classes">Using ES6 class</a></li>
<li><a href="#doc-optional-properties">Optional properties</a></li>
<li><a href="#doc-multiple-types">Multiple types</a></li>
<li><a href="#doc-value-checking">Value checking and enumerations</a></li>
<li><a href="#doc-null-safe">Null-safe object traversal</a></li>
<li><a href="#doc-default-values">Default values</a></li>
<li><a href="#doc-composition">Composition</a></li>
<li><a href="#doc-extensions">Inheritance by extensions</a></li>
<li><a href="#doc-multiple-inheritance">Multiple inheritance</a></li>
<li><a href="#doc-assertions">Assertions for custom tests</a></li>
<li><a href="#doc-private-and-constants">Private and constant properties</a></li>
<li><a href="#doc-array-model">Array models</a></li>
<li><a href="#doc-function-model">Function models</a></li>
<li><a href="#doc-map-models">Map models</a></li>
<li><a href="#doc-set-models">Set models</a></li>
<li><a href="#doc-any-model">Any model</a></li>
<li><a href="#doc-custom-collectors">Custom error collectors</a></li>
<li><a href="#doc-custom-devtool-formatters">Custom devtool formatters</a></li>
<li><a href="#api">Full API</a></li>
<li><a href="#common-models">Commonly used models</a></li>
<li><a href="#common-questions">Common questions</a></li>
</ul>
</div>
<hr>
</nav>
<div id="page">
<header class="header">
<img id="logo" src="docs/res/logo.png" alt="ObjectModel" width="800" height="150">
<hr>
<p class="description">Strong Dynamically Typed Object Modeling for JavaScript</p>
</header>
<section id="introduction" class="grid">
<h1 hidden>Introduction</h1>
<div class="description">
<h2 id="what-is-it">What is this library ?</h2>
<p>ObjectModel intends to bring <strong>strong dynamic type checking</strong> to your web applications.
Contrary to static type-checking solutions like <a href="https://www.typescriptlang.org"
target="_blank" rel="noopener">TypeScript</a> or <a href="https://flowtype.org" target="_blank"
rel="noopener">Flow</a>, ObjectModel can also validate data at runtime: JSON from the server,
form inputs, content from local storages, external libraries...</p>
<p>By leveraging <strong>ES6 Proxies</strong>, this library ensures that your variables always match the
model definition and validation constraints you added to them. Thanks to the generated exceptions,
it will help you spot potential bugs and save you time spent on debugging. ObjectModel is also very
easy to master: no new language to learn, no new tools, no compilation step, just a minimalist and
intuitive API in a plain old JS micro-library.</p>
<p>Validating at runtime also brings many other benefits: you can define your own types, use them in
complex model definitions with custom assertions that can even change depending on your application
state. Actually it goes much further than just type safety. Go on and see for yourself.</p>
</div>
<div id="video-demo">
<iframe data-src="https://www.youtube.com/embed/zmojfyNH_EE?rel=0&showinfo=0" src="" width="640"
height="360" frameborder="0" allowfullscreen>
</iframe>
</div>
</section>
<hr>
<section id="features-and-download" class="grid">
<div id="features">
<h2>What's inside the box ?</h2>
<p>Many features, hopefully neither too much nor too few:</p>
<ul>
<li>Typed structures: objects, arrays, maps, sets, functions...</li>
<li>Union types</li>
<li>Enumerations</li>
<li>Custom assertions</li>
<li>Optional properties</li>
<li>Default values</li>
<li>Null-safe object traversal</li>
<li>Easy composition or inheritance</li>
<li>Constants and private properties based on name conventions</li>
<li>Explicit error messages</li>
<li>Customizable error handlers</li>
<li>all in <strong class="size-gzip">4.06 kB</strong> minified and gzipped,
even less when using tree-shaking
</li>
</ul>
</div>
<div id="download">
<h2>Download</h2>
<h3>Current version: v<span class="version">4.4.5</span></h3>
<ul>
<li>
From <a href="https://www.npmjs.com/package/objectmodel" target="_blank" rel="noopener">
<abbr title="Node Package Manager">npm</abbr></a>:
<code>npm install objectmodel</code>
</li>
<li>From <abbr title="Content Delivery Network">CDN</abbr>:
<a href="http://cdn.pika.dev/objectmodel" rel="noopener">cdn.pika.dev/objectmodel</a>
</li>
<li>Minified <abbr title="ECMAScript module">ESM</abbr> bundle
(<strong class="size-gzip">4.06 kB</strong> gzipped) :
<a href="dist/object-model.min.js">object-model.min.js</a>
</li>
<li>Source files :
<a class="link-zip" href="https://github.com/sylvainpolletvillard/ObjectModel/archive/v4.4.5.zip">object-model-4.4.5.zip</a>
</li>
</ul>
<h3>Previous versions</h3>
<p>If you need to support older browsers, you may have to use an older version of the library instead.
Please take a look at the <a href="#common-questions"
onclick="document.getElementById('browser-support').parentNode.open=true">
Browsers/Node support</a>
section for more information about browser/Node support for each version.
</p>
<p>Full changelog between versions is available on the
<a href="https://github.com/sylvainpolletvillard/ObjectModel/releases" target="_blank"
rel="noopener">Github Releases</a>
page.</p>
</div>
<div>
<h2>Usage</h2>
<p>Since v4.0, ObjectModel is shipped in <abbr title="ECMAScript module">ESM</abbr> format, and written
in modern JavaScript (ES2018).</strong></p>
<div class="panel">
<pre><code class="language-javascript">import { Model } from "objectmodel"</code></pre>
</div>
<p>Not all environments support <abbr title="ECMAScript modules">ESM</abbr> yet, so you should configure
a transpiler/bundler such as
Babel/Webpack for your project. If you just want a ready-to-use
<abbr title="Universal Module Definition">UMD</abbr> version, you can use a transpiling service
such as <a href="http://pika.dev" target="_blank" rel="noopener">pika.dev</a> or
<a href="http://unpkg.com" target="_blank" rel="noopener">unpkg.com</a> like this:
</p>
<div class="panel">
<pre><code class="language-html"><script src="https://umd.cdn.pika.dev/objectmodel/v4"></script></code>
<code class="language-js">const { Model } = objectmodel</code></pre>
</div>
</div>
</section>
<hr>
<h1>Documentation</h1>
<p class="tip">ObjectModel is already loaded on this webpage, so you can try the examples below in your browser
JavaScript console.</p>
<section id="doc-basic-model" class="grid doc-code-code">
<div class="doc">
<h2>Basic models</h2>
<p>Basic models simply validate a variable against the model definition passed as argument, and return
the validated value. <code>BasicModel</code> constructor takes a <i>model definition</i> as the only
argument. They are generally used to declare all the basic generic types that you will use in your
application. You can find a list of <a href="#common-models">common basic models here</a>.</p>
</div>
<div class="panel panel1">
<span class="legend">Model</span>
<pre><code class="language-javascript">import { BasicModel } from "objectmodel"
const NumberModel = BasicModel(Number);
// 'new' keyword is optional for models and model instances</code></pre>
</div>
<div class="panel panel2">
<span class="legend">Instance</span>
<pre><code class="language-javascript">let x = NumberModel("42");</code>
<code class="language-none exception">TypeError: expecting Number, got String "42"</code></pre>
</div>
</section>
<hr>
<section id="doc-object-model" class="grid doc-code-code">
<div class="doc">
<h2>Object models</h2>
<p>Object models validate nested object properties against a definition tree. They provide automatic
validation at initial and future assignments of the properties of the instance objects.</p>
</div>
<div class="panel panel1">
<span class="legend">Model</span>
<pre><code class="language-javascript">import { ObjectModel } from "objectmodel"
const Order = new ObjectModel({
product: {
name: String,
quantity: Number,
},
orderDate: Date
});</code></pre>
</div>
<div class="panel panel2">
<span class="legend">Instance</span>
<pre><code class="language-javascript">const myOrder = new Order({
product: { name: "Apple Pie", quantity: 1 },
orderDate: new Date()
});
myOrder.product.quantity = 2; // no exceptions thrown
myOrder.product.quantity = false; //try to assign a Boolean</code>
<code class="language-none exception">TypeError: expecting product.quantity to be Number, got Boolean false</code></pre>
</div>
</section>
<hr>
<section id="doc-model" class="grid doc-code">
<div class="doc">
<h2>Model constructor</h2>
<p><code>Model</code> is the <strong>base class of all models</strong> and can be used as an alias for
<code>BasicModel</code> and <code>ObjectModel</code> constructors.</p>
</div>
<div class="panel">
<span class="legend">Example</span>
<pre><code class="language-javascript">import { Model, BasicModel, ObjectModel } from "objectmodel"
Model(String) // same as BasicModel(String)
Model({ name: String }) // same as ObjectModel({ name: String })</code></pre>
</div>
</section>
<hr>
<section id="doc-es6-classes" class="grid doc-code-code">
<div class="doc">
<h2>Usage with ES6 classes</h2>
<p>If you are using ES6 classes in your project, it is very easy to define a model for your classes:</p>
</div>
<div class="panel panel1">
<span class="legend">Model</span>
<pre><code class="language-javascript">class Character extends Model({ lastName: String, firstName: String }){
get fullName(){ return `${this.firstName} ${this.lastName}`; }
}</code></pre>
</div>
<div class="panel panel2">
<span class="legend">Instance</span>
<pre><code class="language-javascript">const rick = new Character({ lastName: "Sanchez", firstName: "Rick" });
rick.lastName = 132;</code>
<code class="language-none exception">TypeError: expecting lastName to be String, got Number 132</code>
<code class="language-javascript">console.log(rick.fullName); // "Rick Sanchez"</code></pre>
</div>
</section>
<hr>
<section id="doc-optional-properties" class="grid doc-code-code">
<div class="doc">
<h2>Optional properties</h2>
<p>By default, model properties are mandatory. That means all properties defined are required on
instance declaration, otherwise an exception will be raised. But you can specify a property to be
optional by using the bracket notation, borrowed from the JSDoc specification</p>
</div>
<div class="panel panel1">
<span class="legend">Model</span>
<pre><code class="language-javascript">const User = ObjectModel({
email: String, // mandatory
name: [String] // optional
});</code></pre>
</div>
<div class="panel panel2">
<span class="legend">Instance</span>
<pre><code class="language-javascript">const stan = User({ email: "stan@smith.com" }); // no exceptions
const roger = User({ name: "Roger" }); // email is mandatory</code>
<code class="language-none exception">TypeError: expecting email to be String, got undefined</code></pre>
</div>
</section>
<hr>
<section id="doc-multiple-types" class="grid doc-code-code">
<div class="doc">
<h2>Multiple types</h2>
<p>Several valid types can be specified for one property, aka <strong>union types</strong>.
So optional properties are actually union types between the original type and the values
<code>undefined</code> and <code>null</code>. To declare an optional union type, add
<code>undefined</code> to the list.</p>
</div>
<div class="panel panel1">
<span class="legend">Model</span>
<pre><code class="language-javascript">const Animation = new ObjectModel({
// can be a Number or a String
delay: [Number, String],
// optional property which can be a Boolean or a String
easing: [Boolean, String, undefined]
});</code></pre>
</div>
<div class="panel panel2">
<span class="legend">Instance</span>
<pre><code class="language-javascript">const opening = new Animation({ delay: 300 }); // easing is optional
opening.delay = "fast"; // String is a valid type
opening.delay = null;</code>
<code class="language-none exception">TypeError: expecting delay to be Number or String, got null</code>
<code class="language-javascript">opening.easing = true; // Boolean is a valid type
opening.easing = 1;</code>
<code class="language-none exception">TypeError: expecting easing to be Boolean or String or undefined, got Number 1</code></pre>
</div>
</section>
<hr>
<section id="doc-value-checking" class="grid doc-code">
<div class="doc">
<h2>Value checking and enumerations</h2>
<p>In model definitions, you can also specify values instead of types for model properties. The property
value will have to match the model one. Just like union types, use brackets notation for value
enumerations.
</p>
<p>If a regular expression is passed, the value must match it.</p>
</div>
<div class="panel">
<span class="legend">Model</span>
<pre><code class="language-javascript">const Shirt = new ObjectModel({
// the only acceptable value is "clothes"
category: "clothes",
// valid values: 38, 42, "S", "M", "L", "XL", "XXL"...
size: [Number, "M", /^X{0,2}[SL]$/],
// valid values: "black", "#FF0000", undefined...
color: ["black","white", new RegExp("^#([A-F0-9]{6})$"), undefined]
});</code></pre>
</div>
</section>
<hr>
<section id="doc-null-safe" class="grid doc-code-code">
<div class="doc">
<h2>Null-safe object traversal</h2>
<p>When you want to traverse nested objects, you always have to worry about the null pointer exception.
Some languages such as Groovy have a safe navigation operator represented by <code>?.</code> to
safely navigate through potential null references. In JavaScript, there is no such solution so you
have to manually check for <code>undefined/null</code> values at each level of the object. But
within an object model, declared properties are null-safe for traversal:
every instance complete its structure with undefined properties according to the model definition.
</p>
</div>
<div class="panel panel1">
<span class="legend">Model and instanciation</span>
<pre><code class="language-javascript">const Config = new ObjectModel({
local: {
time: {
format: ["12h","24h", undefined]
}
}
});
const config = { local: undefined };
const new_config = Config(config); // object model</code></pre>
</div>
<div class="panel panel2">
<span class="legend">Traversal</span>
<pre><code class="language-javascript">if(config.local.time.format === "12h"){ hour %= 12; }</code>
<code class="language-none exception">TypeError: Cannot read property 'time' of undefined</code>
<code class="language-javascript">// so to prevent this exception, we have to check this way:
if(config != null
&& config.local != null
&& config.local.time != null
&& config.local.time.format === "12h"){
hour %= 12;
}
// with object models, no worries :)
if(new_config.local.time.format === "12h"){ hour %= 12; }
// new_config.local.time.format returns undefined</code></pre>
</div>
</section>
<hr>
<section id="doc-default-values" class="grid doc-code-code">
<div class="doc">
<h2>Default values assignment</h2>
<p>You can set a default value for any model with <code>model.defaultTo(value)</code>. This default
value will be used if no argument is passed to the model constructor.</p>
</div>
<div class="panel panel1">
<span class="legend">Model</span>
<pre><code class="language-javascript">let N = BasicModel(Number).defaultTo(1)</code></pre>
</div>
<div class="panel panel2">
<span class="legend">Instance</span>
<pre><code class="language-javascript">N(5) + N() === 6</code></pre>
</div>
<div class="doc">
<p>For object models, the <code>defaultTo</code> method can be used to specify default values for some
properties of your object models. If these are not defined at object instanciation, their default
value will be assigned. You can also put them in the model prototype if you prefer to rely on
<a rel="noopener" target="_blank"
href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain">prototypal
inheritance</a>.</p>
</div>
<div class="panel panel1">
<span class="legend">Model</span>
<pre><code class="language-javascript">const FileInfo = ObjectModel({
name: String,
size: [Number],
creationDate: [Date],
writable: Boolean
}).defaultTo({
name: "Untitled file",
size: 0,
writable: true
});</code></pre>
</div>
<div class="panel panel2">
<span class="legend">Instance</span>
<pre><code class="language-javascript">let file = new FileInfo({ writable: false });</code>
<code class="language-javascript">file.name; // name is mandatory but a default value was passed</code>
<code class="language-none log">"Untitled file"</code>
<code class="language-javascript">file.size; // size is optional, but the default value still applies</code>
<code class="language-none log">0</code>
<code class="language-javascript">file.creationDate; // no default value was passed for this property</code>
<code class="language-none log">undefined</code>
<code class="language-javascript">file.writable; // passed value overrides default value</code>
<code class="language-none log">false</code>
<code class="language-javascript">Object.keys(file);</code>
<code class="language-none log">["name","size","creationDate","writable"]</code></pre>
</div>
</section>
<hr>
<section id="doc-composition" class="grid doc-code-code">
<div class="doc">
<h2>Composition with models as types</h2>
<p>Nested properties definitions can be models too, so you can compose structures of models.</p>
<p>When a property value matches a model definition, the value is automatically replaced by an instance
of the corresponding model. This mechanic is referred as <strong>autocasting</strong> and can be
compared to <a href="https://en.wikipedia.org/wiki/Duck_typing" target="_blank" rel="noopener"
class="no-hl">
duck typing
</a>.
Autocasting works for object models properties, but also for Array/Map/Set models items when
inserted, and for FunctionModel arguments and return value.</p>
<p>This naive approach is very time saving and allows you, for example, to parse composed models from
JSON in one step. If there is somehow an ambiguity (such as two suitable models within an union
type), the value is kept unchanged and a warning console message will inform you how to solve this
ambiguity.</p>
</div>
<div class="panel panel1">
<span class="legend">Model</span>
<pre><code class="language-javascript">const Person = ObjectModel({
name: String,
age: [Number]
});
const Lovers = ObjectModel({
husband: Person,
wife: Person
});</code></pre>
</div>
<div class="panel panel2">
<span class="legend">Instance</span>
<pre><code class="language-javascript">const joe = { name: "Joe", age: 42 };
const ann = new Person({
name: joe.name + "'s wife",
age: joe.age - 5
});
const couple = Lovers({
husband: joe, // object autocasted
wife: ann // object model
});
couple.husband instanceof Person === true // has been casted to Person</code></pre>
</div>
</section>
<hr>
<section id="doc-extensions" class="grid doc-code-code">
<div class="doc">
<h2>Inheritance by extension</h2>
<p>Extensions create new models based on existing model definitions. You can declare new properties or
override previous ones. Therefore, it is an easy way to reproduce subtyping and class inheritance
patterns.</p>
</div>
<div class="panel panel1">
<span class="legend">Model</span>
<pre><code class="language-javascript">const Person = ObjectModel({
name: String,
female: Boolean
});
const Mother = Person.extend({
female: true,
child: Person
});</code></pre>
</div>
<div class="panel panel2">
<span class="legend">Instance</span>
<pre><code class="language-javascript">let joe = new Person({ name: "Joe", female: false });
let ann = new Person({ name: "Ann", female: true });
let joanna = new Person({ name: "Joanna", female: true });
ann = new Mother({ name: "Ann", female: true, child: joanna })
ann instanceof Mother && ann instanceof Person // true</code>
<code class="language-javascript">joe = Mother(joe); // try to cast joe to Mother model</code>
<code class="language-none exception">TypeError: expecting female to be true, got Boolean false
expecting child to be {
name: String,
female: Boolean
}, got undefined</code></pre>
</div>
<div class="doc">
<h3>With ES6 classes</h3>
<p>Extended models inherit the parent's prototype chain, so you can easily combine it with class
inheritance. Just make sure to respect the <a
href="https://en.wikipedia.org/wiki/Liskov_substitution_principle" target="_blank"
rel="noopener">Liskov substitution principle</a>
when you extend a type definition.</p>
</div>
<div class="panel panel1">
<span class="legend">Model</span>
<pre><code class="language-javascript">class Person extends ObjectModel({ name: String, female: Boolean }){
constructor({ name, female }){
if(!female) name = `Mr ${name}`
super({ name, female })
}
}
class Mother extends Person.extend({ female: true, child: Person }){
constructor({ name, female, child }){
super({ name: `Mrs ${name}`, female, child })
}
}</code></pre>
</div>
<div class="panel panel2">
<span class="legend">Instance</span>
<pre><code class="language-javascript">let joe = new Person({ name: "Joe", female: false })
let joanna = new Person({ name: "Joanna", female: true })
let ann = new Mother({ name: "Ann", female: true, child: joanna })</code>
<code class="language-javascript">joe.name</code>
<code class="language-none log">Mr Joe</code>
<code class="language-javascript">ann.name</code>
<code class="language-none log">Mrs Ann</code>
<code class="language-javascript">ann.child.name</code>
<code class="language-none log">Joanna</code></pre>
</div>
</section>
<hr>
<section id="doc-multiple-inheritance" class="grid doc-code-code">
<div class="doc">
<h2>Multiple inheritance</h2>
<p>But it goes further: you can do multiple inheritance and mix any number of parent models definitions
and assertions. If some properties have the same name, those of the last object overrides the
others.</p>
</div>
<div class="panel panel1">
<span class="legend">Model</span>
<pre><code class="language-javascript">const Client = Person.extend(User, Order, { store: String });
Client.prototype.sendConfirmationMail = function(){
return this.email + ": Dear " + this.name
+ ", thank you for ordering "
+ this.product.quantity + " " + this.product.name
+ " on " + this.store;
};
Object.keys(Client.definition);</code>
<code class="language-none log">["name", "female", "email", "product", "orderDate", "store"]</code></pre>
</div>
<div class="panel panel2">
<span class="legend">Instance</span>
<pre><code class="language-javascript">const joe = new Client({
name: "Joe",
female: false,
email: "joe@email.net",
product: { name: "diapers", quantity: 100 },
orderDate: new Date(),
store: "daddy.net"
});
joe.sendConfirmationMail();</code>
<code class="language-none log">joe@email.net: Dear Joe, thank you for ordering 100 diapers on daddy.net</code></pre>
</div>
</section>
<hr>
<section id="doc-assertions" class="grid doc-code-code">
<div class="doc">
<h2>Assertions for custom validation tests</h2>
<p>You can add to your models any number of assertions that are custom test functions applied on model
instances. All assertions are called every time the model is changed, and must all return
<code>true</code> to validate. Exceptions thrown in assertions are catched and considered as
assertion failures.</p>
<p>For example, we can get an Integer model by adding <code>Number.isInteger</code> as an assertion to a
basic <code>Number</code> model.</p>
<p>Assertions are inherited from the model prototype, so you can add global assertions on all models by
setting them in <code>Model.prototype</code>. The second argument of the <code>assert</code> method
is an optional message shown when assertion fails. It can be a String or a function returning a
String.</p>
</div>
<div class="panel panel1">
<span class="legend">Model</span>
<pre><code class="language-javascript">const PositiveInteger = BasicModel(Number)
.assert(Number.isInteger)
.assert(n => n >= 0, "should be greater or equal to zero")
function isPrime(n) {
for (let i=2, m=Math.sqrt(n); i <= m ; i++){
if(n%i === 0) return false;
}
return n > 1;
}
const PrimeNumber = PositiveInteger.extend().assert(isPrime);
// extend to not add isPrime assertion to the Integer model
</code></pre>
</div>
<div class="panel panel2">
<span class="legend">Instance</span>
<pre><code class="language-javascript">PositiveInteger(-1);</code>
<code class="language-none exception">TypeError: assertion should be greater or equal to zero returned false for value -1</code>
<code class="language-javascript">PositiveInteger(Math.sqrt(2));</code>
<code class="language-none exception">TypeError: assertion isInteger returned false for value 1.414213562373</code>
<code class="language-javascript">PrimeNumber(83);</code>
<code class="language-none log">83</code>
<code class="language-javascript">PrimeNumber(87);</code>
<code class="language-none exception">TypeError: assertion isPrime returned false for value 87</code></pre>
</div>
</section>
<hr>
<section id="doc-private-and-constants" class="grid doc-code-code">
<div class="doc">
<h2>Private and constant properties</h2>
<p>Some variable naming conventions are commonly used in JavaScript. For example, a leading underscore
is used to specify a <i>_private</i> property which should not be used outside the object own
methods. Also, constants are often in <i>ALL_CAPS</i>. Model definitions follow these conventions by
making <i>_underscored</i> properties not enumerable and not usable outside of the instance's own
methods, and <i>CAPITALIZED</i> properties not writable.</p>
<p>Note: private properties access is granted only when using the instance own methods. Methods declared
in an extended class cannot access to privates. Asynchronous callbacks do not work neither, except
if these callbacks are defined as methods of the model. If this does not fit your usecase, you
should probably not make these properties private.</p>
</div>
<div class="panel panel1">
<span class="legend">Model</span>
<pre><code class="language-javascript">const Circle = ObjectModel({
radius: Number, // public
_index: Number, // private
UNIT: ["px","cm"], // constant
_ID: [Number], // private and constant
}).defaultTo({
_index: 0,
getIndex(){ return this._index },
setIndex(value){ this._index = value }
});
</code></pre>
</div>
<div class="panel panel2">
<span class="legend">Instance</span>
<pre><code class="language-javascript">let c = new Circle({ radius: 120, UNIT: "px", _ID: 1 });
c.radius = 100;
c.UNIT = "cm";</code>
<code class="language-none exception">TypeError: cannot modify constant property UNIT</code>
<code class="language-javascript">c._index = 1;</code>
<code class="language-none exception">TypeError: cannot modify private property _index</code>
<code class="language-javascript">console.log( c._index )</code>
<code class="language-none exception">TypeError: cannot access to private property _index</code>
<code class="language-javascript">c.setIndex(2);
console.log( c.getIndex() )</code>
<code class="language-none log">2</code>
<code class="language-javascript">Object.keys(c); // private variables are not enumerated</code>
<code class="language-none log">["radius", "UNIT"]</code></pre>
</div>
<div class="doc">
<p>You can modify or remove these conventions by overriding the
<code>conventionForPrivate</code> and
<code>conventionForConstant</code> methods in your model or globally in
<code>Model.prototype</code>.
</p>
</div>
<div class="panel panel1">
<pre><code class="language-javascript">// change the private convention for all models
Model.prototype.conventionForPrivate = key => key.startsWith('#');
// remove the constant convention specifically for Circle
Circle.conventionForConstant = () => false;</code></pre>
</div>
<div class="panel panel2">
<pre><code class="language-javascript">// Private and constant conventions have been changed
c._index = 3;
c.UNIT = "cm";
console.log(c._index, c.UNIT); // no more errors</code>
<code class="language-none log">3 "cm"</code></pre>
</div>
</section>
<hr>
<section id="doc-array-model" class="grid doc-code-code">
<div class="doc">
<h2>Array models</h2>
<p>Array models validate the type of all elements in an array.</p>
<p>The validation is done on initial array elements passed to the model, then on new elements added or
modified afterwards.</p>
</div>
<div class="panel panel1">
<span class="legend">Model</span>
<pre><code class="language-javascript">import { ArrayModel } from "objectmodel";
const Cards = new ArrayModel([Number, "J","Q","K"]);
// Hand is an array of 2 Numbers, J, Q, or K
const Hand = Cards.extend()
.assert(a => a.length === 2, "should have two cards");</code></pre>
</div>
<div class="panel panel2">
<span class="legend">Instance</span>
<pre><code class="language-javascript">const myHand = Hand( [7, "K"] );
myHand[0] = "Joker"</code>
<code class="language-none exception">TypeError: expecting Array[0] to be Number or "J" or "Q" or "K", got String "Joker"</code>
<code class="language-javascript">myHand.push("K");</code>
<code class="language-none exception">TypeError: assertion "should have two cards" returned false for value [7, "Joker", "K"]</code></pre>
</div>
<div class="doc">
<p>All the validation options for previous models are also available for array model elements:
type/value checking, optional properties, union types, enumerations, assertions...</p>
</div>
<div class="panel panel1">
<span class="legend">Model</span>
<pre><code class="language-javascript">const Family = ObjectModel({
father: Father,
mother: Mother,
children: ArrayModel(Person), // array of Persons
grandparents: [ArrayModel([Mother, Father])]
// optional array of Mothers or Fathers
});</code></pre>
</div>
<div class="panel panel2">
<span class="legend">Instance</span>
<pre><code class="language-javascript">const joefamily = new Family({
father: joe,
mother: ann,
children: [joanna, "dog"]
});</code>
<code class="language-none exception">TypeError: expecting Array[1] to be { name: String, female: Boolean }, got String "dog"</code>
</pre>
</div>
</section>
<hr>
<section id="doc-function-model" class="grid doc-code-code">
<div class="doc">
<h2>Function models</h2>
<p>Function models provide validation on input (arguments) and output (return value). All the validation
options for Object models are also available for Function models. The arguments passed to
<code>FunctionModel</code> are the types of the arguments the function will receive, and the
<code>return</code> method is used to specify the type of the function return value.</p>
</div>
<div class="panel panel1">
<pre><code class="language-javascript">import { FunctionModel, BasicModel } from "objectmodel";
const Numb = BasicModel(Number).assert(Number.isFinite);
const Operator = BasicModel(["+","-","*","/"])
const Calculator = FunctionModel(Numb, Operator, Numb).return(Numb);
const calc = new Calculator((a, operator, b) => eval(a + operator + b));
</code></pre>
</div>
<div class="panel panel2">
<pre><code class="language-javascript">calc(3, "+", 1);</code>
<code class="language-none log">4</code>
<code class="language-javascript">calc(6, "*", null);</code>
<code class="language-none exception">TypeError: expecting arguments[2] to be Number, got null</code>
<code class="language-javascript">calc(1, "/", 0);</code>
<code class="language-none exception">TypeError: assertion "isFinite" returned false for value Infinity</code></pre>
</div>
<div class="doc">
<p>In classical JavaScript OOP programming, methods are declared in the constructor's
<code>prototype</code>. You can do the same with instances of function models.</p>
<p>Another option is to provide a default implementation in the model definition by using the
<code>defaultTo</code> method. See the <a href="#doc-default-values">Default values</a> section.
The difference is that all the properties in the model definition are required for an object
to be considered suitable for the model. In the following example, an object must have a function
<code>sayMyName</code> to be valid as a Person, while the function <code>greet</code> is not
mandatory.</p>
</div>
<div class="panel panel1">
<span class="legend">Model</span>
<pre><code class="language-javascript">const Person = ObjectModel({
name: String,
// function without arguments returning a String
sayMyName: FunctionModel().return(String)
}).defaultTo({
sayMyName: function(){ return "my name is " + this.name }
})
// takes one Person as argument, returns a String
Person.prototype.greet = FunctionModel(Person).return(String)(
function(otherguy){
return "Hello "+ otherguy.name + ", " + this.sayMyName()
}
)</code></pre>
</div>
<div class="panel panel2">
<span class="legend">Instance</span>
<pre><code class="language-javascript">const joe = new Person({ name: "Joe" });
joe.sayMyName();</code>
<code class="language-none log">my name is Joe</code>
<code class="language-javascript">joe.greet({ name: "Ann", greet: "hi ?" });</code>
<code class="language-none log">Hello Ann, my name is Joe</code>
<code class="language-javascript">joe.greet({ name: "dog", sayMyName: "woof !" });</code>
<code class="language-none exception">TypeError: expecting arguments[0].sayMyName to be "Function", got String "woof !"</code></pre>
</div>
</section>
<hr>
<section id="doc-map-models" class="grid doc-code-code">
<div class="doc">
<h2>Map models</h2>
<p>Map models validate ES6 <code>Map</code> objects by checking both keys and values. The arguments
passed to <code>MapModel</code> are respectively the definition for the keys and the definition for
the values.</p>
</div>
<div class="panel panel1">
<span class="legend">Model</span>
<pre><code class="language-javascript">import { MapModel, Model } from "objectmodel";
const Course = Model([ "math", "english", "history" ])
const Grade = Model([ "A", "B", "C" ])
const Gradebook = MapModel(Course, Grade)
</code></pre>
</div>
<div class="panel panel2">
<span class="legend">Instance</span>
<pre><code class="language-javascript">const joannaGrades = new Gradebook([
["math", "B"],
["english", "C"]
])
joannaGrades.set("videogames", "A")</code>
<code class="language-none exception">TypeError: expecting Map key to be "math" or "english" or "history", got String "videogames"</code>
<code class="language-javascript">joannaGrades.set("history", "nope")</code>
<code class="language-none exception">TypeError: expecting Map["history"] to be "A" or "B" or "C" , got String "nope"</code></pre>
</div>
</section>
<hr>
<section id="doc-set-models" class="grid doc-code-code">
<div class="doc">
<h2>Set models</h2>
<p>Set models validate ES6 <code>Set</code> objects by checking the type of all the elements in the set.
The API is the same as array models.</p>
</div>
<div class="panel panel1">
<span class="legend">Model</span>
<pre><code class="language-javascript">import { SetModel, Model } from "objectmodel";
const Course = Model([ "math", "english", "history" ])
const FavoriteCourses = SetModel(Course)
</code></pre>
</div>
<div class="panel panel2">
<span class="legend">Instance</span>