Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
826e6cb
New single-pass algorithm.
Dec 23, 2024
2818a30
improve morphChildren insertion point null checking
MichaelWest22 Jan 9, 2025
49c5b6b
Add back findIdSetMatch test and handle moveBefore failure
MichaelWest22 Jan 10, 2025
243749f
Improve idMap to only include persistent Ids
MichaelWest22 Jan 10, 2025
e589874
Add findSoftMatch and moveBefore loc tests
MichaelWest22 Jan 10, 2025
a50ee6e
replace bespoke outerHTML morphing algorithm with our core one.
Jan 10, 2025
9fa448f
implement bestMatch feature in morphChildren and tidy up idSets
MichaelWest22 Jan 11, 2025
2580bd0
handle single node better
MichaelWest22 Jan 12, 2025
230b8de
Improve BestMatch
MichaelWest22 Jan 12, 2025
7f9e4e5
cleanup.
Jan 14, 2025
abc3abd
remove bespoke outerHTML morphing functions.
Jan 13, 2025
e4e0aa2
rename fns.
Jan 14, 2025
31456d8
create submodule for createMorphContext.
Jan 14, 2025
c0d127d
parsing a string is normalizing.
Jan 14, 2025
d414cc2
extract morphNode module.
Jan 14, 2025
1510bdd
move a few minor things around.
Jan 14, 2025
74abe68
refactor morph entry point.
Jan 14, 2025
2e07c8c
isolate morphChildren and its subfunctions.
Jan 14, 2025
3a75cc5
inline insertOrMorphNode and extract createNode.
Jan 14, 2025
8c23dca
final movement.
Jan 14, 2025
0b5eeed
let morphChildren return the nodes it morphs.
Jan 14, 2025
3a48317
tighten up init.
Jan 14, 2025
f433564
huge simplifying refactor.
Jan 14, 2025
e982176
inline morphChild.
Jan 14, 2025
e4eeb8a
satisfy tsc.
Jan 14, 2025
14692b4
Fix beforeNodeRemoved hook case and improve test and inline documenta…
MichaelWest22 Jan 14, 2025
d25716c
use removeNodesBetween to fix issue exposed by hook test.
MichaelWest22 Jan 14, 2025
c7811f2
split off preserve focus tests into their own file and move hook test…
Jan 14, 2025
8e7e09f
back to 100% coverage.
Jan 14, 2025
eeb3263
tweak inner loop docs and type checking.
Jan 14, 2025
77ebe63
no longer need to be careful about losing state now that we're moving…
Jan 14, 2025
82392ba
test moveBefore -> insertBefore fallback.
Jan 14, 2025
d6101ce
add restoreFocus option for manually saving and restoring any lost fo…
Jan 13, 2025
017f714
simplify softMatch more and tweak docs.
Jan 14, 2025
a327af9
encapsulate all matching functions into a single findBestMatch entry …
Jan 14, 2025
9c664a2
minor cleanup.
Jan 15, 2025
1567f80
some minor contortions to get us back to 100% coverage.
Jan 15, 2025
596fe55
merge findIdSetMatch and findSoftMatch so that we only iterate once, …
Jan 15, 2025
517ca80
now that we have one loop, we can simplify further.
Jan 15, 2025
f84c66a
perf: go back to calculating outerHTML morphed nodes manually. This i…
Jan 15, 2025
5ce43e9
extract morphOuterHTML.
Jan 15, 2025
2a5e231
fail ci if we fall short of 100% coverage, and rename some npm tasks.
Jan 15, 2025
1f38bdb
try publishing coverage report as an artifact.
Jan 15, 2025
c06a518
update target to es2016 to support includes
MichaelWest22 Jan 16, 2025
e50aec4
for some reason this can hang when assertion fails.
Jan 16, 2025
71c196e
normalize more function argument order and improve docs and tests.
Jan 16, 2025
df4a1fa
these functions aren't pulling their weight anymore. inline.
Jan 16, 2025
9a80532
move id set matching into findBestMatch module, and simplify.
Jan 16, 2025
4ffac5d
this is the correct way to set a boolean attribute to true.
Jan 26, 2025
2a09c5a
bring back old softMatch behaviour to reduce churn.
MichaelWest22 Jan 17, 2025
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
28 changes: 8 additions & 20 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,10 @@ jobs:
- name: Install dependencies
run: npm install
- name: Run tests
run: npm run test-move-before
run: npm run test:move-before

test-force-two-pass:
coverage:
runs-on: ubuntu-latest
env:
DEFAULT_TWO_PASS: true
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -49,23 +47,13 @@ jobs:
- name: Install browsers
run: npx playwright install --with-deps
- name: Run tests
run: npm run ci

test-force-two-pass-move-before:
runs-on: ubuntu-latest
env:
DEFAULT_TWO_PASS: true
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
run: npm run test:coverage
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
cache: 'npm'
- name: Install dependencies
run: npm install
- name: Run tests
run: npm run test-move-before
name: coverage-report
path: coverage/

typecheck:
runs-on: ubuntu-latest
Expand Down
31 changes: 19 additions & 12 deletions CONCEPT.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Given a old node N, we can ask if it might match _any_ child of a parent node P
Given these two enhancements:

* The ability to ask if two nodes are the same based on child information efficiently
* The abilit to ask if a node has a match within an element efficiently
* The ability to ask if a node has a match within an element efficiently

We believe we can improve on the fidelity of DOM matching when compared with plain ID-based matching.

Expand All @@ -105,23 +105,30 @@ Sync the attributes of newNode onto oldNode
Let insertionPoint be the first child of the oldNode
while newNode has children
let newChild be the result of shifting the first child from newNodes children
if the newChild is the same as the insertionPoint
if the newChild matches the insertionPoint
recursively merge newChild into insertionPoint
advance the insertionPoint to its next sibling
else if the newChild has a match within the oldNode
scan the insertionPoint forward to the match, discarding children of the old node
recursively merge newChild into insertionPoint
advance the insertionPoint to its next sibling
else if the insertionPoint has a match within the newNode
insert the newChild before the insertionPoint
else if the insertionPoint and the newChild are compatible
else if the newChild has a match among oldNodes children
move the match before the insertionPoint
recursively merge newChild into the match
else if the newChild and insertionPoint are compatible
recursively merge newChild into insertionPoint
advance the insertionPoint to its next sibling
else if the newChild has a compatible match among oldNodes children
move the match before the insertionPoint
recursively merge newChild into the match
else if the newChild is IDed and has a match later in the oldTree or pantry
move the match before the insertionPoint
recursively merge newChild into the match
else
insert the newChild before the insertionPoint
insert a clone of the newChild before the insertionPoint
end

while insertionPoint is not null
remove the insertion point and advance to the next sibling
if the insertionPoint is IDed and has a match later in the newTree
move the insertionPoint into a temporary pantry div for later reuse
else
delete the insertionPoint
advance the insertionPoint to its next sibling

```
```
12 changes: 1 addition & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ Idiomorph supports the following options:
| `morphStyle` | The style of morphing to use, either `innerHTML` or `outerHTML` | `Idiomorph.morph(..., {morphStyle:'innerHTML'})` |
| `ignoreActive` | If set to `true`, idiomorph will skip the active element | `Idiomorph.morph(..., {ignoreActive:true})` |
| `ignoreActiveValue` | If set to `true`, idiomorph will not update the active element's value | `Idiomorph.morph(..., {ignoreActiveValue:true})` |
| `restoreFocus` | If set to `true`, idiomorph will attempt to restore any lost focus and selection state after the morph. | `Idiomorph.morph(..., {restoreFocus:true})` |
| `head` | Allows you to control how the `head` tag is merged. See the [head](#the-head-tag) section for more details | `Idiomorph.morph(..., {head:{style:merge}})` |
| `callbacks` | Allows you to insert callbacks when events occur in the morph life cycle, see the callback table below | `Idiomorph.morph(..., {callbacks:{beforeNodeAdded:function(node){...}})` |
| `twoPass` | If set to `true`, idiomorph does a second pass to maintain more state. See [Two Pass Mode](#two-pass-mode) | `Idiomorph.morph(..., {twoPass:true})` |

#### Callbacks

Expand All @@ -112,7 +112,6 @@ of the algorithm.
| beforeNodeRemoved(node) | Called before a node is removed from the DOM | return false to not remove the node |
| afterNodeRemoved(node) | Called after a node is removed from the DOM | none |
| beforeAttributeUpdated(attributeName, node, mutationType) | Called before an attribute on an element. `mutationType` is either "updated" or "removed"| return false to not update or remove the attribute |
| beforeNodePantried(node) | Called before moving the node into the pantry during the second pass (if enabled) | return false to not move the node into the pantry |

### The `head` tag

Expand Down Expand Up @@ -142,15 +141,6 @@ of the script.
Similarly, you may wish to preserve an element even if it is not in `new`. You can use the attribute `im-preserve='true'`
in this case to retain the element.

#### Two Pass Mode

If the `twoPass` option is enabled, Idiomorph will try to maintain more state by temporarily moving some nodes into a
hidden pantry div. After the initial morph is complete, it runs a second pass, moving the pantried nodes back into the
document. These are nodes that would otherwise have been removed and readded, losing identity and state. This is
particularly useful for reordering morphs involving elements with non-attribute state, e.g. video elements, elements
preserved with `data-turbo-permanent`, etc. Also, when this option is enabled, Idiomorph will use the upcoming
`Element#moveBefore` API if it exists, falling back to `Element#insertBefore` if not.

#### Additional Configuration

You are also able to override these behaviors, see the `head` config object in the source code.
Expand Down
10 changes: 3 additions & 7 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ This will run the tests using Playwright’s headless browser setup across Chrom

To run all tests against Chrome with experimental `moveBefore` support added, execute:
```bash
npm run test-move-before
npm run test:move-before
```
This will start headless Chrome in a new profile with the `atomic-move` experimental flag set. This runs in a separate job in CI.

Expand All @@ -50,20 +50,16 @@ This runs all the tests in the browser using Mocha instead of web-test-runner fo

If you really want to open web-test-runner in headed mode, you can run:
```bash
npm run debug
npm run test:debug
```
This will start the server, and open the test runner in a browser. From there you can choose a test file to run.

## Forcing Two Pass Mode
If the `DEFAULT_TWO_PASS` environment variable is set before running the tests, Idiomorph will default to two-pass mode. This is useful for running the entire test suite with two-pass on.

## GitHub Actions CI matrix
On each push and PR, GitHub Actions runs the following test matrix:

1. Normal baseline `npm run ci` run
2. With experimental moveBefore enabled in the browser
3. With two-pass mode forced
4. With both moveBefore enabled and two-pass mode forced
3. Typecheck

## Code Coverage Report
After a test run completes, you can open `coverage/lcov-report/index.html` to view the code coverage report. On Ubuntu you can run:
Expand Down
Loading