snice 2.1.2 → 2.1.4

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.
@@ -1,6 +1,6 @@
1
1
  import { element, on, property, query, request, dispatch, watch } from 'snice';
2
- import 'snice/input/snice-input';
3
- import 'snice/select/snice-select';
2
+ import '../input/snice-input';
3
+ import '../select/snice-select';
4
4
  import './snice-cell.ts';
5
5
  import './snice-cell-text.ts';
6
6
  import './snice-cell-number.ts';
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * snice v2.1.1
2
+ * snice v2.1.3
3
3
  * Imperative TypeScript framework for building vanilla web components with decorators, routing, and controllers. No virtual DOM, no build complexity.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
@@ -69,6 +69,8 @@ const OBSERVERS = getSymbol('observers');
69
69
  // Part symbols
70
70
  const PARTS = getSymbol('parts');
71
71
  const PART_TIMERS = getSymbol('part-timers');
72
+ // Dispatch timing symbols
73
+ const DISPATCH_TIMERS = getSymbol('dispatch-timers');
72
74
 
73
75
  function on(eventName, selectorOrOptions, options) {
74
76
  // Handle overloaded parameters
@@ -115,7 +117,9 @@ function setupEventHandlers(instance, element) {
115
117
  instance[CLEANUP] = { events: [], channels: [] };
116
118
  }
117
119
  for (const handler of handlers) {
118
- const originalMethod = handler.method.bind(instance);
120
+ // Get the current method from the instance (preserves decorator stacking)
121
+ const currentMethod = instance[handler.method.name];
122
+ const originalMethod = currentMethod ? currentMethod.bind(instance) : handler.method.bind(instance);
119
123
  const handlerOptions = handler.options || {};
120
124
  // Parse event name for key modifiers
121
125
  const [baseEventName, keyModifier] = handler.eventName.split(':');
@@ -297,12 +301,21 @@ function cleanupEventHandlers(instance) {
297
301
  */
298
302
  function dispatch(eventName, options) {
299
303
  return function (originalMethod, _context) {
300
- // Create timing wrappers for dispatch
301
- let debounceTimeout;
302
- let throttleLastCall = 0;
303
- let throttleTimeout;
304
304
  return function (...args) {
305
- // Call the original method
305
+ // Create timing wrappers for dispatch (per-instance)
306
+ if (!this[DISPATCH_TIMERS]) {
307
+ this[DISPATCH_TIMERS] = new Map();
308
+ }
309
+ const timerKey = `${eventName}_${_context.name}`;
310
+ if (!this[DISPATCH_TIMERS].has(timerKey)) {
311
+ this[DISPATCH_TIMERS].set(timerKey, {
312
+ debounceTimeout: null,
313
+ throttleLastCall: 0,
314
+ throttleTimeout: null
315
+ });
316
+ }
317
+ const timers = this[DISPATCH_TIMERS].get(timerKey);
318
+ // Call the original method with preserved this context
306
319
  const result = originalMethod.apply(this, args);
307
320
  // Helper to dispatch the event
308
321
  const doDispatch = (detail) => {
@@ -322,21 +335,21 @@ function dispatch(eventName, options) {
322
335
  // Helper to handle timed dispatch
323
336
  const timedDispatch = (detail) => {
324
337
  if (options?.debounce) {
325
- clearTimeout(debounceTimeout);
326
- debounceTimeout = setTimeout(() => doDispatch(detail), options.debounce);
338
+ clearTimeout(timers.debounceTimeout);
339
+ timers.debounceTimeout = setTimeout(() => doDispatch(detail), options.debounce);
327
340
  }
328
341
  else if (options?.throttle) {
329
342
  const now = Date.now();
330
- const remaining = options.throttle - (now - throttleLastCall);
343
+ const remaining = options.throttle - (now - timers.throttleLastCall);
331
344
  if (remaining <= 0) {
332
- clearTimeout(throttleTimeout);
333
- throttleLastCall = now;
345
+ clearTimeout(timers.throttleTimeout);
346
+ timers.throttleLastCall = now;
334
347
  doDispatch(detail);
335
348
  }
336
- else if (!throttleTimeout) {
337
- throttleTimeout = setTimeout(() => {
338
- throttleLastCall = Date.now();
339
- throttleTimeout = null;
349
+ else if (!timers.throttleTimeout) {
350
+ timers.throttleTimeout = setTimeout(() => {
351
+ timers.throttleLastCall = Date.now();
352
+ timers.throttleTimeout = null;
340
353
  doDispatch(detail);
341
354
  }, remaining);
342
355
  }
@@ -1428,6 +1441,7 @@ function applyElementFunctionality(constructor) {
1428
1441
  try {
1429
1442
  const partElement = this.shadowRoot.querySelector(`[part="${partName}"]`);
1430
1443
  if (partElement) {
1444
+ // For initial render, call original method directly to avoid timing restrictions
1431
1445
  const partResult = partHandler.method.call(this);
1432
1446
  const partContent = partResult instanceof Promise ? await partResult : partResult;
1433
1447
  if (partContent !== undefined) {
@@ -2031,94 +2045,51 @@ function part(partName, options = {}) {
2031
2045
  });
2032
2046
  }
2033
2047
  const timers = this[PART_TIMERS].get(partName);
2034
- // Call the original method first to get its result
2035
- const result = originalMethod.apply(this, args);
2036
- // Helper function to update DOM
2037
- const updateDOM = (content) => {
2038
- if (this.shadowRoot && content !== undefined) {
2039
- const partElement = this.shadowRoot.querySelector(`[part="${partName}"]`);
2040
- if (partElement) {
2041
- partElement.innerHTML = content;
2048
+ // Helper function to execute method and update DOM
2049
+ const executeAndUpdate = (...methodArgs) => {
2050
+ const result = originalMethod.apply(this, methodArgs);
2051
+ const updateDOM = (content) => {
2052
+ const hasContent = content !== undefined;
2053
+ const hasElement = this.shadowRoot?.querySelector(`[part="${partName}"]`);
2054
+ if (hasContent && hasElement) {
2055
+ hasElement.innerHTML = content;
2042
2056
  }
2043
- }
2057
+ };
2058
+ const isPromise = result instanceof Promise;
2059
+ return isPromise
2060
+ ? result.then(content => { updateDOM(content); return content; })
2061
+ : (updateDOM(result), result);
2044
2062
  };
2045
- // Check if result is a Promise (async method)
2046
- if (result instanceof Promise) {
2047
- // Handle async method
2048
- if (options.debounce !== undefined && options.debounce > 0) {
2049
- // Debounce: defer DOM update, return original Promise
2050
- if (timers.debounceTimer) {
2051
- clearTimeout(timers.debounceTimer);
2052
- }
2053
- timers.debounceTimer = setTimeout(async () => {
2054
- const content = await result;
2055
- updateDOM(content);
2056
- }, options.debounce);
2057
- return result;
2063
+ const hasDebounce = options.debounce !== undefined && options.debounce > 0;
2064
+ const hasThrottle = options.throttle !== undefined && options.throttle > 0;
2065
+ // Handle timing based on priority: debounce > throttle > immediate
2066
+ switch (true) {
2067
+ case hasDebounce: {
2068
+ clearTimeout(timers.debounceTimer);
2069
+ timers.debounceTimer = setTimeout(() => executeAndUpdate(...args), options.debounce);
2070
+ return undefined;
2058
2071
  }
2059
- if (options.throttle !== undefined && options.throttle > 0) {
2060
- // Throttle: handle timing but return original Promise
2072
+ case hasThrottle: {
2073
+ const throttleMs = options.throttle;
2061
2074
  const now = Date.now();
2062
- if (timers.lastThrottleCall === 0 || now - timers.lastThrottleCall >= options.throttle) {
2075
+ const canExecuteImmediately = timers.lastThrottleCall === 0 || now - timers.lastThrottleCall >= throttleMs;
2076
+ if (canExecuteImmediately) {
2063
2077
  timers.lastThrottleCall = now;
2064
- return result.then(content => {
2065
- updateDOM(content);
2066
- return content;
2067
- });
2078
+ return executeAndUpdate(...args);
2068
2079
  }
2069
- else {
2070
- if (!timers.throttleTimer) {
2071
- const remainingTime = options.throttle - (now - timers.lastThrottleCall);
2072
- timers.throttleTimer = setTimeout(async () => {
2073
- timers.throttleTimer = null;
2074
- timers.lastThrottleCall = Date.now();
2075
- const content = await result;
2076
- updateDOM(content);
2077
- }, remainingTime);
2078
- }
2079
- return result;
2080
+ const hasScheduledTimer = !!timers.throttleTimer;
2081
+ if (!hasScheduledTimer) {
2082
+ const remainingTime = throttleMs - (now - timers.lastThrottleCall);
2083
+ timers.throttleTimer = setTimeout(() => {
2084
+ timers.throttleTimer = null;
2085
+ timers.lastThrottleCall = Date.now();
2086
+ executeAndUpdate(...args);
2087
+ }, remainingTime);
2080
2088
  }
2089
+ return undefined;
2081
2090
  }
2082
- // No timing: update DOM after Promise resolves
2083
- return result.then(content => {
2084
- updateDOM(content);
2085
- return content;
2086
- });
2087
- }
2088
- else {
2089
- // Handle sync method
2090
- if (options.debounce !== undefined && options.debounce > 0) {
2091
- // Debounce: defer DOM update, return result immediately
2092
- if (timers.debounceTimer) {
2093
- clearTimeout(timers.debounceTimer);
2094
- }
2095
- timers.debounceTimer = setTimeout(() => {
2096
- updateDOM(result);
2097
- }, options.debounce);
2098
- return result;
2099
- }
2100
- if (options.throttle !== undefined && options.throttle > 0) {
2101
- // Throttle: handle timing for DOM updates
2102
- const now = Date.now();
2103
- if (timers.lastThrottleCall === 0 || now - timers.lastThrottleCall >= options.throttle) {
2104
- timers.lastThrottleCall = now;
2105
- updateDOM(result);
2106
- }
2107
- else {
2108
- if (!timers.throttleTimer) {
2109
- const remainingTime = options.throttle - (now - timers.lastThrottleCall);
2110
- timers.throttleTimer = setTimeout(() => {
2111
- timers.throttleTimer = null;
2112
- timers.lastThrottleCall = Date.now();
2113
- updateDOM(result);
2114
- }, remainingTime);
2115
- }
2116
- }
2117
- return result;
2118
- }
2119
- // No timing: update DOM immediately
2120
- updateDOM(result);
2121
- return result;
2091
+ default:
2092
+ return executeAndUpdate(...args);
2122
2093
  }
2123
2094
  };
2124
2095
  };