Skip to content

Key support. Implements #8232 #9850

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
May 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a22100b
Store component/element keys on RenderTreeFrame
SteveSandersonMS Apr 29, 2019
f876894
Add StackObjectPool, which will be useful momentarily
SteveSandersonMS Apr 29, 2019
31264b9
Support keyed insertions/deletions
SteveSandersonMS Apr 29, 2019
4d7d9ac
Refactor AppendDiffEntriesForRange to prepare for adding "move" logic
SteveSandersonMS Apr 29, 2019
f9e27c8
Handle keyed moves by writing a post-edit permutation list
SteveSandersonMS Apr 30, 2019
b5d1cf0
Shrink KeyedItemInfo struct
SteveSandersonMS Apr 30, 2019
1d2b0d5
Apply permutations on the JS side
SteveSandersonMS Apr 25, 2019
f53c7ce
Include sourcemaps when building client-side Blazor apps with Referen…
SteveSandersonMS Apr 30, 2019
9791ab4
Update struct length of edit frames now it's explicit layout
SteveSandersonMS Apr 30, 2019
426c5b4
Tolerate clashing keys (i.e., produce a valid diff, even if suboptimal)
SteveSandersonMS Apr 30, 2019
02e6829
Tolerate keys being added/removed incorrectly
SteveSandersonMS Apr 30, 2019
8265369
E2E test harness for 'key'
SteveSandersonMS May 1, 2019
b004076
Some more unit test cases
SteveSandersonMS May 1, 2019
bd4782a
Invert diffing logic to prefer matching by key over sequence
SteveSandersonMS May 1, 2019
9b74707
Make unit test cases more adversarial
SteveSandersonMS May 1, 2019
72bc532
First actual E2E test
SteveSandersonMS May 1, 2019
a1b2327
In E2E test, verify correct preservation of components
SteveSandersonMS May 1, 2019
de7dbe3
E2E tests for simple insert/delete cases (with and without keys)
SteveSandersonMS May 1, 2019
0a0afd8
E2E test for reordering. Also extend other tests to verify simultaneo…
SteveSandersonMS May 1, 2019
06c6a9b
E2E test for many simultaneous changes
SteveSandersonMS May 1, 2019
d937401
Update reference sources
SteveSandersonMS May 1, 2019
5c3efe6
CR: Avoid x = y = z
SteveSandersonMS May 1, 2019
eef7828
CR: Only use 'finally' for actual cleanup
SteveSandersonMS May 1, 2019
09201d3
CR: Clean up RenderTreeFrame assignment
SteveSandersonMS May 1, 2019
e125109
CR: Include 'key' in RenderTreeFrame.ToString()
SteveSandersonMS May 1, 2019
c9813f1
CR: Avoid "new T()" in StackObjectPool
SteveSandersonMS May 1, 2019
964b764
CR: Make KeyedItemInfo readonly
SteveSandersonMS May 1, 2019
66a5615
CR: Handle change of frame type with matching keys (and sequence)
SteveSandersonMS May 6, 2019
80f954d
CR: Add E2E test showing form + key scenarios
SteveSandersonMS May 7, 2019
3fa9345
Preserve focus across edits
SteveSandersonMS May 7, 2019
19239a9
Tweak E2E test case
SteveSandersonMS May 7, 2019
9c4d46c
In client-side Blazor, prevent recursive event handler invocations
SteveSandersonMS May 7, 2019
a045b8f
Actual E2E tests for moving form elements
SteveSandersonMS May 7, 2019
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
2 changes: 1 addition & 1 deletion src/Components/Blazor/Build/src/ReferenceFromSource.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

<PropertyGroup>
<BlazorBuildReferenceFromSource>true</BlazorBuildReferenceFromSource>
<BlazorJsPath>$(RepositoryRoot)src\Components\Browser.JS\dist\$(Configuration)\blazor.*.js</BlazorJsPath>
<BlazorJsPath>$(RepositoryRoot)src\Components\Browser.JS\dist\$(Configuration)\blazor.*.js.*</BlazorJsPath>
</PropertyGroup>

<Import Project="$(MSBuildThisFileDirectory)targets/All.props" />
Expand Down
102 changes: 98 additions & 4 deletions src/Components/Browser.JS/dist/Debug/blazor.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -13263,7 +13263,13 @@ var BrowserRenderer = /** @class */ (function () {
clearBetween(rootElementToClear, rootElementToClearEnd);
}
}
var ownerDocument = LogicalElements_1.getClosestDomElement(element).ownerDocument;
var activeElementBefore = ownerDocument && ownerDocument.activeElement;
this.applyEdits(batch, element, 0, edits, referenceFrames);
// Try to restore focus in case it was lost due to an element move
if ((activeElementBefore instanceof HTMLElement) && ownerDocument && ownerDocument.activeElement !== activeElementBefore) {
activeElementBefore.focus();
}
};
BrowserRenderer.prototype.disposeComponent = function (componentId) {
delete this.childComponentLocations[componentId];
Expand All @@ -13277,6 +13283,7 @@ var BrowserRenderer = /** @class */ (function () {
BrowserRenderer.prototype.applyEdits = function (batch, parent, childIndex, edits, referenceFrames) {
var currentDepth = 0;
var childIndexAtCurrentDepth = childIndex;
var permutationList;
var arraySegmentReader = batch.arraySegmentReader;
var editReader = batch.editReader;
var frameReader = batch.frameReader;
Expand Down Expand Up @@ -13365,6 +13372,19 @@ var BrowserRenderer = /** @class */ (function () {
childIndexAtCurrentDepth = currentDepth === 0 ? childIndex : 0; // The childIndex is only ever nonzero at zero depth
break;
}
case RenderBatch_1.EditType.permutationListEntry: {
permutationList = permutationList || [];
permutationList.push({
fromSiblingIndex: childIndexAtCurrentDepth + editReader.siblingIndex(edit),
toSiblingIndex: childIndexAtCurrentDepth + editReader.moveToSiblingIndex(edit),
});
break;
}
case RenderBatch_1.EditType.permutationListEnd: {
LogicalElements_1.permuteLogicalChildren(parent, permutationList);
permutationList = undefined;
break;
}
default: {
var unknownType = editType; // Compile-time verification that the switch was exhaustive
throw new Error("Unknown edit type: " + unknownType);
Expand Down Expand Up @@ -14169,11 +14189,54 @@ function getLogicalChildrenArray(element) {
return element[logicalChildrenPropname];
}
exports.getLogicalChildrenArray = getLogicalChildrenArray;
function getLogicalNextSibling(element) {
var siblings = getLogicalChildrenArray(getLogicalParent(element));
var siblingIndex = Array.prototype.indexOf.call(siblings, element);
return siblings[siblingIndex + 1] || null;
function permuteLogicalChildren(parent, permutationList) {
// The permutationList must represent a valid permutation, i.e., the list of 'from' indices
// is distinct, and the list of 'to' indices is a permutation of it. The algorithm here
// relies on that assumption.
// Each of the phases here has to happen separately, because each one is designed not to
// interfere with the indices or DOM entries used by subsequent phases.
// Phase 1: track which nodes we will move
var siblings = getLogicalChildrenArray(parent);
permutationList.forEach(function (listEntry) {
listEntry.moveRangeStart = siblings[listEntry.fromSiblingIndex];
listEntry.moveRangeEnd = findLastDomNodeInRange(listEntry.moveRangeStart);
});
// Phase 2: insert markers
permutationList.forEach(function (listEntry) {
var marker = listEntry.moveToBeforeMarker = document.createComment('marker');
var insertBeforeNode = siblings[listEntry.toSiblingIndex + 1];
if (insertBeforeNode) {
insertBeforeNode.parentNode.insertBefore(marker, insertBeforeNode);
}
else {
appendDomNode(marker, parent);
}
});
// Phase 3: move descendants & remove markers
permutationList.forEach(function (listEntry) {
var insertBefore = listEntry.moveToBeforeMarker;
var parentDomNode = insertBefore.parentNode;
var elementToMove = listEntry.moveRangeStart;
var moveEndNode = listEntry.moveRangeEnd;
var nextToMove = elementToMove;
while (nextToMove) {
var nextNext = nextToMove.nextSibling;
parentDomNode.insertBefore(nextToMove, insertBefore);
if (nextToMove === moveEndNode) {
break;
}
else {
nextToMove = nextNext;
}
}
parentDomNode.removeChild(insertBefore);
});
// Phase 4: update siblings index
permutationList.forEach(function (listEntry) {
siblings[listEntry.toSiblingIndex] = listEntry.moveRangeStart;
});
}
exports.permuteLogicalChildren = permuteLogicalChildren;
function getClosestDomElement(logicalElement) {
if (logicalElement instanceof Element) {
return logicalElement;
Expand All @@ -14185,6 +14248,12 @@ function getClosestDomElement(logicalElement) {
throw new Error('Not a valid logical element');
}
}
exports.getClosestDomElement = getClosestDomElement;
function getLogicalNextSibling(element) {
var siblings = getLogicalChildrenArray(getLogicalParent(element));
var siblingIndex = Array.prototype.indexOf.call(siblings, element);
return siblings[siblingIndex + 1] || null;
}
function appendDomNode(child, parent) {
// This function only puts 'child' into the DOM in the right place relative to 'parent'
// It does not update the logical children array of anything
Expand All @@ -14208,6 +14277,26 @@ function appendDomNode(child, parent) {
throw new Error("Cannot append node because the parent is not a valid logical element. Parent: " + parent);
}
}
// Returns the final node (in depth-first evaluation order) that is a descendant of the logical element.
// As such, the entire subtree is between 'element' and 'findLastDomNodeInRange(element)' inclusive.
function findLastDomNodeInRange(element) {
if (element instanceof Element) {
return element;
}
var nextSibling = getLogicalNextSibling(element);
if (nextSibling) {
// Simple case: not the last logical sibling, so take the node before the next sibling
return nextSibling.previousSibling;
}
else {
// Harder case: there's no logical next-sibling, so recurse upwards until we find
// a logical ancestor that does have one, or a physical element
var logicalParent = getLogicalParent(element);
return logicalParent instanceof Element
? logicalParent.lastChild
: findLastDomNodeInRange(logicalParent);
}
}
function createSymbolOrFallback(fallback) {
return typeof Symbol === 'function' ? Symbol() : fallback;
}
Expand Down Expand Up @@ -14303,6 +14392,9 @@ var OutOfProcessRenderTreeEditReader = /** @class */ (function () {
OutOfProcessRenderTreeEditReader.prototype.newTreeIndex = function (edit) {
return readInt32LE(this.batchDataUint8, edit + 8); // 3rd int
};
OutOfProcessRenderTreeEditReader.prototype.moveToSiblingIndex = function (edit) {
return readInt32LE(this.batchDataUint8, edit + 8); // 3rd int
};
OutOfProcessRenderTreeEditReader.prototype.removedAttributeName = function (edit) {
var stringIndex = readInt32LE(this.batchDataUint8, edit + 12); // 4th int
return this.stringReader.readString(stringIndex);
Expand Down Expand Up @@ -14458,6 +14550,8 @@ var EditType;
EditType[EditType["stepIn"] = 6] = "stepIn";
EditType[EditType["stepOut"] = 7] = "stepOut";
EditType[EditType["updateMarkup"] = 8] = "updateMarkup";
EditType[EditType["permutationListEntry"] = 9] = "permutationListEntry";
EditType[EditType["permutationListEnd"] = 10] = "permutationListEnd";
})(EditType = exports.EditType || (exports.EditType = {}));
var FrameType;
(function (FrameType) {
Expand Down
106 changes: 99 additions & 7 deletions src/Components/Browser.JS/dist/Debug/blazor.webassembly.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions src/Components/Browser.JS/dist/Release/blazor.server.js

Large diffs are not rendered by default.

Large diffs are not rendered by default.

24 changes: 23 additions & 1 deletion src/Components/Browser.JS/src/Rendering/BrowserRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { RenderBatch, ArraySegment, RenderTreeEdit, RenderTreeFrame, EditType, FrameType, ArrayValues } from './RenderBatch/RenderBatch';
import { EventDelegator } from './EventDelegator';
import { EventForDotNet, UIEventArgs } from './EventForDotNet';
import { LogicalElement, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement, getLogicalChildrenArray, getLogicalSiblingEnd } from './LogicalElements';
import { LogicalElement, PermutationListEntry, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement, getLogicalChildrenArray, getLogicalSiblingEnd, permuteLogicalChildren, getClosestDomElement } from './LogicalElements';
import { applyCaptureIdToElement } from './ElementReferenceCapture';
const selectValuePropname = '_blazorSelectValue';
const sharedTemplateElemForParsing = document.createElement('template');
Expand Down Expand Up @@ -47,7 +47,15 @@ export class BrowserRenderer {
}
}

const ownerDocument = getClosestDomElement(element).ownerDocument;
const activeElementBefore = ownerDocument && ownerDocument.activeElement;

this.applyEdits(batch, element, 0, edits, referenceFrames);

// Try to restore focus in case it was lost due to an element move
if ((activeElementBefore instanceof HTMLElement) && ownerDocument && ownerDocument.activeElement !== activeElementBefore) {
activeElementBefore.focus();
}
}

public disposeComponent(componentId: number) {
Expand All @@ -65,6 +73,7 @@ export class BrowserRenderer {
private applyEdits(batch: RenderBatch, parent: LogicalElement, childIndex: number, edits: ArraySegment<RenderTreeEdit>, referenceFrames: ArrayValues<RenderTreeFrame>) {
let currentDepth = 0;
let childIndexAtCurrentDepth = childIndex;
let permutationList: PermutationListEntry[] | undefined;

const arraySegmentReader = batch.arraySegmentReader;
const editReader = batch.editReader;
Expand Down Expand Up @@ -152,6 +161,19 @@ export class BrowserRenderer {
childIndexAtCurrentDepth = currentDepth === 0 ? childIndex : 0; // The childIndex is only ever nonzero at zero depth
break;
}
case EditType.permutationListEntry: {
permutationList = permutationList || [];
permutationList.push({
fromSiblingIndex: childIndexAtCurrentDepth + editReader.siblingIndex(edit),
toSiblingIndex: childIndexAtCurrentDepth + editReader.moveToSiblingIndex(edit),
});
break;
}
case EditType.permutationListEnd: {
permuteLogicalChildren(parent, permutationList!);
permutationList = undefined;
break;
}
default: {
const unknownType: never = editType; // Compile-time verification that the switch was exhaustive
throw new Error(`Unknown edit type: ${unknownType}`);
Expand Down
Loading