selective-ui 1.2.3 → 1.2.5

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.
Files changed (59) hide show
  1. package/dist/selective-ui.css.map +1 -1
  2. package/dist/selective-ui.esm.js +5462 -1043
  3. package/dist/selective-ui.esm.js.map +1 -1
  4. package/dist/selective-ui.esm.min.js +2 -2
  5. package/dist/selective-ui.esm.min.js.br +0 -0
  6. package/dist/selective-ui.min.js +2 -2
  7. package/dist/selective-ui.min.js.br +0 -0
  8. package/dist/selective-ui.umd.js +5463 -1044
  9. package/dist/selective-ui.umd.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/ts/adapter/mixed-adapter.ts +312 -65
  12. package/src/ts/components/accessorybox.ts +248 -28
  13. package/src/ts/components/directive.ts +91 -11
  14. package/src/ts/components/option-handle.ts +191 -28
  15. package/src/ts/components/placeholder.ts +111 -16
  16. package/src/ts/components/popup/empty-state.ts +162 -0
  17. package/src/ts/components/popup/loading-state.ts +160 -0
  18. package/src/ts/components/{popup.ts → popup/popup.ts} +167 -71
  19. package/src/ts/components/searchbox.ts +225 -20
  20. package/src/ts/components/selectbox.ts +498 -120
  21. package/src/ts/core/base/adapter.ts +200 -53
  22. package/src/ts/core/base/fenwick.ts +147 -0
  23. package/src/ts/core/base/lifecycle.ts +258 -0
  24. package/src/ts/core/base/model.ts +120 -31
  25. package/src/ts/core/base/recyclerview.ts +55 -18
  26. package/src/ts/core/base/view.ts +87 -19
  27. package/src/ts/core/base/virtual-recyclerview.ts +475 -202
  28. package/src/ts/core/model-manager.ts +166 -85
  29. package/src/ts/core/search-controller.ts +236 -38
  30. package/src/ts/global.ts +6 -6
  31. package/src/ts/index.ts +6 -6
  32. package/src/ts/models/group-model.ts +159 -32
  33. package/src/ts/models/option-model.ts +213 -54
  34. package/src/ts/services/dataset-observer.ts +72 -10
  35. package/src/ts/services/ea-observer.ts +92 -15
  36. package/src/ts/services/effector.ts +181 -32
  37. package/src/ts/services/refresher.ts +30 -6
  38. package/src/ts/services/resize-observer.ts +132 -15
  39. package/src/ts/services/select-observer.ts +115 -50
  40. package/src/ts/types/components/searchbox.type.ts +1 -1
  41. package/src/ts/types/core/base/adapter.type.ts +2 -1
  42. package/src/ts/types/core/base/lifecycle.type.ts +62 -0
  43. package/src/ts/types/core/base/model.type.ts +3 -1
  44. package/src/ts/types/core/base/recyclerview.type.ts +2 -8
  45. package/src/ts/types/core/base/view.type.ts +36 -24
  46. package/src/ts/types/utils/ievents.type.ts +6 -1
  47. package/src/ts/utils/callback-scheduler.ts +112 -34
  48. package/src/ts/utils/ievents.ts +91 -29
  49. package/src/ts/utils/istorage.ts +1 -1
  50. package/src/ts/utils/selective.ts +474 -88
  51. package/src/ts/views/group-view.ts +170 -21
  52. package/src/ts/views/option-view.ts +349 -68
  53. package/src/ts/components/empty-state.ts +0 -68
  54. package/src/ts/components/loading-state.ts +0 -66
  55. /package/src/css/components/{empty-state.css → popup/empty-state.css} +0 -0
  56. /package/src/css/components/{loading-state.css → popup/loading-state.css} +0 -0
  57. /package/src/css/components/{popup.css → popup/popup.css} +0 -0
  58. /package/src/css/{components/optgroup.css → views/group-view.css} +0 -0
  59. /package/src/css/{components/option.css → views/option-view.css} +0 -0
@@ -0,0 +1,258 @@
1
+ import { LifecycleHookContext, LifecycleHooks, LifecycleState } from "src/ts/types/core/base/lifecycle.type";
2
+
3
+ type LifecycleHookName = keyof LifecycleHooks;
4
+
5
+ /**
6
+ * Minimal lifecycle finite-state machine (FSM) with a lightweight hook system.
7
+ *
8
+ * ### Responsibility
9
+ * - Provide a **strict**, **guarded** lifecycle FSM:
10
+ * `NEW → INITIALIZED → MOUNTED → UPDATED → DESTROYED`
11
+ * - Provide an in-memory hook registry to observe lifecycle transitions:
12
+ * `onInit`, `onMount`, `onUpdate`, `onDestroy`
13
+ *
14
+ * This class is designed to be extended by core primitives (Model/View/Adapter/Controller)
15
+ * so they share consistent lifecycle semantics without coupling to any rendering runtime.
16
+ *
17
+ * ### FSM & Idempotency
18
+ * - `init()` is **idempotent**: only transitions `NEW → INITIALIZED`; otherwise **no-op**.
19
+ * - `mount()` is **guarded**: only transitions `INITIALIZED → MOUNTED`; otherwise **no-op**.
20
+ * - `update()` is **repeatable** once mounted: allowed in `MOUNTED` and `UPDATED`.
21
+ * It always emits `onUpdate` and keeps state at `UPDATED`.
22
+ * - `destroy()` is **idempotent**: once `DESTROYED`, subsequent calls are **no-op**.
23
+ *
24
+ * ### Hook semantics
25
+ * - Hooks are stored in a `Set` per hook name:
26
+ * - de-duplicates identical callback references,
27
+ * - preserves insertion order for deterministic execution.
28
+ * - Hook callbacks receive a {@link LifecycleHookContext} containing:
29
+ * - `state` (current state after transition),
30
+ * - `prevState` (state prior to the transition).
31
+ * - Hook exceptions are caught and forwarded to {@link handleHookError},
32
+ * preventing a single subscriber from breaking the lifecycle flow.
33
+ *
34
+ * ### Memory & teardown
35
+ * - All registered hooks are cleared on `destroy()` via {@link clearHooks}.
36
+ * - Post-destroy calls to lifecycle methods do not emit further hooks.
37
+ *
38
+ * @see {@link LifecycleState}
39
+ * @see {@link LifecycleHooks}
40
+ * @see {@link LifecycleHookContext}
41
+ */
42
+ export class Lifecycle {
43
+ /**
44
+ * Current lifecycle state.
45
+ *
46
+ * Starts at {@link LifecycleState.NEW} and transitions through the FSM via
47
+ * {@link init}, {@link mount}, {@link update}, {@link destroy}.
48
+ */
49
+ protected state: LifecycleState = LifecycleState.NEW;
50
+
51
+ /**
52
+ * Registered lifecycle hooks.
53
+ *
54
+ * Uses a Set per hook to:
55
+ * - Avoid duplicate registrations
56
+ * - Preserve insertion order for deterministic execution
57
+ *
58
+ * @remarks
59
+ * This map is initialized with keys for all supported hooks in the constructor.
60
+ * Callbacks are cleared on {@link destroy}.
61
+ */
62
+ private hooks: Map<LifecycleHookName, Set<(ctx: LifecycleHookContext) => void>> = new Map();
63
+
64
+ /**
65
+ * Constructs the lifecycle manager and pre-registers hook containers.
66
+ *
67
+ * No hooks are executed during construction; consumers must call
68
+ * {@link init}, {@link mount}, {@link update}, or {@link destroy}.
69
+ */
70
+ constructor() {
71
+ this.hooks.set("onInit", new Set());
72
+ this.hooks.set("onMount", new Set());
73
+ this.hooks.set("onUpdate", new Set());
74
+ this.hooks.set("onDestroy", new Set());
75
+ }
76
+
77
+ /**
78
+ * Subscribes a callback to a lifecycle hook.
79
+ *
80
+ * Hook callbacks are invoked in insertion order. Duplicate callback references are ignored
81
+ * due to Set semantics.
82
+ *
83
+ * @param {LifecycleHookName} hook - Hook name to subscribe to.
84
+ * @param {(ctx: LifecycleHookContext) => void} fn - Callback invoked when the hook is emitted.
85
+ * @returns {this} The current instance (chainable).
86
+ */
87
+ on(hook: LifecycleHookName, fn: (ctx: LifecycleHookContext) => void): this {
88
+ this.hooks.get(hook)!.add(fn);
89
+ return this;
90
+ }
91
+
92
+ /**
93
+ * Unsubscribes a previously registered callback from a lifecycle hook.
94
+ *
95
+ * Safe to call even if the callback was never registered (no-op).
96
+ *
97
+ * @param {LifecycleHookName} hook - Hook name to unsubscribe from.
98
+ * @param {(ctx: LifecycleHookContext) => void} fn - Callback to remove.
99
+ * @returns {this} The current instance (chainable).
100
+ */
101
+ off(hook: LifecycleHookName, fn: (ctx: LifecycleHookContext) => void): this {
102
+ this.hooks.get(hook)!.delete(fn);
103
+ return this;
104
+ }
105
+
106
+ /**
107
+ * Emits a lifecycle hook by executing all registered callbacks for that hook.
108
+ *
109
+ * Execution model:
110
+ * - Callbacks run in insertion order.
111
+ * - Errors thrown by callbacks are caught and forwarded to {@link handleHookError}.
112
+ *
113
+ * @param {LifecycleHookName} hook - The hook to emit.
114
+ * @param {LifecycleState} prevState - The state prior to the transition.
115
+ * @returns {void}
116
+ *
117
+ * @internal
118
+ * Prefer invoking the public lifecycle methods ({@link init}, {@link mount}, {@link update}, {@link destroy})
119
+ * which call `emit()` at the correct time and enforce FSM guards.
120
+ */
121
+ protected emit(hook: LifecycleHookName, prevState: LifecycleState): void {
122
+ const ctx: LifecycleHookContext = {
123
+ state: this.state,
124
+ prevState,
125
+ };
126
+
127
+ for (const fn of this.hooks.get(hook)!) {
128
+ try {
129
+ fn(ctx);
130
+ } catch (err) {
131
+ this.handleHookError(err, hook);
132
+ }
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Handles errors thrown by lifecycle hook callbacks.
138
+ *
139
+ * Default behavior logs to `console.error` with a hook-scoped prefix.
140
+ * Subclasses may override to integrate with application logging/telemetry.
141
+ *
142
+ * @param {unknown} error - Error thrown by a hook callback.
143
+ * @param {LifecycleHookName} hook - Hook name during which the error occurred.
144
+ * @returns {void}
145
+ * @protected
146
+ */
147
+ protected handleHookError(error: unknown, hook: LifecycleHookName): void {
148
+ console.error(`[Lifecycle:${hook}]`, error);
149
+ }
150
+
151
+ /**
152
+ * Transitions `NEW → INITIALIZED` and emits `onInit`.
153
+ *
154
+ * Idempotent: **no-op** unless current state is {@link LifecycleState.NEW}.
155
+ *
156
+ * @returns {void}
157
+ * @see {@link LifecycleHooks.onInit}
158
+ */
159
+ init(): void {
160
+ if (this.state !== LifecycleState.NEW) return;
161
+
162
+ const prev = this.state;
163
+ this.state = LifecycleState.INITIALIZED;
164
+ this.emit("onInit", prev);
165
+ }
166
+
167
+ /**
168
+ * Transitions `INITIALIZED → MOUNTED` and emits `onMount`.
169
+ *
170
+ * Guarded: **no-op** unless current state is {@link LifecycleState.INITIALIZED}.
171
+ *
172
+ * @returns {void}
173
+ * @see {@link LifecycleHooks.onMount}
174
+ */
175
+ mount(): void {
176
+ if (this.state !== LifecycleState.INITIALIZED) return;
177
+
178
+ const prev = this.state;
179
+ this.state = LifecycleState.MOUNTED;
180
+ this.emit("onMount", prev);
181
+ }
182
+
183
+ /**
184
+ * Emits `onUpdate` and transitions to/keeps state `UPDATED`.
185
+ *
186
+ * Allowed states:
187
+ * - `MOUNTED` → `UPDATED`
188
+ * - `UPDATED` → `UPDATED` (repeatable updates still emit)
189
+ *
190
+ * Guarded: **no-op** unless current state is `MOUNTED` or `UPDATED`.
191
+ *
192
+ * @returns {void}
193
+ * @see {@link LifecycleHooks.onUpdate}
194
+ */
195
+ update(): void {
196
+ if (
197
+ this.state !== LifecycleState.MOUNTED &&
198
+ this.state !== LifecycleState.UPDATED
199
+ ) {
200
+ return;
201
+ }
202
+
203
+ const prev = this.state;
204
+ this.state = LifecycleState.UPDATED;
205
+ this.emit("onUpdate", prev);
206
+ }
207
+
208
+ /**
209
+ * Transitions to `DESTROYED`, emits `onDestroy`, then clears all hook registrations.
210
+ *
211
+ * Idempotent: **no-op** if already {@link LifecycleState.DESTROYED}.
212
+ *
213
+ * @returns {void}
214
+ * @see {@link LifecycleHooks.onDestroy}
215
+ */
216
+ destroy(): void {
217
+ if (this.state === LifecycleState.DESTROYED) return;
218
+
219
+ const prev = this.state;
220
+ this.state = LifecycleState.DESTROYED;
221
+ this.emit("onDestroy", prev);
222
+ this.clearHooks();
223
+ }
224
+
225
+ /**
226
+ * Returns the current lifecycle state.
227
+ *
228
+ * @returns {LifecycleState} Current FSM state.
229
+ */
230
+ getState(): LifecycleState {
231
+ return this.state;
232
+ }
233
+
234
+ /**
235
+ * Checks whether the lifecycle is in the specified state.
236
+ *
237
+ * @param {LifecycleState} state - State to compare against.
238
+ * @returns {boolean} `true` if current state matches; otherwise `false`.
239
+ */
240
+ is(state: LifecycleState): boolean {
241
+ return this.state === state;
242
+ }
243
+
244
+ /**
245
+ * Clears all registered lifecycle hooks.
246
+ *
247
+ * Called automatically during {@link destroy}. After clearing, the hook containers remain
248
+ * allocated (map keys persist) but contain no subscribers.
249
+ *
250
+ * @returns {void}
251
+ * @private
252
+ */
253
+ private clearHooks(): void {
254
+ for (const set of this.hooks.values()) {
255
+ set.clear();
256
+ }
257
+ }
258
+ }
@@ -1,81 +1,170 @@
1
1
  import { ModelContract } from "src/ts/types/core/base/model.type";
2
2
  import { ViewContract } from "src/ts/types/core/base/view.type";
3
+ import { Lifecycle } from "./lifecycle";
4
+ import { LifecycleState } from "src/ts/types/core/base/lifecycle.type";
3
5
 
4
6
  /**
5
- * @template TTarget
6
- * @template TTags
7
- * @template TView
7
+ * Base model primitive that binds a domain object to a target DOM element and an optional View.
8
+ *
9
+ * This class is the **Model** part of the library's Model/View separation:
10
+ * - The **Model** owns references to the authoritative DOM source (`targetElement`) and configuration (`options`).
11
+ * - The **View** (if attached) owns rendering and DOM event wiring for the model.
12
+ * - Higher-level infrastructure (e.g., Adapter / RecyclerView) orchestrates when models are created,
13
+ * bound to views, and updated.
14
+ *
15
+ * ### Lifecycle (Strict FSM)
16
+ * - Constructor calls {@link Lifecycle.init} immediately, transitioning `NEW → INITIALIZED`.
17
+ * - This base model does not call `mount()` by itself; mounting is typically handled by the View layer.
18
+ * - {@link updateTarget} triggers {@link Lifecycle.update}, which emits `onUpdate` lifecycle hooks in
19
+ * `MOUNTED/UPDATED` states (and is guarded otherwise).
20
+ * - {@link destroy} transitions to `DESTROYED`, clears references, and destroys the associated view.
21
+ *
22
+ * ### Idempotency / No-ops
23
+ * - {@link destroy} is idempotent once in {@link LifecycleState.DESTROYED}.
24
+ * - {@link updateTarget} is safe to call multiple times; consumers should treat repeated assignments
25
+ * as a no-op when the target does not change (this base class does not compare equality).
26
+ *
27
+ * ### Ownership & side effects
28
+ * - This model **owns** its `view` reference and will call `view.destroy()` during {@link destroy}.
29
+ * - The model itself does not mutate the DOM, except reading from `targetElement` (e.g., {@link value}).
30
+ * Any DOM side effects are expected to live in the View implementation.
31
+ *
32
+ * @template TTarget - The DOM element type this model is bound to (e.g., HTMLOptionElement).
33
+ * @template TTags - Named element map used by the view (view-specific DOM handles).
34
+ * @template TView - View implementation associated with this model.
35
+ * @template TOptions - Configuration/options type carried by the model.
36
+ *
8
37
  * @implements {ModelContract<TTarget, TView>}
38
+ * @extends Lifecycle
39
+ * @see {@link ViewContract}
40
+ * @see {@link LifecycleState}
9
41
  */
10
42
  export class Model<
11
43
  TTarget extends HTMLElement,
12
44
  TTags extends Record<string, HTMLElement>,
13
45
  TView extends ViewContract<TTags>,
14
46
  TOptions = unknown
15
- > implements ModelContract<TTarget, TView> {
47
+ > extends Lifecycle implements ModelContract<TTarget, TView> {
48
+
49
+ /**
50
+ * The currently bound target DOM element.
51
+ *
52
+ * This element typically represents the source-of-truth node in the host DOM (e.g., a native `<option>`).
53
+ * May be replaced via {@link updateTarget} during reconciliation.
54
+ */
16
55
  public targetElement: TTarget | null = null;
17
56
 
57
+ /**
58
+ * Configuration options supplied at construction time.
59
+ * Stored as-is and intended to be consumed by subclasses and/or the view layer.
60
+ */
18
61
  public options: TOptions;
19
62
 
63
+ /**
64
+ * View instance responsible for rendering this model.
65
+ *
66
+ * Ownership: this model will destroy the view on {@link destroy}.
67
+ * The view may be attached/assigned by external orchestrators (Adapter/RecyclerView) after construction.
68
+ */
20
69
  public view: TView | null = null;
21
70
 
71
+ /**
72
+ * Position index used by list infrastructure for ordering/tracking.
73
+ * Semantics are library-specific (e.g., top-level index or adapter position).
74
+ */
22
75
  public position = -1;
23
76
 
77
+ /**
78
+ * Indicates whether this model has completed its initial binding step.
79
+ * Typically set by the adapter/view binding layer to prevent duplicate listener wiring.
80
+ */
24
81
  public isInit = false;
25
82
 
83
+ /**
84
+ * Indicates whether this model has been removed/destroyed from the active dataset.
85
+ * Set to `true` during {@link destroy}.
86
+ */
26
87
  public isRemoved = false;
88
+
27
89
  /**
28
- * Returns the current value from the underlying target element's "value" attribute.
29
- * For single-select, this is typically a string; for multi-select, may be an array depending on usage.
90
+ * Returns the current "value" associated with the bound target element.
91
+ *
92
+ * Implementation note:
93
+ * - Reads from the target element's `"value"` attribute via `getAttribute("value")`.
94
+ * - Returns `null` when no target is bound or the attribute is not present.
95
+ *
96
+ * @returns {string | null | string[]} The value representation of the target element.
30
97
  */
31
98
  public get value(): string | null | string[] {
32
99
  return this.targetElement?.getAttribute("value") ?? null;
33
100
  }
34
101
 
35
102
  /**
36
- * Constructs a Model instance with configuration options and optional bindings to a target element and view.
37
- * Stores references for later updates and rendering.
103
+ * Creates a new model instance and initializes lifecycle state.
104
+ *
105
+ * - Captures {@link options}.
106
+ * - Optionally binds an initial {@link targetElement} and {@link view}.
107
+ * - Calls {@link Lifecycle.init} immediately (`NEW → INITIALIZED`).
38
108
  *
39
109
  * @param {TOptions} options - Configuration options for the model.
40
- * @param {TTarget|null} [targetElement=null] - The underlying element (e.g., <option> or group node).
41
- * @param {TView|null} [view=null] - The associated view responsible for rendering the model.
110
+ * @param {TTarget | null} [targetElement=null] - Optional DOM element to bind.
111
+ * @param {TView | null} [view=null] - Optional view responsible for rendering this model.
42
112
  */
43
- public constructor(options: TOptions, targetElement: TTarget | null = null, view: TView | null = null) {
113
+ public constructor(
114
+ options: TOptions,
115
+ targetElement: TTarget | null = null,
116
+ view: TView | null = null
117
+ ) {
118
+ super();
44
119
  this.options = options;
45
120
  this.targetElement = targetElement;
46
121
  this.view = view;
122
+
123
+ this.init();
47
124
  }
48
125
 
49
126
  /**
50
- * Updates the bound target element reference and invokes the change hook.
127
+ * Rebinds this model to a new target DOM element and marks the model as updated.
51
128
  *
52
- * @param {TTarget|null} targetElement - The new target element to bind to the model.
129
+ * Typical usage:
130
+ * - Reconciliation when the underlying DOM node is replaced (e.g., `<option>` node recreated).
131
+ * - Keeping model identity stable while swapping its backing DOM node.
132
+ *
133
+ * Side effects:
134
+ * - Assigns {@link targetElement}.
135
+ * - Calls {@link Lifecycle.update} (guarded by lifecycle state).
136
+ *
137
+ * @param {TTarget | null} targetElement - The new DOM element to associate with this model.
138
+ * @returns {void}
53
139
  */
54
- public update(targetElement: TTarget | null): void {
140
+ public updateTarget(targetElement: TTarget | null): void {
55
141
  this.targetElement = targetElement;
56
- this.onTargetChanged();
142
+ this.update();
57
143
  }
58
144
 
59
145
  /**
60
- * Cleans up references and invokes the removal hook when the model is no longer needed.
146
+ * Destroys this model and releases owned resources.
147
+ *
148
+ * Behavior:
149
+ * - Idempotent once lifecycle is {@link LifecycleState.DESTROYED}.
150
+ * - Clears {@link targetElement}.
151
+ * - Destroys the associated {@link view} (if present) and clears the reference.
152
+ * - Marks {@link isRemoved} as `true`.
153
+ * - Calls {@link Lifecycle.destroy} to transition to `DESTROYED` and clear hooks.
154
+ *
155
+ * @returns {void}
156
+ * @override
61
157
  */
62
- public remove() {
158
+ public override destroy(): void {
159
+ if (this.is(LifecycleState.DESTROYED)) {
160
+ return;
161
+ }
162
+
63
163
  this.targetElement = null;
64
- this.view?.getView()?.remove?.();
164
+ this.view?.destroy();
65
165
  this.view = null;
66
166
  this.isRemoved = true;
67
- this.onRemove();
68
- }
69
-
70
- /**
71
- * Hook invoked whenever the target element changes.
72
- * Override in subclasses to react to attribute/content updates (e.g., text, disabled state).
73
- */
74
- public onTargetChanged(): void { }
75
167
 
76
- /**
77
- * Hook invoked whenever the target element is removed.
78
- * Override in subclasses to react to removal of the element.
79
- */
80
- public onRemove(): void {}
168
+ super.destroy();
169
+ }
81
170
  }
@@ -1,18 +1,34 @@
1
1
  import type { ModelContract } from "../../types/core/base/model.type";
2
2
  import type { AdapterContract } from "../../types/core/base/adapter.type";
3
3
  import type { RecyclerViewContract } from "../../types/core/base/recyclerview.type";
4
+ import { Lifecycle } from "./lifecycle";
5
+ import { LifecycleState } from "src/ts/types/core/base/lifecycle.type";
4
6
 
5
7
  /**
6
- * @template TItem
7
- * @template TAdapter
8
+ * RecyclerView renders models provided by an Adapter into a container element.
9
+ *
10
+ * Responsibilities:
11
+ * - Maintain a root container (`viewElement`) where item views are rendered
12
+ * - Attach an Adapter and wire item-change lifecycle:
13
+ * - `onPropChanging('items')` → clear container before items change
14
+ * - `onPropChanged('items')` → re-render after items change
15
+ * - Expose rendering utilities: `render()`, `clear()`, `refresh()`
16
+ * - Participate in the standard lifecycle (`init` → `mount` → `update` → `destroy`)
17
+ *
18
+ * @template TItem - The model type handled by the adapter.
19
+ * @template TAdapter - The adapter type that manages items and updates the view.
20
+ *
8
21
  * @implements {RecyclerViewContract<TAdapter>}
9
22
  */
10
23
  export class RecyclerView<
11
24
  TItem extends ModelContract<any, any>,
12
25
  TAdapter extends AdapterContract<TItem>
13
- > implements RecyclerViewContract<TAdapter> {
26
+ > extends Lifecycle implements RecyclerViewContract<TAdapter> {
27
+
28
+ /** Root container that hosts rendered item views. */
14
29
  public viewElement: HTMLDivElement | null = null;
15
30
 
31
+ /** The adapter that manages models and updates the RecyclerView on changes. */
16
32
  public adapter: TAdapter | null = null;
17
33
 
18
34
  /**
@@ -21,23 +37,20 @@ export class RecyclerView<
21
37
  * @param {HTMLDivElement|null} [viewElement=null] - The root element where the adapter will render items.
22
38
  */
23
39
  constructor(viewElement: HTMLDivElement | null = null) {
40
+ super();
24
41
  this.viewElement = viewElement;
25
- }
26
-
27
- /**
28
- * Sets or updates the container element used to render the adapter's item views.
29
- *
30
- * @param {HTMLDivElement} viewElement - The root element for rendering.
31
- */
32
- public setView(viewElement: HTMLDivElement): void {
33
- this.viewElement = viewElement;
42
+ this.init();
34
43
  }
35
44
 
36
45
  /**
37
46
  * Attaches an adapter to the RecyclerView and wires item-change lifecycle:
38
- * - onPropChanging("items"): clears the container before items change,
39
- * - onPropChanged("items"): re-renders after items change,
40
- * then performs an initial render.
47
+ * - `onPropChanging('items')`: clears the container before items change
48
+ * - `onPropChanged('items')`: re-renders after items change
49
+ *
50
+ * Then performs:
51
+ * - `adapter.mount()` to initialize the adapter
52
+ * - `this.mount()` to mark the RecyclerView as mounted
53
+ * - An initial `render()` to sync the UI
41
54
  *
42
55
  * @param {TAdapter} adapter - The adapter managing models and their views.
43
56
  */
@@ -52,12 +65,15 @@ export class RecyclerView<
52
65
  this.render();
53
66
  });
54
67
 
68
+ adapter.mount();
69
+
70
+ this.mount();
55
71
  this.render();
56
72
  }
57
73
 
58
74
  /**
59
75
  * Removes all child nodes from the rendering container, if present.
60
- * Used prior to re-rendering or when items are changing.
76
+ * Typically used right before re-rendering or when items are about to change.
61
77
  */
62
78
  public clear(): void {
63
79
  if (!this.viewElement) return;
@@ -67,19 +83,40 @@ export class RecyclerView<
67
83
  /**
68
84
  * Renders the current adapter contents into the container.
69
85
  * No-ops if either the adapter or the container is not set.
86
+ * Emits the `update` lifecycle after delegating rendering to the adapter.
70
87
  */
71
88
  public render(): void {
72
89
  if (!this.adapter || !this.viewElement) return;
73
90
  this.adapter.updateRecyclerView(this.viewElement);
91
+ this.update();
74
92
  }
75
93
 
76
94
  /**
77
95
  * Forces a re-render of the current adapter state into the container.
78
96
  * Useful when visual updates are required without changing the data.
79
- *
80
- * @param isUpdate - Indicates if this refresh is due to an update operation.
97
+ *
98
+ * @param {boolean} isUpdate - Indicates if this refresh originates from an update operation.
99
+ * (Reserved for future use; no impact on logic.)
81
100
  */
82
101
  public refresh(isUpdate: boolean): void {
83
102
  this.render();
84
103
  }
104
+
105
+ /**
106
+ * Destroys the RecyclerView, detaching from its adapter and container.
107
+ *
108
+ * - Delegates teardown to the adapter
109
+ * - Clears strong references (adapter, viewElement)
110
+ * - Ends the lifecycle
111
+ */
112
+ public override destroy(): void {
113
+ if (this.is(LifecycleState.DESTROYED)) {
114
+ return;
115
+ }
116
+
117
+ this.viewElement = null;
118
+ this.adapter = null;
119
+
120
+ super.destroy();
121
+ }
85
122
  }