Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/src/content/examples/subtemplate-events-each.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
title: 'Subtemplate Events'
id: 'subtemplate-events-each'
exampleType: 'folder'
category: 'Framework'
subcategory: 'Events'
tags: ['component', 'events', 'subtemplate', 'each']
tip: 'Each `{>}` invocation clones the subtemplate — state and events are per instance, with no keys or ids threaded through the parent'
description: Subtemplates handle their own state and events for each item
---
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
:host {
display: block;
font-family: var(--page-font);
}

.counters {
margin: 0;
padding: 0;
list-style: none;

.counter {
display: flex;
align-items: center;
gap: var(--gap-xs);
padding: var(--padding-3xs) var(--padding-xs);
border: var(--subtle-border);
border-radius: var(--border-radius);
margin-bottom: var(--gap-3xs);

.label {
flex: 1;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<ul class="counters">
{#each item in items}
{>counter label=item.label}
{/each}
</ul>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { defineComponent, getText } from '@semantic-ui/component';

const css = await getText('./component.css');
const template = await getText('./component.html');

const counter = defineComponent({
template: await getText('./counter.html'),
defaultState: {
clicks: 0,
},
events: {
'click ui-button'({ state }) {
state.clicks.increment();
},
},
});

defineComponent({
tagName: 'counter-list',
template,
css,
subTemplates: { counter },
defaultState: {
items: [
{ id: 1, label: 'one' },
{ id: 2, label: 'two' },
{ id: 3, label: 'three' },
],
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<li class="counter">
<span class="label">{label}</span>
<ui-button>Clicked {clicks}</ui-button>
</li>
85 changes: 85 additions & 0 deletions packages/renderer/test/browser/subtemplate-events.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Subtemplate-own events for {>name} invocations inside an each block.
// Two shapes per engine: the invocation wrapped in an element, and the
// invocation as the top level of the each item content. The DOM and the
// rendered output are identical — only the bind-time parent of the {>}
// comment differs.

import { defineComponent } from '@semantic-ui/component';
import { beforeEach, describe, expect, it } from 'vitest';
import { RENDERING_ENGINES } from './test-utils.js';

RENDERING_ENGINES.forEach((engine) => {
describe(engine, () => {
let tagCounter = 0;
function uniqueTag() {
return `test-subtpl-events-${engine}-${++tagCounter}`;
}

function clickOn(element) {
element.dispatchEvent(
new MouseEvent('click', { bubbles: true, composed: true, cancelable: true }),
);
}

async function mount(tag) {
const el = document.createElement(tag);
document.body.appendChild(el);
await el.rendered;
return el;
}

beforeEach(() => {
document.body.innerHTML = '';
});

function defineRow() {
let fired = 0;
const row = defineComponent({
renderingEngine: engine,
template: '<li><a class="lbl">{name}</a></li>',
events: {
'click .lbl'() {
fired++;
},
},
});
return { row, count: () => fired };
}

describe('subtemplate events inside each', () => {
it('fires when the invocation is wrapped in an element', async () => {
const tag = uniqueTag();
const { row, count } = defineRow();
defineComponent({
tagName: tag,
renderingEngine: engine,
subTemplates: { row },
template: '<ul>{#each item in items}<div class="wrap">{>row name=item.name}</div>{/each}</ul>',
defaultState: {
items: [{ id: 1, name: 'one' }],
},
});
const el = await mount(tag);
clickOn(el.shadowRoot.querySelector('.lbl'));
expect(count()).toBe(1);
});

it('fires when the invocation is the top level of the item content', async () => {
const tag = uniqueTag();
const { row, count } = defineRow();
defineComponent({
tagName: tag,
renderingEngine: engine,
subTemplates: { row },
template: '<ul>{#each item in items}{>row name=item.name}{/each}</ul>',
defaultState: {
items: [{ id: 1, name: 'one' }],
},
});
const el = await mount(tag);
clickOn(el.shadowRoot.querySelector('.lbl'));
expect(count()).toBe(1);
});
});
});
});
6 changes: 5 additions & 1 deletion packages/templating/src/template.js
Original file line number Diff line number Diff line change
Expand Up @@ -751,8 +751,12 @@ export const Template = class Template {
// Find the direct child of the renderRoot that is an ancestor of the event.target
// then confirm position
isNodeInTemplate(node) {
// subtemplates attach while their content is still in the build
// fragment, so stored parentNode can be a dead fragment — the start
// anchor's live parent is the real boundary at event time
const parentNode = this.startNode?.parentNode || this.parentNode;
const getRootChild = (node) => {
while (node && node.parentNode !== this.parentNode) {
while (node && node.parentNode !== parentNode) {
if (node.parentNode === null && node.host) {
node = node.host;
}
Expand Down
Loading