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.
- package/dist/selective-ui.css.map +1 -1
- package/dist/selective-ui.esm.js +5462 -1043
- package/dist/selective-ui.esm.js.map +1 -1
- package/dist/selective-ui.esm.min.js +2 -2
- package/dist/selective-ui.esm.min.js.br +0 -0
- package/dist/selective-ui.min.js +2 -2
- package/dist/selective-ui.min.js.br +0 -0
- package/dist/selective-ui.umd.js +5463 -1044
- package/dist/selective-ui.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/ts/adapter/mixed-adapter.ts +312 -65
- package/src/ts/components/accessorybox.ts +248 -28
- package/src/ts/components/directive.ts +91 -11
- package/src/ts/components/option-handle.ts +191 -28
- package/src/ts/components/placeholder.ts +111 -16
- package/src/ts/components/popup/empty-state.ts +162 -0
- package/src/ts/components/popup/loading-state.ts +160 -0
- package/src/ts/components/{popup.ts → popup/popup.ts} +167 -71
- package/src/ts/components/searchbox.ts +225 -20
- package/src/ts/components/selectbox.ts +498 -120
- package/src/ts/core/base/adapter.ts +200 -53
- package/src/ts/core/base/fenwick.ts +147 -0
- package/src/ts/core/base/lifecycle.ts +258 -0
- package/src/ts/core/base/model.ts +120 -31
- package/src/ts/core/base/recyclerview.ts +55 -18
- package/src/ts/core/base/view.ts +87 -19
- package/src/ts/core/base/virtual-recyclerview.ts +475 -202
- package/src/ts/core/model-manager.ts +166 -85
- package/src/ts/core/search-controller.ts +236 -38
- package/src/ts/global.ts +6 -6
- package/src/ts/index.ts +6 -6
- package/src/ts/models/group-model.ts +159 -32
- package/src/ts/models/option-model.ts +213 -54
- package/src/ts/services/dataset-observer.ts +72 -10
- package/src/ts/services/ea-observer.ts +92 -15
- package/src/ts/services/effector.ts +181 -32
- package/src/ts/services/refresher.ts +30 -6
- package/src/ts/services/resize-observer.ts +132 -15
- package/src/ts/services/select-observer.ts +115 -50
- package/src/ts/types/components/searchbox.type.ts +1 -1
- package/src/ts/types/core/base/adapter.type.ts +2 -1
- package/src/ts/types/core/base/lifecycle.type.ts +62 -0
- package/src/ts/types/core/base/model.type.ts +3 -1
- package/src/ts/types/core/base/recyclerview.type.ts +2 -8
- package/src/ts/types/core/base/view.type.ts +36 -24
- package/src/ts/types/utils/ievents.type.ts +6 -1
- package/src/ts/utils/callback-scheduler.ts +112 -34
- package/src/ts/utils/ievents.ts +91 -29
- package/src/ts/utils/istorage.ts +1 -1
- package/src/ts/utils/selective.ts +474 -88
- package/src/ts/views/group-view.ts +170 -21
- package/src/ts/views/option-view.ts +349 -68
- package/src/ts/components/empty-state.ts +0 -68
- package/src/ts/components/loading-state.ts +0 -66
- /package/src/css/components/{empty-state.css → popup/empty-state.css} +0 -0
- /package/src/css/components/{loading-state.css → popup/loading-state.css} +0 -0
- /package/src/css/components/{popup.css → popup/popup.css} +0 -0
- /package/src/css/{components/optgroup.css → views/group-view.css} +0 -0
- /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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
29
|
-
*
|
|
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
|
-
*
|
|
37
|
-
*
|
|
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] -
|
|
41
|
-
* @param {TView|null} [view=null] -
|
|
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(
|
|
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
|
-
*
|
|
127
|
+
* Rebinds this model to a new target DOM element and marks the model as updated.
|
|
51
128
|
*
|
|
52
|
-
*
|
|
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
|
|
140
|
+
public updateTarget(targetElement: TTarget | null): void {
|
|
55
141
|
this.targetElement = targetElement;
|
|
56
|
-
this.
|
|
142
|
+
this.update();
|
|
57
143
|
}
|
|
58
144
|
|
|
59
145
|
/**
|
|
60
|
-
*
|
|
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
|
|
158
|
+
public override destroy(): void {
|
|
159
|
+
if (this.is(LifecycleState.DESTROYED)) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
63
163
|
this.targetElement = null;
|
|
64
|
-
this.view?.
|
|
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
|
-
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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(
|
|
39
|
-
* - onPropChanged(
|
|
40
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
}
|