Skip to content

Commit 7389a43

Browse files
committed
refactor(template-builder): encapsulate interact.js inside ElementRenderer
Each component now owns exactly its own responsibilities: ElementRenderer - Imports interact.js and sets up its own interactable in did-insert - Tears it down in will-destroy (no parent involvement) - Emits @onmove(uuid, {x,y}) when a drag ends - Emits @onresize(uuid, {x,y,width,height}) when a resize ends - Emits @onselect(element) when tapped - Reads @canvasWidth/@canvasHeight for boundary clamping - Reads @zoom at event time (no closure staleness) - Reads rotation from el.dataset.rotation (written by _applyTransform) so the applyTransform closure never holds a stale element reference Canvas - Removed: interact.js import, _interactables map, setupElement, teardownElement, _setupInteract, didInsertCanvas, willDestroyCanvas - Owns only: canvas dimensions/style, selectedUuid tracking, handleSelectElement, handleDeselectAll - Passes @canvasWidth/@canvasHeight/@zoom to each ElementRenderer - Forwards @onMoveElement → @onmove, @onResizeElement → @onresize TemplateBuilder - Added resizeElement action (separate from moveElement) - moveElement: drag-end only, syncs {x,y} - resizeElement: resize-end only, syncs {x,y,width,height} - Both are silent in-place mutations (no re-render, no undo entry) - Removed @onUpdateElement from canvas wiring (canvas never needed it) This eliminates the entire class of bugs caused by the parent managing child DOM lifecycle: no more interact.js destruction on property updates, no stale closure references, no need for data-attribute workarounds.
1 parent 446ca53 commit 7389a43

File tree

6 files changed

+271
-404
lines changed

6 files changed

+271
-404
lines changed

addon/components/template-builder.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
@zoom={{this.zoom}}
8787
@onSelectElement={{this.selectElement}}
8888
@onMoveElement={{this.moveElement}}
89-
@onUpdateElement={{this.updateElement}}
89+
@onResizeElement={{this.resizeElement}}
9090
@onDeselectAll={{this.deselectAll}}
9191
/>
9292
</div>

addon/components/template-builder.js

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -259,16 +259,29 @@ export default class TemplateBuilderComponent extends Component {
259259
}
260260

261261
/**
262-
* Silently sync the position/size of an element after a drag or resize
263-
* gesture completes. Does NOT trigger any Glimmer re-render.
262+
* Silently sync an element's position after a drag gesture ends.
263+
* Mutates in-place — no Glimmer re-render, no undo entry.
264+
* interact.js has already updated the DOM; this just keeps the data model
265+
* in sync so the correct position is included in the next save.
264266
*/
265267
@action
266-
moveElement(uuid, changes) {
268+
moveElement(uuid, { x, y }) {
267269
const el = this._content.find((e) => e.uuid === uuid);
268270
if (!el) return;
269-
// Mutate in-place only. No _content replacement, no selectedElement write.
270-
// Zero re-renders. interact.js instances remain alive.
271-
Object.assign(el, changes);
271+
Object.assign(el, { x, y });
272+
}
273+
274+
/**
275+
* Silently sync an element's position and size after a resize gesture ends.
276+
* Mutates in-place — no Glimmer re-render, no undo entry.
277+
* interact.js has already updated the DOM; this just keeps the data model
278+
* in sync so the correct dimensions are included in the next save.
279+
*/
280+
@action
281+
resizeElement(uuid, { x, y, width, height }) {
282+
const el = this._content.find((e) => e.uuid === uuid);
283+
if (!el) return;
284+
Object.assign(el, { x, y, width, height });
272285
}
273286

274287
/**
Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,23 @@
11
{{! Template Builder Canvas }}
2+
{{! This component is a thin rendering shell. Each ElementRenderer owns its
3+
own interact.js instance and emits @onMove / @onResize / @onSelect. }}
24
<div
35
id={{this.canvasId}}
46
class="tb-canvas shadow-xl mx-auto select-none"
57
style={{this.canvasStyle}}
6-
{{did-insert this.didInsertCanvas}}
7-
{{will-destroy this.willDestroyCanvas}}
8-
{{on "click" this.deselectAll}}
8+
{{on "click" this.handleDeselectAll}}
99
...attributes
1010
>
11-
{{! key="uuid" tells Glimmer to track each ElementRenderer by the element's
12-
uuid rather than by array index. When updateElement replaces an element
13-
object with a new spread copy (same uuid, new reference), Glimmer reuses
14-
the existing component instance and DOM node — so interact.js stays alive —
15-
but passes the new object as @element, causing the template to re-render
16-
and all getters (textContent, textStyle, wrapperStyle, etc.) to re-evaluate. }}
1711
{{#each this.elements key="uuid" as |element|}}
1812
<TemplateBuilder::ElementRenderer
1913
@element={{element}}
2014
@isSelected={{eq element.uuid this.selectedUuid}}
21-
@rotation={{element.rotation}}
2215
@zoom={{this.zoom}}
23-
@onSelect={{fn this.selectElement element}}
24-
@onDidInsert={{fn this.setupElement element}}
25-
@onWillDestroy={{fn this.teardownElement element}}
16+
@canvasWidth={{this.canvasWidthPx}}
17+
@canvasHeight={{this.canvasHeightPx}}
18+
@onSelect={{this.handleSelectElement}}
19+
@onMove={{@onMoveElement}}
20+
@onResize={{@onResizeElement}}
2621
/>
2722
{{/each}}
2823
</div>

0 commit comments

Comments
 (0)