├── .bmp.yml
├── .editorconfig
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .vscode
└── settings.json
├── LICENSE
├── Makefile
├── README.md
├── deno.json
├── dnt.ts
├── mod.ts
└── src
├── add_hidden_item.ts
├── decorators
├── component.ts
├── component_test.ts
├── emits.ts
├── emits_test.ts
├── index.ts
├── inner_html.ts
├── inner_html_test.ts
├── is.ts
├── is_test.ts
├── on.ts
├── on_click.ts
├── on_click_at.ts
├── on_test.ts
├── on_use_handler.ts
├── pub.ts
├── pub_test.ts
├── sub.ts
├── sub_test.ts
├── wired.ts
└── wired_test.ts
├── def.ts
├── def_test.ts
├── dom_polyfill_deno.ts
├── dom_polyfill_node.ts
├── get.ts
├── get_test.ts
├── init_component.ts
├── init_component_test.ts
├── install.ts
├── install_test.ts
├── make.ts
├── make_test.ts
├── mod.ts
├── mount.ts
├── mount_test.ts
├── plugins
├── debug_plugin.ts
├── debug_plugin_test.ts
├── outside_events_plugin.ts
└── outside_events_plugin_test.ts
├── prep.ts
├── prep_test.ts
├── registry.ts
├── td_deno.ts
├── td_node.ts
├── test_fixture.ts
├── test_helper.ts
├── unmount.ts
├── unmount_test.ts
└── util
├── check.ts
├── const.ts
├── debug_message.ts
├── document.ts
└── event_trigger.ts
/.bmp.yml:
--------------------------------------------------------------------------------
1 | version: 1.8.2
2 | commit: ':bookmark: chore(version): bump to v%.%.%'
3 | files:
4 | dnt.ts: 'version: "%.%.%"'
5 | README.md:
6 | - 'https://shields.io/badge/deno.land/x-v%.%.%-green'
7 | - 'https://deno.land/x/capsid@v%.%.%/mod.ts'
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 | [*]
3 | indent_style = space
4 | insert_final_newline = true
5 | trim_trailing_whitespace = true
6 | indent_size = 2
7 | [Makefile]
8 | indent_style = tab
9 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 | on: [push, pull_request]
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - uses: actions/checkout@v2
8 | - uses: denoland/setup-deno@v1
9 | with:
10 | deno-version: '1.x'
11 | - name: Run fmt check
12 | run: make fmt-check
13 | - name: Run unit tests
14 | run: make test
15 | - name: Run dnt
16 | run: make dnt
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /coverage
2 | /dist
3 | /cov
4 | /dist.js
5 | /dist.min.js
6 | /node
7 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enable": true,
3 | "deno.lint": true,
4 | "deno.unstable": true,
5 | "deno.config": "./deno.json",
6 | "deno.suggest.imports.hosts": {
7 | "https://deno.land": true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 | Copyright © 2015 Yoshiya Hinosawa ( @kt3k )
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in
12 | all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 | THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: test
2 | test:
3 | deno test --config deno.json -A --unstable --no-check --ignore=node
4 |
5 | .PHONY: cov
6 | cov:
7 | deno test --coverage=cov --config deno.json -A --unstable --no-check
8 |
9 | .PHONY: fmt
10 | fmt:
11 | deno fmt --config deno.json
12 |
13 | .PHONY: fmt-check
14 | fmt-check:
15 | deno fmt --check --config deno.json
16 |
17 | .PHONY: lint
18 | lint:
19 | deno lint --config deno.json
20 |
21 | .PHONY: dist
22 | dist:
23 | deno bundle --config deno.json src/mod.ts > dist.js
24 |
25 | .PHONY: min
26 | min:
27 | $(MAKE) dist
28 | terser --compress --mangle -o dist.min.js -- dist.js
29 |
30 | .PHONY: size
31 | size:
32 | $(MAKE) min
33 | deno run --allow-read https://deno.land/x/gzip_size@v0.2.3/cli.ts --include-original dist.min.js
34 |
35 | .PHONY: dnt
36 | dnt:
37 | deno run -A dnt.ts
38 |
39 | .PHONY: npm-publish
40 | npm-publish:
41 | rm -rf node
42 | $(MAKE) dnt
43 | cd node && npm publish
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [](https://github.com/capsidjs/capsid/actions/workflows/ci.yml)
4 | [](https://deno.land/x/capsid)
5 | [](https://npm.im/capsid)
6 |
7 | - **Declarative DOM programming library** based on **TypeScript decorators**
8 | - :leaves: **Small.** **1.79 kb.** **No dependencies.**
9 | - :sunny: **No special syntax.** Capsid uses **standard** HTML and TypeScript,
10 | and it **doesn't** use any **non-standard** syntax like JSX, Vue template,
11 | etc.
12 | - :bulb: **Simple.** No virtual DOMs. Capsid encourages the traditional event
13 | driven programming in a new style.
14 |
15 | # :butterfly: [Mirroring Example][Mirroring Example]
16 |
17 | This example illustrates the basic ideas of `capsid`.
18 |
19 | ```ts
20 | import { component, on, wired } from "capsid";
21 |
22 | // Declares `mirroring` component.
23 | // HTML elements which have `mirroring` class will be mounted by this component.
24 | @component("mirroring")
25 | class Mirroring {
26 | // Wires `dest` property to dom which is selected by `.dest` selector.
27 | @wired(".dest")
28 | dest!: HTMLParagraphElement;
29 |
30 | // Wires `src` property to dom which is selected by `.src` selector.
31 | @wired(".src")
32 | src!: HTMLInputElement;
33 |
34 | // Declares `input` event listener
35 | @on("input")
36 | onReceiveData() {
37 | this.dest.textContent = this.src.value;
38 | }
39 | }
40 | ```
41 |
42 | ```html
43 |
47 | ```
48 |
49 | `@component("mirroring")` registers the class as the component `mirroring`.
50 |
51 | `@wired` binds a dom element to the field which is queried by the given
52 | selector. `@on("input")` declares the following method is the `input` event
53 | handler. In the event handler `src` value is copied to `dest` content, which
54 | results the mirroring of the input values to the textContent of `.dest`
55 | paragraph.
56 |
57 | [See the demo][Mirroring Example]
58 |
59 | # :cd: Install
60 |
61 | ## Via npm
62 |
63 | npm install --save capsid
64 |
65 | then:
66 |
67 | ```js
68 | import { component } from "capsid";
69 | ```
70 |
71 | Note: You need TypeScript for using capsid because it depends on TypeScript
72 | decorators. You can easily start using TypeScript by using bundlers like
73 | [parcel][parcel]
74 |
75 | ## Via deno.land/x
76 |
77 | If you prefer [Deno](https://deno.land/), you can import capsid via
78 | `deno.land/x` registry.
79 |
80 | ```js
81 | import { component } from "https://deno.land/x/capsid@v1.8.2/mod.ts";
82 | ```
83 |
84 | # Decorators
85 |
86 | ```js
87 | import { component, emits, innerHTML, is, on, pub, sub, wired } from "capsid";
88 | ```
89 |
90 | - `@component(name)`
91 | - _class decorator_
92 | - registers as a capsid components.
93 | - `@on(event, { at })`
94 | - _method decorator_
95 | - registers as an event listener on the component.
96 | - `@on.click` is a shorthand for `@on('click')`.
97 | - `@on.click.at(selector)` is a shorthand for
98 | `@on('click', { at: selector })`.
99 | - `@emits(event)`
100 | - _method decorator_
101 | - makes the decorated method an event emitter.
102 | - `@wired(selector)`
103 | - _field decorator_
104 | - wires the elements to the decorated field by the given selector.
105 | - optionally `@wired.all(selector)`
106 | - `@is(name)`
107 | - _class decorator_
108 | - Adds the class name to the given element.
109 | - `@innerHTML(html: string)`
110 | - _class decorator_
111 | - Sets the given html string as innerHTML of the element at the mount timing.
112 | - `@pub(event: string, selector?: string)`
113 | - _methods decorator_
114 | - Publishes the event to the elements which have `sub:event` class.
115 | - `@sub(event: string)`
116 | - _class decorator_
117 | - Adds the `sub:event` class to the given element.
118 |
119 | ## `@component(name: string)`
120 |
121 | capsid.component(className) is class decorator. With this decorator, you can
122 | regiter the js class as class component.
123 |
124 | This is a shorthand of `capsid.def('component', Component)`.
125 |
126 | ```js
127 | import { component } from 'capsid'
128 |
129 | @component('timer')
130 | class Timer {
131 | ...definitions...
132 | }
133 | ```
134 |
135 | The above registers `Timer` class as `timer` component.
136 |
137 | ## `@on(event: string)`
138 |
139 | `@on` is a method decorator. With this decorator, you can register the method as
140 | the event handler of the element.
141 |
142 | ```js
143 | import { on, component } from 'capsid'
144 |
145 | @component('foo-btn')
146 | class FooButton {
147 |
148 | @on('click')
149 | onClick (e) {
150 | ...definitions...
151 | }
152 | }
153 | ```
154 |
155 | The above binds `onClick` method to its element's 'click' event automatically.
156 |
157 | The above is equivalent of:
158 |
159 | ```js
160 | class FooButton {
161 | __mount__ () {
162 | this.el.addEventListener('click', e => {
163 | this.onClick(e)
164 | })
165 | }
166 |
167 | onClick (e) {
168 | ...definitions...
169 | }
170 | }
171 |
172 | capsid.def('foo-btn', FooButton)
173 | ```
174 |
175 | ## `@on(event: string, { at }: { at: string })`
176 |
177 | `@on(name, { at: selector })` is a method decorator. It's similar to `@on`, but
178 | it only handles the event from `selector` in the component.
179 |
180 | ```js
181 | import { on, component } from 'capsid'
182 |
183 | @component('btn')
184 | class Btn {
185 | @on('click', { at: '.btn' })
186 | onBtnClick (e) {
187 | ...definitions...
188 | }
189 | }
190 | ```
191 |
192 | In the above example, `onBtnClick` method listens to the click event of the
193 | `.btn` element in the `Btn`'s element.
194 |
195 | ## `@on.click`
196 |
197 | `@on.click` is a shorthand for `@on('click')`.
198 |
199 | ```js
200 | class Foo {
201 | @on.click
202 | onClick {
203 | // handling of the click of the Foo component
204 | }
205 | }
206 | ```
207 |
208 | ## `@on.click.at(selector: string)`
209 |
210 | `@on.click.at(selector)` is a shorthand for `@on('click', { at: selector })`
211 |
212 | ```js
213 | class Foo {
214 | @on.click.at(".edit-button")
215 | onClickAtEditButton() {
216 | // handling of the click of the edit button
217 | }
218 | }
219 | ```
220 |
221 | **NOTE:** You can add this type of short hand by calling
222 | `on.useHandler(eventName)`.
223 |
224 | ```js
225 | on.useHandler("change");
226 |
227 | class Foo {
228 | @on.change.at(".title-input") // <= This is enabled by the above useHandler call.
229 | onChangeAtTitleInput() {
230 | // handles the change event of title input field.
231 | }
232 | }
233 | ```
234 |
235 | ## `@emits(event: string)`
236 |
237 | `@emits(eventName)` triggers the event at the end of the method.
238 |
239 | ```js
240 | import { emits, component } from 'capsid'
241 |
242 | @component('manager')
243 | class Manager {
244 | @emits('manager.ended')
245 | start() {
246 | ...definitions...
247 | }
248 | }
249 | ```
250 |
251 | In the above example, `start` method triggers the `manager.ended` event when it
252 | finished. The returns value of the method is passed as `detail` of the event
253 | object. So you can pass the data from children to parents.
254 |
255 | If the method returns a promise, then the event is triggered _after_ the promise
256 | is resolved.
257 |
258 | ```js
259 | const { emits, component } = require('capsid')
260 |
261 | @component('manager')
262 | class Manager {
263 | @emits('manager.ended')
264 | start () {
265 | ...definitions...
266 |
267 | return promise
268 | }
269 | }
270 | ```
271 |
272 | In the above example, `manager.ended` event is triggered after `promise` is
273 | resolved. The resolved value of the promise is passed as `detail` of the event
274 | object.
275 |
276 | ## `@wired(selector: string) field`
277 |
278 | - @param {string} selector The selector to look up the element in the component
279 |
280 | This wires the decorated field to the element selected by the given selector.
281 | The wired element is a unusal dom element (HTMLElement), not a capsid component
282 | instance.
283 |
284 | If the selector matches to the multiple elements, then the first one is used.
285 |
286 | ## `@wired.all(selector: string) field`
287 |
288 | - @param {string} selector The selector to look up the elements in the component
289 |
290 | This wires the decorated field to the all elements selected by the given
291 | selector. This is similar to `@wired` decorator, but it wires all the elements,
292 | not the first one.
293 |
294 | ## `@is(...classNames: string[])`
295 |
296 | Adds the given class names to the element when it's mounted.
297 |
298 | ```ts
299 | @component("foo")
300 | @is("bar-observer")
301 | class Foo {
302 | }
303 |
304 | make("foo", document.body);
305 |
306 | document.body.classList.contains("bar-observer");
307 | // => true
308 | ```
309 |
310 | This decorator is useful when a component has several different roles. You can
311 | adds the role of the component by specifying `@is('class-name')`.
312 |
313 | ## `@innerHTML(html: string)`
314 |
315 | Sets the given html string as the innerHTML of the element at mount timing.
316 |
317 | ```ts
318 | @component("foo")
319 | @innerHTML(`
320 | hello
321 | `)
322 | class Foo {
323 | }
324 |
325 | make("foo", document.body);
326 |
327 | document.body.innerHTML;
328 | // => hello
329 | ```
330 |
331 | ## `@pub(event: string)`
332 |
333 | The method dispatches the `event` to the elements which have `sub:{event}`
334 | class. For example, if the method has `@pub('foo')`, then it dispatches `foo`
335 | event to the elements which have `sub:foo` class. The dispatched events don't
336 | buble up the dom tree.
337 |
338 | ```ts
339 | @component("my-comp")
340 | class MyComp {
341 | @pub("foo")
342 | method() {
343 | // something ...
344 | }
345 | }
346 | ```
347 |
348 | The returned value or resolved value of the decorator becomes the `detail` prop
349 | of the dispatched custom event.
350 |
351 | ## `@pub(event: string, selector: string)`
352 |
353 | The method dispatches `event` to the given `selector`.
354 |
355 | ```ts
356 | @component("my-comp")
357 | class MyComp {
358 | @pub("foo", "#foo-receiver")
359 | method() {
360 | // something ...
361 | }
362 | }
363 | ```
364 |
365 | ## `@sub(event: string)`
366 |
367 | This class decorator adds the `sub:event` class to the given component. For
368 | example if you use `@sub('foo')`, the component have `sub:foo` class, which
369 | means this class becomes the subscriber of `foo` event in combination with
370 | `@pub('foo')` decorator.
371 |
372 | ```ts
373 | @component("my-comp")
374 | @sub("foo")
375 | class MyComp {
376 | @on("foo")
377 | handler() {
378 | // ... do something
379 | }
380 | }
381 | ```
382 |
383 | # APIs
384 |
385 | These are advanced APIs of capsid. You usually don't need these APIs for
386 | building an app, but these could be useful if you write capsid plugins or
387 | reusable capsid modules. These APIs are used for building decorators of capsid.
388 |
389 | ```js
390 | import { def, get, install, make, mount, prep, unmount } from "capsid";
391 | ```
392 |
393 | - `def(name, constructor)`
394 | - Registers class-component.
395 | - `prep([name], [element])`
396 | - Initialize class-component on the given range.
397 | - `make(name, element)`
398 | - Initializes the element with the component of the given name and return the
399 | coelement instance.
400 | - `mount(Constructor, element)`
401 | - Initializes the element with the component of the given class and return the
402 | coelement.
403 | - `unmount(name, element)`
404 | - unmount the component from the element by its name.
405 | - `get(name, element)`
406 | - Gets the coelement instance from the given element.
407 | - `install(capsidModule, options)`
408 | - installs the capsid module with the given options.
409 |
410 | ## `def(name, constructor)`
411 |
412 | - @param {string} name The class name of the component
413 | - @param {Function} constructor The constructor of the coelement of the
414 | component
415 |
416 | This registers `constructor` as the constructor of the coelement of the class
417 | component of the given name `name`. The constructor is called with a jQuery
418 | object of the dom as the first parameter and the instance of the coelement is
419 | attached to the dom. The instance of coelement can be obtained by calling
420 | `elem.cc.get(name)`.
421 |
422 | Example:
423 |
424 | ```js
425 | class TodoItem {
426 | // ...behaviours...
427 | }
428 |
429 | capsid.def("todo-item", TodoItem);
430 | ```
431 |
432 | ```html
433 |
434 | ```
435 |
436 | ## `prep([name], [element])`
437 |
438 | - @param {string} [name] The capsid component name to intialize
439 | - @param {HTMLElement} [element] The range to initialize
440 |
441 | This initializes the capsid components of the given name under the given
442 | element. If the element is omitted, it initializes in the entire page. If the
443 | name is omitted, then it initializes all the registered class components in the
444 | given range.
445 |
446 | ## `make(name, element)`
447 |
448 | - @param {string} name The capsid component name to initialize
449 | - @param {HTMLElement} element The element to initialize
450 | - @return {} created coelement
451 |
452 | Initializes the element as the capsid component and returns the coelement
453 | instance.
454 |
455 | ```js
456 | const timer = make("timer", dom);
457 | ```
458 |
459 | ## `mount(Constructor, element)`
460 |
461 | - @param {Function} Constructor The constructor which defines the capsid
462 | component
463 | - @param {HTMLElemen} element The element to mount the component
464 | - @return {} The created coelement
465 |
466 | Initializes the element with the component of the given class and return the
467 | coelement.
468 |
469 | ```js
470 | class Component {
471 | __mount__ () {
472 | this.el.foo = 1
473 | }
474 | }
475 |
476 | const div = document.createElement('div')
477 |
478 | capsid.mount(Component, div)
479 |
480 | div.foo === 1 # => true
481 | ```
482 |
483 | Usually you don't need to use this API. If you're writing library using capsid,
484 | you might sometimes need to create an unnamed component and need this API then.
485 |
486 | ## `unmount(name, element)`
487 |
488 | - @param {string} name The component name
489 | - @param {HTMLElement} element The element
490 |
491 | Unmounts the component of the given name from the element.
492 |
493 | Example:
494 |
495 | ```js
496 | @component("foo")
497 | class Foo {
498 | @on("input")
499 | remove() {
500 | unmount("foo", this.el);
501 | }
502 | }
503 | ```
504 |
505 | The above example unmounts itself when it receives `input` event.
506 |
507 | ## `get(name, element)`
508 |
509 | - @param {string} name The capsid component name to get
510 | - @param {HTMLElement} element The element
511 | - @return The coelement instance
512 |
513 | Gets the component instance from the element.
514 |
515 | ```js
516 | const timer = capsid.get("timer", el);
517 | ```
518 |
519 | The above gets timer coelement from `el`, which is instance of `Timer` class.
520 |
521 | ### `install(capsidModule[, options])`
522 |
523 | - @param {CapsidModule} capsidModule The module to install
524 | - @param {Object} options The options to pass to the module
525 |
526 | This installs the capsid module.
527 |
528 | ```js
529 | capsid.install(require("capsid-popper"), { name: "my-app-popper" });
530 | ```
531 |
532 | See [capsid-module][capsid-module] repository for details.
533 |
534 | # Plugins
535 |
536 | ## Debug plugin
537 |
538 | `debug plugin` outputs information useful for debugging capsid app.
539 |
540 | ### Install
541 |
542 | Via npm:
543 |
544 | ```js
545 | import { install } from "capsid";
546 | import debug from "capsid/debug";
547 | install(debug);
548 | ```
549 |
550 | Via CDN:
551 |
552 | ```html
553 |
554 |
555 |
556 | ```
557 |
558 | And you'll get additional debug information in console.
559 |
560 |
561 |
562 | ## Outside Events Plugin
563 |
564 | ### Install
565 |
566 | Via npm:
567 |
568 | ```js
569 | import { install } from "capsid";
570 | import outside from "capsid/outside";
571 | install(outside);
572 | ```
573 |
574 | Via cdn:
575 |
576 | ```html
577 |
578 |
579 |
582 | ```
583 |
584 | With `outside-events-plugin`, you can bind methods to events _outside_ of your
585 | coponent's element. (This event need to bubble up to `document`)
586 |
587 | ```js
588 | @component("modal")
589 | class Modal {
590 | @on.outside("click")
591 | close() {
592 | this.el.classList.remove("is-shown");
593 | }
594 |
595 | open() {
596 | this.el.classList.add("is-shown");
597 | }
598 | }
599 | ```
600 |
601 | The above `modal` component gets `is-shown` class removed from the element when
602 | the outside of modal is clicked.
603 |
604 | #### prior art of capsid outside plugin
605 |
606 | - [jQuery outside events](https://github.com/cowboy/jquery-outside-events)
607 | - [react-onclickoutside](https://github.com/Pomax/react-onclickoutside)
608 |
609 | # Initialization
610 |
611 | There are 2 ways to initialize components:
612 |
613 | 1. [When document is ready][DOMContentLoaded] (automatic).
614 | 2. When `capsid.prep()` is called (manual).
615 |
616 | All components are initialized automatically when document is ready. You don't
617 | need to care about those elements which exist before document is ready. See
618 | [Hello Example][Hello Example] or [Clock Example][Clock Example] for example.
619 |
620 | If you add elements after document is ready (for example, after ajax requests),
621 | call `capsid.prep()` and that initializes all the components.
622 |
623 | ```js
624 | const addPartOfPage = async () => {
625 | const { html } = await axios.get('path/to/something.html')
626 |
627 | containerElemenent.innerHTML = html
628 |
629 | capsid.prep() // <= this initializes all the elements which are not yet initialized.
630 | })
631 | ```
632 |
633 | # Capsid Lifecycle
634 |
635 | Capsid has 2 lifecycle events: `mount` and `unmount`.
636 |
637 | ```
638 | nothing -> [mount] -> component mounted -> [unmount] -> nothing
639 | ```
640 |
641 | ## Lifecycle events
642 |
643 | - `mount`
644 | - HTML elements are mounted by the components.
645 | - An element is coupled with the corresponding coelement and they start
646 | working together.
647 |
648 | - `unmount`
649 | - An element is decouple with the coelement.
650 | - All events are removed and coelement is discarded.
651 | - You need to call `unmount(class, element)` to trigger the unmount event.
652 |
653 | ## Explanation of `mount`
654 |
655 | At `mount` event, these things happen.
656 |
657 | - The component class's `instance` (coelement) is created.
658 | - `instance`.el is set to corresponding dom element.
659 | - `before mount`-hooks are invoked.
660 | - This includes the initialization of event handlers, class names, innerHTML,
661 | and custom plugin's hooks.
662 | - if `instance` has **mount** method, then `instance.__mount__()` is called.
663 |
664 | The above happens in this order. Therefore you can access `this.el` and you can
665 | invoke the events at `this.el` in `__mount__` method.
666 |
667 | ## Lifecycle Methods
668 |
669 | ### `constructor`
670 |
671 | The constructor is called at the start of `mount`ing. You cannot access
672 | `this.el` here. If you need to interact with `this.el`, use `__mount__` method.
673 |
674 | ### `__mount__`
675 |
676 | `__mount__()` is called at the **end** of the mount event. When it is called,
677 | the dom element and event handlers are ready and available through `this.el`.
678 |
679 | ### `__unmount__`
680 |
681 | `__unmount__()` is called when component is unmounted. If your component put
682 | resources on global space, you should discard them here to avoid memory leak.
683 |
684 | # Coelement
685 |
686 | Coelement is the instance of Component class, which is attached to html element.
687 | You can get coelement from the element using `get` API.
688 |
689 | # History
690 |
691 | - 2022-01-03 v1.8.1 Modify npm package contents. #212
692 | - 2022-01-03 v1.8.0 Migrated to Deno. #212
693 | - 2020-04-02 v1.7.0 Better make/get/unmount types.
694 | - 2020-03-30 v1.6.2 Fix submodule export for TypeScript.
695 | - 2020-03-28 v1.6.1 Fix debug plugin.
696 | - 2020-03-28 v1.6.0 Automatic intialization of components inside `@innerHTML`.
697 | - 2020-03-21 v1.5.0 Extend `@pub` decorator and remove `@notifies`.
698 | - 2020-03-21 v1.4.0 Add `@innerHTML` decorator.
699 | - 2020-03-15 v1.3.0 Add `@pub` and `@sub` decorators.
700 | - 2020-03-14 v1.2.0 Add `@is` decorator.
701 | - 2020-03-13 v1.1.0 Add type declaration.
702 | - 2020-03-12 v1.0.0 Support TypeScript decorators. Drop babel decorators
703 | support.
704 | - 2019-06-09 v0.29.2 Throw error when empty selector is given (`@notifies`)
705 | - 2018-12-01 v0.29.0 Switch to TypeScript.
706 | - 2018-11-22 v0.28.0 Switch to new decorator. Remove jquery-plugin.
707 | - 2018-08-07 v0.26.1 Fix bug of unmount and on handler.
708 | - 2018-07-12 v0.26.0 Add debug log contents.
709 | - 2018-06-22 v0.25.0 Add `@on.useHandler`.
710 | - 2018-06-22 v0.24.0 Add `@on.click.at`.
711 | - 2018-05-20 v0.23.5 Fix unmount bug.
712 | - 2018-04-18 v0.23.4 Fix unmount bug.
713 | - 2018-04-10 v0.23.0 Change debug format.
714 | - 2018-04-09 v0.22.0 Rename **init** to **mount**.
715 | - 2018-04-08 v0.21.0 Add `unmount`.
716 | - 2018-04-04 v0.20.3 Change initialized class name.
717 | - 2018-03-08 v0.20.0 Add install function.
718 | - 2017-12-31 v0.19.0 Add wired, wired.all and wired.component decorators.
719 | - 2017-12-05 v0.18.3 Add an error message.
720 | - 2017-10-12 v0.18.0 Add Outside Events plugin.
721 | - 2017-10-01 v0.17.0 Add Debug plugin.
722 | - 2017-09-09 v0.16.0 Rename `@emit` to `@emits` and `@pub` to `@notifies`
723 | - 2017-09-06 v0.15.1 Change component init sequence.
724 | - 2017-09-05 v0.15.0 Add `mount` API. Remove `init` API.
725 | - 2017-08-04 v0.14.0 Make `@on` listeners ready at **init** call.
726 | - 2017-08-03 v0.13.0 Add pub decorator.
727 | - 2017-07-15 v0.12.0 Add wire.$el and wire.elAll to jquery plugin.
728 | - 2017-07-13 v0.11.0 Add wire.el and wire.elAll.
729 | - 2017-07-11 v0.10.0 Add emit.first rename emit.last to emit.
730 | - 2017-07-10 v0.9.0 Add on.click shorthand.
731 | - 2017-03-01 v0.8.0 Modify init sequence.
732 | - 2017-02-26 v0.7.0 Add static capsid object to each coelement class.
733 | - 2017-02-26 v0.6.0 static **init** rule.
734 | - 2017-02-25 v0.5.0 coelem.capsid, initComponent APIs.
735 | - 2017-01-19 v0.3.0 API reorganization.
736 | - 2017-01-19 v0.2.2 Rename to capsid.
737 | - 2017-01-17 v0.1.1 Add plugin system.
738 |
739 | # History of class-component.js (former project)
740 |
741 | - 2017-01-02 v13.0.0 Add **init** instead of init.
742 | - 2017-01-01 v12.1.1 Fix bug of event bubbling.
743 | - 2017-01-01 v12.1.0 Remove @emit.first. Use native dispatchEvent.
744 | - 2016-12-31 v12.0.0 Remove **cc_init** feature. Add init feature.
745 | - 2016-09-30 v10.7.1 Refactor @emit.last decorator
746 | - 2016-09-11 v10.7.0 Add @on(event, {at}) @emit.first and @emit.last
747 | - 2016-08-22 v10.6.2 Refactor the entrypoint.
748 | - 2016-08-22 v10.6.1 Improved the event listener registration process.
749 | - 2016-08-20 v10.6.0 Cleaned up some private APIs.
750 | - 2016-08-20 v10.5.0 Cleaned up codebase and made the bundle smaller. Remove
751 | some private APIs.
752 | - 2016-08-17 v10.4.1 Made built version smaller.
753 | - 2016-08-16 v10.4.0 Switched to babel-preset-es2015-loose.
754 | - 2016-08-16 v10.3.0 Modified bare @wire decorator.
755 | - 2016-08-02 v10.2.0 Added bare @component decorator.
756 | - 2016-07-21 v10.1.0 Added @wire decorator.
757 | - 2016-06-19 v10.0.0 Removed deprecated decorators `@event` and `@trigger`, use
758 | `@on` and `@emit` instead.
759 | - 2016-06-09 v9.2.0 Fixed bug of `@emit().last` decorator.
760 |
761 | # Examples
762 |
763 | - :wave: [Hello Example][Hello Example]
764 | - :stopwatch: [Clock Example][Clock Example]
765 | - :level_slider: [Counter Example][Counter Example]
766 | - :butterfly: [Mirroring Example][Mirroring Example]
767 |
768 | - [todomvc2](https://github.com/capsidjs/todomvc2)
769 | - [TodoMVC](http://todomvc.com/) in capsid.
770 |
771 | # License
772 |
773 | MIT
774 |
775 | [flux]: http://facebook.github.io/flux
776 | [evex]: http://github.com/capsidjs/evex
777 | [Hello Example]: https://codesandbox.io/s/hello-world-capsidjs-example-k5dgl
778 | [Clock Example]: https://codesandbox.io/s/clock-capsidjs-example-i9d7k
779 | [Counter Example]: https://codesandbox.io/s/km023p21nv
780 | [Mirroring Example]: https://codesandbox.io/s/p7m3xv3mvq
781 | [DOMContentLoaded]: https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded
782 | [capsid-module]: https://github.com/capsidjs/capsid-module
783 | [parcel]: https://parceljs.org/
784 |
--------------------------------------------------------------------------------
/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["deno.ns", "deno.unstable", "dom", "esnext"]
4 | },
5 | "fmt": {
6 | "files": {
7 | "exclude": ["node", "dist.js", "dist.min.js"]
8 | }
9 | },
10 | "lint": {
11 | "files": {
12 | "exclude": ["node", "dist.js", "dist.min.js"]
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/dnt.ts:
--------------------------------------------------------------------------------
1 | import { build } from "https://raw.githubusercontent.com/kt3k/dnt/a20e97cfa0e92a3c688002d2fe838dba2ebb50bb/mod.ts";
2 | import { join } from "https://deno.land/std@0.119.0/path/mod.ts";
3 |
4 | const outDir = "node";
5 |
6 | await build({
7 | entryPoints: ["./src/mod.ts"],
8 | outDir,
9 | shims: {
10 | deno: "dev",
11 | },
12 | redirects: {
13 | "./src/dom_polyfill_deno.ts": "./src/dom_polyfill_node.ts",
14 | "./src/td_deno.ts": "./src/td_node.ts",
15 | },
16 | test: true,
17 | package: {
18 | name: "capsid",
19 | version: "1.8.2",
20 | description:
21 | "Declarative DOM programming library based on TypeScript decorators",
22 | license: "MIT",
23 | repository: {
24 | type: "git",
25 | url: "git+https://github.com/capsidjs/capsid.git",
26 | },
27 | bugs: {
28 | url: "https://github.com/capsidjs/capsid/issues",
29 | },
30 | devDependencies: {
31 | jsdom: "^19.0.0",
32 | "@types/jsdom": "^16.2.14",
33 | testdouble: "^3.16.4",
34 | },
35 | },
36 | });
37 |
38 | Deno.copyFileSync("LICENSE", join(outDir, "LICENSE"));
39 | Deno.copyFileSync("README.md", join(outDir, "README.md"));
40 |
--------------------------------------------------------------------------------
/mod.ts:
--------------------------------------------------------------------------------
1 | export * from "./src/mod.ts";
2 |
--------------------------------------------------------------------------------
/src/add_hidden_item.ts:
--------------------------------------------------------------------------------
1 | import { BEFORE_MOUNT_KEY } from "./util/const.ts";
2 |
3 | // deno-lint-ignore no-explicit-any
4 | const addHiddenItem = (target: any, key: string, hook: unknown) => {
5 | target[key] = (target[key] || []).concat(hook);
6 | };
7 |
8 | // deno-lint-ignore no-explicit-any
9 | export const addMountHook = (target: any, hook: unknown) => {
10 | addHiddenItem(target, BEFORE_MOUNT_KEY, hook);
11 | };
12 |
13 | export default addHiddenItem;
14 |
--------------------------------------------------------------------------------
/src/decorators/component.ts:
--------------------------------------------------------------------------------
1 | import def from "../def.ts";
2 | import check from "../util/check.ts";
3 |
4 | /**
5 | * The decorator for class component registration.
6 | *
7 | * @param name The html class name to mount
8 | */
9 | // deno-lint-ignore no-explicit-any
10 | const component = (name: string): (desc: any) => void => {
11 | check(
12 | typeof name === "string" && !!name,
13 | "Component name must be a non-empty string",
14 | );
15 |
16 | // deno-lint-ignore ban-types
17 | return (Cls: Function) => {
18 | def(name, Cls);
19 | };
20 | };
21 |
22 | export default component;
23 |
--------------------------------------------------------------------------------
/src/decorators/component_test.ts:
--------------------------------------------------------------------------------
1 | import { component, make } from "../mod.ts";
2 | import { assert, clearComponents, genel } from "../test_helper.ts";
3 |
4 | Deno.test("@component(name)", async (t) => {
5 | await t.step(
6 | "works as a class decorator and registers the class as a class component of the given name",
7 | async () => {
8 | @component("decorated-component")
9 | class Foo {
10 | el?: HTMLElement;
11 |
12 | __mount__() {
13 | this.el!.setAttribute("this-is", "decorated-component");
14 | }
15 | }
16 |
17 | const el = genel.div``;
18 |
19 | const foo = make("decorated-component", el);
20 |
21 | assert(foo instanceof Foo);
22 | assert(el.getAttribute("this-is") === "decorated-component");
23 |
24 | await clearComponents();
25 | },
26 | );
27 | });
28 |
--------------------------------------------------------------------------------
/src/decorators/emits.ts:
--------------------------------------------------------------------------------
1 | import { triggerToElements } from "../util/event_trigger.ts";
2 | import check from "../util/check.ts";
3 |
4 | /**
5 | * `@emits(event)` decorator
6 | *
7 | * This decorator adds the event emission at the end of the method.
8 | * If the method returns the promise, then the event is emitted when it is resolved.
9 | * @param event The event name
10 | */
11 | const emits = (event: string) =>
12 | (
13 | // deno-lint-ignore no-explicit-any
14 | target: any,
15 | key: string,
16 | // deno-lint-ignore no-explicit-any
17 | descriptor: any,
18 | ) => {
19 | const method = descriptor.value;
20 | const constructor = target.constructor;
21 |
22 | check(
23 | !!event,
24 | `Unable to emits an empty event: constructor=${constructor.name} key=${key}`,
25 | );
26 |
27 | descriptor.value = function () {
28 | const result = method.apply(this, arguments);
29 | triggerToElements([this.el], event, true, result);
30 | return result;
31 | };
32 | };
33 |
34 | export default emits;
35 |
--------------------------------------------------------------------------------
/src/decorators/emits_test.ts:
--------------------------------------------------------------------------------
1 | import { def, emits, make } from "../mod.ts";
2 | import {
3 | assert,
4 | assertEquals,
5 | assertThrows,
6 | clearComponents,
7 | deferred,
8 | genel,
9 | } from "../test_helper.ts";
10 |
11 | Deno.test("@emits(event)", async (t) => {
12 | await t.step("throws when the empty event is given", async () => {
13 | assertThrows(
14 | () => {
15 | class Component {
16 | // deno-lint-ignore no-explicit-any
17 | @emits(undefined as any)
18 | emitter() {
19 | console.log();
20 | }
21 | }
22 | console.log(Component);
23 | },
24 | Error,
25 | "Unable to emits an empty event: constructor=Component key=emitter",
26 | );
27 | await clearComponents();
28 | });
29 |
30 | await t.step(
31 | "makes the method emit the event with the returned value",
32 | async () => {
33 | const p = deferred();
34 | class Component {
35 | @emits("event-foo")
36 | foo() {
37 | return 321;
38 | }
39 | }
40 |
41 | def("component", Component);
42 |
43 | const el = genel.div``;
44 |
45 | // deno-lint-ignore no-explicit-any
46 | el.addEventListener("event-foo" as any, (e: CustomEvent) => {
47 | assert(e.detail === 321);
48 |
49 | p.resolve();
50 | });
51 |
52 | make("component", el).foo();
53 | await clearComponents();
54 | await p;
55 | },
56 | );
57 |
58 | await t.step(
59 | "makes the method emit the event with the resolved value after the promise resolved",
60 | async () => {
61 | const p = deferred();
62 |
63 | class Component {
64 | @emits("event-foo")
65 | foo() {
66 | return new Promise((resolve) => {
67 | setTimeout(() => {
68 | resolve(123);
69 | }, 100);
70 | });
71 | }
72 | }
73 | def("component", Component);
74 |
75 | const el = genel.div``;
76 |
77 | // deno-lint-ignore no-explicit-any
78 | el.addEventListener("event-foo" as any, (e: CustomEvent) => {
79 | assertEquals(e.detail, 123);
80 |
81 | p.resolve();
82 | });
83 |
84 | make("component", el).foo();
85 | await clearComponents();
86 | await p;
87 | },
88 | );
89 | });
90 |
--------------------------------------------------------------------------------
/src/decorators/index.ts:
--------------------------------------------------------------------------------
1 | import on from "./on.ts";
2 | import useHandler from "./on_use_handler.ts";
3 |
4 | on.useHandler = useHandler;
5 | on.useHandler("click");
6 |
7 | export { on };
8 | export { default as emits } from "./emits.ts";
9 | export { default as wired } from "./wired.ts";
10 | export { default as component } from "./component.ts";
11 | export { default as is } from "./is.ts";
12 | export { default as innerHTML } from "./inner_html.ts";
13 | export { default as pub } from "./pub.ts";
14 | export { default as sub } from "./sub.ts";
15 |
--------------------------------------------------------------------------------
/src/decorators/inner_html.ts:
--------------------------------------------------------------------------------
1 | import prep from "../prep.ts";
2 | import { addMountHook } from "../add_hidden_item.ts";
3 |
4 | /**
5 | * is decorator adds the class names to the given element when it's mounted.
6 | * @param args The list of class names
7 | */
8 | export default (innerHTML: string) =>
9 | // deno-lint-ignore ban-types
10 | (Cls: Function) => {
11 | addMountHook(Cls, (el: HTMLElement) => {
12 | el.innerHTML = innerHTML;
13 | prep(null, el);
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/src/decorators/inner_html_test.ts:
--------------------------------------------------------------------------------
1 | import innerHTML from "./inner_html.ts";
2 | import component from "./component.ts";
3 | import make from "../make.ts";
4 | import get from "../get.ts";
5 | import { assert, clearComponents } from "../test_helper.ts";
6 |
7 | Deno.test("@is", async (t) => {
8 | await t.step("adds the class names to the element", async () => {
9 | const html = `
10 | hello
11 | `;
12 | @component("foo")
13 | @innerHTML(html)
14 | class Foo {}
15 |
16 | const el = document.createElement("div");
17 | const coel = make("foo", el);
18 |
19 | assert(coel instanceof Foo);
20 | assert(el.innerHTML, html);
21 | await clearComponents();
22 | });
23 |
24 | await t.step("initializes the component inside the innerHTML", async () => {
25 | const html = `
26 | hello
27 | `;
28 | @component("foo")
29 | @innerHTML(html)
30 | // deno-lint-ignore no-unused-vars
31 | class Foo {}
32 |
33 | @component("bar")
34 | class Bar {}
35 |
36 | const el = document.createElement("div");
37 | make("foo", el);
38 |
39 | const bar = get("bar", el.querySelector(".bar")!);
40 |
41 | assert(bar instanceof Bar);
42 | await clearComponents();
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/src/decorators/is.ts:
--------------------------------------------------------------------------------
1 | import { addMountHook } from "../add_hidden_item.ts";
2 |
3 | /**
4 | * is decorator adds the class names to the given element when it's mounted.
5 | * @param args The list of class names
6 | */
7 | export default (...args: string[]) =>
8 | // deno-lint-ignore ban-types
9 | (Cls: Function) => {
10 | addMountHook(Cls, (el: HTMLElement) => {
11 | el.classList.add(...args);
12 | });
13 | };
14 |
--------------------------------------------------------------------------------
/src/decorators/is_test.ts:
--------------------------------------------------------------------------------
1 | import is from "./is.ts";
2 | import component from "./component.ts";
3 | import make from "../make.ts";
4 | import { assert, clearComponents } from "../test_helper.ts";
5 |
6 | Deno.test("@is", async (t) => {
7 | await t.step("adds the class names to the element", async () => {
8 | @component("foo")
9 | @is("bar-observer")
10 | class Foo {}
11 |
12 | const el = document.createElement("div");
13 | const coel = make("foo", el);
14 |
15 | assert(coel instanceof Foo);
16 | assert(el.classList.contains("bar-observer"));
17 | await clearComponents();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/decorators/on.ts:
--------------------------------------------------------------------------------
1 | import { KEY_EVENT_LISTENERS } from "../util/const.ts";
2 | import debugMessage from "../util/debug_message.ts";
3 | import check from "../util/check.ts";
4 | import addHiddenItem, { addMountHook } from "../add_hidden_item.ts";
5 |
6 | /**
7 | * The decorator for registering event listener info to the method.
8 | * @param event The event name
9 | * @param at The selector
10 | */
11 | // deno-lint-ignore no-explicit-any
12 | const on: any = (event: string, { at }: { at?: string } = {}) =>
13 | (
14 | // deno-lint-ignore no-explicit-any
15 | target: any,
16 | key: string,
17 | // deno-lint-ignore no-explicit-any
18 | _: any,
19 | ) => {
20 | const constructor = target.constructor;
21 | check(
22 | !!event,
23 | `Empty event handler is given: constructor=${constructor.name} key=${key}`,
24 | );
25 | /**
26 | * @param el The element
27 | * @param coel The coelement
28 | * @param name The component name
29 | */
30 | // deno-lint-ignore no-explicit-any
31 | addMountHook(constructor, (el: HTMLElement, coel: any) => {
32 | const listener = (e: Event): void => {
33 | if (
34 | !at ||
35 | [].some.call(el.querySelectorAll(at), (node: Node) => {
36 | return node === e.target || node.contains(e.target as Node);
37 | })
38 | ) {
39 | // TODO(kt3k): selectively inject __DEV__ variable
40 | const __DEV__ = true;
41 | if (__DEV__) {
42 | debugMessage({
43 | type: "event",
44 | module: "💊",
45 | color: "#e0407b",
46 | e,
47 | el,
48 | coel,
49 | });
50 | }
51 |
52 | coel[key](e);
53 | }
54 | };
55 |
56 | /**
57 | * Removes the event listener.
58 | */
59 | listener.remove = () => {
60 | el.removeEventListener(event, listener);
61 | };
62 |
63 | /**
64 | * Store event listeners to remove it later.
65 | */
66 | addHiddenItem(coel, KEY_EVENT_LISTENERS, listener);
67 | el.addEventListener(event, listener);
68 | });
69 | };
70 |
71 | export default on;
72 |
--------------------------------------------------------------------------------
/src/decorators/on_click.ts:
--------------------------------------------------------------------------------
1 | import on from "./on.ts";
2 |
3 | export default on("click");
4 |
--------------------------------------------------------------------------------
/src/decorators/on_click_at.ts:
--------------------------------------------------------------------------------
1 | import on from "./on.ts";
2 |
3 | /**
4 | * @param at The selector
5 | */
6 | export default (at: string) => on("click", { at });
7 |
--------------------------------------------------------------------------------
/src/decorators/on_test.ts:
--------------------------------------------------------------------------------
1 | import { def, make, on } from "../mod.ts";
2 | import {
3 | assertEquals,
4 | assertThrows,
5 | clearComponents,
6 | deferred,
7 | genel,
8 | } from "../test_helper.ts";
9 |
10 | Deno.test("@on(event)", async (t) => {
11 | await t.step("throws when the event is empty", async () => {
12 | assertThrows(
13 | () => {
14 | class Component {
15 | @on(undefined)
16 | handler() {
17 | console.log();
18 | }
19 | }
20 |
21 | def("component", Component);
22 | },
23 | Error,
24 | "Empty event handler is given: constructor=Component key=handler",
25 | );
26 | await clearComponents();
27 | });
28 |
29 | await t.step(
30 | "registers the method as the event listener of the given event name",
31 | async () => {
32 | const p = deferred();
33 | class Component {
34 | @on("click")
35 | handler() {
36 | p.resolve();
37 | }
38 | }
39 |
40 | def("component", Component);
41 |
42 | const el = genel.div``;
43 |
44 | make("component", el);
45 |
46 | el.dispatchEvent(new Event("click"));
47 | await p;
48 | await clearComponents();
49 | },
50 | );
51 |
52 | await t.step(
53 | "registers the method as the event listener for children classes",
54 | async () => {
55 | const p = deferred();
56 | class Foo {
57 | @on("click")
58 | handler() {
59 | p.resolve();
60 | }
61 | }
62 | class Bar extends Foo {}
63 | class Baz extends Bar {}
64 |
65 | def("baz", Baz);
66 |
67 | const el = genel.div``;
68 | make("baz", el);
69 | el.dispatchEvent(new Event("click"));
70 | await p;
71 | await clearComponents();
72 | },
73 | );
74 | });
75 |
76 | Deno.test("@on(event, { at: selector })", async (t) => {
77 | await t.step(
78 | "registers the method as the event listener of the given event name and selector",
79 | async () => {
80 | const p = deferred();
81 | class Foo {
82 | @on("foo-event", { at: ".inner" })
83 | foo() {
84 | p.resolve();
85 | }
86 | @on("bar-event", { at: ".inner" })
87 | bar() {
88 | p.reject(new Error("bar should not be called"));
89 | }
90 | }
91 | def("foo", Foo);
92 |
93 | const el = genel.div`
94 |
95 | `;
96 |
97 | make("foo", el);
98 |
99 | if (document.body) {
100 | document.body.appendChild(el);
101 | }
102 |
103 | el.dispatchEvent(new CustomEvent("bar-event", { bubbles: true }));
104 | // FIXME(kt3k): deno_dom doesn't handle bubbling correctly
105 | // We need the following event handler as a workaround.
106 | el.querySelector(".inner")!.addEventListener("foo-event", () => {});
107 | el.querySelector(".inner")!.dispatchEvent(
108 | new CustomEvent("foo-event", { bubbles: true }),
109 | );
110 |
111 | if (document.body) {
112 | document.body.removeChild(el);
113 | }
114 | await p;
115 | await clearComponents();
116 | },
117 | );
118 | });
119 |
120 | Deno.test("@on.click", async (t) => {
121 | await t.step("binds method to click event", async () => {
122 | const p = deferred();
123 | class Component {
124 | @on.click
125 | handler() {
126 | p.resolve();
127 | }
128 | }
129 |
130 | def("foo", Component);
131 |
132 | const el = genel.div``;
133 | make("foo", el);
134 | el.dispatchEvent(new Event("click"));
135 | await p;
136 | await clearComponents();
137 | });
138 | });
139 |
140 | Deno.test("@on.click.at", async (t) => {
141 | await t.step("binds method to click event at the given element", async () => {
142 | let res = 0;
143 |
144 | class Component {
145 | @on.click.at(".foo")
146 | foo() {
147 | res += 1;
148 | }
149 | @on.click.at(".bar")
150 | bar() {
151 | res += 2;
152 | }
153 | }
154 |
155 | def("component", Component);
156 |
157 | const el = genel.div`
158 |
159 |
160 | `;
161 |
162 | make("component", el);
163 | const foo = el.querySelector(".foo")!;
164 |
165 | // FIXME(kt3k): deno_dom doesn't handle bubbling correctly
166 | // We need the following event handler as a workaround.
167 | foo.addEventListener("click", () => {});
168 | foo.dispatchEvent(new Event("click", { bubbles: true }));
169 |
170 | assertEquals(res, 1);
171 | await clearComponents();
172 | });
173 | });
174 |
--------------------------------------------------------------------------------
/src/decorators/on_use_handler.ts:
--------------------------------------------------------------------------------
1 | import on from "./on.ts";
2 |
3 | /**
4 | * Registers the on[eventName] and on[eventName].at decorators.
5 | * @param {string} handlerName
6 | */
7 | export default (handlerName: string) => {
8 | on[handlerName] = on(handlerName);
9 | on[handlerName].at = (selector: string) => on(handlerName, { at: selector });
10 | };
11 |
--------------------------------------------------------------------------------
/src/decorators/pub.ts:
--------------------------------------------------------------------------------
1 | import { triggerToElements } from "../util/event_trigger.ts";
2 | import check from "../util/check.ts";
3 |
4 | /**
5 | * Publishes the given event to the elements which has `sub:${event}` class.
6 | * For example `@pub('foo')` publishes the `foo` event to the elements
7 | * which have `sub:foo` class.
8 | * @param event The event name
9 | * @param targetSelector? The target selector. Default .sub\:{event}
10 | */
11 | export default (event: string, targetSelector?: string) =>
12 | (
13 | // deno-lint-ignore no-explicit-any
14 | target: any,
15 | key: string,
16 | // deno-lint-ignore no-explicit-any
17 | descriptor: any,
18 | ) => {
19 | const method = descriptor.value;
20 | const constructor = target.constructor;
21 |
22 | check(
23 | !!event,
24 | `Unable to publish empty event: constructor=${constructor.name} key=${key}`,
25 | );
26 |
27 | const selector = targetSelector || `.sub\\:${event}`;
28 |
29 | descriptor.value = function () {
30 | const result = method.apply(this, arguments);
31 | triggerToElements(
32 | // deno-lint-ignore no-explicit-any
33 | [].concat.apply([], document.querySelectorAll(selector) as any),
34 | event,
35 | false,
36 | result,
37 | );
38 | return result;
39 | };
40 | };
41 |
--------------------------------------------------------------------------------
/src/decorators/pub_test.ts:
--------------------------------------------------------------------------------
1 | import component from "./component.ts";
2 | import pub from "./pub.ts";
3 | import on from "./on.ts";
4 | import { def, prep } from "../mod.ts";
5 | import {
6 | assertEquals,
7 | assertThrows,
8 | clearComponents,
9 | deferred,
10 | genel,
11 | } from "../test_helper.ts";
12 |
13 | Deno.test("@pub(event)", async (t) => {
14 | await t.step("throws error when empty event is given", async () => {
15 | assertThrows(
16 | () => {
17 | class Component {
18 | // deno-lint-ignore no-explicit-any
19 | @pub(undefined as any)
20 | method() {
21 | console.log();
22 | }
23 | }
24 |
25 | def("component", Component);
26 | },
27 | Error,
28 | "Unable to publish empty event: constructor=Component key=method",
29 | );
30 | await clearComponents();
31 | });
32 |
33 | await t.step(
34 | "publishes the event to the elements of the sub:event class",
35 | async () => {
36 | const CUSTOM_EVENT = "foo-bar";
37 |
38 | class Component {
39 | @pub(CUSTOM_EVENT)
40 | @on("foo")
41 | publish() {
42 | console.log();
43 | }
44 | }
45 |
46 | def("component", Component);
47 |
48 | const el = genel.div`
49 |
50 |
51 |
55 |
65 | `;
66 |
67 | const child0 = el.querySelector(".child0")!;
68 | const child1 = el.querySelector(".child1")!;
69 | const child2 = el.querySelector(".child2")!;
70 | const comp = el.querySelector(".component")!;
71 |
72 | document.body.appendChild(el);
73 |
74 | prep();
75 |
76 | const promise0 = new Promise((resolve) =>
77 | child0.addEventListener(CUSTOM_EVENT, resolve)
78 | );
79 | const promise1 = new Promise((resolve) =>
80 | child1.addEventListener(CUSTOM_EVENT, resolve)
81 | );
82 | const promise2 = new Promise((resolve) =>
83 | child2.addEventListener(CUSTOM_EVENT, resolve)
84 | );
85 |
86 | comp.dispatchEvent(new CustomEvent("foo"));
87 |
88 | await Promise.all([promise0, promise1, promise2]);
89 |
90 | document.body.removeChild(el);
91 | await clearComponents();
92 | },
93 | );
94 |
95 | await t.step("publishes events with the return value as detail", async () => {
96 | const p = deferred();
97 | const CUSTOM_EVENT = "foo-bar";
98 |
99 | class Component {
100 | @pub(CUSTOM_EVENT)
101 | @on("foo")
102 | publish() {
103 | return { foo: 123, bar: "baz" };
104 | }
105 | }
106 |
107 | def("component", Component);
108 |
109 | const el = genel.div`
110 |
111 |
112 | `;
113 | document.body.appendChild(el);
114 | const target = el.querySelector(".target");
115 | const comp = el.querySelector(".component");
116 |
117 | prep();
118 |
119 | // deno-lint-ignore no-explicit-any
120 | target!.addEventListener(CUSTOM_EVENT as any, (e: CustomEvent) => {
121 | assertEquals(e.detail, { foo: 123, bar: "baz" });
122 | document.body.removeChild(el);
123 | p.resolve();
124 | });
125 |
126 | comp!.dispatchEvent(new CustomEvent("foo"));
127 | await p;
128 | await clearComponents();
129 | });
130 |
131 | await t.step(
132 | "publishes events with the resolved value as detail if it is async function",
133 | async () => {
134 | const p = deferred();
135 | const CUSTOM_EVENT = "foo-bar";
136 |
137 | class Component {
138 | @pub(CUSTOM_EVENT)
139 | @on("foo")
140 | publish() {
141 | return Promise.resolve({ foo: 123, bar: "baz" });
142 | }
143 | }
144 |
145 | def("component", Component);
146 |
147 | const el = genel.div`
148 |
149 |
150 | `;
151 | document.body.appendChild(el);
152 | const target = el.querySelector(".target")!;
153 | const comp = el.querySelector(".component")!;
154 |
155 | prep();
156 |
157 | // deno-lint-ignore no-explicit-any
158 | target.addEventListener(CUSTOM_EVENT as any, (e: CustomEvent) => {
159 | assertEquals(e.detail, { foo: 123, bar: "baz" });
160 | document.body.removeChild(el);
161 | p.resolve();
162 | });
163 |
164 | comp.dispatchEvent(new CustomEvent("foo"));
165 | await p;
166 | await clearComponents();
167 | },
168 | );
169 | });
170 |
171 | Deno.test("@pub(event, selector)", async (t) => {
172 | await t.step("publishes events to the given selector", async () => {
173 | const p = deferred();
174 | const CUSTOM_EVENT = "foo-bar";
175 |
176 | @component("component")
177 | // deno-lint-ignore no-unused-vars
178 | class Component {
179 | @pub(CUSTOM_EVENT, "#foo-bar-receiver")
180 | @on("foo")
181 | publish() {
182 | return { foo: 123, bar: "baz" };
183 | }
184 | }
185 |
186 | const el = genel.div`
187 |
188 |
189 | `;
190 | document.body.appendChild(el);
191 | const target = el.querySelector(".target");
192 | const comp = el.querySelector(".component");
193 |
194 | prep();
195 |
196 | // deno-lint-ignore no-explicit-any
197 | target!.addEventListener(CUSTOM_EVENT as any, (e: CustomEvent) => {
198 | assertEquals(e.detail, { foo: 123, bar: "baz" });
199 | document.body.removeChild(el);
200 | p.resolve();
201 | });
202 |
203 | comp!.dispatchEvent(new CustomEvent("foo"));
204 | await p;
205 | await clearComponents();
206 | });
207 | });
208 |
--------------------------------------------------------------------------------
/src/decorators/sub.ts:
--------------------------------------------------------------------------------
1 | import is from "./is.ts";
2 |
3 | export default (...args: string[]) =>
4 | // deno-lint-ignore ban-types
5 | (Cls: Function) => {
6 | is(...args.map((event) => "sub:" + event))(Cls);
7 | };
8 |
--------------------------------------------------------------------------------
/src/decorators/sub_test.ts:
--------------------------------------------------------------------------------
1 | import sub from "./sub.ts";
2 | import component from "./component.ts";
3 | import make from "../make.ts";
4 | import { assert, clearComponents } from "../test_helper.ts";
5 |
6 | Deno.test("@sub(event)", async (t) => {
7 | await t.step("adds the class names to the element", async () => {
8 | @component("foo")
9 | @sub("bar")
10 | class Foo {}
11 |
12 | const el = document.createElement("div");
13 | const coel = make("foo", el);
14 |
15 | assert(coel instanceof Foo);
16 | assert(el.classList.contains("sub:bar"));
17 | await clearComponents();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/decorators/wired.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Wires the element of the given selector to the property.
3 | *
4 | * class A {
5 | * @wired('input') input: HTMLInputElement
6 | *
7 | * @on.click
8 | * onClick() {
9 | * axios.post('my-api', { value: this.input.value })
10 | * }
11 | * }
12 | */
13 | const wired = (sel: string) =>
14 | // deno-lint-ignore no-explicit-any
15 | (_target: any, _key: string): any => {
16 | return {
17 | get() {
18 | return this.el.querySelector(sel);
19 | },
20 | set() {},
21 | };
22 | };
23 |
24 | /**
25 | * Wires all the elements to the property.
26 | *
27 | * class A {
28 | * @wired.all('li') items: HTMLElement
29 | *
30 | * @on.click
31 | * doEffect() {
32 | * this.items.forEach(li => {
33 | * li.classList.add('effect')
34 | * })
35 | * }
36 | * }
37 | */
38 | const wiredAll = (sel: string) =>
39 | // deno-lint-ignore no-explicit-any
40 | (_target: any, _key: string): any => {
41 | return {
42 | get() {
43 | return this.el.querySelectorAll(sel);
44 | },
45 | };
46 | };
47 |
48 | wired.all = wiredAll;
49 |
50 | export default wired;
51 |
--------------------------------------------------------------------------------
/src/decorators/wired_test.ts:
--------------------------------------------------------------------------------
1 | import { def, make, wired } from "../mod.ts";
2 | import { assertEquals, clearComponents, genel } from "../test_helper.ts";
3 |
4 | Deno.test("@wired(selector)", async (t) => {
5 | await t.step("wires the element in the component", async () => {
6 | class Component {
7 | @wired(".elm")
8 | elm?: HTMLDivElement;
9 | }
10 |
11 | def("wire-el-test", Component);
12 |
13 | const el = genel.div`
14 |
15 | `;
16 |
17 | const component = make
("wire-el-test", el);
18 | assertEquals(component.elm!.nodeName, "DIV");
19 | assertEquals(component.elm, el.firstChild);
20 | await clearComponents();
21 | });
22 | });
23 |
24 | Deno.test("@wired.all(selector)", async (t) => {
25 | await t.step("wires the all elements in the component", async () => {
26 | class Component {
27 | @wired.all(".elm")
28 | elms?: HTMLElement[];
29 | }
30 |
31 | def("comp", Component);
32 |
33 | const el = genel.div`
34 |
35 |
36 | `;
37 |
38 | const component = make("comp", el);
39 | assertEquals(component.elms!.length, 2);
40 | assertEquals(component.elms![0], el.firstChild);
41 | assertEquals(component.elms![1], el.lastChild);
42 | await clearComponents();
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/src/def.ts:
--------------------------------------------------------------------------------
1 | import registry from "./registry.ts";
2 | import prep from "./prep.ts";
3 | import initComponent from "./init_component.ts";
4 |
5 | import check from "./util/check.ts";
6 | import { ready } from "./util/document.ts";
7 | import { COELEMENT_DATA_KEY_PREFIX, COMPONENT_NAME_KEY } from "./util/const.ts";
8 | import { addMountHook } from "./add_hidden_item.ts";
9 |
10 | /**
11 | * Registers the class-component for the given name and constructor and returns the constructor.
12 | * @param name The component name
13 | * @param Constructor The constructor of the class component
14 | * @return The registered component class
15 | */
16 | // deno-lint-ignore ban-types
17 | const def = (name: string, Constructor: Function) => {
18 | check(
19 | typeof name === "string",
20 | "`name` of a class component has to be a string.",
21 | );
22 | check(
23 | typeof Constructor === "function",
24 | "`Constructor` of a class component has to be a function",
25 | );
26 | // deno-lint-ignore no-explicit-any
27 | (Constructor as any)[COMPONENT_NAME_KEY] = name;
28 | const initClass = `${name}-💊`;
29 |
30 | // deno-lint-ignore no-explicit-any
31 | addMountHook(Constructor, (el: HTMLElement, coel: any) => {
32 | // deno-lint-ignore no-explicit-any
33 | (el as any)[COELEMENT_DATA_KEY_PREFIX + name] = coel;
34 | // FIXME(kt3k): the below can be written as .add(name, initClass)
35 | // when deno_dom fixes add class.
36 | el.classList.add(name);
37 | el.classList.add(initClass);
38 | });
39 |
40 | /**
41 | * Initializes the html element by the configuration.
42 | * @param el The html element
43 | */
44 | const initializer = (el: HTMLElement) => {
45 | if (!el.classList.contains(initClass)) {
46 | initComponent(Constructor, el);
47 | }
48 | };
49 |
50 | // The selector
51 | initializer.sel = `.${name}:not(.${initClass})`;
52 |
53 | registry[name] = initializer;
54 |
55 | ready().then(() => {
56 | prep(name);
57 | });
58 | };
59 |
60 | export default def;
61 |
--------------------------------------------------------------------------------
/src/def_test.ts:
--------------------------------------------------------------------------------
1 | import { def, make } from "./mod.ts";
2 | import { assert, assertThrows, clearComponents } from "./test_helper.ts";
3 |
4 | Deno.test("def", async (t) => {
5 | await t.step("throws an error when the first param is not a string", () => {
6 | assertThrows(() => {
7 | // deno-lint-ignore no-explicit-any
8 | def(null as any, class A {});
9 | }, Error);
10 | });
11 |
12 | await t.step(
13 | "throws an error when the second param is not a function",
14 | () => {
15 | assertThrows(() => {
16 | // deno-lint-ignore no-explicit-any
17 | def("register-test2", null as any);
18 | }, Error);
19 | },
20 | );
21 |
22 | await t.step("registers the given class by the given name component", () => {
23 | class A {}
24 | def("assign-test2", A);
25 |
26 | const el = document.createElement("div");
27 | const coel = make("assign-test2", el);
28 |
29 | assert(coel instanceof A);
30 | });
31 |
32 | await clearComponents();
33 | });
34 |
--------------------------------------------------------------------------------
/src/dom_polyfill_deno.ts:
--------------------------------------------------------------------------------
1 | import { DOMParser } from "https://raw.githubusercontent.com/b-fuze/deno-dom/9e4814e8d8117d8410fabb726d118e144e49fbf3/deno-dom-wasm.ts";
2 | // deno-lint-ignore no-explicit-any
3 | (globalThis as any).document = new DOMParser().parseFromString(
4 | "",
5 | "text/html",
6 | );
7 | // deno-lint-ignore no-explicit-any
8 | (document as any).readyState = "complete";
9 |
--------------------------------------------------------------------------------
/src/dom_polyfill_node.ts:
--------------------------------------------------------------------------------
1 | import jsdom from "jsdom";
2 | const { JSDOM } = jsdom;
3 | const dom = new JSDOM(`Hello world
`);
4 | // deno-lint-ignore no-explicit-any
5 | (globalThis as any).document = dom.window.document;
6 | // deno-lint-ignore no-explicit-any
7 | (globalThis as any).CustomEvent = dom.window.CustomEvent;
8 | // deno-lint-ignore no-explicit-any
9 | (globalThis as any).Event = dom.window.Event;
10 |
--------------------------------------------------------------------------------
/src/get.ts:
--------------------------------------------------------------------------------
1 | import check, { checkComponentNameIsValid } from "./util/check.ts";
2 | import { COELEMENT_DATA_KEY_PREFIX } from "./util/const.ts";
3 |
4 | /**
5 | * Gets the eoelement instance of the class-component of the given name
6 | * @param name The class-component name
7 | * @param el The element
8 | */
9 | export default (name: string, el: Element): T => {
10 | checkComponentNameIsValid(name);
11 |
12 | // deno-lint-ignore no-explicit-any
13 | const coel = (el as any)[COELEMENT_DATA_KEY_PREFIX + name] as any;
14 |
15 | check(coel, `no coelement named: ${name}, on the dom: ${el.tagName}`);
16 |
17 | return coel;
18 | };
19 |
--------------------------------------------------------------------------------
/src/get_test.ts:
--------------------------------------------------------------------------------
1 | import { def, get, make } from "./mod.ts";
2 | import { Foo } from "./test_fixture.ts";
3 | import { assert, clearComponents } from "./test_helper.ts";
4 |
5 | Deno.test("get", async (t) => {
6 | def("foo", Foo);
7 |
8 | await t.step("gets the coelement instance from the element", () => {
9 | const el = document.createElement("div");
10 |
11 | make("foo", el);
12 |
13 | const coel = get("foo", el);
14 |
15 | assert(coel instanceof Foo);
16 | assert(coel.el === el);
17 | });
18 |
19 | await clearComponents();
20 | });
21 |
--------------------------------------------------------------------------------
/src/init_component.ts:
--------------------------------------------------------------------------------
1 | import { BEFORE_MOUNT_KEY } from "./util/const.ts";
2 |
3 | /**
4 | * Initialize component by the class constructor.
5 | * @param Constructor The coelement class
6 | * @param el The element
7 | * @return The created coelement instance
8 | */
9 | // deno-lint-ignore no-explicit-any
10 | export default (Constructor: any, el: HTMLElement): any => {
11 | const coel = new Constructor();
12 |
13 | // Assigns element to coelement's .el property
14 | coel.el = el;
15 |
16 | // Initialize `before mount` hooks
17 | // This includes:
18 | // - initialization of event handlers
19 | // - initialization of innerHTML
20 | // - initialization of class names
21 | const list = Constructor[BEFORE_MOUNT_KEY];
22 | if (Array.isArray(list)) {
23 | list.forEach((cb) => {
24 | cb(el, coel);
25 | });
26 | }
27 |
28 | if (typeof coel.__mount__ === "function") {
29 | coel.__mount__();
30 | }
31 |
32 | return coel;
33 | };
34 |
--------------------------------------------------------------------------------
/src/init_component_test.ts:
--------------------------------------------------------------------------------
1 | import * as capsid from "./mod.ts";
2 | import initComponent from "./init_component.ts";
3 | import { assert, assertEquals, clearComponents } from "./test_helper.ts";
4 |
5 | const { on } = capsid;
6 |
7 | Deno.test("initComponent", async (t) => {
8 | await t.step(
9 | "initializes the element as a component by the given constructor",
10 | () => {
11 | class A {}
12 |
13 | const el = document.createElement("div");
14 | const coel = initComponent(A, el);
15 |
16 | assertEquals(coel.el, el);
17 | assert(coel instanceof A);
18 | },
19 | );
20 |
21 | await t.step("calls __mount__", async () => {
22 | let resolve: () => void;
23 | const p = new Promise((r) => {
24 | resolve = r;
25 | });
26 | class A {
27 | el?: HTMLElement;
28 |
29 | __mount__() {
30 | assertEquals(this.el, el);
31 |
32 | resolve();
33 | }
34 | }
35 |
36 | const el = document.createElement("div");
37 |
38 | initComponent(A, el);
39 | await p;
40 | });
41 |
42 | await t.step("__mount__ runs after @on handlers are set", async () => {
43 | let resolve: () => void;
44 | const p = new Promise((r) => {
45 | resolve = r;
46 | });
47 | class A {
48 | el?: HTMLElement;
49 |
50 | __mount__() {
51 | this.el!.dispatchEvent(new CustomEvent("click"));
52 | }
53 |
54 | @on.click
55 | onClick() {
56 | resolve();
57 | }
58 | }
59 |
60 | initComponent(A, document.createElement("div"));
61 | await p;
62 | });
63 |
64 | await clearComponents();
65 | });
66 |
--------------------------------------------------------------------------------
/src/install.ts:
--------------------------------------------------------------------------------
1 | import def from "./def.ts";
2 | import prep from "./prep.ts";
3 | import make from "./make.ts";
4 | import mount from "./mount.ts";
5 | import unmount from "./unmount.ts";
6 | import get from "./get.ts";
7 | import { addMountHook } from "./add_hidden_item.ts";
8 | import {
9 | component,
10 | emits,
11 | innerHTML,
12 | is,
13 | on,
14 | pub,
15 | sub,
16 | wired,
17 | } from "./decorators/index.ts";
18 | import check from "./util/check.ts";
19 | import __registry__ from "./registry.ts";
20 |
21 | interface CapsidModule {
22 | // deno-lint-ignore ban-types
23 | install: Function;
24 | }
25 |
26 | /**
27 | * Installs the capsid module or plugin.
28 | *
29 | * @param capsidModule
30 | * @param options
31 | */
32 | // deno-lint-ignore ban-types
33 | const install = (capsidModule: CapsidModule, options?: object) => {
34 | check(
35 | typeof capsidModule.install === "function",
36 | "The given capsid module does not have `install` method. Please check the install call.",
37 | );
38 |
39 | capsidModule.install({
40 | def,
41 | prep,
42 | make,
43 | mount,
44 | unmount,
45 | get,
46 | install,
47 | addMountHook,
48 | component,
49 | emits,
50 | innerHTML,
51 | is,
52 | on,
53 | pub,
54 | sub,
55 | wired,
56 | __registry__,
57 | }, options || {});
58 | };
59 |
60 | export default install;
61 |
--------------------------------------------------------------------------------
/src/install_test.ts:
--------------------------------------------------------------------------------
1 | import * as capsid from "./mod.ts";
2 | import { assertEquals, assertThrows } from "./test_helper.ts";
3 |
4 | Deno.test("install", async (t) => {
5 | await t.step("calls install method of the given module", async () => {
6 | let resolve: () => void;
7 | const p = new Promise((r) => {
8 | resolve = r;
9 | });
10 | const options = { foo: "bar" };
11 |
12 | capsid.install(
13 | {
14 | install(capsidObj: unknown, options0: unknown) {
15 | assertEquals(capsidObj, capsid);
16 | assertEquals(options0, options);
17 |
18 | resolve();
19 | },
20 | },
21 | options,
22 | );
23 | await p;
24 | });
25 |
26 | await t.step(
27 | "throws when the given module does not have the install method",
28 | () => {
29 | assertThrows(
30 | () => {
31 | // deno-lint-ignore no-explicit-any
32 | capsid.install({} as any);
33 | },
34 | Error,
35 | "The given capsid module does not have `install` method. Please check the install call.",
36 | );
37 | },
38 | );
39 | });
40 |
--------------------------------------------------------------------------------
/src/make.ts:
--------------------------------------------------------------------------------
1 | import get from "./get.ts";
2 | import { checkComponentNameIsValid } from "./util/check.ts";
3 | import registry from "./registry.ts";
4 |
5 | /**
6 | * Initializes the given element as the class-component.
7 | * @param name The name of the class component
8 | * @param el The element to initialize
9 | * @return
10 | */
11 | export default (name: string, el: HTMLElement) => {
12 | checkComponentNameIsValid(name);
13 |
14 | registry[name](el);
15 |
16 | return get(name, el);
17 | };
18 |
--------------------------------------------------------------------------------
/src/make_test.ts:
--------------------------------------------------------------------------------
1 | import { def, get, make } from "./mod.ts";
2 | import { Foo } from "./test_fixture.ts";
3 | import { assert, assertEquals, clearComponents } from "./test_helper.ts";
4 |
5 | Deno.test("make", async (t) => {
6 | def("foo", Foo);
7 |
8 | await t.step(
9 | "initializes the element as an class-component of the given name",
10 | () => {
11 | const el = document.createElement("div");
12 |
13 | make("foo", el);
14 |
15 | assert(el.getAttribute("is_foo") === "true");
16 | },
17 | );
18 |
19 | await t.step("returns an instance of coelement", () => {
20 | assert(make("foo", document.createElement("div")) instanceof Foo);
21 | });
22 |
23 | await t.step("doesn't initialize element twice", () => {
24 | let a = 0;
25 | class A {
26 | __mount__() {
27 | a++;
28 | }
29 | }
30 | def("bar", A);
31 |
32 | const el = document.createElement("div");
33 | make("bar", el);
34 | make("bar", el);
35 |
36 | assertEquals(a, 1);
37 | });
38 |
39 | await t.step(
40 | "in __mount__, it can get component instance from el by the name",
41 | async () => {
42 | let resolve: () => void;
43 | const p = new Promise((r) => {
44 | resolve = r;
45 | });
46 |
47 | class Component {
48 | el?: HTMLElement;
49 |
50 | __mount__() {
51 | assertEquals(get("bar", this.el!), this);
52 |
53 | resolve();
54 | }
55 | }
56 |
57 | def("bar", Component);
58 |
59 | make("bar", document.createElement("div"));
60 | await p;
61 | },
62 | );
63 |
64 | await clearComponents();
65 | });
66 |
--------------------------------------------------------------------------------
/src/mod.ts:
--------------------------------------------------------------------------------
1 | import def from "./def.ts";
2 | import prep from "./prep.ts";
3 | import make from "./make.ts";
4 | import mount from "./mount.ts";
5 | import unmount from "./unmount.ts";
6 | import get from "./get.ts";
7 | import install from "./install.ts";
8 | import { addMountHook } from "./add_hidden_item.ts";
9 | import {
10 | component,
11 | emits,
12 | innerHTML,
13 | is,
14 | on,
15 | pub,
16 | sub,
17 | wired,
18 | } from "./decorators/index.ts";
19 | import __registry__ from "./registry.ts";
20 |
21 | export {
22 | __registry__,
23 | addMountHook,
24 | component,
25 | def,
26 | emits,
27 | get,
28 | innerHTML,
29 | install,
30 | is,
31 | make,
32 | mount,
33 | on,
34 | prep,
35 | pub,
36 | sub,
37 | unmount,
38 | wired,
39 | };
40 |
--------------------------------------------------------------------------------
/src/mount.ts:
--------------------------------------------------------------------------------
1 | import mount from "./init_component.ts";
2 |
3 | export default mount;
4 |
--------------------------------------------------------------------------------
/src/mount_test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals } from "./test_helper.ts";
2 | import { mount } from "./mod.ts";
3 |
4 | Deno.test("mount initializes the element by the given component class", async () => {
5 | let resolve: () => void;
6 | const p = new Promise((r) => {
7 | resolve = r;
8 | });
9 |
10 | class Component {
11 | el?: HTMLElement;
12 |
13 | __mount__() {
14 | assertEquals(this.el, div);
15 | resolve();
16 | }
17 | }
18 |
19 | const div = document.createElement("div");
20 |
21 | mount(Component, div);
22 | await p;
23 | });
24 |
--------------------------------------------------------------------------------
/src/plugins/debug_plugin.ts:
--------------------------------------------------------------------------------
1 | import { COMPONENT_NAME_KEY } from "../util/const.ts";
2 |
3 | const install = () => {
4 | // deno-lint-ignore no-explicit-any
5 | (globalThis as any).capsidDebugMessage = (message: any) => {
6 | switch (message.type) {
7 | case "event":
8 | onEventMessage(message);
9 | break;
10 | default:
11 | console.log(`Unknown message: ${JSON.stringify(message)}`);
12 | }
13 | };
14 | };
15 |
16 | /**
17 | * Gets the bold colored style.
18 | */
19 | const boldColor = (color: string): string =>
20 | `color: ${color}; font-weight: bold;`;
21 |
22 | /**
23 | * Gets the displayable component name.
24 | */
25 | // deno-lint-ignore no-explicit-any
26 | const getComponentName = (coel: any): string => {
27 | const { constructor } = coel;
28 | return `${constructor[COMPONENT_NAME_KEY] || constructor.name}`;
29 | };
30 |
31 | const defaultEventColor = "#f012be";
32 |
33 | const onEventMessage = ({
34 | coel,
35 | e,
36 | module,
37 | color,
38 | }: {
39 | // deno-lint-ignore no-explicit-any
40 | coel: any;
41 | e: Event;
42 | module: string;
43 | color?: string;
44 | }) => {
45 | const event = e.type;
46 | const component = getComponentName(coel);
47 |
48 | console.groupCollapsed(
49 | `${module}> %c${event}%c on %c${component}`,
50 | boldColor(color || defaultEventColor),
51 | "",
52 | boldColor("#1a80cc"),
53 | );
54 | console.log(e);
55 |
56 | if (e.target) {
57 | console.log(e.target);
58 | }
59 |
60 | if (coel.el) {
61 | console.log(coel.el);
62 | }
63 |
64 | console.groupEnd();
65 | };
66 |
67 | export default { install };
68 |
--------------------------------------------------------------------------------
/src/plugins/debug_plugin_test.ts:
--------------------------------------------------------------------------------
1 | import { install } from "../mod.ts";
2 | import debugPlugin from "./debug_plugin.ts";
3 | import { td } from "../test_helper.ts";
4 |
5 | Deno.test("debug-plugin", async (t) => {
6 | const afterEach = () => {
7 | td.reset();
8 | // deno-lint-ignore no-explicit-any
9 | delete (globalThis as any).capsidDebugMessage;
10 | };
11 |
12 | await t.step("logs event and component names with event type message", () => {
13 | install(debugPlugin);
14 |
15 | const el = document.createElement("a");
16 | const e = { type: "click", target: el };
17 | const coel = { constructor: { name: "foo" }, el };
18 |
19 | td.replace(console, "groupCollapsed");
20 | td.replace(console, "log");
21 | td.replace(console, "groupEnd");
22 | // deno-lint-ignore no-explicit-any
23 | (globalThis as any).capsidDebugMessage({
24 | type: "event",
25 | e,
26 | coel,
27 | module: "module",
28 | });
29 |
30 | td.verify(
31 | console.groupCollapsed(
32 | "module> %cclick%c on %cfoo",
33 | "color: #f012be; font-weight: bold;",
34 | "",
35 | "color: #1a80cc; font-weight: bold;",
36 | ),
37 | );
38 | td.verify(console.log(e));
39 | td.verify(console.groupEnd());
40 |
41 | afterEach();
42 | });
43 |
44 | await t.step("logs error message with unknown type message", () => {
45 | install(debugPlugin);
46 |
47 | td.replace(console, "log");
48 | // deno-lint-ignore no-explicit-any
49 | (globalThis as any).capsidDebugMessage({ type: "unknown" });
50 |
51 | td.verify(
52 | console.log(`Unknown message: ${JSON.stringify({ type: "unknown" })}`),
53 | );
54 |
55 | afterEach();
56 | });
57 | });
58 |
--------------------------------------------------------------------------------
/src/plugins/outside_events_plugin.ts:
--------------------------------------------------------------------------------
1 | import debugMessage from "../util/debug_message.ts";
2 |
3 | // deno-lint-ignore no-explicit-any
4 | const install = (capsid: any) => {
5 | const { on, addMountHook } = capsid;
6 |
7 | on.outside = (event: string) =>
8 | // deno-lint-ignore no-explicit-any
9 | (target: any, key: string, _: any) => {
10 | // deno-lint-ignore no-explicit-any
11 | addMountHook(target.constructor, (el: HTMLElement, coel: any) => {
12 | const listener = (e: Event): void => {
13 | // deno-lint-ignore no-explicit-any
14 | if (el !== e.target && !el.contains(e.target as any)) {
15 | // TODO(kt3k): selectively inject __DEV__ variable
16 | const __DEV__ = true;
17 | if (__DEV__) {
18 | debugMessage({
19 | type: "event",
20 | module: "outside-events",
21 | color: "#39cccc",
22 | el,
23 | e,
24 | coel,
25 | });
26 | }
27 |
28 | coel[key](e);
29 | }
30 | };
31 |
32 | document.addEventListener(event, listener);
33 | });
34 | };
35 | };
36 |
37 | export default { install };
38 |
--------------------------------------------------------------------------------
/src/plugins/outside_events_plugin_test.ts:
--------------------------------------------------------------------------------
1 | import "../test_helper.ts";
2 | import outsideEventsPlugin from "./outside_events_plugin.ts";
3 | import { install, mount, on } from "../mod.ts";
4 |
5 | Deno.test("outside-events-plugin", async (t) => {
6 | install(outsideEventsPlugin);
7 |
8 | await t.step("on.outside adds outside event handler", async () => {
9 | let resolve: () => void | undefined;
10 | const p = new Promise((r) => {
11 | resolve = r;
12 | });
13 | class Component {
14 | @on.outside("click")
15 | handleOutsideClick() {
16 | resolve();
17 | }
18 | }
19 |
20 | const div = document.createElement("div");
21 | document.body.appendChild(div);
22 |
23 | mount(Component, div);
24 |
25 | document.dispatchEvent(new Event("click"));
26 | await p;
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/prep.ts:
--------------------------------------------------------------------------------
1 | import { checkComponentNameIsValid } from "./util/check.ts";
2 | import registry from "./registry.ts";
3 |
4 | /**
5 | * Initializes the class components of the given name in the range of the given element.
6 | * @param name The class name
7 | * @param el The dom where class componets are initialized
8 | * @throws when the class name is invalid type.
9 | */
10 | export default (name?: string | null, el?: Element): void => {
11 | let classNames;
12 |
13 | if (!name) {
14 | classNames = Object.keys(registry);
15 | } else {
16 | checkComponentNameIsValid(name);
17 |
18 | classNames = [name];
19 | }
20 |
21 | classNames.map((className) => {
22 | [].map.call(
23 | (el || document).querySelectorAll(registry[className].sel),
24 | registry[className],
25 | );
26 | });
27 | };
28 |
--------------------------------------------------------------------------------
/src/prep_test.ts:
--------------------------------------------------------------------------------
1 | import { def, prep } from "./mod.ts";
2 | import { Foo } from "./test_fixture.ts";
3 | import { assertEquals, assertThrows, clearComponents } from "./test_helper.ts";
4 |
5 | Deno.test("prep", async (t) => {
6 | def("foo", Foo);
7 | def("foo-2", Foo);
8 |
9 | const clear = () => {
10 | if (document.body) {
11 | document.body.innerHTML = "";
12 | }
13 | };
14 |
15 | await t.step("initializes the class component of the given name", () => {
16 | clear();
17 | const el = document.createElement("div");
18 | el.setAttribute("class", "foo");
19 |
20 | if (document.body) {
21 | document.body.appendChild(el);
22 | }
23 |
24 | prep("foo");
25 |
26 | assertEquals(el.getAttribute("is_foo"), "true");
27 | });
28 |
29 | await t.step("initializes all when call with empty args", () => {
30 | const el = document.createElement("div");
31 | el.setAttribute("class", "foo");
32 |
33 | const el2 = document.createElement("div");
34 | el2.setAttribute("class", "foo-2");
35 |
36 | if (document.body) {
37 | document.body.appendChild(el);
38 | document.body.appendChild(el2);
39 | }
40 |
41 | prep();
42 |
43 | assertEquals(el.getAttribute("is_foo"), "true");
44 | assertEquals(el2.getAttribute("is_foo"), "true");
45 | });
46 |
47 | await t.step(
48 | "throws an error when the given name of class-component is not registered",
49 | () => {
50 | assertThrows(() => {
51 | prep("does-not-exist");
52 | }, Error);
53 | },
54 | );
55 |
56 | await clearComponents();
57 | });
58 |
--------------------------------------------------------------------------------
/src/registry.ts:
--------------------------------------------------------------------------------
1 | interface Initializer {
2 | // deno-lint-ignore no-explicit-any
3 | (el: HTMLElement, coel?: any): void;
4 | sel: string;
5 | }
6 | interface RegistryType {
7 | [key: string]: Initializer;
8 | }
9 |
10 | /**
11 | * The registry of component initializers.
12 | */
13 | const registry: RegistryType = {};
14 |
15 | export default registry;
16 |
--------------------------------------------------------------------------------
/src/td_deno.ts:
--------------------------------------------------------------------------------
1 | import "https://unpkg.com/testdouble@3.16.3/dist/testdouble.js";
2 |
3 | // deno-lint-ignore no-explicit-any
4 | const td = (globalThis as any).td;
5 | export { td };
6 |
--------------------------------------------------------------------------------
/src/td_node.ts:
--------------------------------------------------------------------------------
1 | import * as td from "testdouble";
2 |
3 | export { td };
4 |
--------------------------------------------------------------------------------
/src/test_fixture.ts:
--------------------------------------------------------------------------------
1 | export class Foo {
2 | el?: HTMLElement;
3 |
4 | __mount__() {
5 | this.el!.setAttribute("is_foo", "true");
6 | }
7 | }
8 |
9 | export class Bar {
10 | el?: HTMLElement;
11 |
12 | __mount__() {
13 | this.el!.setAttribute("is_bar", "true");
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/test_helper.ts:
--------------------------------------------------------------------------------
1 | export {
2 | assert,
3 | assertEquals,
4 | assertThrows,
5 | } from "https://deno.land/std@0.119.0/testing/asserts.ts";
6 | export { deferred } from "https://deno.land/std@0.119.0/async/deferred.ts";
7 | import genel_ from "https://esm.sh/genel";
8 | import * as capsid from "./mod.ts";
9 | import "./dom_polyfill_deno.ts";
10 | export { td } from "./td_deno.ts";
11 | import { ready } from "./util/document.ts";
12 |
13 | // deno-lint-ignore no-explicit-any
14 | const genel = genel_ as any;
15 | export { genel };
16 |
17 | export async function clearComponents() {
18 | await ready();
19 | Object.keys(capsid.__registry__).forEach((key) => {
20 | delete capsid.__registry__[key];
21 | });
22 | }
23 |
--------------------------------------------------------------------------------
/src/unmount.ts:
--------------------------------------------------------------------------------
1 | import get from "./get.ts";
2 | import {
3 | COELEMENT_DATA_KEY_PREFIX,
4 | KEY_EVENT_LISTENERS,
5 | } from "./util/const.ts";
6 |
7 | export default (name: string, el: HTMLElement): void => {
8 | const coel = get(name, el);
9 |
10 | // @ts-ignore use coel.__unmount__
11 | if (typeof coel.__unmount__ === "function") {
12 | // @ts-ignore use coel.__unmout__
13 | coel.__unmount__();
14 | }
15 |
16 | el.classList.remove(name, `${name}-💊`);
17 | // deno-lint-ignore no-explicit-any
18 | ((coel as any)[KEY_EVENT_LISTENERS] || []).forEach((listener: any) => {
19 | listener.remove();
20 | });
21 |
22 | // deno-lint-ignore no-explicit-any
23 | delete (el as any)[COELEMENT_DATA_KEY_PREFIX + name];
24 | // @ts-ignore use coel.el
25 | delete coel.el;
26 | };
27 |
--------------------------------------------------------------------------------
/src/unmount_test.ts:
--------------------------------------------------------------------------------
1 | import { def, get, make, on, unmount } from "./mod.ts";
2 | import { assert, assertEquals, clearComponents, genel } from "./test_helper.ts";
3 |
4 | Deno.test("unmount", async (t) => {
5 | await t.step("removes class name, reference and event handlers", async () => {
6 | class Foo {
7 | el?: Element;
8 |
9 | @on.click
10 | @on("foo")
11 | method() {
12 | throw new Error("event handler called!");
13 | }
14 | }
15 |
16 | def("foo", Foo);
17 |
18 | const el = genel.div``;
19 | const coel = make("foo", el);
20 |
21 | assert(el.classList.contains("foo"));
22 | assertEquals(coel.el, el);
23 | assertEquals(get("foo", el), coel);
24 |
25 | unmount("foo", el);
26 |
27 | assert(!el.classList.contains("foo"));
28 | assertEquals(coel.el, undefined);
29 |
30 | el.dispatchEvent(new Event("click"));
31 | el.dispatchEvent(new CustomEvent("foo"));
32 |
33 | await new Promise((resolve) => setTimeout(() => resolve(), 100));
34 | await clearComponents();
35 | });
36 |
37 | await t.step(
38 | "unmounts anscestor class's event handler correctly",
39 | async () => {
40 | class Foo {
41 | @on.click
42 | @on("foo")
43 | method() {
44 | throw new Error("event handler called!");
45 | }
46 | }
47 |
48 | class Bar extends Foo {}
49 |
50 | def("bar", Bar);
51 |
52 | const el = genel.div``;
53 | make("bar", el);
54 |
55 | unmount("bar", el);
56 |
57 | el.dispatchEvent(new Event("click"));
58 | el.dispatchEvent(new CustomEvent("foo"));
59 |
60 | await new Promise((resolve) => setTimeout(() => resolve(), 100));
61 | await clearComponents();
62 | },
63 | );
64 |
65 | await t.step("calls __unmount__ if exists", async () => {
66 | let resolve: () => void | undefined;
67 | const p = new Promise((r) => {
68 | resolve = r;
69 | });
70 | class Foo {
71 | __unmount__() {
72 | resolve();
73 | }
74 | }
75 |
76 | def("foo", Foo);
77 |
78 | const el = genel.div``;
79 |
80 | make("foo", el);
81 |
82 | unmount("foo", el);
83 | await p;
84 | await clearComponents();
85 | });
86 |
87 | await t.step(
88 | "does not unmount listeners of different component which mounted on the same element",
89 | async () => {
90 | let resolve: () => void | undefined;
91 | const p = new Promise((r) => {
92 | resolve = r;
93 | });
94 | class Foo {}
95 | class Bar {
96 | @on.click
97 | method() {
98 | resolve();
99 | }
100 | }
101 |
102 | def("foo", Foo);
103 | def("bar", Bar);
104 |
105 | const el = genel.div``;
106 |
107 | make("foo", el);
108 | make("bar", el);
109 | unmount("foo", el);
110 |
111 | el.dispatchEvent(new Event("click"));
112 | await p;
113 | await clearComponents();
114 | },
115 | );
116 | });
117 |
--------------------------------------------------------------------------------
/src/util/check.ts:
--------------------------------------------------------------------------------
1 | import registry from "../registry.ts";
2 | /**
3 | * Asserts the given condition holds, otherwise throws.
4 | * @param assertion The assertion expression
5 | * @param message The assertion message
6 | */
7 | export default function check(assertion: boolean, message: string): void {
8 | if (!assertion) {
9 | throw new Error(message);
10 | }
11 | }
12 |
13 | /**
14 | * Asserts the given name is a valid component name.
15 | * @param name The component name
16 | */
17 | // deno-lint-ignore no-explicit-any
18 | export function checkComponentNameIsValid(name: any): void {
19 | check(typeof name === "string", "The name should be a string");
20 | check(
21 | !!registry[name],
22 | `The coelement of the given name is not registered: ${name}`,
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/util/const.ts:
--------------------------------------------------------------------------------
1 | export const COELEMENT_DATA_KEY_PREFIX = "C$";
2 | export const KEY_EVENT_LISTENERS = "K$";
3 | export const COMPONENT_NAME_KEY = "N$";
4 | export const IS_KEY = "S$";
5 | export const BEFORE_MOUNT_KEY = "B$";
6 |
--------------------------------------------------------------------------------
/src/util/debug_message.ts:
--------------------------------------------------------------------------------
1 | // deno-lint-ignore no-explicit-any
2 | declare let capsidDebugMessage: any;
3 |
4 | export default (message: unknown) => {
5 | if (typeof capsidDebugMessage === "function") {
6 | capsidDebugMessage(message);
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/src/util/document.ts:
--------------------------------------------------------------------------------
1 | const READY_STATE_CHANGE = "readystatechange";
2 |
3 | let p: Promise;
4 | export function ready() {
5 | return p = p || new Promise((resolve) => {
6 | const doc = document;
7 | const checkReady = () => {
8 | if (doc.readyState === "complete") {
9 | resolve();
10 | doc.removeEventListener(READY_STATE_CHANGE, checkReady);
11 | }
12 | };
13 |
14 | doc.addEventListener(READY_STATE_CHANGE, checkReady);
15 |
16 | checkReady();
17 | });
18 | }
19 |
--------------------------------------------------------------------------------
/src/util/event_trigger.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Triggers the event to the given elements.
3 | * @param el The element
4 | * @param type The event type
5 | * @param detail The optional detail object
6 | */
7 | export const triggerToElements = (
8 | elements: HTMLElement[],
9 | type: string,
10 | bubbles: boolean,
11 | // deno-lint-ignore no-explicit-any
12 | result: any,
13 | ) => {
14 | // deno-lint-ignore no-explicit-any
15 | const emit = (r: any) => {
16 | elements.forEach((el) => {
17 | el.dispatchEvent(new CustomEvent(type, { detail: r, bubbles }));
18 | });
19 | };
20 | if (result && result.then) {
21 | result.then(emit);
22 | } else {
23 | emit(result);
24 | }
25 | };
26 |
--------------------------------------------------------------------------------