Morphing the DOM
When HTML arrives with mode outer (the default) or inner, sigmx does not replace the target: it morphs it. The existing element is walked alongside the new one, attributes are synced, text is updated, and children are matched up so that as few nodes as possible are created or destroyed.
Identity by id
Section titled “Identity by id”An element whose id appears in both the old and the new tree is kept, wherever it ends up. It may be reordered or even moved out of a removed ancestor, and it keeps everything the browser attached to it: focus, selection, typed input values, scroll position, playing media, third-party widgets.
Elements without an id are matched positionally by tag name.
---import { todos } from '../../data/todos';import TodoList from '../partials/TodoList.astro';---<form class="ex todos" data-signals="{ newTodo: '' }" data-on:submit="@post('/api/todos')" data-indicator:saving> <header class="bar"> <b>Todos</b> <span class="chip" data-text="$saving ? 'saving…' : 'server-rendered list'"></span> </header> <TodoList todos={todos} /> <div class="composer"> <input class="field" data-bind:new-todo placeholder="Add a todo and press Enter" required> <button class="btn" data-attr:disabled="!$newTodo.trim() || $saving">Add</button> </div> <p class="hint muted">Each patch re-renders the whole list on the server; notes you type in a row survive because rows keep their <code>id</code>.</p></form><style is:global> .todos { background: var(--ex-surface); } .todos .bar { display: flex; align-items: center; justify-content: space-between; padding: .7rem 1rem; border-bottom: 1px solid var(--ex-line); } .todos #todo-list { list-style: none; margin: 0; padding: .4rem 0; } .todos #todo-list li { display: grid; grid-template-columns: auto 1fr minmax(8rem, 14rem) auto; gap: .7rem; align-items: center; padding: .55rem 1rem; border-bottom: 1px solid var(--ex-line); animation: msg-in .25s ease-out; } .todos #todo-list li:last-child { border-bottom: 0; } .todos #todo-list li.empty { grid-template-columns: 1fr; color: var(--ex-muted); text-align: center; padding: 1.2rem; } @media (max-width: 30rem) { .todos #todo-list li { grid-template-columns: auto 1fr auto; } .todos #todo-list li .note { grid-column: 1 / -1; width: 100%; } } .todos .tick { width: 1.1rem; height: 1.1rem; border-radius: 50%; border: 2px solid var(--ex-accent); display: inline-block; } .todos .note { font-size: .85rem; padding: .35rem .6rem; } .todos .remove { width: 2rem; height: 2rem; border-radius: 50%; border: 0; background: transparent; color: var(--ex-muted); cursor: pointer; font-size: 1.1rem; line-height: 1; } .todos .remove:hover { background: color-mix(in srgb, var(--ex-bad) 14%, transparent); color: var(--ex-bad); } .todos .composer { display: flex; gap: .5rem; padding: .7rem 1rem; border-top: 1px solid var(--ex-line); background: var(--ex-surface-2); } .todos .hint { margin: 0; padding: .4rem 1rem .7rem; font-size: .78rem; background: var(--ex-surface-2); }</style>Each <li> has an id, so the notes you type survive every patch even though the server re-renders the whole list.
Form state
Section titled “Form state”Typed values are only overwritten when the server’s value, checked or selected attribute changed between the old and new markup. A re-render with the same default leaves the user’s edits alone; a genuine change from the server wins.
Opting out
Section titled “Opting out”data-ignore-morphon both the old and the new element leaves that subtree untouched.data-preserve-attr="class style"lists attributes the old element keeps.
Scripts
Section titled “Scripts”<script> elements inserted by a patch are executed; those already on the page are not run twice.
Full documents
Section titled “Full documents”A response containing <html>, <head> or <body> is parsed as a document and morphed into the current one, head included.
Using the morph directly
Section titled “Using the morph directly”import { morph, morphInner } from 'sigmx/plugins'
morph(document.getElementById('list'), newListElement)morphInner(container, fragment)