gonia 0.3.1 → 0.3.3

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.
@@ -228,11 +228,15 @@ function processElement(el, registry) {
228
228
  // Check if any directive needs a scope
229
229
  let scope = findParentScope(el, true) ?? {};
230
230
  let directiveCreatedScope = false;
231
- // Collect full directive names for conflict detection
232
- const directiveFullNames = [];
231
+ // Collect unique directive names for conflict detection
232
+ const directiveNameSet = new Set();
233
233
  for (const { name } of directives) {
234
234
  const fullName = `g-${name}`;
235
- directiveFullNames.push(fullName);
235
+ const isNew = !directiveNameSet.has(fullName);
236
+ directiveNameSet.add(fullName);
237
+ // Only process first occurrence
238
+ if (!isNew)
239
+ continue;
236
240
  const registration = getDirective(fullName);
237
241
  if (!directiveCreatedScope && directiveNeedsScope(fullName)) {
238
242
  // Create a new scope that inherits from parent
@@ -246,7 +250,7 @@ function processElement(el, registry) {
246
250
  }
247
251
  // Apply assigns with conflict detection
248
252
  if (directiveCreatedScope) {
249
- applyAssigns(scope, directiveFullNames);
253
+ applyAssigns(scope, [...directiveNameSet]);
250
254
  }
251
255
  const ctx = createContext(Mode.CLIENT, scope);
252
256
  contextCache.set(el, ctx);
@@ -433,16 +437,16 @@ async function processDirectiveElements() {
433
437
  if (options.scope) {
434
438
  const parentScope = findParentScope(el);
435
439
  scope = createElementScope(el, parentScope);
436
- // Collect all directive names on this element for conflict detection
437
- const directiveNames = [name];
440
+ // Collect unique directive names on this element for conflict detection
441
+ const directiveNameSet = new Set([name]);
438
442
  for (const attr of el.attributes) {
439
443
  const attrReg = getDirective(attr.name);
440
444
  if (attrReg) {
441
- directiveNames.push(attr.name);
445
+ directiveNameSet.add(attr.name);
442
446
  }
443
447
  }
444
448
  // Apply assigns with conflict detection
445
- applyAssigns(scope, directiveNames);
449
+ applyAssigns(scope, [...directiveNameSet]);
446
450
  }
447
451
  else {
448
452
  scope = findParentScope(el, true) ?? {};
@@ -66,6 +66,22 @@ function getSelector(localRegistry) {
66
66
  selectors.push('slot');
67
67
  // Match g-scope for inline scope initialization (TODO: make prefix configurable)
68
68
  selectors.push('[g-scope]');
69
+ // Match common g-bind:* attributes for dynamic binding
70
+ // These need to be indexed so their expressions can be evaluated with proper scope
71
+ selectors.push('[g-bind\\:class]');
72
+ selectors.push('[g-bind\\:style]');
73
+ selectors.push('[g-bind\\:href]');
74
+ selectors.push('[g-bind\\:src]');
75
+ selectors.push('[g-bind\\:id]');
76
+ selectors.push('[g-bind\\:value]');
77
+ selectors.push('[g-bind\\:disabled]');
78
+ selectors.push('[g-bind\\:checked]');
79
+ selectors.push('[g-bind\\:placeholder]');
80
+ selectors.push('[g-bind\\:title]');
81
+ selectors.push('[g-bind\\:alt]');
82
+ selectors.push('[g-bind\\:name]');
83
+ selectors.push('[g-bind\\:type]');
84
+ // Note: Can't do wildcard for data-* attributes in CSS, but hasBindAttributes handles them
69
85
  return selectors.join(',');
70
86
  }
71
87
  /**
@@ -190,15 +206,41 @@ export async function render(html, state, registry) {
190
206
  const window = new Window();
191
207
  const document = window.document;
192
208
  const index = [];
209
+ const indexedDirectives = new Map(); // Track indexed (element, directive) pairs
193
210
  const selector = getSelector(registry);
211
+ // Helper to add to index only if not already indexed for this (element, directive) pair
212
+ const addToIndex = (item) => {
213
+ const existing = indexedDirectives.get(item.el);
214
+ if (existing?.has(item.name)) {
215
+ return false; // Already indexed
216
+ }
217
+ if (!existing) {
218
+ indexedDirectives.set(item.el, new Set([item.name]));
219
+ }
220
+ else {
221
+ existing.add(item.name);
222
+ }
223
+ index.push(item);
224
+ return true;
225
+ };
194
226
  const observer = new window.MutationObserver((mutations) => {
227
+ // Collect all direct addedNodes first to avoid processing them as descendants
228
+ const directNodes = new Set();
229
+ for (const mutation of mutations) {
230
+ for (const node of mutation.addedNodes) {
231
+ if (node.nodeType === 1) {
232
+ directNodes.add(node);
233
+ }
234
+ }
235
+ }
195
236
  for (const mutation of mutations) {
196
237
  for (const node of mutation.addedNodes) {
197
238
  if (node.nodeType !== 1)
198
239
  continue;
199
240
  const el = node;
200
241
  const matches = el.matches(selector) ? [el] : [];
201
- const descendants = [...el.querySelectorAll(selector)];
242
+ // Filter out descendants that will be processed as direct addedNodes
243
+ const descendants = [...el.querySelectorAll(selector)].filter(desc => !directNodes.has(desc));
202
244
  for (const match of [...matches, ...descendants]) {
203
245
  // Skip elements inside template content (used as placeholders)
204
246
  if (match.closest('template')) {
@@ -206,7 +248,7 @@ export async function render(html, state, registry) {
206
248
  }
207
249
  // Handle native <slot> elements
208
250
  if (match.tagName === 'SLOT') {
209
- index.push({
251
+ addToIndex({
210
252
  el: match,
211
253
  name: 'slot',
212
254
  directive: null,
@@ -227,7 +269,7 @@ export async function render(html, state, registry) {
227
269
  }
228
270
  }
229
271
  if (!hasDirective) {
230
- index.push({
272
+ addToIndex({
231
273
  el: match,
232
274
  name: 'scope',
233
275
  directive: null,
@@ -237,6 +279,27 @@ export async function render(html, state, registry) {
237
279
  });
238
280
  }
239
281
  }
282
+ // Handle g-bind:* elements that don't have other directives
283
+ // Add a placeholder so they get processed for dynamic attribute binding
284
+ if (hasBindAttributes(match)) {
285
+ let hasDirective = false;
286
+ for (const name of getDirectiveNames()) {
287
+ if (match.hasAttribute(name)) {
288
+ hasDirective = true;
289
+ break;
290
+ }
291
+ }
292
+ if (!hasDirective && !match.hasAttribute('g-scope')) {
293
+ addToIndex({
294
+ el: match,
295
+ name: 'bind',
296
+ directive: null,
297
+ expr: '',
298
+ priority: DirectivePriority.NORMAL,
299
+ isNativeSlot: false
300
+ });
301
+ }
302
+ }
240
303
  // Check all registered directives from global registry
241
304
  const tagName = match.tagName.toLowerCase();
242
305
  for (const name of getDirectiveNames()) {
@@ -247,7 +310,7 @@ export async function render(html, state, registry) {
247
310
  // Check if this is a custom element directive (tag name matches)
248
311
  if (tagName === name) {
249
312
  if (options.template || options.scope || options.provide || options.using) {
250
- index.push({
313
+ addToIndex({
251
314
  el: match,
252
315
  name,
253
316
  directive: fn,
@@ -261,7 +324,7 @@ export async function render(html, state, registry) {
261
324
  // Check if this is an attribute directive
262
325
  const attr = match.getAttribute(name);
263
326
  if (attr !== null) {
264
- index.push({
327
+ addToIndex({
265
328
  el: match,
266
329
  name,
267
330
  directive: fn,
@@ -280,7 +343,7 @@ export async function render(html, state, registry) {
280
343
  const fullName = `g-${name}`;
281
344
  if (getDirective(fullName))
282
345
  continue;
283
- index.push({
346
+ addToIndex({
284
347
  el: match,
285
348
  name,
286
349
  directive,
@@ -370,13 +433,14 @@ export async function render(html, state, registry) {
370
433
  }
371
434
  }
372
435
  }
373
- // Collect all directive names for conflict detection
374
- const directiveNames = [];
436
+ // Collect unique directive names for conflict detection
437
+ const directiveNameSet = new Set();
375
438
  for (const item of directives) {
376
439
  if (!item.isNativeSlot && item.directive !== null) {
377
- directiveNames.push(item.name);
440
+ directiveNameSet.add(item.name);
378
441
  }
379
442
  }
443
+ const directiveNames = [...directiveNameSet];
380
444
  // Check if any directive needs scope - create once if so
381
445
  let elementScope = null;
382
446
  for (const name of directiveNames) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gonia",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "A lightweight, SSR-first reactive UI library with declarative directives",
5
5
  "type": "module",
6
6
  "license": "MIT",