From 5141ee930e32b3bb15c7435953501d6e2d00b430 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 15 Jul 2019 12:38:00 +0100 Subject: [PATCH] For @key, support both strict (default) and loose modes --- .../src/RenderTree/ComponentFlags.cs | 17 +++++ .../Components/src/RenderTree/ElementFlags.cs | 17 +++++ .../src/RenderTree/RenderTreeBuilder.cs | 21 +++++- .../src/RenderTree/RenderTreeDiffBuilder.cs | 51 ++++++++++++-- .../src/RenderTree/RenderTreeFrame.cs | 44 ++++++++---- .../Components/test/RenderTreeBuilderTest.cs | 60 ++++++++++++++-- .../test/RenderTreeDiffBuilderTest.cs | 69 ++++++++++++++++--- .../Web.JS/dist/Release/blazor.webassembly.js | 2 +- src/Components/test/E2ETest/Tests/KeyTest.cs | 26 ++++++- .../BasicTestApp/KeyCasesComponent.razor | 6 ++ .../BasicTestApp/KeyCasesTreeNode.razor | 44 ++++++++---- 11 files changed, 308 insertions(+), 49 deletions(-) create mode 100644 src/Components/Components/src/RenderTree/ComponentFlags.cs create mode 100644 src/Components/Components/src/RenderTree/ElementFlags.cs diff --git a/src/Components/Components/src/RenderTree/ComponentFlags.cs b/src/Components/Components/src/RenderTree/ComponentFlags.cs new file mode 100644 index 0000000000..4c39fa63eb --- /dev/null +++ b/src/Components/Components/src/RenderTree/ComponentFlags.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Components.RenderTree +{ + /// + /// Describes flags associated with a whose + /// equals . + /// + [Flags] + public enum ComponentFlags: short + { + LooseKey = 1, + } +} diff --git a/src/Components/Components/src/RenderTree/ElementFlags.cs b/src/Components/Components/src/RenderTree/ElementFlags.cs new file mode 100644 index 0000000000..359adbee4f --- /dev/null +++ b/src/Components/Components/src/RenderTree/ElementFlags.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Components.RenderTree +{ + /// + /// Describes flags associated with a whose + /// equals . + /// + [Flags] + public enum ElementFlags: short + { + LooseKey = 1, + } +} diff --git a/src/Components/Components/src/RenderTree/RenderTreeBuilder.cs b/src/Components/Components/src/RenderTree/RenderTreeBuilder.cs index ec545b2dc0..a58bcc372a 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeBuilder.cs +++ b/src/Components/Components/src/RenderTree/RenderTreeBuilder.cs @@ -520,10 +520,25 @@ namespace Microsoft.AspNetCore.Components.RenderTree /// /// The value for the key. public void SetKey(object value) + => SetKey(value, looseKey: false); + + /// + /// Assigns the specified key value to the current element or component. + /// + /// The value for the key. + /// If true, null values and duplicates will be ignored. If false, null values and duplicates will be treated as an error. + public void SetKey(object value, bool looseKey) { if (value == null) { - throw new ArgumentNullException(nameof(value)); + if (looseKey) + { + return; + } + else + { + throw new ArgumentNullException(nameof(value)); + } } var parentFrameIndex = GetCurrentParentFrameIndex(); @@ -537,10 +552,10 @@ namespace Microsoft.AspNetCore.Components.RenderTree switch (parentFrame.FrameType) { case RenderTreeFrameType.Element: - parentFrame = parentFrame.WithElementKey(value); // It's a ref var, so this writes to the array + parentFrame = parentFrame.WithElementKey(value, looseKey); // It's a ref var, so this writes to the array break; case RenderTreeFrameType.Component: - parentFrame = parentFrame.WithComponentKey(value); // It's a ref var, so this writes to the array + parentFrame = parentFrame.WithComponentKey(value, looseKey); // It's a ref var, so this writes to the array break; default: throw new InvalidOperationException($"Cannot set a key on a frame of type {parentFrame.FrameType}."); diff --git a/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs b/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs index e11b12bb24..bc88a3f2c2 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs +++ b/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs @@ -50,6 +50,8 @@ namespace Microsoft.AspNetCore.Components.RenderTree // if you plan to refactor this, be sure to benchmark the old and new versions // on Mono WebAssembly. + var origOldStartIndex = oldStartIndex; + var origNewStartIndex = newStartIndex; var hasMoreOld = oldEndIndexExcl > oldStartIndex; var hasMoreNew = newEndIndexExcl > newStartIndex; var prevOldSeq = -1; @@ -108,7 +110,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree // Keys don't match if (keyedItemInfos == null) { - keyedItemInfos = BuildKeyToInfoLookup(diffContext, oldStartIndex, oldEndIndexExcl, newStartIndex, newEndIndexExcl); + keyedItemInfos = BuildKeyToInfoLookup(diffContext, origOldStartIndex, oldEndIndexExcl, origNewStartIndex, newEndIndexExcl); } var oldKeyItemInfo = oldKey != null ? keyedItemInfos[oldKey] : new KeyedItemInfo(-1, -1, false); @@ -304,7 +306,15 @@ namespace Microsoft.AspNetCore.Components.RenderTree var key = KeyValue(ref frame); if (key != null) { - result[key] = new KeyedItemInfo(oldStartIndex, -1, isUnique: !result.ContainsKey(key)); + var isUnique = !result.ContainsKey(key); + if (isUnique || KeyIsLoose(ref frame)) + { + result[key] = new KeyedItemInfo(oldStartIndex, -1, isUnique); + } + else + { + ThrowExceptionForDuplicateKey(key); + } } oldStartIndex = NextSiblingIndex(frame, oldStartIndex); @@ -316,9 +326,22 @@ namespace Microsoft.AspNetCore.Components.RenderTree var key = KeyValue(ref frame); if (key != null) { - result[key] = result.TryGetValue(key, out var existingEntry) - ? new KeyedItemInfo(existingEntry.OldIndex, newStartIndex, isUnique: existingEntry.NewIndex < 0) - : new KeyedItemInfo(-1, newStartIndex, isUnique: true); + if (!result.TryGetValue(key, out var existingEntry)) + { + result[key] = new KeyedItemInfo(-1, newStartIndex, isUnique: true); + } + else + { + var isUnique = existingEntry.NewIndex < 0; + if (isUnique || KeyIsLoose(ref frame)) + { + result[key] = new KeyedItemInfo(existingEntry.OldIndex, newStartIndex, isUnique); + } + else + { + ThrowExceptionForDuplicateKey(key); + } + } } newStartIndex = NextSiblingIndex(frame, newStartIndex); @@ -327,6 +350,11 @@ namespace Microsoft.AspNetCore.Components.RenderTree return result; } + private static void ThrowExceptionForDuplicateKey(object key) + { + throw new InvalidOperationException($"More than one sibling has the same key value, '{key}'. Key values must be unique, or 'loose' key mode must be used."); + } + private static object KeyValue(ref RenderTreeFrame frame) { switch (frame.FrameType) @@ -340,6 +368,19 @@ namespace Microsoft.AspNetCore.Components.RenderTree } } + private static bool KeyIsLoose(ref RenderTreeFrame frame) + { + switch (frame.FrameType) + { + case RenderTreeFrameType.Element: + return frame.ElementFlags.HasFlag(ElementFlags.LooseKey); + case RenderTreeFrameType.Component: + return frame.ComponentFlags.HasFlag(ComponentFlags.LooseKey); + default: + return false; + } + } + // Handles the diff for attribute nodes only - this invariant is enforced by the caller. // // The diff for attributes is different because we allow attributes to appear in any order. diff --git a/src/Components/Components/src/RenderTree/RenderTreeFrame.cs b/src/Components/Components/src/RenderTree/RenderTreeFrame.cs index 1e61f34a1d..30cbf6d845 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeFrame.cs +++ b/src/Components/Components/src/RenderTree/RenderTreeFrame.cs @@ -53,6 +53,12 @@ namespace Microsoft.AspNetCore.Components.RenderTree // RenderTreeFrameType.Element // -------------------------------------------------------------------------------- + /// + /// If the property equals + /// gets the flags associated with the frame. + /// + [FieldOffset(6)] public readonly ElementFlags ElementFlags; + /// /// If the property equals /// gets the number of frames in the subtree for which this frame is the root. @@ -116,6 +122,12 @@ namespace Microsoft.AspNetCore.Components.RenderTree // RenderTreeFrameType.Component // -------------------------------------------------------------------------------- + /// + /// If the property equals + /// gets the flags associated with the frame. + /// + [FieldOffset(6)] public readonly ComponentFlags ComponentFlags; + /// /// If the property equals /// gets the number of frames in the subtree for which this frame is the root. @@ -213,22 +225,24 @@ namespace Microsoft.AspNetCore.Components.RenderTree [FieldOffset(16)] public readonly string MarkupContent; // Element constructor - private RenderTreeFrame(int sequence, int elementSubtreeLength, string elementName, object elementKey) + private RenderTreeFrame(int sequence, ElementFlags elementFlags, int elementSubtreeLength, string elementName, object elementKey) : this() { Sequence = sequence; FrameType = RenderTreeFrameType.Element; + ElementFlags = elementFlags; ElementSubtreeLength = elementSubtreeLength; ElementName = elementName; ElementKey = elementKey; } // Component constructor - private RenderTreeFrame(int sequence, int componentSubtreeLength, Type componentType, ComponentState componentState, object componentKey) + private RenderTreeFrame(int sequence, ComponentFlags componentFlags, int componentSubtreeLength, Type componentType, ComponentState componentState, object componentKey) : this() { Sequence = sequence; FrameType = RenderTreeFrameType.Component; + ComponentFlags = componentFlags; ComponentSubtreeLength = componentSubtreeLength; ComponentType = componentType; ComponentKey = componentKey; @@ -299,7 +313,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree } internal static RenderTreeFrame Element(int sequence, string elementName) - => new RenderTreeFrame(sequence, elementSubtreeLength: 0, elementName, null); + => new RenderTreeFrame(sequence, elementFlags: default, elementSubtreeLength: 0, elementName, null); internal static RenderTreeFrame Text(int sequence, string textContent) => new RenderTreeFrame(sequence, isMarkup: false, textOrMarkup: textContent); @@ -311,10 +325,10 @@ namespace Microsoft.AspNetCore.Components.RenderTree => new RenderTreeFrame(sequence, attributeName: name, attributeValue: value, attributeEventHandlerId: 0, attributeEventUpdatesAttributeName: null); internal static RenderTreeFrame ChildComponent(int sequence, Type componentType) - => new RenderTreeFrame(sequence, componentSubtreeLength: 0, componentType, null, null); + => new RenderTreeFrame(sequence, componentFlags: default, componentSubtreeLength: 0, componentType, null, null); internal static RenderTreeFrame PlaceholderChildComponentWithSubtreeLength(int subtreeLength) - => new RenderTreeFrame(0, componentSubtreeLength: subtreeLength, typeof(IComponent), null, null); + => new RenderTreeFrame(0, componentFlags: default, componentSubtreeLength: subtreeLength, typeof(IComponent), null, null); internal static RenderTreeFrame Region(int sequence) => new RenderTreeFrame(sequence, regionSubtreeLength: 0); @@ -326,16 +340,16 @@ namespace Microsoft.AspNetCore.Components.RenderTree => new RenderTreeFrame(sequence, componentReferenceCaptureAction: componentReferenceCaptureAction, parentFrameIndex: parentFrameIndex); internal RenderTreeFrame WithElementSubtreeLength(int elementSubtreeLength) - => new RenderTreeFrame(Sequence, elementSubtreeLength: elementSubtreeLength, ElementName, ElementKey); + => new RenderTreeFrame(Sequence, elementFlags: ElementFlags, elementSubtreeLength: elementSubtreeLength, ElementName, ElementKey); internal RenderTreeFrame WithComponentSubtreeLength(int componentSubtreeLength) - => new RenderTreeFrame(Sequence, componentSubtreeLength: componentSubtreeLength, ComponentType, ComponentState, ComponentKey); + => new RenderTreeFrame(Sequence, componentFlags: ComponentFlags, componentSubtreeLength: componentSubtreeLength, ComponentType, ComponentState, ComponentKey); internal RenderTreeFrame WithAttributeSequence(int sequence) => new RenderTreeFrame(sequence, attributeName: AttributeName, AttributeValue, AttributeEventHandlerId, AttributeEventUpdatesAttributeName); internal RenderTreeFrame WithComponent(ComponentState componentState) - => new RenderTreeFrame(Sequence, componentSubtreeLength: ComponentSubtreeLength, ComponentType, componentState, ComponentKey); + => new RenderTreeFrame(Sequence, componentFlags: ComponentFlags, componentSubtreeLength: ComponentSubtreeLength, ComponentType, componentState, ComponentKey); internal RenderTreeFrame WithAttributeEventHandlerId(int eventHandlerId) => new RenderTreeFrame(Sequence, attributeName: AttributeName, AttributeValue, eventHandlerId, AttributeEventUpdatesAttributeName); @@ -352,11 +366,17 @@ namespace Microsoft.AspNetCore.Components.RenderTree internal RenderTreeFrame WithElementReferenceCaptureId(string elementReferenceCaptureId) => new RenderTreeFrame(Sequence, elementReferenceCaptureAction: ElementReferenceCaptureAction, elementReferenceCaptureId); - internal RenderTreeFrame WithElementKey(object elementKey) - => new RenderTreeFrame(Sequence, elementSubtreeLength: ElementSubtreeLength, ElementName, elementKey); + internal RenderTreeFrame WithElementKey(object elementKey, bool looseKey) + { + var flags = looseKey ? (ElementFlags | ElementFlags.LooseKey) : ElementFlags; + return new RenderTreeFrame(Sequence, elementFlags: flags, elementSubtreeLength: ElementSubtreeLength, ElementName, elementKey); + } - internal RenderTreeFrame WithComponentKey(object componentKey) - => new RenderTreeFrame(Sequence, componentSubtreeLength: ComponentSubtreeLength, ComponentType, ComponentState, componentKey); + internal RenderTreeFrame WithComponentKey(object componentKey, bool looseKey) + { + var flags = looseKey ? (ComponentFlags | ComponentFlags.LooseKey) : ComponentFlags; + return new RenderTreeFrame(Sequence, componentFlags: flags, componentSubtreeLength: ComponentSubtreeLength, ComponentType, ComponentState, componentKey); + } /// // Just to be nice for debugging and unit tests. diff --git a/src/Components/Components/test/RenderTreeBuilderTest.cs b/src/Components/Components/test/RenderTreeBuilderTest.cs index 6d03c1261c..4c090d320b 100644 --- a/src/Components/Components/test/RenderTreeBuilderTest.cs +++ b/src/Components/Components/test/RenderTreeBuilderTest.cs @@ -1475,8 +1475,10 @@ namespace Microsoft.AspNetCore.Components.Test frame => AssertFrame.Element(frame, "elem", 1, 0)); } - [Fact] - public void CanAddKeyToElement() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CanAddKeyToElement(bool looseKey) { // Arrange var builder = new RenderTreeBuilder(new TestRenderer()); @@ -1485,7 +1487,7 @@ namespace Microsoft.AspNetCore.Components.Test // Act builder.OpenElement(0, "elem"); builder.AddAttribute(1, "attribute before", "before value"); - builder.SetKey(keyValue); + builder.SetKey(keyValue, looseKey); builder.AddAttribute(2, "attribute after", "after value"); builder.CloseElement(); @@ -1496,13 +1498,16 @@ namespace Microsoft.AspNetCore.Components.Test { AssertFrame.Element(frame, "elem", 3, 0); Assert.Same(keyValue, frame.ElementKey); + Assert.Equal(looseKey, frame.ElementFlags.HasFlag(ElementFlags.LooseKey)); }, frame => AssertFrame.Attribute(frame, "attribute before", "before value", 1), frame => AssertFrame.Attribute(frame, "attribute after", "after value", 2)); } - [Fact] - public void CanAddKeyToComponent() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CanAddKeyToComponent(bool looseKey) { // Arrange var builder = new RenderTreeBuilder(new TestRenderer()); @@ -1511,7 +1516,7 @@ namespace Microsoft.AspNetCore.Components.Test // Act builder.OpenComponent(0); builder.AddAttribute(1, "param before", 123); - builder.SetKey(keyValue); + builder.SetKey(keyValue, looseKey); builder.AddAttribute(2, "param after", 456); builder.CloseComponent(); @@ -1522,6 +1527,7 @@ namespace Microsoft.AspNetCore.Components.Test { AssertFrame.Component(frame, 3, 0); Assert.Same(keyValue, frame.ComponentKey); + Assert.Equal(looseKey, frame.ComponentFlags.HasFlag(ComponentFlags.LooseKey)); }, frame => AssertFrame.Attribute(frame, "param before", 123, 1), frame => AssertFrame.Attribute(frame, "param after", 456, 2)); @@ -1576,6 +1582,48 @@ namespace Microsoft.AspNetCore.Components.Test Assert.Equal("value", ex.ParamName); } + [Fact] + public void IgnoresNullElementKeyIfLoose() + { + // Arrange + var builder = new RenderTreeBuilder(new TestRenderer()); + + // Act + builder.OpenElement(0, "elem"); + builder.SetKey(null, looseKey: true); + builder.CloseElement(); + + // Assert + Assert.Collection( + builder.GetFrames().AsEnumerable(), + frame => + { + AssertFrame.Element(frame, "elem", 1, 0); + Assert.Null(frame.ElementKey); + }); + } + + [Fact] + public void IgnoresNullComponentKeyIfLoose() + { + // Arrange + var builder = new RenderTreeBuilder(new TestRenderer()); + + // Act + builder.OpenComponent(0); + builder.SetKey(null, looseKey: true); + builder.CloseComponent(); + + // Assert + Assert.Collection( + builder.GetFrames().AsEnumerable(), + frame => + { + AssertFrame.Component(frame, 1, 0); + Assert.Null(frame.ComponentKey); + }); + } + [Fact] public void ProcessDuplicateAttributes_DoesNotRemoveDuplicatesWithoutAddMultipleAttributes() { diff --git a/src/Components/Components/test/RenderTreeDiffBuilderTest.cs b/src/Components/Components/test/RenderTreeDiffBuilderTest.cs index 5054d574b4..cc0b29ea5b 100644 --- a/src/Components/Components/test/RenderTreeDiffBuilderTest.cs +++ b/src/Components/Components/test/RenderTreeDiffBuilderTest.cs @@ -315,7 +315,41 @@ namespace Microsoft.AspNetCore.Components.Test } [Fact] - public void HandlesClashingKeys_FirstUsage() + public void HandlesClashingKeysInOldTree_Strict() + { + // Arrange + AddWithKey(oldTree, "key1", "attrib1a"); + AddWithKey(oldTree, "key2", "attrib2"); + AddWithKey(oldTree, "key1", "attrib3"); + + AddWithKey(newTree, "key1", "attrib1b"); + AddWithKey(newTree, "key2", "attrib2"); + AddWithKey(newTree, "key3", "attrib3"); + + // Act/Assert + var ex = Assert.Throws(() => GetSingleUpdatedComponent()); + Assert.Equal("More than one sibling has the same key value, 'key1'. Key values must be unique, or 'loose' key mode must be used.", ex.Message); + } + + [Fact] + public void HandlesClashingKeysInNewTree_Strict() + { + // Arrange + AddWithKey(oldTree, "key1", "attrib1a"); + AddWithKey(oldTree, "key2", "attrib2"); + AddWithKey(oldTree, "key3", "attrib3"); + + AddWithKey(newTree, "key1", "attrib1b"); + AddWithKey(newTree, "key2", "attrib2"); + AddWithKey(newTree, "key1", "attrib3"); + + // Act/Assert + var ex = Assert.Throws(() => GetSingleUpdatedComponent()); + Assert.Equal("More than one sibling has the same key value, 'key1'. Key values must be unique, or 'loose' key mode must be used.", ex.Message); + } + + [Fact] + public void HandlesClashingKeys_Loose_FirstUsage() { // This scenario is problematic for the algorithm if it uses a "first key // usage wins" policy for duplicate keys. It would not end up with attrib1b @@ -326,15 +360,20 @@ namespace Microsoft.AspNetCore.Components.Test // dictionary match" policy, we don't preserve any of the key1 items, and // the diff is valid. + // Since we only pass the "loose" flag for the key that would otherwise + // trigger an exception due to a clash, this shows that "loose" only matters + // in that case. It would be fine to pass loose:true for all the others too, + // as would normally be done in .razor, but it makes no difference. + // Arrange AddWithKey(oldTree, "key3", "attrib3"); AddWithKey(oldTree, "key1", "attrib1a"); - AddWithKey(oldTree, "key1", "attrib1a"); + AddWithKey(oldTree, "key1", "attrib1a", looseKey: true); AddWithKey(oldTree, "key2", "attrib2"); AddWithKey(newTree, "key1", "attrib1a"); AddWithKey(newTree, "key2", "attrib2"); - AddWithKey(newTree, "key1", "attrib1b"); + AddWithKey(newTree, "key1", "attrib1b", looseKey: true); // Act var (result, referenceFrames) = GetSingleUpdatedComponent(); @@ -362,7 +401,7 @@ namespace Microsoft.AspNetCore.Components.Test } [Fact] - public void HandlesClashingKeys_LastUsage() + public void HandlesClashingKeys_Loose_LastUsage() { // This scenario is problematic for the algorithm if it uses a "last key // usage wins" policy for duplicate keys. It would not end up with attrib1b @@ -373,14 +412,19 @@ namespace Microsoft.AspNetCore.Components.Test // dictionary match" policy, we don't preserve any of the key1 items, and // the diff is valid. + // Since we only pass the "loose" flag for the key that would otherwise + // trigger an exception due to a clash, this shows that "loose" only matters + // in that case. It would be fine to pass loose:true for all the others too, + // as would normally be done in .razor, but it makes no difference. + // Arrange AddWithKey(oldTree, "key1", "attrib1a"); AddWithKey(oldTree, "key2", "attrib2"); - AddWithKey(oldTree, "key1", "attrib1b"); + AddWithKey(oldTree, "key1", "attrib1b", looseKey: true); AddWithKey(newTree, "key2", "attrib2"); AddWithKey(newTree, "key1", "attrib1b"); - AddWithKey(newTree, "key1", "attrib1a"); + AddWithKey(newTree, "key1", "attrib1a", looseKey: true); // Act var (result, referenceFrames) = GetSingleUpdatedComponent(); @@ -2193,10 +2237,19 @@ namespace Microsoft.AspNetCore.Components.Test .Select(x => (T)x.Component) .ToList(); - private static void AddWithKey(RenderTreeBuilder builder, object key, string attributeValue = null) + private static void AddWithKey(RenderTreeBuilder builder, object key, string attributeValue = null, bool looseKey = false) { builder.OpenElement(0, "el"); - builder.SetKey(key); + + if (looseKey) + { + builder.SetKey(key, looseKey: true); + } + else + { + // Using this overload here (not explicitly passing any flag) to show that the default is strict + builder.SetKey(key); + } if (attributeValue != null) { diff --git a/src/Components/Web.JS/dist/Release/blazor.webassembly.js b/src/Components/Web.JS/dist/Release/blazor.webassembly.js index f1c1aada4b..8537398ba0 100644 --- a/src/Components/Web.JS/dist/Release/blazor.webassembly.js +++ b/src/Components/Web.JS/dist/Release/blazor.webassembly.js @@ -1 +1 @@ -!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=52)}([,,,,,,,,,,,,,,,,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(30),o=n(17),a={};function i(e,t,n){var o=a[e];o||(o=a[e]=new r.BrowserRenderer(e)),o.attachRootComponentToLogicalElement(n,t)}t.attachRootComponentToLogicalElement=i,t.attachRootComponentToElement=function(e,t,n){var r=document.querySelector(t);if(!r)throw new Error("Could not find any element matching selector '"+t+"'.");i(e,o.toLogicalElement(r,!0),n)},t.renderBatch=function(e,t){var n=a[e];if(!n)throw new Error("There is no browser renderer with ID "+e+".");for(var r=t.arrayRangeReader,o=t.updatedComponents(),i=r.values(o),u=r.count(o),l=t.referenceFrames(),s=r.values(l),c=t.diffReader,f=0;f0&&!t)throw new Error("New logical elements must start empty, or allowExistingContents must be true");return e[r]=[],e}function u(e,t,n){var a=e;if(e instanceof Comment&&(s(a)&&s(a).length>0))throw new Error("Not implemented: inserting non-empty logical container");if(l(a))throw new Error("Not implemented: moving existing logical children");var i=s(t);if(n0;)e(r,0);var a=r;a.parentNode.removeChild(a)},t.getLogicalParent=l,t.getLogicalSiblingEnd=function(e){return e[a]||null},t.getLogicalChild=function(e,t){return s(e)[t]},t.isSvgElement=function(e){return"http://www.w3.org/2000/svg"===c(e).namespaceURI},t.getLogicalChildrenArray=s,t.permuteLogicalChildren=function(e,t){var n=s(e);t.forEach(function(e){e.moveRangeStart=n[e.fromSiblingIndex],e.moveRangeEnd=function e(t){if(t instanceof Element)return t;var n=f(t);if(n)return n.previousSibling;var r=l(t);return r instanceof Element?r.lastChild:e(r)}(e.moveRangeStart)}),t.forEach(function(t){var r=t.moveToBeforeMarker=document.createComment("marker"),o=n[t.toSiblingIndex+1];o?o.parentNode.insertBefore(r,o):d(r,e)}),t.forEach(function(e){for(var t=e.moveToBeforeMarker,n=t.parentNode,r=e.moveRangeStart,o=e.moveRangeEnd,a=r;a;){var i=a.nextSibling;if(n.insertBefore(a,t),a===o)break;a=i}n.removeChild(t)}),t.forEach(function(e){n[e.toSiblingIndex]=e.moveRangeStart})},t.getClosestDomElement=c},,,function(e,t,n){"use strict";var r;!function(e){window.DotNet=e;var t=[],n={},r={},o=1,a=null;function i(e){t.push(e)}function u(e,t){for(var n=[],r=2;r0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]-1?a.substring(0,u):"",s=u>-1?a.substring(u+1):a,c=t.monoPlatform.findMethod(e,l,s,i);t.monoPlatform.callMethod(c,null,r)},callMethod:function(e,n,r){if(r.length>4)throw new Error("Currently, MonoPlatform supports passing a maximum of 4 arguments from JS to .NET. You tried to pass "+r.length+".");var o=Module.stackSave();try{for(var a=Module.stackAlloc(r.length),u=Module.stackAlloc(4),l=0;l0&&!t)throw new Error("New logical elements must start empty, or allowExistingContents must be true");return e[r]=[],e}function u(e,t,n){var a=e;if(e instanceof Comment&&(s(a)&&s(a).length>0))throw new Error("Not implemented: inserting non-empty logical container");if(l(a))throw new Error("Not implemented: moving existing logical children");var i=s(t);if(n0;)e(r,0);var a=r;a.parentNode.removeChild(a)},t.getLogicalParent=l,t.getLogicalSiblingEnd=function(e){return e[a]||null},t.getLogicalChild=function(e,t){return s(e)[t]},t.isSvgElement=function(e){return"http://www.w3.org/2000/svg"===c(e).namespaceURI},t.getLogicalChildrenArray=s,t.permuteLogicalChildren=function(e,t){var n=s(e);t.forEach(function(e){e.moveRangeStart=n[e.fromSiblingIndex],e.moveRangeEnd=function e(t){if(t instanceof Element)return t;var n=f(t);if(n)return n.previousSibling;var r=l(t);return r instanceof Element?r.lastChild:e(r)}(e.moveRangeStart)}),t.forEach(function(t){var r=t.moveToBeforeMarker=document.createComment("marker"),o=n[t.toSiblingIndex+1];o?o.parentNode.insertBefore(r,o):d(r,e)}),t.forEach(function(e){for(var t=e.moveToBeforeMarker,n=t.parentNode,r=e.moveRangeStart,o=e.moveRangeEnd,a=r;a;){var i=a.nextSibling;if(n.insertBefore(a,t),a===o)break;a=i}n.removeChild(t)}),t.forEach(function(e){n[e.toSiblingIndex]=e.moveRangeStart})},t.getClosestDomElement=c},,,function(e,t,n){"use strict";var r;!function(e){window.DotNet=e;var t=[],n={},r={},o=1,a=null;function i(e){t.push(e)}function u(e,t){for(var n=[],r=2;r0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]-1?a.substring(0,u):"",s=u>-1?a.substring(u+1):a,c=t.monoPlatform.findMethod(e,l,s,i);t.monoPlatform.callMethod(c,null,r)},callMethod:function(e,n,r){if(r.length>4)throw new Error("Currently, MonoPlatform supports passing a maximum of 4 arguments from JS to .NET. You tried to pass "+r.length+".");var o=Module.stackSave();try{for(var a=Module.stackAlloc(r.length),u=Module.stackAlloc(4),l=0;l(), completeItemStates); } - private void PerformTest(Node[] before, Node[] after) + private void PerformTest(Node[] before, Node[] after, bool useLooseKeys = false) { var rootBefore = new Node(null, "root", before); var rootAfter = new Node(null, "root", after); @@ -283,6 +302,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var textbox = appElem.FindElement(By.TagName("textarea")); var updateButton = appElem.FindElement(By.TagName("button")); + if (useLooseKeys) + { + appElem.FindElement(By.Id("key-loose")).Click(); + } + SetTextAreaValueFast(textbox, jsonBefore); updateButton.Click(); ValidateRenderedOutput(appElem, rootBefore, validatePreservation: false); diff --git a/src/Components/test/testassets/BasicTestApp/KeyCasesComponent.razor b/src/Components/test/testassets/BasicTestApp/KeyCasesComponent.razor index 582913ae27..6e2d887031 100644 --- a/src/Components/test/testassets/BasicTestApp/KeyCasesComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/KeyCasesComponent.razor @@ -4,6 +4,10 @@

Model

+

Output

@@ -69,5 +73,7 @@ // This is so the descendants can detect and display whether they are // newly-instantiated on any given render public int UpdateCount { get; set; } + + public bool UseLooseKeys { get; set; } } } diff --git a/src/Components/test/testassets/BasicTestApp/KeyCasesTreeNode.razor b/src/Components/test/testassets/BasicTestApp/KeyCasesTreeNode.razor index 340e1046cd..c44d32528a 100644 --- a/src/Components/test/testassets/BasicTestApp/KeyCasesTreeNode.razor +++ b/src/Components/test/testassets/BasicTestApp/KeyCasesTreeNode.razor @@ -1,3 +1,4 @@ +@using Microsoft.AspNetCore.Components.RenderTree
@Data.Label   [ @@ -16,19 +17,9 @@ @if (Data.Children?.Any() ?? false) { -
@{ - foreach (var child in Data.Children) - { - if (child.Key != null) - { - - } - else - { - - } - } - }
+
+ @((RenderFragment)RenderKeyCasesTreeNodes) +
}
@@ -50,4 +41,31 @@ { firstCreatedOnUpdateCount = RenderContext.UpdateCount; } + + void RenderKeyCasesTreeNodes(RenderTreeBuilder builder) + { + // This is equivalent to: + // @foreach (var child in Data.Children) + // { + // if (key != null) + // { + // + // } + // else + // { + // + // } + // } + // TODO: Once the compiler supports @key:loose, eliminate this and just use regular Razor syntax + foreach (var child in Data.Children) + { + builder.OpenComponent(0); + if (child.Key != null) + { + builder.SetKey(child.Key, looseKey: RenderContext.UseLooseKeys); + } + builder.AddAttribute(1, nameof(Data), child); + builder.CloseComponent(); + } + } }