From 696c84e488f40cf249b2304f42ed4e06830e65a3 Mon Sep 17 00:00:00 2001 From: sebkolind Date: Mon, 27 May 2024 15:36:25 +0200 Subject: [PATCH 01/16] ci: add workflows/release-next.yml --- .github/workflows/release-next.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/release-next.yml diff --git a/.github/workflows/release-next.yml b/.github/workflows/release-next.yml new file mode 100644 index 0000000..8f74ab4 --- /dev/null +++ b/.github/workflows/release-next.yml @@ -0,0 +1,23 @@ +name: Build and Release (next) + +on: + push: + branches: + - next + tags: + - v*-[0-9] # v0.0.3-1, v0.0.3-2, etc. + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: '20' + - run: npm ci + - run: npm run build + - run: npm run test + - uses: JS-DevTools/npm-publish@v3 + with: + token: ${{ secrets.NPM_TOKEN }} From 2cf6639de6204976d40bf6d9856bfbd87cca65bd Mon Sep 17 00:00:00 2001 From: sebkolind Date: Mon, 27 May 2024 18:38:48 +0200 Subject: [PATCH 02/16] ci: run on `next` branch --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0846199..5f3e94c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,7 @@ on: pull_request: branches: - main + - next jobs: build: From 9c63c2833b7c60b7da83c4109ef66da67774512e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20L=2E=20K=2E=20S=C3=B8rensen?= Date: Mon, 27 May 2024 20:27:56 +0200 Subject: [PATCH 03/16] feat(tags): add `mounted` (#42) --- CHANGELOG.md | 6 ++++++ src/__tests__/attributes.test.ts | 8 ++++++++ src/__tests__/tags.test.ts | 8 ++++++++ src/attributes.ts | 8 ++++++-- src/tags.ts | 2 ++ src/types.ts | 8 ++++++-- src/walker.ts | 6 ++++++ 7 files changed, 42 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e08d71..273a455 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- Add `mounted` to `tags` to run code after the tag has been mounted + ## [0.0.32] - 2024-05-20 ### Fixed diff --git a/src/__tests__/attributes.test.ts b/src/__tests__/attributes.test.ts index 1fb664d..3d66c0b 100644 --- a/src/__tests__/attributes.test.ts +++ b/src/__tests__/attributes.test.ts @@ -34,4 +34,12 @@ describe('attributes.ts', () => { expect(el.value).toBe('test'); }); + + test("doesn't add `mounted` as an attribute", () => { + const el = document.createElement('div'); + + addAttribute(el, 'mounted', 'test'); + + expect(el.hasAttribute('mounted')).toBe(false); + }); }); diff --git a/src/__tests__/tags.test.ts b/src/__tests__/tags.test.ts index ce810ab..89ef009 100644 --- a/src/__tests__/tags.test.ts +++ b/src/__tests__/tags.test.ts @@ -68,4 +68,12 @@ describe('tags.ts', () => { expect(el.$tent.attributes['data-bar']).toBe('baz'); expect(el.$tent.attributes['onclick']).toBe(fn); }); + + test('mounted', () => { + const fn = jest.fn(); + const el = createTag(['div', 'test', { mounted: fn }]); + + expect(fn).toHaveBeenCalledTimes(1); + expect(fn).toHaveBeenCalledWith({ el }); + }); }); diff --git a/src/attributes.ts b/src/attributes.ts index 1529e2e..bafe1db 100644 --- a/src/attributes.ts +++ b/src/attributes.ts @@ -1,10 +1,14 @@ -import { type TentNode, type Attrs } from './types'; +import { type TentNode, type Attrs, type TagAttrsValues } from './types'; function addAttribute( el: TentNode | HTMLElement, key: string, - value: string | boolean | number, + value: TagAttrsValues, ) { + if (key === 'mounted') { + return; + } + if (typeof value === 'boolean') { if (value) { el.setAttribute(key, ''); diff --git a/src/tags.ts b/src/tags.ts index 0edfee3..c8ca138 100644 --- a/src/tags.ts +++ b/src/tags.ts @@ -26,6 +26,8 @@ function createTag(context: Context) { el.append(typeof children === 'number' ? children.toString() : children); } + attributes?.mounted?.({ el }); + return el; } diff --git a/src/types.ts b/src/types.ts index fc6e146..c16b87d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,9 +23,13 @@ export type TentNode = Node & }; export type Children = string | number | TentNode | (Node | string | Context)[]; -export type Context = [string, Children, object | undefined]; +export type Context = [string, Children, TagAttrs | undefined]; +export type TagAttrsValues = string | boolean | number | Function; +type TagAttrs = Record & { + mounted?: ({ el }: { el: TentNode }) => void; +}; export type Tags = Record< string, - (children: Children, attrs?: object) => TentNode + (children: Children, attrs?: TagAttrs) => TentNode >; diff --git a/src/walker.ts b/src/walker.ts index a68a739..fc3dd29 100644 --- a/src/walker.ts +++ b/src/walker.ts @@ -19,6 +19,12 @@ function walker(oldNode: TentNode, newNode: TentNode) { return; } + if (oldNode.$tent == null || newNode.$tent == null) { + oldNode.replaceWith(newNode); + + return; + } + // Remove attributes that are not present in the new node for (const key in oldNode.$tent.attributes) { if (newNode.$tent.attributes[key] == null) { From 3a9b57e6c99098e3c0466718a29b2c4b831514fc Mon Sep 17 00:00:00 2001 From: sebkolind Date: Mon, 27 May 2024 20:34:12 +0200 Subject: [PATCH 04/16] 0.0.33-0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3120e25..784ddec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tentjs/tent", - "version": "0.0.32", + "version": "0.0.33-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tentjs/tent", - "version": "0.0.32", + "version": "0.0.33-0", "license": "MIT", "devDependencies": { "@parcel/packager-ts": "^2.10.3", diff --git a/package.json b/package.json index 018c510..8ea6829 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tentjs/tent", - "version": "0.0.32", + "version": "0.0.33-0", "description": "A jsx-free, super-lightweight and zero-dependency library to add interactivity to the web - without all the nonsense.", "source": "src/main.ts", "main": "dist/main.js", From 95229fc929b340ba8eeeef300544c47404d93bef Mon Sep 17 00:00:00 2001 From: sebkolind Date: Mon, 27 May 2024 20:37:25 +0200 Subject: [PATCH 05/16] ci: only release on tags --- .github/workflows/release-next.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/release-next.yml b/.github/workflows/release-next.yml index 8f74ab4..5ece51d 100644 --- a/.github/workflows/release-next.yml +++ b/.github/workflows/release-next.yml @@ -1,9 +1,7 @@ name: Build and Release (next) on: - push: - branches: - - next + create: tags: - v*-[0-9] # v0.0.3-1, v0.0.3-2, etc. From 3ea60fbded4d4a9e4572431b25025c3d2d318c6f Mon Sep 17 00:00:00 2001 From: sebkolind Date: Mon, 27 May 2024 20:54:55 +0200 Subject: [PATCH 06/16] docs(changelog): v0.0.33-0 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 273a455..e11bbb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.0.33-0] ### Added From 0347342ee08edaf39cabe87e573583e65edafbbb Mon Sep 17 00:00:00 2001 From: sebkolind Date: Mon, 27 May 2024 21:01:03 +0200 Subject: [PATCH 07/16] docs(changelog): add missing date for v0.0.33-0 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e11bbb7..b563e40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.0.33-0] +## [0.0.33-0] - 2024-05-27 ### Added From eb31d8f174c1cdf6bb61ff917f1fa74ecb420139 Mon Sep 17 00:00:00 2001 From: sebkolind Date: Tue, 28 May 2024 15:19:10 +0200 Subject: [PATCH 08/16] fix(attr): remove `attr` as helper method BREAKING CHANGE: This will remove the `attr` made available when using `view` and `mounted`. Instead you can use `el.dataset` and type your expected incoming data-* attributes with `Component<{}, Attrs>` and define those in `Attrs`. --- src/attributes.ts | 27 +-------------------------- src/main.ts | 15 ++++----------- src/types.ts | 2 -- 3 files changed, 5 insertions(+), 39 deletions(-) diff --git a/src/attributes.ts b/src/attributes.ts index bafe1db..db716eb 100644 --- a/src/attributes.ts +++ b/src/attributes.ts @@ -28,29 +28,4 @@ function addAttribute( } } -/** - * @deprecated - * Use `el.dataset` instead, will be removed in the next major version. - */ -function getAttribute(el: HTMLElement | Element) { - return (name: K): A[K] | undefined => { - const attr = el.attributes.getNamedItem(name as string); - - if (!attr) { - return; - } - - const value = attr.value; - - if (value === '') { - // TODO: This might not be the desired behavior. - // I should find a better way to handle this, - // what I want to avoid is returning `T | undefined | 'true'` - return 'true' as A[K]; - } - - return value as A[K]; - }; -} - -export { addAttribute, getAttribute }; +export { addAttribute }; diff --git a/src/main.ts b/src/main.ts index 66d1c9d..6800700 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,11 +1,4 @@ -import { getAttribute } from './attributes'; -import { - type Children, - type Context, - type Component, - type TentNode, - type Attrs, -} from './types'; +import type { Children, Context, Component, TentNode, Attrs } from './types'; import { createTag, tags } from './tags'; import { walker } from './walker'; @@ -43,7 +36,7 @@ function mount( const s = Reflect.set(obj, prop, value); - walker(node, view({ state: proxy, el, attr: getAttribute(el) })); + walker(node, view({ state: proxy, el })); return s; }, @@ -51,14 +44,14 @@ function mount( const proxy = new Proxy({ ...state }, handler); - node = view({ state: proxy, el, attr: getAttribute(el) }); + node = view({ state: proxy, el }); node.$tent = { attributes: {}, }; el.append(node); - mounted?.({ state: proxy, el, attr: getAttribute(el) }); + mounted?.({ state: proxy, el }); } export { diff --git a/src/types.ts b/src/types.ts index c16b87d..e9120ac 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,8 +1,6 @@ type ComponentContext = { state: S; el: TentNode; - // @deprecated Use `el.dataset` instead - attr: (name: K) => A[K] | undefined; }; export type Attrs = {} | undefined; From 760c96b491c6675d2617b084fe0fafb7ac473881 Mon Sep 17 00:00:00 2001 From: sebkolind Date: Wed, 29 May 2024 15:26:55 +0200 Subject: [PATCH 09/16] perf: use for-loop instead of forEach When looping use `for` loops instead, which _should_ be slightly faster. Right? --- src/tags.ts | 18 ++++++++++++------ src/walker.ts | 23 ++++++++++++----------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/tags.ts b/src/tags.ts index c8ca138..854cd8a 100644 --- a/src/tags.ts +++ b/src/tags.ts @@ -19,9 +19,11 @@ function createTag(context: Context) { } if (Array.isArray(children)) { - children.forEach((c) => { + for (let i = 0; i < children.length; i++) { + const c = children[i]; + el.append(Array.isArray(c) ? createTag(c) : c); - }); + } } else { el.append(typeof children === 'number' ? children.toString() : children); } @@ -32,7 +34,7 @@ function createTag(context: Context) { } const tags: Tags = {}; -[ +const tagsArray = [ 'div', 'p', 'ul', @@ -78,8 +80,12 @@ const tags: Tags = {}; 'aside', 'small', 'b', -].forEach( - (tag) => (tags[tag] = (children, attrs) => createTag([tag, children, attrs])), -); +]; + +for (let i = 0; i < tagsArray.length; i++) { + const tag = tagsArray[i]; + + tags[tag] = (children, attrs) => createTag([tag, children, attrs]); +} export { tags, createTag }; diff --git a/src/walker.ts b/src/walker.ts index fc3dd29..c76ec33 100644 --- a/src/walker.ts +++ b/src/walker.ts @@ -50,26 +50,27 @@ function walker(oldNode: TentNode, newNode: TentNode) { } if (oc.length < nc.length) { - nc.forEach((x, index) => { - if (oc[index] == null) { - oldNode.append(x); + for (let i = 0; i < nc.length; i++) { + if (oc[i] == null) { + oldNode.append(nc[i]); } - }); + } } if (oc.length > nc.length) { - oc.forEach((c, i) => { + for (let i = 0; i < oc.length; i++) { if (nc[i] == null) { - c.remove(); + oc[i].remove(); } - }); + } } - oc.forEach((oChild, index) => { - const nChild = nc[index]; + for (let i = 0; i < oc.length; i++) { + const oChild = oc[i]; + const nChild = nc[i]; if (nChild == null) { - return; + continue; } if (oChild.tagName !== nChild.tagName) { @@ -77,7 +78,7 @@ function walker(oldNode: TentNode, newNode: TentNode) { } walker(oChild, nChild); - }); + } } export { walker }; From c4a32df3a81ec8aa7c49082538854b60a76f85a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20L=2E=20K=2E=20S=C3=B8rensen?= Date: Sun, 2 Jun 2024 12:43:29 +0200 Subject: [PATCH 10/16] fix(Component): require `state` if `S` is defined (#45) --- CHANGELOG.md | 6 ++++++ src/__tests__/component.test.ts | 29 +++++++++++++++++++++++++++++ src/main.ts | 21 ++++++++------------- src/types.ts | 4 ++-- src/walker.ts | 8 ++------ 5 files changed, 47 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b563e40..1c7812d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- `Component` now requires `state` if `S` is defined ([#43](https://github.com/tentjs/tent/issues/43)) + ## [0.0.33-0] - 2024-05-27 ### Added diff --git a/src/__tests__/component.test.ts b/src/__tests__/component.test.ts index 44cae56..40a05f2 100644 --- a/src/__tests__/component.test.ts +++ b/src/__tests__/component.test.ts @@ -5,6 +5,10 @@ import { getByText, getByTestId, fireEvent } from '@testing-library/dom'; const { div, p, button } = tags; +beforeEach(() => { + document.body.innerHTML = ''; +}); + const Counter: Component<{ count: number }> = { state: { count: 0 }, view: ({ state }) => @@ -47,4 +51,29 @@ describe('components', () => { expect(mounted).toHaveBeenCalledTimes(1); }); + + test('with state', () => { + const WithState: Component<{ count: number }> = { + state: { count: 0 }, + view: ({ state }) => div(p(`Count: ${state.count}`)), + }; + + mount(document.body, WithState); + + const el = getByText(document.body, /Count: 0/); + + expect(el).toBeDefined(); + }); + + test('without state', () => { + const WithoutState: Component = { + view: () => div(p(`No state`)), + }; + + mount(document.body, WithoutState); + + const el = getByText(document.body, /No state/); + + expect(el).toBeDefined(); + }); }); diff --git a/src/main.ts b/src/main.ts index 6800700..6e825f8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,17 +6,14 @@ function mount( element: HTMLElement | Element | TentNode | null, component: Component, ) { - if (element == null) { - return; - } + if (element == null) return; let node: TentNode; - const { state = {} as S, view, mounted } = component; + const { view, mounted } = component; + const state = 'state' in component ? component.state : ({} as S); const el = element as TentNode; - el.$tent = { - attributes: {}, - }; + el.$tent = { attributes: {} }; const handler = { get(obj: S, key: string) { @@ -45,9 +42,7 @@ function mount( const proxy = new Proxy({ ...state }, handler); node = view({ state: proxy, el }); - node.$tent = { - attributes: {}, - }; + node.$tent = { attributes: {} }; el.append(node); @@ -55,11 +50,11 @@ function mount( } export { - mount, tags, + mount, createTag, - type Component, - type Children, type Context, type TentNode, + type Children, + type Component, }; diff --git a/src/types.ts b/src/types.ts index e9120ac..23f2b9d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,9 +6,9 @@ type ComponentContext = { export type Attrs = {} | undefined; export type Component = { view: (context: ComponentContext) => TentNode; - state?: S; mounted?: (context: ComponentContext) => void; -}; +} & State; +type State = {} extends S ? {} : { state: S }; export type TentNode = Node & Element & diff --git a/src/walker.ts b/src/walker.ts index c76ec33..07181ac 100644 --- a/src/walker.ts +++ b/src/walker.ts @@ -45,9 +45,7 @@ function walker(oldNode: TentNode, newNode: TentNode) { addAttribute(oldNode, key, attrs[key]); } - if (oc.length === 0 && nc.length === 0) { - return; - } + if (oc.length === 0 && nc.length === 0) return; if (oc.length < nc.length) { for (let i = 0; i < nc.length; i++) { @@ -69,9 +67,7 @@ function walker(oldNode: TentNode, newNode: TentNode) { const oChild = oc[i]; const nChild = nc[i]; - if (nChild == null) { - continue; - } + if (nChild == null) continue; if (oChild.tagName !== nChild.tagName) { oChild.replaceWith(nChild); From de2408365aa6d6b3e6d9aecc07f9ef6450e65052 Mon Sep 17 00:00:00 2001 From: sebkolind Date: Sun, 2 Jun 2024 12:49:50 +0200 Subject: [PATCH 11/16] docs(CHANGELOG): v0.0.33-1 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c7812d..2a398e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.0.33-1] - 2024-06-02 ### Fixed From 022a3729b11495b1c444919a61e304238cbbe6bb Mon Sep 17 00:00:00 2001 From: sebkolind Date: Sun, 2 Jun 2024 12:49:58 +0200 Subject: [PATCH 12/16] 0.0.33-1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 784ddec..5aeef43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tentjs/tent", - "version": "0.0.33-0", + "version": "0.0.33-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tentjs/tent", - "version": "0.0.33-0", + "version": "0.0.33-1", "license": "MIT", "devDependencies": { "@parcel/packager-ts": "^2.10.3", diff --git a/package.json b/package.json index 8ea6829..b449cb9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tentjs/tent", - "version": "0.0.33-0", + "version": "0.0.33-1", "description": "A jsx-free, super-lightweight and zero-dependency library to add interactivity to the web - without all the nonsense.", "source": "src/main.ts", "main": "dist/main.js", From bfa7b35ce1d716f2dd3f17f825fea611829c4a70 Mon Sep 17 00:00:00 2001 From: sebkolind Date: Sun, 2 Jun 2024 12:56:01 +0200 Subject: [PATCH 13/16] ci: remove redundant `next` release workflow All next tags will be released on the regular release workflow. --- .github/workflows/release-next.yml | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 .github/workflows/release-next.yml diff --git a/.github/workflows/release-next.yml b/.github/workflows/release-next.yml deleted file mode 100644 index 5ece51d..0000000 --- a/.github/workflows/release-next.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Build and Release (next) - -on: - create: - tags: - - v*-[0-9] # v0.0.3-1, v0.0.3-2, etc. - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 - with: - node-version: '20' - - run: npm ci - - run: npm run build - - run: npm run test - - uses: JS-DevTools/npm-publish@v3 - with: - token: ${{ secrets.NPM_TOKEN }} From 2139352cb2d22c7b35440eb3d9c8ab3a0b24278b Mon Sep 17 00:00:00 2001 From: sebkolind Date: Tue, 4 Jun 2024 20:01:19 +0200 Subject: [PATCH 14/16] `mount(el, component, attributes)` proposal --- src/__tests__/attributes.test.ts | 15 ++++++- src/__tests__/main.test.ts | 68 +++++++++++++++++++++++++++++++- src/attributes.ts | 16 ++++++-- src/main.ts | 3 ++ src/tags.ts | 17 ++------ src/walker.ts | 18 ++++----- 6 files changed, 107 insertions(+), 30 deletions(-) diff --git a/src/__tests__/attributes.test.ts b/src/__tests__/attributes.test.ts index 3d66c0b..e5b595b 100644 --- a/src/__tests__/attributes.test.ts +++ b/src/__tests__/attributes.test.ts @@ -1,4 +1,5 @@ -import { addAttribute } from '../attributes'; +import { addAttribute, addAttributes } from '../attributes'; +import { tags } from '../tags'; describe('attributes.ts', () => { test('adds a simple attribute', () => { @@ -42,4 +43,16 @@ describe('attributes.ts', () => { expect(el.hasAttribute('mounted')).toBe(false); }); + + test('addAttributes adds all attributes', () => { + const el = tags.div([]); + + addAttributes(el, { + id: 'test', + foo: 'bar', + }); + + expect(el.getAttribute('id')).toBe('test'); + expect(el.getAttribute('foo')).toBe('bar'); + }); }); diff --git a/src/__tests__/main.test.ts b/src/__tests__/main.test.ts index 52e4931..efc94cd 100644 --- a/src/__tests__/main.test.ts +++ b/src/__tests__/main.test.ts @@ -1,4 +1,9 @@ -import { Component, mount, tags } from '../main'; +import { fireEvent, getByRole, getByTestId } from '@testing-library/dom'; +import { mount, tags, type Component } from '../main'; + +beforeEach(() => { + document.body.innerHTML = ''; +}); describe('main', () => { test('`null` element', () => { @@ -34,4 +39,65 @@ describe('main', () => { }), ).not.toThrow(); }); + + test('that attributes are set when using `mount` with attrs set', () => { + const target = document.createElement('div'); + target.setAttribute('data-testid', 'test'); + document.body.append(target); + + const TestComponent: Component = { + view: () => tags.div('Hey, this is me!'), + }; + + mount(target, TestComponent, { + id: 'foo', + foo: 'bar', + }); + + const el = getByTestId(document.body, 'test'); + + expect(el.getAttribute('id')).toBe('foo'); + expect(el.getAttribute('foo')).toBe('bar'); + }); + + test('that attributes are set when using `mount` and dynamic mounting', () => { + const target = document.createElement('div'); + target.setAttribute('data-testid', 'test'); + document.body.append(target); + + const modalTarget = document.createElement('div'); + modalTarget.setAttribute('data-testid', 'modal'); + document.body.append(modalTarget); + + const Parent: Component = { + view: () => + tags.button('Click me', { + onclick() { + mount(modalTarget, Modal, { modalId: 'foo' }); + }, + }), + }; + + const Modal: Component = { + view: () => tags.p('I am a modal'), + }; + + mount(target, Parent, { + id: 'foo', + foo: 'bar', + }); + + const el = getByTestId(document.body, 'test'); + + expect(el.getAttribute('id')).toBe('foo'); + expect(el.getAttribute('foo')).toBe('bar'); + + const btn = getByRole(document.body, 'button'); + + fireEvent.click(btn); + + const modal = getByTestId(document.body, 'modal'); + + expect(modal.getAttribute('modalId')).toBe('foo'); + }); }); diff --git a/src/attributes.ts b/src/attributes.ts index db716eb..5a606d6 100644 --- a/src/attributes.ts +++ b/src/attributes.ts @@ -1,13 +1,21 @@ import { type TentNode, type Attrs, type TagAttrsValues } from './types'; +function addAttributes(el: TentNode, attributes: A) { + for (const key in attributes) { + const value = attributes[key as string]; + + el.$tent.attributes[key as string] = value; + + addAttribute(el, key, value); + } +} + function addAttribute( el: TentNode | HTMLElement, key: string, value: TagAttrsValues, ) { - if (key === 'mounted') { - return; - } + if (key === 'mounted') return; if (typeof value === 'boolean') { if (value) { @@ -28,4 +36,4 @@ function addAttribute( } } -export { addAttribute }; +export { addAttributes, addAttribute }; diff --git a/src/main.ts b/src/main.ts index 6e825f8..b3f2a35 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,12 @@ import type { Children, Context, Component, TentNode, Attrs } from './types'; import { createTag, tags } from './tags'; import { walker } from './walker'; +import { addAttributes } from './attributes'; function mount( element: HTMLElement | Element | TentNode | null, component: Component, + attributes?: A, ) { if (element == null) return; @@ -14,6 +16,7 @@ function mount( const el = element as TentNode; el.$tent = { attributes: {} }; + addAttributes(el, attributes); const handler = { get(obj: S, key: string) { diff --git a/src/tags.ts b/src/tags.ts index 854cd8a..d73d294 100644 --- a/src/tags.ts +++ b/src/tags.ts @@ -1,22 +1,13 @@ -import { addAttribute } from './attributes'; -import { type Tags, type Context, type TentNode } from './types'; +import { addAttributes } from './attributes'; +import type { Tags, Context, TentNode } from './types'; function createTag(context: Context) { const [tag, children, attributes] = context; const el = document.createElement(tag) as TentNode; - el.$tent = { - attributes: {}, - }; - - for (const key in attributes) { - const value = attributes[key]; - - el.$tent.attributes[key] = value; - - addAttribute(el, key, value); - } + el.$tent = { attributes: {} }; + addAttributes(el, attributes); if (Array.isArray(children)) { for (let i = 0; i < children.length; i++) { diff --git a/src/walker.ts b/src/walker.ts index 07181ac..f7fe1f6 100644 --- a/src/walker.ts +++ b/src/walker.ts @@ -1,15 +1,15 @@ -import { addAttribute } from './attributes'; -import { type Attrs, type TentNode } from './types'; +import { addAttributes } from './attributes'; +import type { Attrs, TentNode } from './types'; -function walker(oldNode: TentNode, newNode: TentNode) { +function walker(oldNode: TentNode, newNode: TentNode) { if (oldNode.tagName !== newNode.tagName) { oldNode.replaceWith(newNode); return; } - const nc = Array.from(newNode.childNodes, (n) => n as TentNode); - const oc = Array.from(oldNode.childNodes, (n) => n as TentNode); + const nc = Array.from(newNode.childNodes, (n) => n as TentNode); + const oc = Array.from(oldNode.childNodes, (n) => n as TentNode); if (oldNode.nodeType === Node.TEXT_NODE) { if (oldNode.nodeValue !== newNode.nodeValue) { @@ -36,14 +36,10 @@ function walker(oldNode: TentNode, newNode: TentNode) { } // Add attributes that are not present in the old node - const attrs = { + addAttributes(oldNode, { ...oldNode.$tent.attributes, ...newNode.$tent.attributes, - }; - - for (const key in attrs) { - addAttribute(oldNode, key, attrs[key]); - } + }); if (oc.length === 0 && nc.length === 0) return; From a2d09926fd3342882c55090ea68e8cf39703a0e1 Mon Sep 17 00:00:00 2001 From: sebkolind Date: Tue, 4 Jun 2024 20:06:02 +0200 Subject: [PATCH 15/16] docs(CHANGELOG): update unreleased --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a398e5..440ee72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- Add ability to pass attributes to a component on mount + ## [0.0.33-1] - 2024-06-02 ### Fixed From 6e71ce459611ff64e260c06dc4f054a60cc64ef6 Mon Sep 17 00:00:00 2001 From: sebkolind Date: Wed, 5 Jun 2024 14:06:48 +0200 Subject: [PATCH 16/16] move setting $tent.attributes --- src/attributes.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/attributes.ts b/src/attributes.ts index 5a606d6..398606b 100644 --- a/src/attributes.ts +++ b/src/attributes.ts @@ -4,8 +4,6 @@ function addAttributes(el: TentNode, attributes: A) { for (const key in attributes) { const value = attributes[key as string]; - el.$tent.attributes[key as string] = value; - addAttribute(el, key, value); } } @@ -17,6 +15,10 @@ function addAttribute( ) { if (key === 'mounted') return; + if ('$tent' in el) { + el.$tent.attributes[key] = value; + } + if (typeof value === 'boolean') { if (value) { el.setAttribute(key, '');