gonia 0.3.1 → 0.3.2

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,7 +206,23 @@ 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) => {
195
227
  for (const mutation of mutations) {
196
228
  for (const node of mutation.addedNodes) {
@@ -206,7 +238,7 @@ export async function render(html, state, registry) {
206
238
  }
207
239
  // Handle native <slot> elements
208
240
  if (match.tagName === 'SLOT') {
209
- index.push({
241
+ addToIndex({
210
242
  el: match,
211
243
  name: 'slot',
212
244
  directive: null,
@@ -227,7 +259,7 @@ export async function render(html, state, registry) {
227
259
  }
228
260
  }
229
261
  if (!hasDirective) {
230
- index.push({
262
+ addToIndex({
231
263
  el: match,
232
264
  name: 'scope',
233
265
  directive: null,
@@ -237,6 +269,27 @@ export async function render(html, state, registry) {
237
269
  });
238
270
  }
239
271
  }
272
+ // Handle g-bind:* elements that don't have other directives
273
+ // Add a placeholder so they get processed for dynamic attribute binding
274
+ if (hasBindAttributes(match)) {
275
+ let hasDirective = false;
276
+ for (const name of getDirectiveNames()) {
277
+ if (match.hasAttribute(name)) {
278
+ hasDirective = true;
279
+ break;
280
+ }
281
+ }
282
+ if (!hasDirective && !match.hasAttribute('g-scope')) {
283
+ addToIndex({
284
+ el: match,
285
+ name: 'bind',
286
+ directive: null,
287
+ expr: '',
288
+ priority: DirectivePriority.NORMAL,
289
+ isNativeSlot: false
290
+ });
291
+ }
292
+ }
240
293
  // Check all registered directives from global registry
241
294
  const tagName = match.tagName.toLowerCase();
242
295
  for (const name of getDirectiveNames()) {
@@ -247,7 +300,7 @@ export async function render(html, state, registry) {
247
300
  // Check if this is a custom element directive (tag name matches)
248
301
  if (tagName === name) {
249
302
  if (options.template || options.scope || options.provide || options.using) {
250
- index.push({
303
+ addToIndex({
251
304
  el: match,
252
305
  name,
253
306
  directive: fn,
@@ -261,7 +314,7 @@ export async function render(html, state, registry) {
261
314
  // Check if this is an attribute directive
262
315
  const attr = match.getAttribute(name);
263
316
  if (attr !== null) {
264
- index.push({
317
+ addToIndex({
265
318
  el: match,
266
319
  name,
267
320
  directive: fn,
@@ -280,7 +333,7 @@ export async function render(html, state, registry) {
280
333
  const fullName = `g-${name}`;
281
334
  if (getDirective(fullName))
282
335
  continue;
283
- index.push({
336
+ addToIndex({
284
337
  el: match,
285
338
  name,
286
339
  directive,
@@ -370,13 +423,14 @@ export async function render(html, state, registry) {
370
423
  }
371
424
  }
372
425
  }
373
- // Collect all directive names for conflict detection
374
- const directiveNames = [];
426
+ // Collect unique directive names for conflict detection
427
+ const directiveNameSet = new Set();
375
428
  for (const item of directives) {
376
429
  if (!item.isNativeSlot && item.directive !== null) {
377
- directiveNames.push(item.name);
430
+ directiveNameSet.add(item.name);
378
431
  }
379
432
  }
433
+ const directiveNames = [...directiveNameSet];
380
434
  // Check if any directive needs scope - create once if so
381
435
  let elementScope = null;
382
436
  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.2",
4
4
  "description": "A lightweight, SSR-first reactive UI library with declarative directives",
5
5
  "type": "module",
6
6
  "license": "MIT",