@inglorious/web 2.6.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -216,7 +216,7 @@ Inglorious Web is minimal, predictable, and tiny.
216
216
 
217
217
  ## **HTMX / Alpine / Vanilla DOM**
218
218
 
219
- You are closer philosophically to **HTMX** and **vanilla JS**, but with a declarative rendering model and entity-based state.
219
+ Inglorious Web is closer philosophically to **HTMX** and **vanilla JS**, but with a declarative rendering model and entity-based state.
220
220
 
221
221
  ---
222
222
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/web",
3
- "version": "2.6.0",
3
+ "version": "3.0.0",
4
4
  "description": "A new web framework that leverages the power of the Inglorious Store combined with the performance and simplicity of lit-html.",
5
5
  "author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
@@ -26,6 +26,26 @@
26
26
  "types": "./types/index.d.ts",
27
27
  "import": "./src/index.js"
28
28
  },
29
+ "./form": {
30
+ "types": "./types/form.d.ts",
31
+ "import": "./src/form/index.js"
32
+ },
33
+ "./list": {
34
+ "types": "./types/list.d.ts",
35
+ "import": "./src/list/index.js"
36
+ },
37
+ "./router": {
38
+ "types": "./types/router.d.ts",
39
+ "import": "./src/router/index.js"
40
+ },
41
+ "./select": {
42
+ "types": "./types/select.d.ts",
43
+ "import": "./src/select/index.js"
44
+ },
45
+ "./table": {
46
+ "types": "./types/table.d.ts",
47
+ "import": "./src/table/index.js"
48
+ },
29
49
  "./table/base.css": "./src/table/base.css",
30
50
  "./table/theme.css": "./src/table/theme.css",
31
51
  "./select/base.css": "./src/select/base.css",
@@ -38,14 +58,14 @@
38
58
  "publishConfig": {
39
59
  "access": "public"
40
60
  },
61
+ "sideEffects": false,
41
62
  "dependencies": {
42
63
  "lit-html": "^3.3.1",
43
- "@inglorious/store": "7.1.4",
44
- "@inglorious/utils": "3.7.0"
64
+ "@inglorious/utils": "3.7.0",
65
+ "@inglorious/store": "8.0.0"
45
66
  },
46
67
  "devDependencies": {
47
68
  "prettier": "^3.6.2",
48
- "vite": "^7.1.3",
49
69
  "vitest": "^4.0.15",
50
70
  "@inglorious/eslint-config": "1.1.1"
51
71
  },
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { beforeEach, describe, expect, it, vi } from "vitest"
5
5
 
6
- import { form, getFieldError, getFieldValue, isFieldTouched } from "./form.js"
6
+ import { form, getFieldError, getFieldValue, isFieldTouched } from "."
7
7
 
8
8
  describe("form", () => {
9
9
  let entity
@@ -22,12 +22,28 @@ describe("form", () => {
22
22
  })
23
23
 
24
24
  describe("init()", () => {
25
+ it("should add global event listeners for click and submit", () => {
26
+ const spy = vi.spyOn(document, "addEventListener")
27
+ form.init(entity)
28
+ expect(spy).toHaveBeenCalledWith("click", expect.any(Function))
29
+ expect(spy).toHaveBeenCalledWith("submit", expect.any(Function))
30
+ spy.mockRestore()
31
+ })
32
+ })
33
+
34
+ describe("create()", () => {
35
+ it("should initialize the form", () => {
36
+ entity.values = {}
37
+ form.create(entity)
38
+ expect(entity.values).toEqual(entity.initialValues)
39
+ })
40
+
25
41
  it("should reset the form to its initial state", () => {
26
42
  // mess up the state first
27
43
  entity.values = {}
28
44
  entity.isPristine = false
29
45
 
30
- form.init(entity)
46
+ form.create(entity)
31
47
 
32
48
  expect(entity.values).toEqual(entity.initialValues)
33
49
  expect(entity.values).not.toBe(entity.initialValues) // should be a clone
@@ -44,27 +60,11 @@ describe("form", () => {
44
60
  tags: [null, null],
45
61
  })
46
62
  })
47
-
48
- it("should add global event listeners for click and submit", () => {
49
- const spy = vi.spyOn(document, "addEventListener")
50
- form.init(entity)
51
- expect(spy).toHaveBeenCalledWith("click", expect.any(Function))
52
- expect(spy).toHaveBeenCalledWith("submit", expect.any(Function))
53
- spy.mockRestore()
54
- })
55
- })
56
-
57
- describe("create()", () => {
58
- it("should reset the form", () => {
59
- entity.values = {}
60
- form.create(entity, "test-form")
61
- expect(entity.values).toEqual(entity.initialValues)
62
- })
63
63
  })
64
64
 
65
65
  describe("reset()", () => {
66
66
  it("should reset the form to its initial state", () => {
67
- form.create(entity, "test-form")
67
+ form.create(entity)
68
68
  entity.values.name = "Jane Doe"
69
69
  entity.isPristine = false
70
70
 
@@ -77,7 +77,7 @@ describe("form", () => {
77
77
 
78
78
  describe("fieldChange()", () => {
79
79
  beforeEach(() => {
80
- form.create(entity, "test-form")
80
+ form.create(entity)
81
81
  })
82
82
 
83
83
  it("should update a field's value, mark it as touched, and set form to dirty", () => {
@@ -124,7 +124,7 @@ describe("form", () => {
124
124
 
125
125
  describe("fieldBlur()", () => {
126
126
  beforeEach(() => {
127
- form.create(entity, "test-form")
127
+ form.create(entity)
128
128
  })
129
129
 
130
130
  it("should mark a field as touched", () => {
@@ -144,7 +144,7 @@ describe("form", () => {
144
144
 
145
145
  describe("field array operations", () => {
146
146
  beforeEach(() => {
147
- form.create(entity, "test-form")
147
+ form.create(entity)
148
148
  })
149
149
 
150
150
  it("fieldArrayAppend: should append a value and metadata", () => {
@@ -198,7 +198,7 @@ describe("form", () => {
198
198
 
199
199
  describe("validate()", () => {
200
200
  beforeEach(() => {
201
- form.create(entity, "test-form")
201
+ form.create(entity)
202
202
  })
203
203
 
204
204
  it("should set errors and isValid based on the validation function", () => {
@@ -231,7 +231,7 @@ describe("form", () => {
231
231
  let api
232
232
 
233
233
  beforeEach(() => {
234
- form.create(entity, "test-form")
234
+ form.create(entity)
235
235
  api = { notify: vi.fn() }
236
236
  })
237
237
 
@@ -271,7 +271,7 @@ describe("form", () => {
271
271
 
272
272
  describe("validationComplete()", () => {
273
273
  it("should update form state after async validation", () => {
274
- form.create(entity, "test-form")
274
+ form.create(entity)
275
275
  entity.isValidating = true
276
276
  const errors = { name: "Error" }
277
277
 
@@ -285,7 +285,7 @@ describe("form", () => {
285
285
 
286
286
  describe("validationError()", () => {
287
287
  it("should update form state after an async validation error", () => {
288
- form.create(entity, "test-form")
288
+ form.create(entity)
289
289
  entity.isValidating = true
290
290
 
291
291
  form.validationError(entity, { error: "Network Error" })
@@ -1,21 +1,21 @@
1
1
  import { clone, get, set } from "@inglorious/utils/data-structures/object.js"
2
2
 
3
3
  /**
4
- * @typedef {import('../types/form.js').FormEntity} FormEntity
5
- * @typedef {import('../types/form.js').FormFieldChangePayload} FormFieldChangePayload
6
- * @typedef {import('../types/form.js').FormFieldBlurPayload} FormFieldBlurPayload
4
+ * @typedef {import('../../types/form.js').FormEntity} FormEntity
5
+ * @typedef {import('../../types/form.js').FormFieldChangePayload} FormFieldChangePayload
6
+ * @typedef {import('../../types/form.js').FormFieldBlurPayload} FormFieldBlurPayload
7
7
  *
8
8
  * @typedef {object} FormValidatePayload
9
9
  * @property {string|number} entityId - The ID of the target form entity.
10
- * @property {(values: object) => import('../types/form.js').FormErrors<object>} validate - A function that validates the entire form's values and returns a complete error object.
10
+ * @property {(values: object) => import('../../types/form.js').FormErrors<object>} validate - A function that validates the entire form's values and returns a complete error object.
11
11
  *
12
12
  * @typedef {object} FormValidateAsyncPayload
13
13
  * @property {string|number} entityId - The ID of the target form entity.
14
- * @property {(values: object) => Promise<import('../types/form.js').FormErrors<object>>} validate - An async function that validates the entire form's values.
14
+ * @property {(values: object) => Promise<import('../../types/form.js').FormErrors<object>>} validate - An async function that validates the entire form's values.
15
15
  *
16
16
  * @typedef {object} FormValidationCompletePayload
17
17
  * @property {string|number} entityId - The ID of the target form entity.
18
- * @property {import('../types/form.js').FormErrors<object>} errors - The validation errors.
18
+ * @property {import('../../types/form.js').FormErrors<object>} errors - The validation errors.
19
19
  * @property {boolean} isValid - Whether the form is valid.
20
20
  *
21
21
  * @typedef {object} FormValidationErrorPayload
@@ -26,6 +26,8 @@ import { clone, get, set } from "@inglorious/utils/data-structures/object.js"
26
26
  const NO_ITEMS_REMOVED = 0
27
27
  const ONE_ITEM_REMOVED = 1
28
28
 
29
+ let areListenersInitialized = false
30
+
29
31
  /**
30
32
  * A type definition for managing form state within an entity-based system.
31
33
  * It handles initialization, field changes, validation, and submission.
@@ -37,33 +39,32 @@ export const form = {
37
39
  * Initializes the form entity by resetting it to its initial state.
38
40
  * @param {FormEntity} entity - The form entity.
39
41
  */
40
- init(entity) {
41
- resetForm(entity)
42
+ init() {
43
+ if (areListenersInitialized) return
42
44
 
43
- document.addEventListener("click", (e) => {
44
- const button = e.target.closest("button")
45
+ document.addEventListener("click", (event) => {
46
+ const button = event.target.closest("button")
45
47
 
46
48
  if (!button || button.getAttribute("type") === "submit") return
47
49
 
48
- e.preventDefault()
50
+ event.preventDefault()
49
51
  })
50
52
 
51
- document.addEventListener("submit", (e) => {
52
- const form = e.target.closest("form")
53
+ document.addEventListener("submit", (event) => {
54
+ const form = event.target.closest("form")
53
55
 
54
56
  if (!form) return
55
-
56
- e.preventDefault()
57
+ event.preventDefault()
57
58
  })
59
+
60
+ areListenersInitialized = true
58
61
  },
59
62
 
60
63
  /**
61
- * Resets the form entity when a 'create' event matches its ID.
64
+ * Resets the form entity with default state.
62
65
  * @param {FormEntity} entity - The form entity.
63
- * @param {string|number} entityId - The ID from the create event.
64
66
  */
65
- create(entity, id) {
66
- if (id !== entity.id) return
67
+ create(entity) {
67
68
  resetForm(entity)
68
69
  },
69
70
 
package/src/index.js CHANGED
@@ -1,13 +1,8 @@
1
- export { form, getFieldError, getFieldValue, isFieldTouched } from "./form.js"
2
- export { list } from "./list.js"
3
1
  export { mount } from "./mount.js"
4
- export { router } from "./router.js"
5
- export { select } from "./select.js"
6
- export { table } from "./table.js"
7
2
  export { createStore } from "@inglorious/store"
8
3
  export { createDevtools } from "@inglorious/store/client/devtools.js"
9
4
  export { createSelector } from "@inglorious/store/select.js"
10
- export { trigger } from "@inglorious/store/test"
5
+ export { trigger } from "@inglorious/store/test.js"
11
6
  export { html, render, svg } from "lit-html"
12
7
  export { choose } from "lit-html/directives/choose.js"
13
8
  export { classMap } from "lit-html/directives/class-map.js"
@@ -5,16 +5,26 @@ import { styleMap } from "lit-html/directives/style-map.js"
5
5
  const LIST_START = 0
6
6
  const PRETTY_INDEX = 1
7
7
 
8
- export const list = {
9
- init(entity) {
10
- resetList(entity)
11
- },
8
+ /**
9
+ * @typedef {import('../../types/list').ListEntity} ListEntity
10
+ * @typedef {import('../../types/mount').Api} Api
11
+ * @typedef {import('lit-html').TemplateResult} TemplateResult
12
+ */
12
13
 
13
- create(entity, id) {
14
- if (id !== entity.id) return
14
+ export const list = {
15
+ /**
16
+ * Initializes the list entity with default state.
17
+ * @param {ListEntity} entity
18
+ */
19
+ create(entity) {
15
20
  resetList(entity)
16
21
  },
17
22
 
23
+ /**
24
+ * Handles the scroll event to update the visible range.
25
+ * @param {ListEntity} entity
26
+ * @param {HTMLElement} containerEl
27
+ */
18
28
  scroll(entity, containerEl) {
19
29
  const scrollTop = containerEl.scrollTop
20
30
  const { items, bufferSize, itemHeight, estimatedHeight, viewportHeight } =
@@ -39,6 +49,11 @@ export const list = {
39
49
  entity.visibleRange = { start, end }
40
50
  },
41
51
 
52
+ /**
53
+ * Mounts the list, measuring the first item to determine item height.
54
+ * @param {ListEntity} entity
55
+ * @param {HTMLElement} containerEl
56
+ */
42
57
  mount(entity, containerEl) {
43
58
  const firstItem = containerEl.querySelector("[data-index]")
44
59
  if (!firstItem) return
@@ -50,6 +65,12 @@ export const list = {
50
65
  }
51
66
  },
52
67
 
68
+ /**
69
+ * Renders the virtualized list component.
70
+ * @param {ListEntity} entity
71
+ * @param {Api} api
72
+ * @returns {TemplateResult}
73
+ */
53
74
  render(entity, api) {
54
75
  const { items, visibleRange, viewportHeight, itemHeight, estimatedHeight } =
55
76
  entity
@@ -109,11 +130,23 @@ export const list = {
109
130
  `
110
131
  },
111
132
 
112
- renderItem(item, index) {
133
+ /**
134
+ * Default item renderer.
135
+ * @param {any} item
136
+ * @param {number} index
137
+ * @param {Api} api
138
+ * @returns {TemplateResult}
139
+ */
140
+ // eslint-disable-next-line no-unused-vars
141
+ renderItem(item, index, api) {
113
142
  return html`<div>${index + PRETTY_INDEX}. ${JSON.stringify(item)}</div>`
114
143
  },
115
144
  }
116
145
 
146
+ /**
147
+ * Resets the list entity state.
148
+ * @param {ListEntity} entity
149
+ */
117
150
  function resetList(entity) {
118
151
  entity.scrollTop = 0
119
152
  entity.visibleRange ??= { start: 0, end: 20 }
@@ -4,7 +4,7 @@
4
4
  import { html } from "lit-html"
5
5
  import { beforeEach, describe, expect, it, vi } from "vitest"
6
6
 
7
- import { list } from "./list.js"
7
+ import { list } from "."
8
8
 
9
9
  describe("list", () => {
10
10
  let entity
@@ -20,9 +20,9 @@ describe("list", () => {
20
20
  }
21
21
  })
22
22
 
23
- describe("init() and create()", () => {
23
+ describe("and create()", () => {
24
24
  it("should set default list properties on init", () => {
25
- list.init(entity)
25
+ list.create(entity)
26
26
  expect(entity.scrollTop).toBe(0)
27
27
  expect(entity.visibleRange).toEqual({ start: 0, end: 20 })
28
28
  expect(entity.viewportHeight).toBe(600)
@@ -35,7 +35,7 @@ describe("list", () => {
35
35
  entity.viewportHeight = 800
36
36
  entity.visibleRange = { start: 10, end: 30 }
37
37
 
38
- list.init(entity)
38
+ list.create(entity)
39
39
 
40
40
  expect(entity.viewportHeight).toBe(800)
41
41
  expect(entity.visibleRange).toEqual({ start: 10, end: 30 })
@@ -43,7 +43,7 @@ describe("list", () => {
43
43
  })
44
44
 
45
45
  it("should reset the list on create", () => {
46
- list.create(entity, "test-list")
46
+ list.create(entity)
47
47
  expect(entity.scrollTop).toBe(0)
48
48
  expect(entity.visibleRange).toEqual({ start: 0, end: 20 })
49
49
  })
@@ -51,7 +51,7 @@ describe("list", () => {
51
51
 
52
52
  describe("scroll()", () => {
53
53
  beforeEach(() => {
54
- list.init(entity)
54
+ list.create(entity)
55
55
  })
56
56
 
57
57
  it("should calculate visible range based on itemHeight", () => {
@@ -116,7 +116,7 @@ describe("list", () => {
116
116
 
117
117
  describe("mount()", () => {
118
118
  it("should measure and set itemHeight and update visibleRange", () => {
119
- list.init(entity)
119
+ list.create(entity)
120
120
  const itemEl = document.createElement("div")
121
121
  vi.spyOn(itemEl, "offsetHeight", "get").mockReturnValue(40)
122
122
 
@@ -133,7 +133,7 @@ describe("list", () => {
133
133
  })
134
134
 
135
135
  it("should do nothing if no item element is found", () => {
136
- list.init(entity)
136
+ list.create(entity)
137
137
  const originalEntity = { ...entity }
138
138
  const containerEl = {
139
139
  querySelector: vi.fn().mockReturnValue(null),
@@ -149,7 +149,7 @@ describe("list", () => {
149
149
  let api
150
150
 
151
151
  beforeEach(() => {
152
- list.init(entity)
152
+ list.create(entity)
153
153
  api = {
154
154
  notify: vi.fn(),
155
155
  getType: vi.fn().mockReturnValue({
@@ -1,12 +1,14 @@
1
1
  /**
2
- * @typedef {import("../types/router").RouterType} RouterType
3
- * @typedef {import("../types/router").RouterEntity} RouterEntity
4
- * @typedef {import("../types/router").Api} Api
2
+ * @typedef {import("../../types/router").RouterType} RouterType
3
+ * @typedef {import("../../types/router").RouterEntity} RouterEntity
4
+ * @typedef {import("../../types/router").Api} Api
5
5
  */
6
6
 
7
7
  const SKIP_FULL_MATCH_GROUP = 1 // .match() result at index 0 is the full string
8
8
  const REMOVE_COLON_PREFIX = 1
9
9
 
10
+ let areListenersInitialized = false
11
+
10
12
  /**
11
13
  * Client-side router for entity-based systems. Handles URL changes, link interception, and browser history management.
12
14
  * @type {RouterType}
@@ -34,6 +36,9 @@ export const router = {
34
36
  })
35
37
  }
36
38
 
39
+ if (areListenersInitialized) return
40
+ areListenersInitialized = true
41
+
37
42
  // Listen for browser back/forward
38
43
  window.addEventListener("popstate", () => {
39
44
  const path = window.location.pathname + window.location.search
@@ -78,6 +83,18 @@ export const router = {
78
83
  })
79
84
  },
80
85
 
86
+ create(entity) {
87
+ entity.routes ??= {}
88
+ },
89
+
90
+ routeAdd(entity, route) {
91
+ entity.routes[route.path] = route.entityType
92
+ },
93
+
94
+ routeRemove(entity, path) {
95
+ delete [entity.routes[path]]
96
+ },
97
+
81
98
  /**
82
99
  * Handles navigation to a new route.
83
100
  * @param {RouterEntity} entity - The router entity.
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
5
5
 
6
- import { router } from "./router.js"
6
+ import { router } from "."
7
7
 
8
8
  describe("router", () => {
9
9
  let entity
@@ -44,33 +44,25 @@ describe("router", () => {
44
44
  })
45
45
 
46
46
  describe("init()", () => {
47
- it("should initialize with the current window.location", () => {
47
+ it("should initialize with the current window.location, set up a popstate listener, and set up a click listener for link interception", () => {
48
48
  vi.spyOn(window, "location", "get").mockReturnValue({
49
49
  pathname: "/users/123",
50
50
  search: "?sort=asc",
51
51
  hash: "#details",
52
52
  origin: "http://localhost:3000",
53
53
  })
54
+ const windowSpy = vi.spyOn(window, "addEventListener")
55
+ const documentSpy = vi.spyOn(document, "addEventListener")
54
56
 
55
- router.init(entity, {}, api)
57
+ router.init(entity, undefined, api)
56
58
 
57
59
  expect(api.notify).toHaveBeenCalledWith("#router:navigate", {
58
60
  to: "/users/123?sort=asc",
59
61
  params: { id: "123" },
60
62
  replace: true,
61
63
  })
62
- })
63
-
64
- it("should set up a popstate listener", () => {
65
- const spy = vi.spyOn(window, "addEventListener")
66
- router.init(entity, {}, api)
67
- expect(spy).toHaveBeenCalledWith("popstate", expect.any(Function))
68
- })
69
-
70
- it("should set up a click listener for link interception", () => {
71
- const spy = vi.spyOn(document, "addEventListener")
72
- router.init(entity, {}, api)
73
- expect(spy).toHaveBeenCalledWith("click", expect.any(Function))
64
+ expect(windowSpy).toHaveBeenCalledWith("popstate", expect.any(Function))
65
+ expect(documentSpy).toHaveBeenCalledWith("click", expect.any(Function))
74
66
  })
75
67
  })
76
68
 
@@ -0,0 +1,7 @@
1
+ import { logic } from "./logic.js"
2
+ import { rendering } from "./rendering.js"
3
+
4
+ export const select = {
5
+ ...logic,
6
+ ...rendering,
7
+ }
@@ -7,20 +7,10 @@
7
7
 
8
8
  export const logic = {
9
9
  /**
10
- * Initializes the select entity with default state.
10
+ * Resets the select entity with default state.
11
11
  * @param {SelectEntity} entity
12
12
  */
13
- init(entity) {
14
- initSelect(entity)
15
- },
16
-
17
- /**
18
- * Resets the select entity when a 'create' event payload matches its ID.
19
- * @param {SelectEntity} entity
20
- * @param {string|number} id
21
- */
22
- create(entity, id) {
23
- if (id !== entity.id) return
13
+ create(entity) {
24
14
  initSelect(entity)
25
15
  },
26
16
 
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { beforeEach, describe, expect, it } from "vitest"
5
5
 
6
- import { select } from "./select.js"
6
+ import { select } from "."
7
7
  import {
8
8
  filterOptions,
9
9
  findOptionIndex,
@@ -12,7 +12,7 @@ import {
12
12
  getOptionValue,
13
13
  groupOptions,
14
14
  isOptionSelected,
15
- } from "./select/logic.js"
15
+ } from "./logic.js"
16
16
 
17
17
  const sampleOptions = [
18
18
  { value: "br", label: "Brazil" },
@@ -35,9 +35,9 @@ describe("select", () => {
35
35
  })
36
36
 
37
37
  describe("logic", () => {
38
- describe("init() and create()", () => {
38
+ describe("create()", () => {
39
39
  it("should initialize with default state", () => {
40
- select.init(entity)
40
+ select.create(entity)
41
41
 
42
42
  expect(entity.isOpen).toBe(false)
43
43
  expect(entity.searchTerm).toBe("")
@@ -58,14 +58,14 @@ describe("select", () => {
58
58
 
59
59
  it("should initialize multi-select with empty array", () => {
60
60
  entity.isMulti = true
61
- select.init(entity)
61
+ select.create(entity)
62
62
  expect(entity.selectedValue).toEqual([])
63
63
  })
64
64
  })
65
65
 
66
66
  describe("open() and close()", () => {
67
67
  beforeEach(() => {
68
- select.init(entity)
68
+ select.create(entity)
69
69
  })
70
70
 
71
71
  it("open: should open the dropdown", () => {
@@ -101,7 +101,7 @@ describe("select", () => {
101
101
 
102
102
  describe("toggle()", () => {
103
103
  beforeEach(() => {
104
- select.init(entity)
104
+ select.create(entity)
105
105
  })
106
106
 
107
107
  it("should open if closed", () => {
@@ -118,7 +118,7 @@ describe("select", () => {
118
118
 
119
119
  describe("optionSelect()", () => {
120
120
  beforeEach(() => {
121
- select.init(entity)
121
+ select.create(entity)
122
122
  })
123
123
 
124
124
  it("should select an option in single-select mode", () => {
@@ -130,7 +130,7 @@ describe("select", () => {
130
130
 
131
131
  it("should add option in multi-select mode", () => {
132
132
  entity.isMulti = true
133
- select.init(entity)
133
+ select.create(entity)
134
134
  entity.isOpen = true // Open dropdown first
135
135
  const option = sampleOptions[0]
136
136
  select.optionSelect(entity, option)
@@ -140,7 +140,7 @@ describe("select", () => {
140
140
 
141
141
  it("should remove option in multi-select mode if already selected", () => {
142
142
  entity.isMulti = true
143
- select.init(entity)
143
+ select.create(entity)
144
144
  const option = sampleOptions[0]
145
145
  select.optionSelect(entity, option) // Add
146
146
  select.optionSelect(entity, option) // Remove
@@ -156,7 +156,7 @@ describe("select", () => {
156
156
 
157
157
  describe("clear()", () => {
158
158
  beforeEach(() => {
159
- select.init(entity)
159
+ select.create(entity)
160
160
  })
161
161
 
162
162
  it("should clear selection in single-select mode", () => {
@@ -167,7 +167,7 @@ describe("select", () => {
167
167
 
168
168
  it("should clear selection in multi-select mode", () => {
169
169
  entity.isMulti = true
170
- select.init(entity)
170
+ select.create(entity)
171
171
  entity.selectedValue = ["br", "us"]
172
172
  select.clear(entity)
173
173
  expect(entity.selectedValue).toEqual([])
@@ -183,7 +183,7 @@ describe("select", () => {
183
183
 
184
184
  describe("searchChange()", () => {
185
185
  beforeEach(() => {
186
- select.init(entity)
186
+ select.create(entity)
187
187
  })
188
188
 
189
189
  it("should update searchTerm and filter options", () => {
@@ -211,7 +211,7 @@ describe("select", () => {
211
211
 
212
212
  describe("keyboard navigation", () => {
213
213
  beforeEach(() => {
214
- select.init(entity)
214
+ select.create(entity)
215
215
  })
216
216
 
217
217
  it("focusNext: should move to next option", () => {
@@ -1,10 +1,10 @@
1
1
  import { choose, html } from "@inglorious/web"
2
2
 
3
- import { dateFilter } from "./filters/date"
4
- import { numberFilter } from "./filters/number"
5
- import { rangeFilter } from "./filters/range"
6
- import { selectFilter } from "./filters/select"
7
- import { textFilter } from "./filters/text"
3
+ import { dateFilter } from "./filters/date.js"
4
+ import { numberFilter } from "./filters/number.js"
5
+ import { rangeFilter } from "./filters/range.js"
6
+ import { selectFilter } from "./filters/select.js"
7
+ import { textFilter } from "./filters/text.js"
8
8
 
9
9
  export const filters = {
10
10
  render(entity, column, api) {
@@ -0,0 +1,7 @@
1
+ import { logic } from "./logic.js"
2
+ import { rendering } from "./rendering.js"
3
+
4
+ export const table = {
5
+ ...logic,
6
+ ...rendering,
7
+ }
@@ -7,20 +7,10 @@
7
7
 
8
8
  export const logic = {
9
9
  /**
10
- * Initializes the table entity.
10
+ * Resets the table entity with default state.
11
11
  * @param {TableEntity} entity
12
12
  */
13
- init(entity) {
14
- initTable(entity)
15
- },
16
-
17
- /**
18
- * Resets the table entity when a 'create' event payload matches its ID.
19
- * @param {TableEntity} entity
20
- * @param {string|number} id
21
- */
22
- create(entity, id) {
23
- if (id !== entity.id) return
13
+ create(entity) {
24
14
  initTable(entity)
25
15
  },
26
16
 
@@ -2,8 +2,8 @@ import { html } from "lit-html"
2
2
  import { classMap } from "lit-html/directives/class-map.js"
3
3
  import { ref } from "lit-html/directives/ref.js"
4
4
 
5
- import { filters } from "./filters"
6
- import { getPaginationInfo, getRows, getSortDirection } from "./logic"
5
+ import { filters } from "./filters.js"
6
+ import { getPaginationInfo, getRows, getSortDirection } from "./logic.js"
7
7
 
8
8
  const DIVISOR = 2
9
9
  const FIRST_PAGE = 0
@@ -9,7 +9,7 @@ vi.mock("./table/filters.js", () => ({
9
9
  filters: { render: () => html`` },
10
10
  }))
11
11
 
12
- import { table } from "./table.js"
12
+ import { table } from "."
13
13
  import {
14
14
  getPaginationInfo,
15
15
  getRows,
@@ -17,7 +17,7 @@ import {
17
17
  getTotalRows,
18
18
  isAllSelected,
19
19
  isSomeSelected,
20
- } from "./table/logic.js"
20
+ } from "./logic.js"
21
21
 
22
22
  const sampleData = [
23
23
  { id: 1, name: "Charlie", age: 35, active: true, role: "Admin" },
@@ -61,14 +61,13 @@ describe("table", () => {
61
61
  pagination: { page: 0, pageSize: 2 },
62
62
  search: { value: "" },
63
63
  }
64
- table.init(entity)
65
64
  })
66
65
 
67
66
  describe("logic", () => {
68
- describe("init() and create()", () => {
67
+ describe("create()", () => {
69
68
  it("should initialize with default state", () => {
70
69
  const newEntity = { data: [{ id: 1, name: "Test" }] }
71
- table.init(newEntity)
70
+ table.create(newEntity)
72
71
  expect(newEntity.sorts).toEqual([])
73
72
  expect(newEntity.filters).toEqual({})
74
73
  expect(newEntity.selection).toEqual([])
@@ -79,6 +78,10 @@ describe("table", () => {
79
78
  })
80
79
 
81
80
  describe("sortChange()", () => {
81
+ beforeEach(() => {
82
+ table.create(entity)
83
+ })
84
+
82
85
  it("should add a new sort", () => {
83
86
  table.sortChange(entity, "name")
84
87
  expect(entity.sorts).toEqual([{ column: "name", direction: "asc" }])
@@ -105,6 +108,10 @@ describe("table", () => {
105
108
  })
106
109
 
107
110
  describe("filterChange()", () => {
111
+ beforeEach(() => {
112
+ table.create(entity)
113
+ })
114
+
108
115
  it("should add a filter", () => {
109
116
  table.filterChange(entity, { columnId: "name", value: "Alice" })
110
117
  expect(entity.filters.name).toBe("Alice")
@@ -124,6 +131,10 @@ describe("table", () => {
124
131
  })
125
132
 
126
133
  describe("pagination", () => {
134
+ beforeEach(() => {
135
+ table.create(entity)
136
+ })
137
+
127
138
  it("pageNext: should go to the next page", () => {
128
139
  table.pageNext(entity)
129
140
  expect(entity.pagination.page).toBe(1)
@@ -155,6 +166,10 @@ describe("table", () => {
155
166
  })
156
167
 
157
168
  describe("selection", () => {
169
+ beforeEach(() => {
170
+ table.create(entity)
171
+ })
172
+
158
173
  it("rowToggle: should select an unselected row", () => {
159
174
  table.rowToggle(entity, 1)
160
175
  expect(entity.selection).toContain(1)
@@ -189,6 +204,10 @@ describe("table", () => {
189
204
  })
190
205
 
191
206
  describe("getters and selectors", () => {
207
+ beforeEach(() => {
208
+ table.create(entity)
209
+ })
210
+
192
211
  it("getRows: should return sorted, filtered, and paginated rows", () => {
193
212
  // Sort by age descending
194
213
  table.sortChange(entity, "age")
@@ -256,6 +275,8 @@ describe("table", () => {
256
275
  renderFooter: vi.fn(() => html`<div>Footer</div>`),
257
276
  }),
258
277
  }
278
+
279
+ table.create(entity)
259
280
  })
260
281
 
261
282
  it("render: should call sub-renderers", () => {
@@ -1,83 +1,83 @@
1
- .iw-table {
2
- .iw-table-header {
3
- border-bottom: 1px solid grey;
4
- padding: 1em 0;
5
- row-gap: 1em;
6
- }
7
-
8
- .iw-table-header-row {
9
- align-items: flex-start;
10
- column-gap: 1em;
11
- }
12
-
13
- .iw-table-header-column {
14
- row-gap: 0.5em;
15
- }
16
-
17
- .iw-table-header-title {
18
- font-weight: bold;
19
- white-space: nowrap;
20
- }
21
-
22
- .iw-table-body {
23
- max-height: 35em;
24
- overflow: auto;
25
- border-bottom: 1px solid grey;
26
- }
27
-
28
- .iw-table-row {
29
- align-items: center;
30
- column-gap: 1em;
31
- }
32
-
33
- .iw-table-row-even {
34
- background-color: aliceblue;
35
- }
36
-
37
- .iw-table-row-selected {
38
- border-left: 1px solid cornflowerblue;
39
- border-right: 1px solid cornflowerblue;
40
- }
41
-
42
- :not(.iw-table-row-selected) + .iw-table-row-selected {
43
- border-top: 1px solid cornflowerblue;
44
- margin-top: -1px;
45
- }
46
-
47
- .iw-table-row-selected + :not(.iw-table-row-selected) {
48
- border-top: 1px solid cornflowerblue;
49
- margin-top: -1px;
50
- }
51
-
52
- .iw-table-cell {
53
- padding: 1em;
54
- }
55
-
56
- .iw-table-cell-number,
57
- .iw-table-cell-date {
58
- text-align: right;
59
- }
60
-
61
- .iw-table-cell-boolean {
62
- text-align: center;
63
- }
64
-
65
- .iw-table-footer {
66
- padding: 1em 0;
67
- }
68
-
69
- .iw-table-footer-row {
70
- justify-content: space-between;
71
- align-items: center;
72
- column-gap: 1em;
73
- }
74
-
75
- .iw-table-page-input {
76
- min-width: 4em;
77
- text-align: right;
78
- }
79
-
80
- .iw-table-pagination-button {
81
- white-space: nowrap;
82
- }
83
- }
1
+ .iw-table {
2
+ .iw-table-header {
3
+ border-bottom: 1px solid grey;
4
+ padding: 1em 0;
5
+ row-gap: 1em;
6
+ }
7
+
8
+ .iw-table-header-row {
9
+ align-items: flex-start;
10
+ column-gap: 1em;
11
+ }
12
+
13
+ .iw-table-header-column {
14
+ row-gap: 0.5em;
15
+ }
16
+
17
+ .iw-table-header-title {
18
+ font-weight: bold;
19
+ white-space: nowrap;
20
+ }
21
+
22
+ .iw-table-body {
23
+ max-height: 35em;
24
+ overflow: auto;
25
+ border-bottom: 1px solid grey;
26
+ }
27
+
28
+ .iw-table-row {
29
+ align-items: center;
30
+ column-gap: 1em;
31
+ }
32
+
33
+ .iw-table-row-even {
34
+ background-color: aliceblue;
35
+ }
36
+
37
+ .iw-table-row-selected {
38
+ border-left: 1px solid cornflowerblue;
39
+ border-right: 1px solid cornflowerblue;
40
+ }
41
+
42
+ :not(.iw-table-row-selected) + .iw-table-row-selected {
43
+ border-top: 1px solid cornflowerblue;
44
+ margin-top: -1px;
45
+ }
46
+
47
+ .iw-table-row-selected + :not(.iw-table-row-selected) {
48
+ border-top: 1px solid cornflowerblue;
49
+ margin-top: -1px;
50
+ }
51
+
52
+ .iw-table-cell {
53
+ padding: 1em;
54
+ }
55
+
56
+ .iw-table-cell-number,
57
+ .iw-table-cell-date {
58
+ text-align: right;
59
+ }
60
+
61
+ .iw-table-cell-boolean {
62
+ text-align: center;
63
+ }
64
+
65
+ .iw-table-footer {
66
+ padding: 1em 0;
67
+ }
68
+
69
+ .iw-table-footer-row {
70
+ justify-content: space-between;
71
+ align-items: center;
72
+ column-gap: 1em;
73
+ }
74
+
75
+ .iw-table-page-input {
76
+ min-width: 4em;
77
+ text-align: right;
78
+ }
79
+
80
+ .iw-table-pagination-button {
81
+ white-space: nowrap;
82
+ }
83
+ }
package/types/form.d.ts CHANGED
@@ -191,20 +191,10 @@ export interface FormValidationErrorPayload {
191
191
  */
192
192
  export declare const form: {
193
193
  /**
194
- * Initializes the form entity by resetting it to its initial state.
194
+ * Initializes the form entity with default state.
195
195
  * @param entity The form entity.
196
196
  */
197
- init<T extends FormValues>(entity: FormEntity<T>): void
198
-
199
- /**
200
- * Resets the form entity when a 'create' event payload matches its ID.
201
- * @param entity The form entity.
202
- * @param entityId The entity ID from the create event, used to target a specific form.
203
- */
204
- create<T extends FormValues>(
205
- entity: FormEntity<T>,
206
- entityId: string | number,
207
- ): void
197
+ create<T extends FormValues>(entity: FormEntity<T>): void
208
198
 
209
199
  /**
210
200
  * Appends an item to a field array.
package/types/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./form"
2
+ export * from "./list"
2
3
  export * from "./mount"
3
4
  export * from "./router"
4
5
  export * from "./select"
@@ -0,0 +1,76 @@
1
+ import type { TemplateResult } from "lit-html"
2
+ import type { Api } from "./mount"
3
+
4
+ /**
5
+ * Represents the visible range of items in the list.
6
+ */
7
+ export interface VisibleRange {
8
+ start: number
9
+ end: number
10
+ }
11
+
12
+ /**
13
+ * Represents the state of a list entity.
14
+ */
15
+ export interface ListEntity<T = any> {
16
+ /** A unique identifier for the list entity. */
17
+ id: string | number
18
+ /** The entity type (usually 'list'). */
19
+ type: string
20
+ /** The array of items to render. */
21
+ items: T[]
22
+ /** The current scroll position of the list container. */
23
+ scrollTop: number
24
+ /** The range of items currently visible (plus buffer). */
25
+ visibleRange: VisibleRange
26
+ /** The height of the viewport in pixels. */
27
+ viewportHeight: number
28
+ /** The number of extra items to render above and below the visible range. */
29
+ bufferSize: number
30
+ /** The fixed height of each item in pixels, or null if measuring. */
31
+ itemHeight: number | null
32
+ /** The estimated height of an item, used before measurement. */
33
+ estimatedHeight: number
34
+ /** Any other custom properties. */
35
+ [key: string]: any
36
+ }
37
+
38
+ /**
39
+ * The list type implementation.
40
+ */
41
+ export declare const list: {
42
+ /**
43
+ * Initializes the list entity with default state.
44
+ * @param entity The list entity.
45
+ */
46
+ create(entity: ListEntity): void
47
+
48
+ /**
49
+ * Handles the scroll event to update the visible range.
50
+ * @param entity The list entity.
51
+ * @param containerEl The scrolling container element.
52
+ */
53
+ scroll(entity: ListEntity, containerEl: HTMLElement): void
54
+
55
+ /**
56
+ * Mounts the list, measuring the first item to determine item height.
57
+ * @param entity The list entity.
58
+ * @param containerEl The scrolling container element.
59
+ */
60
+ mount(entity: ListEntity, containerEl: HTMLElement): void
61
+
62
+ /**
63
+ * Renders the virtualized list component.
64
+ * @param entity The list entity.
65
+ * @param api The store API.
66
+ */
67
+ render(entity: ListEntity, api: Api): TemplateResult
68
+
69
+ /**
70
+ * Default item renderer.
71
+ * @param item The item to render.
72
+ * @param index The index of the item.
73
+ * @param api The store API.
74
+ */
75
+ renderItem(item: any, index: number, api: Api): TemplateResult
76
+ }
package/types/select.d.ts CHANGED
@@ -61,9 +61,9 @@ export interface SelectEntity {
61
61
  export declare const select: {
62
62
  /**
63
63
  * Initializes the select entity with default state.
64
- * @param entity The select entity.
64
+ * @param {SelectEntity} entity
65
65
  */
66
- init(entity: SelectEntity): void
66
+ create(entity: SelectEntity): void
67
67
 
68
68
  /**
69
69
  * Renders the select component.
package/types/table.d.ts CHANGED
@@ -68,17 +68,10 @@ export interface TableEntity<T = any> {
68
68
  */
69
69
  export declare const table: {
70
70
  /**
71
- * Initializes the table entity.
71
+ * Initializes the table entity with default state.
72
72
  * @param entity The table entity.
73
73
  */
74
- init(entity: TableEntity): void
75
-
76
- /**
77
- * Resets the table entity when a 'create' event payload matches its ID.
78
- * @param entity The table entity.
79
- * @param id The ID of the entity to create/reset.
80
- */
81
- create(entity: TableEntity, id: string): void
74
+ create(entity: TableEntity): void
82
75
 
83
76
  /**
84
77
  * Toggles sorting for a specific column.
package/src/select.js DELETED
@@ -1,7 +0,0 @@
1
- import { logic } from "./select/logic.js"
2
- import { rendering } from "./select/rendering.js"
3
-
4
- export const select = {
5
- ...logic,
6
- ...rendering,
7
- }
package/src/table.js DELETED
@@ -1,7 +0,0 @@
1
- import { logic } from "./table/logic"
2
- import { rendering } from "./table/rendering"
3
-
4
- export const table = {
5
- ...logic,
6
- ...rendering,
7
- }