leanweb 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,712 @@
1
+ # <a href="https://leanweb.app"><img src='https://leanweb.app/favicon.svg' alt='Leanweb' width='32'/></a> Leanweb
2
+
3
+ Builds framework agnostic web components.
4
+
5
+ ## Installation
6
+
7
+ - `npm install leanweb -g` as a global tool, or
8
+ - `npm install leanweb -D` in the project as a dev dependency.
9
+
10
+ If leanweb is installed as a dev dependency, you will need to run
11
+ `npx lw`, otherwise just run `lw` if it is installed as global tool.
12
+
13
+ I don't see any reason leanweb should be installed as `npm install leanweb`.
14
+
15
+ ## Background
16
+
17
+ I like the idea in Angular that 3 files (html/js/scss) as a component are in
18
+ charge of a box, like a div, a rectangle area. But I don't like Angular in that
19
+ my code has to be depending on so many bloated dependencies to run. I created
20
+ leanweb as a set of tools to help create web components based web projects,
21
+ which:
22
+
23
+ - are based on native DOM and web components api
24
+ - are pure Javascript, no fancy framework
25
+ - are assistive, not restrictive
26
+ - are more standards, less proprietary
27
+ - are built to last
28
+
29
+ The principle is simply that 3 files (html/js/scss) as a web component will
30
+ control a box.
31
+
32
+ ## Getting started
33
+
34
+ In this demo, I assume leanweb is installed as a global tool by running
35
+
36
+ ```
37
+ npm i leanweb -g
38
+ ```
39
+
40
+ ### `leanweb init` or `lw init`
41
+
42
+ Create a directory called `demo` for this demo project.
43
+
44
+ ```bash
45
+ $ mkdir demo
46
+ $ cd demo
47
+ demo$ lw init
48
+ demo$
49
+ ```
50
+
51
+ Now a `src/` directory are created at the project root. `src/leanweb.json`
52
+ looks like:
53
+
54
+ ```json
55
+ {
56
+ "name": "demo",
57
+ "version": "0.4.5",
58
+ "components": ["root"],
59
+ "imports": [],
60
+ "resources": ["resources/"]
61
+ }
62
+ ```
63
+
64
+ which suggests a root web component `demo-root` is created. In `src/` directory,
65
+ an `index.html`, an empty `demo.scss` and an empty `global-styles.scss` files
66
+ are created, in `global-styles.scss` we can add global styles. `demo-root` web
67
+ component directory is created at `src/components/root/`. There are 3 files in
68
+ this directory:
69
+
70
+ - root.html
71
+ - root.js
72
+ - root.scss
73
+
74
+ `root.html`
75
+
76
+ ```html
77
+ <div>demo-root works!</div>
78
+ ```
79
+
80
+ `root.js` defines your new web component `demo-root`, which is a web component
81
+ based on standard DOM api.
82
+
83
+ `root.js`
84
+
85
+ ```javascript
86
+ import LWElement from "./../../lib/lw-element.js";
87
+ import ast from "./ast.js";
88
+
89
+ customElements.define(
90
+ "demo-root",
91
+ class extends LWElement {
92
+ // LWElement extends HTMLElement
93
+ constructor() {
94
+ super(ast);
95
+ }
96
+ }
97
+ );
98
+ ```
99
+
100
+ `root.scss` is empty, which is for you to add web component specific styles.
101
+
102
+ ### `leanweb serve` or `lw serve`
103
+
104
+ Run `lw serve` and you should see a browser window open. Try make some
105
+ changes in the code, and save, the browser should refresh automatically to
106
+ reflect your changes.
107
+ <img src='https://leanweb.app/images/leanweb-serve.png' alt='lw serve' width='640'/>
108
+
109
+ ### `leanweb electron` or `lw electron`
110
+
111
+ Run `lw electron` or even `lw elec` and you should see an electron app window
112
+ open as follows:
113
+
114
+ <img src='https://leanweb.app/images/leanweb-electron.png' alt='lw electron' width='640'/>
115
+
116
+ ### `leanweb generate` or `lw generate`
117
+
118
+ Let's create a `login` web component with `lw generate` or `lw g`.
119
+
120
+ ```bash
121
+ demo$ lw g login
122
+ demo$
123
+ ```
124
+
125
+ Now the `leanweb.json` has one more entry in the component list:
126
+
127
+ ```json
128
+ {
129
+ "name": "demo",
130
+ "version": "0.4.5",
131
+ "components": ["root", "login"],
132
+ "imports": [],
133
+ "resources": ["resources/"]
134
+ }
135
+ ```
136
+
137
+ `demo-login` is the newly generated web component. The web component name is
138
+ prefixed with project name `demo-`. Inside `src/components/`, a new web
139
+ component directory `login` is created containing 3 files:
140
+
141
+ - login.html
142
+ - login.js
143
+ - login.scss
144
+
145
+ Now let's make two changes, first open up `src/components/root/root.html`, and
146
+ add a new line `<demo-login></demo-login>`. The new `root.html` should look
147
+ like the following after the change:
148
+
149
+ ```html
150
+ <div>demo-root works!</div>
151
+ <demo-login></demo-login>
152
+ ```
153
+
154
+ Then open up `src/components/login/login.scss`, and add the following style:
155
+
156
+ ```scss
157
+ div {
158
+ color: red;
159
+ }
160
+ ```
161
+
162
+ And you should see the changes in the browser. Please note the styles added to
163
+ the `login` component does not affect other components.
164
+
165
+ <img src='https://leanweb.app/images/leanweb-serve-1.png' alt='lw serve' width='640'/>
166
+
167
+ Run `lw electron` again, and you will see the same changes reflected in
168
+ the electron app.
169
+
170
+ <img src='https://leanweb.app/images/leanweb-electron-1.png' alt='lw electron' width='640'/>
171
+
172
+ ### `leanweb dist` or `lw dist`
173
+
174
+ Run `lw dist`, and a `dist` directory will be created with minified files
175
+ for production.
176
+
177
+ ### `leanweb clean` or `lw clean`
178
+
179
+ `lw clean` will delete `build/` and `dist/` directories.
180
+
181
+ ### `leanweb upgrade` or `lw u`
182
+
183
+ `lw upgrade` will upgrade `src/lib/` directory if there is a new version
184
+ available.
185
+
186
+ ### `leanweb destroy` or `lw destroy`
187
+
188
+ `lw destroy project-name` will remove the `src/`, `build/` and `dist/`
189
+ directory. Please note the `src/` directory will be deleted by this command.
190
+
191
+ ### `leanweb help` or `lw help`
192
+
193
+ `lw help command-name` will print help information for the command. For
194
+ example, `lw help dist` or `lw h di` will print help information for
195
+ `lean dist`.
196
+
197
+ ### `leanweb version` or `lw version`
198
+
199
+ `lw version` will print version information.
200
+
201
+ ## lw directives
202
+
203
+ ### lw
204
+
205
+ Contents inside a tag with `lw` directive are considered expressions that will
206
+ be evaluated. In the example below, the `<span lw>name</span>` will be
207
+ evaluated as `<span>Leanweb</span>`, because the variable `name` is defined
208
+ in the web component js file with the value `Leanweb`.
209
+
210
+ ```html
211
+ Hello <span lw>name</span>!
212
+ ```
213
+
214
+ ```javascript
215
+ // ...
216
+ name = "Leanweb";
217
+ // ...
218
+ ```
219
+
220
+ ```
221
+ Hello Leanweb!
222
+ ```
223
+
224
+ ### lw-if
225
+
226
+ ```html
227
+ <span lw-if='name==="Leanweb"'>Leanweb</span>
228
+ ```
229
+
230
+ The `span` DOM node will be shown if `name==="Leanweb"` will evaluate true,
231
+ otherwise, it will not be shown.
232
+
233
+ ### lw-for
234
+
235
+ The following example shows how `lw-for` directive helps to generate DOM nodes
236
+ for each `item` in the `items` array.
237
+
238
+ ```html
239
+ <div lw lw-for="item, $index in items">$index+': '+item</div>
240
+ ```
241
+
242
+ ```javascript
243
+ // ...
244
+ items = ["one", "two", "three"];
245
+ // ...
246
+ ```
247
+
248
+ ```
249
+ 0: one
250
+ 1: two
251
+ 2: three
252
+ ```
253
+
254
+ ### lw-model and lw-on:
255
+
256
+ ```html
257
+ <input type="text" lw-model="name" />
258
+ <span lw>name</span>
259
+ <br />
260
+ <button lw-on:click="resetName()">Reset Name</button>
261
+ ```
262
+ You could bind multiple events like `lw-on:click,change=handler($event, $node)`.
263
+
264
+ ```javascript
265
+ // ...
266
+ resetName() {
267
+ this.name = 'Leanweb';
268
+ }
269
+ // ...
270
+ ```
271
+
272
+ <img src='https://leanweb.app/images/lw-model.gif' alt='lw-model'/>
273
+
274
+ ### lw-class:
275
+
276
+ ```html
277
+ <div lw lw-for="item, $index in items" lw-class:active="isActive($index)">
278
+ item
279
+ </div>
280
+ ```
281
+
282
+ ```javascript
283
+ // ...
284
+ items = ['one', 'two', 'three'];
285
+ isActive(index) {
286
+ return index === 1;
287
+ }
288
+ // ...
289
+ ```
290
+
291
+ ```scss
292
+ .active {
293
+ color: red;
294
+ }
295
+ ```
296
+
297
+ <img src='https://leanweb.app/images/lw-class.png' alt='lw-class' width='640'/>
298
+
299
+ ### lw-bind:
300
+
301
+ ```html
302
+ <img lw-bind:src="imgSrc" lw-bind:width="imageWidth" />
303
+ ```
304
+
305
+ ```javascript
306
+ // ...
307
+ imgSrc = "https://leanweb.app/images/az.gif";
308
+ imageWidth = 400;
309
+ // ...
310
+ ```
311
+
312
+ <img src='https://leanweb.app/images/lw-bind.png' alt='lw-bind' width='640'/>
313
+
314
+ ### lw-input:
315
+
316
+ `lw-input` is used to pass and share data from parent to children.
317
+
318
+ `demo-parent.html`
319
+
320
+ ```html
321
+ <demo-child lw-input:userData="user"></demo-child>
322
+ ```
323
+
324
+ `demo-parent.js`
325
+
326
+ ```javascript
327
+ // ...
328
+ user = { firstname: "Qian", lastname: "Chen" };
329
+ // ...
330
+ ```
331
+
332
+ The child is able to access the `user` object passed in with `lw-input:`
333
+ directive from `inputReady()` method.
334
+ `demo-child.js`
335
+
336
+ ```javascript
337
+ // ...
338
+ inputReady() {
339
+ console.log(this.userData);
340
+ }
341
+ // ...
342
+ ```
343
+
344
+ ## Form Binding
345
+
346
+ Here is a few examples how Leanweb helps web components work with form binding.
347
+
348
+ ### Checkbox
349
+
350
+ ```javascript
351
+ // ...
352
+ items = ['one', 'two', 'three'];
353
+ toggleCheckboxes() {
354
+ if (this.checkedValues.length) {
355
+ this.checkedValues.length = 0;
356
+ } else {
357
+ this.checkedValues = [...this.items];
358
+ }
359
+ }
360
+ checkedValues = [];
361
+ // ...
362
+ ```
363
+
364
+ ```html
365
+ <button lw-on:click="toggleCheckboxes()">Toggle Checkboxes</button>
366
+ <div lw-for="item, $index in items">
367
+ <input type="checkbox" lw-bind:value="item" lw-model="checkedValues" />
368
+ <span lw>item</span>
369
+ </div>
370
+ <span lw>checkedValues</span>
371
+ ```
372
+
373
+ <img src='https://leanweb.app/images/leanweb-form-binding-checkbox.gif' alt='Leanweb Form Binding Checkbox'/>
374
+
375
+ ### Select
376
+
377
+ ```javascript
378
+ // ...
379
+ items = ['one', 'two', 'three'];
380
+ selectTwo() {
381
+ this.selectedOption = 'two';
382
+ }
383
+ selectedOption;
384
+ // ...
385
+ ```
386
+
387
+ ```html
388
+ <button lw-on:click="selectTwo()">Select Two</button>
389
+ <div>
390
+ <select lw-model="selectedOption">
391
+ <option lw lw-for="item, $index in items">item</option>
392
+ </select>
393
+ </div>
394
+ <span lw> selectedOption </span>
395
+ ```
396
+
397
+ <img src='https://leanweb.app/images/leanweb-form-binding-select.gif' alt='Leanweb Form Binding Select' />
398
+
399
+ ### Multiple Select
400
+
401
+ ```javascript
402
+ // ...
403
+ items = ['one', 'two', 'three'];
404
+ toggleAllOptions() {
405
+ if (this.selectedOptions.length) {
406
+ this.selectedOptions.length = 0;
407
+ } else {
408
+ this.selectedOptions = [...this.items];
409
+ }
410
+ }
411
+ selectedOptions = [];
412
+ // ...
413
+ ```
414
+
415
+ ```html
416
+ <button lw-on:click="toggleAllOptions()">Toggle All</button>
417
+ <div>
418
+ <select lw-model="selectedOptions" multiple>
419
+ <option lw lw-for="item, $index in items">item</option>
420
+ </select>
421
+ </div>
422
+ <span lw> selectedOptions </span>
423
+ ```
424
+
425
+ <img src='https://leanweb.app/images/leanweb-form-binding-multiple-select.gif' alt='Leanweb Form Binding Multiple Select' />
426
+
427
+ ### Radio Button
428
+
429
+ ```javascript
430
+ // ...
431
+ items = ['one', 'two', 'three'];
432
+ chooseTwo() {
433
+ this.picked = 'two';
434
+ }
435
+ picked;
436
+ // ...
437
+ ```
438
+
439
+ ```html
440
+ <button lw-on:click="chooseTwo()">Choose Two</button>
441
+ <div lw-for="item, $index in items">
442
+ <input
443
+ type="radio"
444
+ name="pickOne"
445
+ lw-bind:value="item"
446
+ lw-model="picked"
447
+ /><span lw>item</span>
448
+ </div>
449
+ <span lw>picked</span>
450
+ ```
451
+
452
+ <img src='https://leanweb.app/images/leanweb-form-binding-radio-button.gif' alt='Leanweb Form Binding Radio Button' />
453
+
454
+ ### Range
455
+
456
+ ```javascript
457
+ // ...
458
+ selectRange50() {
459
+ this.selectedRange = 50;
460
+ }
461
+ selectedRange = 10;
462
+ // ...
463
+ ```
464
+
465
+ ```html
466
+ <button lw-on:click="selectRange50()">Select Range 50</button> <br />
467
+ <input type="range" lw-model="selectedRange" />
468
+ <span lw>selectedRange</span>
469
+ ```
470
+
471
+ <img src='https://leanweb.app/images/leanweb-form-binding-range.gif' alt='Leanweb Form Binding Range' />
472
+
473
+ ## Import libraries from `node_modules`
474
+
475
+ Assuming npm module `lodash-es` is installed, you could use any of the
476
+ following `import` statements for your web component class:
477
+
478
+ ```javascript
479
+ import { get } from "lodash-es"; // find from node_modules
480
+ import get from "lodash-es/get.js"; // find from node_modules
481
+ import * as _ from "lodash-es"; // find from node_modules
482
+ ```
483
+
484
+ Importing a JSON file:
485
+
486
+ ```javascript
487
+ import someJSON from "./some.json";
488
+ ```
489
+
490
+ Importing CSS/SCSS:
491
+
492
+ ```javascript
493
+ import agate from "highlight.js/scss/agate.scss";
494
+
495
+ // customElements.define('demo-root',
496
+ // class extends LWElement { // LWElement extends HTMLElement
497
+ // constructor() {
498
+ // super(ast);
499
+ super.applyStyles(agate);
500
+ // }
501
+ // }
502
+ //);
503
+ ```
504
+
505
+ ## Component Communication
506
+
507
+ The following project demonstrates how Leanweb helps web components to talk to
508
+ each other.
509
+
510
+ <img src='https://leanweb.app/images/leanweb-pub-sub.gif' alt='Leanweb Component Communication'/>
511
+
512
+ `pub.js`
513
+
514
+ ```javascript
515
+ // import LWElement from './../../lib/lw-element.js';
516
+ // import ast from './ast.js';
517
+
518
+ // customElements.define('demo-pub',
519
+ // class extends LWElement { // LWElement extends HTMLElement
520
+ // constructor() {
521
+ // super(ast);
522
+
523
+ setInterval(() => {
524
+ this.time = new Date(Date.now()).toLocaleString();
525
+ leanweb.eventBus.dispatchEvent("time", this.time);
526
+ this.update();
527
+ }, 1000);
528
+
529
+ // }
530
+ // }
531
+ // );
532
+ ```
533
+
534
+ `pub.html`
535
+
536
+ ```html
537
+ <div class="pub">
538
+ <span>Time Publisher</span>
539
+ <span lw>time</span>
540
+ </div>
541
+ ```
542
+
543
+ `sub.js`
544
+
545
+ ```javascript
546
+ // import LWElement from './../../lib/lw-element.js';
547
+ // import ast from './ast.js';
548
+
549
+ // customElements.define('demo-sub',
550
+ // class extends LWElement { // LWElement extends HTMLElement
551
+ // constructor() {
552
+ // super(ast);
553
+ // }
554
+
555
+ sub() {
556
+ this.listener = leanweb.eventBus.addEventListener('time', event => {
557
+ this.time = event.data;
558
+ this.update();
559
+ });
560
+ this.subscribed = true;
561
+ }
562
+
563
+ unsub() {
564
+ leanweb.eventBus.removeEventListener(this.listener);
565
+ this.subscribed = false;
566
+ }
567
+ // }
568
+ // );
569
+ ```
570
+
571
+ `sub.html`
572
+
573
+ ```html
574
+ <div class="sub">
575
+ <span>Time Subscriber</span>
576
+ <span lw>time</span>
577
+ <div class="buttons">
578
+ <button lw-bind:disabled="subscribed" lw-on:click="sub()">
579
+ Subscribe Time
580
+ </button>
581
+ <button lw-bind:disabled="!subscribed" lw-on:click="unsub()">
582
+ UnSubscribe Time
583
+ </button>
584
+ </div>
585
+ </div>
586
+ ```
587
+
588
+ Source code of this demo https://github.com/elgs/leanweb-pub-sub-demo.
589
+
590
+ ## API
591
+
592
+ ### globalThis.leanweb
593
+
594
+ `leanweb` is the only foot print on `globalThis` scope.
595
+
596
+ #### updateComponents(...tagNames)
597
+
598
+ `updateComponents` is used to update all component DOMs or DOMs of specific
599
+ component tag names. `updateComponents` takes any number of component tag
600
+ names as arguments. If no argument is provided, it will update all component
601
+ DOMs app wide.
602
+
603
+ #### eventBus
604
+
605
+ An instance of `LWEventBus` managed by `leanweb` to pass DOM update events.
606
+
607
+ #### urlHash
608
+
609
+ `urlHash` is a reference to `window.location.hash` which can be used for
610
+ routing.
611
+
612
+ #### urlHashPath
613
+
614
+ `urlHashPath` is used to set or get the `path` part in the urlHash. If the
615
+ `urlHash` is `#/login?a=b&a=b&c=d`, `urlHashPath` will be `#/login`.
616
+
617
+ #### urlHashParams
618
+
619
+ `urlHashParams` is used to set or get the `parameters` in the urlHash. If the
620
+ `urlHash` is `#/login?a=b&a=b&c=d`, `urlHashParams` will be
621
+ `{a: ['b', 'b'], c: 'd'}`.
622
+
623
+ ### LWElement
624
+
625
+ `LWElement` extends `HTMLElement`, and Leanweb components extend `LWElement`.
626
+ So Leanweb components are just more specific versions of the standard
627
+ `HTMLElement`. `LWElement` helps to wire up the `lw` directives in the HTML and
628
+ provides some convenient methods to update the DOM.
629
+
630
+ #### update(rootNode = this.shadowRoot)
631
+
632
+ The `update` method provides a convenient way to update the DOM when the model
633
+ changes. You should feel free to use old way to update DOM. The `update` just
634
+ makes life a little easier. `update` takes `rootNode` as parameter, which
635
+ allows you to specify which DOM element to start with. The default value is
636
+ the current`shadowRoot`.
637
+
638
+ LWElement will call update in the following scenarios:
639
+
640
+ 1. after all `lw` directives are initially bound to DOM;
641
+ 2. after `lw-on:` event is fired;
642
+ 3. after `lw-model` change is fired;
643
+
644
+ You may need to call the `update()` method manually in other events. For
645
+ example:
646
+
647
+ 1. in your setTimeout/setInterval callbacks;
648
+ 2. in `LWEventBus` callbacks;
649
+ 3. in any network api callbacks;
650
+
651
+ #### domReady()
652
+
653
+ `domReady()` will be called after all initial DOM events are bound, and all
654
+ DOM interpolations are evaluated. This method is meant to be overridden and is a
655
+ great place to send events to the event bus.
656
+
657
+ #### inputReady()
658
+
659
+ `inputReady()` will be called after all input data from parent's `lw-input:`
660
+ are ready. In this method, children are able to access the passed in data
661
+ shared by parents.
662
+
663
+ #### turnedOn()
664
+
665
+ Called when the componnet `lw-if` is evaluated `true`.
666
+
667
+ #### turnedOff()
668
+
669
+ Called when the componnet `lw-if` is evaluated `false`.
670
+
671
+ #### urlHashChanged()
672
+
673
+ If `urlHashChanged()` is defined as a function, it will be called whenever the
674
+ urlHash changes. This could be useful to update the DOM in component routing.
675
+
676
+ #### applyStyles(...styles)
677
+
678
+ `applyStyles` will apply the styles that is imported from a css or scss into
679
+ the web component DOM.
680
+
681
+ ### LWEventBus
682
+
683
+ `LWElement` comes with a global instance of `LWEventBus` that helps web
684
+ components to talk to each other by sending and receiving events and data. You
685
+ could use your own way for component communication. `LWEventBus` is just a
686
+ choice for you.
687
+
688
+ #### addEventListener(eventName, callback)
689
+
690
+ You can use `leanweb.eventBus` to get the global instance of event bus, and
691
+ use `leanweb.eventBus.addEventListener(eventName, callback)` to subscribe to
692
+ a type of event from the event bus. `addEventListener` takes two parameters.
693
+ The first `eventName` is the name of the event, and the second `callback` is a
694
+ function that will get called when a event is sent to the event bus. The
695
+ callback function that takes a parameter `event`, which contains `eventName`
696
+ and `data` fields. `addEventListener` returns the eventListener instance
697
+ being added, which could be passed in `removeEventListener` as parameter.
698
+
699
+ #### removeEventListener(listener)
700
+
701
+ `removeEventListener` removes the listener from the event bus, so it stops
702
+ being notified when a next event is fired.
703
+
704
+ #### dispatchEvent(eventName, data = null)
705
+
706
+ `dispatchEvent` is used to send an event to the event bus. It takes two
707
+ parameters. `eventName` is the name of the event, and `data` is the payload data
708
+ of the event.
709
+
710
+ ## More examples and tutorials
711
+
712
+ https://leanweb.app