├── .codesandbox
└── ci.json
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .npmignore
├── .npmrc
├── .prettierignore
├── LICENCE
├── README.md
├── examples
└── basic
│ ├── about.html
│ ├── contact.html
│ ├── index.html
│ ├── package.json
│ ├── src
│ ├── components
│ │ ├── App.ts
│ │ ├── Footer.ts
│ │ ├── Header.ts
│ │ └── MainButton.ts
│ ├── fetchDOM.ts
│ ├── helpers
│ │ └── defaultTransitions.ts
│ ├── index.css
│ ├── index.ts
│ └── pages
│ │ ├── About.ts
│ │ ├── Contact.ts
│ │ └── Home.ts
│ ├── tsconfig.json
│ └── vite.config.ts
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── src
├── Component.ts
└── index.ts
├── tests
├── Component.add.test.ts
├── Component.find.test.ts
├── Component.init.test.ts
├── Component.lifecycle.test.ts
├── helpers
│ └── wait.ts
└── templates
│ ├── AboutPageMock.ts
│ └── HomePageMock.ts
├── tsconfig.json
├── tsup.config.ts
└── turbo.json
/.codesandbox/ci.json:
--------------------------------------------------------------------------------
1 | {
2 | "buildCommand": "build",
3 | "packages": ["./packages/*"],
4 | "sandboxes": [
5 | "/examples/basic"
6 | ],
7 | "node": "18"
8 | }
9 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 | on: [push]
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 | strategy:
7 | matrix:
8 | node-version: [18.x]
9 | steps:
10 | - uses: actions/checkout@v2
11 | - name: Use Node.js ${{ matrix.node-version }}
12 | uses: actions/setup-node@v1
13 | with:
14 | node-version: ${{ matrix.node-version }}
15 |
16 | - name: Checkout
17 | uses: actions/checkout@v3
18 | with:
19 | fetch-depth: 0
20 |
21 | - name: Install pnpm
22 | uses: pnpm/action-setup@v2
23 | with:
24 | version: 8
25 |
26 | - name: Install dependencies
27 | run: pnpm install
28 |
29 | - name: build
30 | run: pnpm run build
31 |
32 | - name: test
33 | run: pnpm run test
34 |
35 | - name: size
36 | run: pnpm run size
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules
3 | dist
4 | tsconfig.tsbuildinfo
5 | tsconfig-build.tsbuildinfo
6 | .husky
7 | .DS_Store
8 | .turbo
9 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .cache
3 | .github
4 | example
5 | examples
6 | node_modules
7 | src
8 | test
9 | .babelrc
10 | .prettierignore
11 | .prettierrc
12 | stories
13 | tsconfig.json
14 | pnpm-lock.yaml
15 | pnpm-workspace.yaml
16 | dist
17 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | strict-peer-dependencies=false
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .cache
2 | node_modules
3 | dist
4 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Willy Brauner
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
Compose
3 |
4 | 
5 |
6 | 
7 | 
8 | 
9 | 
10 |
11 | Compose is a small library that help to links your javascript to your DOM.
12 | _⚠️ This library is work in progress, the API is subject to change until the v1.0 release._
13 |
14 |
15 |
16 |
17 |
18 | ## Summary
19 |
20 | - [Installation](#Installation)
21 | - [Component](#Component)
22 | - [add](#add)
23 | - [addAll](#addAll)
24 | - [find](#find)
25 | - [findAll](#findAll)
26 | - [lifecycle](#lifecycle)
27 | - [beforeMount](#beforeMount)
28 | - [mounted](#mounted)
29 | - [unmounted](#unmounted)
30 | - [workflow](#Workflow)
31 | - [Credits](#Credits)
32 | - [Licence](#Licence)
33 |
34 | ## Installation
35 |
36 | ```shell
37 | $ npm i @wbe/compose
38 | ```
39 |
40 | ## Component
41 |
42 | ### add
43 |
44 | This method allows to 'add' new Component instance to the tree.
45 | It returns a single instance and associated properties.
46 |
47 | Add component inside the class:
48 |
49 | ```js
50 | class Foo extends Component {
51 | bar = this.add(Bar)
52 |
53 | method() {
54 | // then, access child Bar instance
55 | this.bar.root
56 | this.bar.mounted()
57 | this.bar.unmounted()
58 | // etc...
59 | }
60 | }
61 | ```
62 |
63 | The method accepts a static props parameter which we can access from the new Bar component via `this.props`.
64 |
65 | ```js
66 | bar = this.add(Bar, { props: { foo: "bar" } })
67 | ```
68 |
69 | ### addAll
70 |
71 | `addAll` will return an array of instances.
72 |
73 | ```html
74 |
75 |
76 |
77 |
78 | ```
79 |
80 | ```js
81 | class Foo extends Component {
82 | bars = this.addAll(Bar)
83 | // Returns array of Bar: [Bar, Bar]
84 | }
85 | ```
86 |
87 | ### `find`
88 |
89 | `find` is a simple `this.root.querySelector()` wrapper.
90 | This method allows retrieving `BEM` element of current $root component.
91 |
92 | ```html
93 |
94 |
Hello
95 |
96 | ```
97 |
98 | ```js
99 | class Bar extends Component {
100 | //
Hello
can be query with:
101 | $title = this.find("_title")
102 | // or
103 | $title = this.find("Bar_title")
104 | }
105 | ```
106 |
107 | ### `findAll`
108 |
109 | `findAll` is a simple `this.$root.querySelectorAll()` wrapper.
110 | This method returns a DOM Element array.
111 |
112 | ```html
113 |
(...args: any[]) => C,
78 | options?: Partial>,
79 | ): C {
80 | const name = options?.name || classComponent?.["name"]
81 | const element = this.root.querySelector(`.${name}`)
82 | return element ? new classComponent
(element, options) : null
83 | }
84 |
85 | /**
86 | * Add multiple children components
87 | */
88 | public addAll(
89 | classComponent: new
(...args: any[]) => C extends (infer U)[] ? U : never,
90 | options?: Partial>,
91 | ): C {
92 | const arr = []
93 | const name = options?.name || classComponent?.["name"]
94 | const elements = this.root.querySelectorAll(`.${name}`)
95 | if (!elements?.length) return arr as any
96 |
97 | // map on each elements (because elements return an array)
98 | for (let i = 0; i < elements.length; i++) {
99 | const classInstance = new classComponent
(elements[i], options)
100 | arr.push(classInstance)
101 | }
102 | return arr as C
103 | }
104 |
105 | // --------------------------------------------------------------------------- FIND
106 |
107 | /**
108 | * Find single HTML element from parent root
109 | */
110 | public find(className: string): T {
111 | return this.root?.querySelector(
112 | className.startsWith("_") ? `.${this.name}${className}` : `.${className}`,
113 | )
114 | }
115 |
116 | /**
117 | * Find HTML element list from parent root
118 | */
119 | public findAll(className: string): T {
120 | const els = this.root?.querySelectorAll(
121 | className.startsWith("_") ? `.${this.name}${className}` : `.${className}`,
122 | )
123 | return Array.from(els || []) as T
124 | }
125 |
126 | // --------------------------------------------------------------------------- TRANSITIONS
127 |
128 | public playIn(): Promise {
129 | return Promise.resolve()
130 | }
131 |
132 | public playOut(): Promise {
133 | return Promise.resolve()
134 | }
135 |
136 | // --------------------------------------------------------------------------- CORE
137 |
138 | /**
139 | * Process callback function on each children components
140 | * A children component is an instance of Component
141 | * @param callback
142 | */
143 | #onChildrenComponents(callback: (component) => void): void {
144 | Object.keys(this)?.forEach((child) => {
145 | const curr = this?.[child]
146 | if (Array.isArray(curr)) {
147 | curr.forEach((c) => {
148 | if (c instanceof Component) callback(c)
149 | })
150 | } else if (curr instanceof Component) callback(curr)
151 | })
152 | }
153 |
154 | /**
155 | * Watch children components changed
156 | * If this current component is removed (with his children), unmount children
157 | */
158 | #watchChildren(): void {
159 | this.#observer = new MutationObserver((mutationsList) => {
160 | for (const mutation of mutationsList) {
161 | for (const node of mutation.removedNodes as any) {
162 | const nodeRemovedId = this.#getComponentId(node as any)
163 | const parentNode = node.parentNode?.querySelector(`*[${ID_ATTR}='${nodeRemovedId}']`)
164 | if (nodeRemovedId && parentNode) continue
165 |
166 | this.#onChildrenComponents((component) => {
167 | if (!component) return
168 | if (nodeRemovedId === component?.id && component?.isMounted) {
169 | component._unmounted()
170 | component?.observer?.disconnect()
171 | }
172 | })
173 | }
174 | }
175 | })
176 |
177 | if (this.root) {
178 | this.#observer.observe(this.root, {
179 | subtree: true,
180 | childList: true,
181 | })
182 | }
183 | }
184 |
185 | /**
186 | * Get component ID
187 | * @param $node
188 | */
189 | #getComponentId($node: HTMLElement): number {
190 | return $node?.getAttribute?.(ID_ATTR) && parseInt($node.getAttribute(ID_ATTR))
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Component } from "./Component"
2 |
--------------------------------------------------------------------------------
/tests/Component.add.test.ts:
--------------------------------------------------------------------------------
1 | // @vitest-environment happy-dom
2 | import { beforeEach, expect, it } from "vitest"
3 | import { Window } from "happy-dom"
4 | import { HomePageMock } from "./templates/HomePageMock"
5 | import { Component } from "../src"
6 |
7 | let window = new Window()
8 | let document = window.document
9 |
10 | beforeEach(() => {
11 | window = new Window()
12 | document = window.document
13 | })
14 |
15 | class Button extends Component {
16 | static attrName = "Button"
17 | }
18 |
19 | class NotExistInDOM extends Component {
20 | static attrName = "NotExistInDOM"
21 | }
22 |
23 | it("Should add properly", async () => {
24 | class HomePage extends Component {
25 | public button = this.add(Button)
26 | }
27 | document.write(HomePageMock())
28 | const root = document.querySelector(".HomePage") as any
29 | const homePage = new HomePage(root)
30 | expect(homePage.button).toBeInstanceOf(Button)
31 | expect(homePage.button.root).toBe(root.querySelector(".Button"))
32 | })
33 |
34 | it("Should addAll properly", async () => {
35 | class HomePage extends Component {
36 | public buttons = this.addAll