Support "Region" frames in JS-side code
They only appear in a prepended subtree, because the .NET-side diffing code resolves them out if they are top-level to any given edit
This commit is contained in:
parent
848f24536a
commit
2da17602ed
|
|
@ -90,20 +90,22 @@ export class BrowserRenderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
insertFrame(componentId: number, parent: Element, childIndex: number, frames: System_Array<RenderTreeFramePointer>, frame: RenderTreeFramePointer, frameIndex: number) {
|
insertFrame(componentId: number, parent: Element, childIndex: number, frames: System_Array<RenderTreeFramePointer>, frame: RenderTreeFramePointer, frameIndex: number): number {
|
||||||
const frameType = renderTreeFrame.frameType(frame);
|
const frameType = renderTreeFrame.frameType(frame);
|
||||||
switch (frameType) {
|
switch (frameType) {
|
||||||
case FrameType.element:
|
case FrameType.element:
|
||||||
this.insertElement(componentId, parent, childIndex, frames, frame, frameIndex);
|
this.insertElement(componentId, parent, childIndex, frames, frame, frameIndex);
|
||||||
break;
|
return 1;
|
||||||
case FrameType.text:
|
case FrameType.text:
|
||||||
this.insertText(parent, childIndex, frame);
|
this.insertText(parent, childIndex, frame);
|
||||||
break;
|
return 1;
|
||||||
case FrameType.attribute:
|
case FrameType.attribute:
|
||||||
throw new Error('Attribute frames should only be present as leading children of element frames.');
|
throw new Error('Attribute frames should only be present as leading children of element frames.');
|
||||||
case FrameType.component:
|
case FrameType.component:
|
||||||
this.insertComponent(parent, childIndex, frame);
|
this.insertComponent(parent, childIndex, frame);
|
||||||
break;
|
return 1;
|
||||||
|
case FrameType.region:
|
||||||
|
return this.insertFrameRange(componentId, parent, childIndex, frames, frameIndex + 1, frameIndex + renderTreeFrame.subtreeLength(frame));
|
||||||
default:
|
default:
|
||||||
const unknownType: never = frameType; // Compile-time verification that the switch was exhaustive
|
const unknownType: never = frameType; // Compile-time verification that the switch was exhaustive
|
||||||
throw new Error(`Unknown frame type: ${unknownType}`);
|
throw new Error(`Unknown frame type: ${unknownType}`);
|
||||||
|
|
@ -199,11 +201,12 @@ export class BrowserRenderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
insertFrameRange(componentId: number, parent: Element, childIndex: number, frames: System_Array<RenderTreeFramePointer>, startIndex: number, endIndexExcl: number) {
|
insertFrameRange(componentId: number, parent: Element, childIndex: number, frames: System_Array<RenderTreeFramePointer>, startIndex: number, endIndexExcl: number): number {
|
||||||
|
const origChildIndex = childIndex;
|
||||||
for (let index = startIndex; index < endIndexExcl; index++) {
|
for (let index = startIndex; index < endIndexExcl; index++) {
|
||||||
const frame = getTreeFramePtr(frames, index);
|
const frame = getTreeFramePtr(frames, index);
|
||||||
this.insertFrame(componentId, parent, childIndex, frames, frame, index);
|
const numChildrenInserted = this.insertFrame(componentId, parent, childIndex, frames, frame, index);
|
||||||
childIndex++;
|
childIndex += numChildrenInserted;
|
||||||
|
|
||||||
// Skip over any descendants, since they are already dealt with recursively
|
// Skip over any descendants, since they are already dealt with recursively
|
||||||
const subtreeLength = renderTreeFrame.subtreeLength(frame);
|
const subtreeLength = renderTreeFrame.subtreeLength(frame);
|
||||||
|
|
@ -211,6 +214,8 @@ export class BrowserRenderer {
|
||||||
index += subtreeLength - 1;
|
index += subtreeLength - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (childIndex - origChildIndex); // Total number of children inserted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ export enum FrameType {
|
||||||
text = 2,
|
text = 2,
|
||||||
attribute = 3,
|
attribute = 3,
|
||||||
component = 4,
|
component = 4,
|
||||||
|
region = 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nominal type to ensure only valid pointers are passed to the renderTreeFrame functions.
|
// Nominal type to ensure only valid pointers are passed to the renderTreeFrame functions.
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
|
||||||
/// Marks a previously appended region frame as closed. Calls to this method
|
/// Marks a previously appended region frame as closed. Calls to this method
|
||||||
/// must be balanced with calls to <see cref="OpenRegion"/>.
|
/// must be balanced with calls to <see cref="OpenRegion"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal void CloseRegion()
|
public void CloseRegion()
|
||||||
{
|
{
|
||||||
var indexOfEntryBeingClosed = _openElementIndices.Pop();
|
var indexOfEntryBeingClosed = _openElementIndices.Pop();
|
||||||
ref var entry = ref _entries.Buffer[indexOfEntryBeingClosed];
|
ref var entry = ref _entries.Buffer[indexOfEntryBeingClosed];
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,30 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
||||||
Assert.Equal("I computed: 202", computedValueElement.Text);
|
Assert.Equal("I computed: 202", computedValueElement.Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanRenderRegionsWhilePreservingSurroundingElements()
|
||||||
|
{
|
||||||
|
// Initially, the region isn't shown
|
||||||
|
var appElement = MountTestComponent<RenderBlockComponent>();
|
||||||
|
var originalButton = appElement.FindElement(By.TagName("button"));
|
||||||
|
var regionElements = appElement.FindElements(By.CssSelector("p[name=region-element]"));
|
||||||
|
Assert.Empty(regionElements);
|
||||||
|
|
||||||
|
// The JS-side DOM builder handles regions correctly, placing elements
|
||||||
|
// after the region after the corresponding elements
|
||||||
|
Assert.Equal("The end", appElement.FindElements(By.CssSelector("div > *:last-child")).Single().Text);
|
||||||
|
|
||||||
|
// When we click the button, the region is shown
|
||||||
|
originalButton.Click();
|
||||||
|
regionElements = appElement.FindElements(By.CssSelector("p[name=region-element]"));
|
||||||
|
Assert.Single(regionElements);
|
||||||
|
|
||||||
|
// The button itself was preserved, so we can click it again and see the effect
|
||||||
|
originalButton.Click();
|
||||||
|
regionElements = appElement.FindElements(By.CssSelector("p[name=region-element]"));
|
||||||
|
Assert.Empty(regionElements);
|
||||||
|
}
|
||||||
|
|
||||||
private IWebElement MountTestComponent<TComponent>() where TComponent: IComponent
|
private IWebElement MountTestComponent<TComponent>() where TComponent: IComponent
|
||||||
{
|
{
|
||||||
var componentTypeName = typeof(TComponent).FullName;
|
var componentTypeName = typeof(TComponent).FullName;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
// 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;
|
||||||
|
using Microsoft.AspNetCore.Blazor.Components;
|
||||||
|
using Microsoft.AspNetCore.Blazor.RenderTree;
|
||||||
|
|
||||||
|
namespace BasicTestApp
|
||||||
|
{
|
||||||
|
public class RenderBlockComponent : IComponent, IHandleEvent
|
||||||
|
{
|
||||||
|
private RenderHandle _renderHandle;
|
||||||
|
private bool _showRegion;
|
||||||
|
|
||||||
|
// Important: Notice that the sequence numbers inside the region are higher
|
||||||
|
// that the sequence numbers outside it. Without the region delimiter, the
|
||||||
|
// differencer would think the following nodes had been removed, then the
|
||||||
|
// region was inserted, followed by a new copy of the following nodes. That's
|
||||||
|
// not as efficient and wouldn't preserve focus etc.
|
||||||
|
private Action<RenderTreeBuilder> _exampleRegion = builder =>
|
||||||
|
{
|
||||||
|
// TODO: Support some kind of RenderBlock primitive
|
||||||
|
// which is an Action<RenderTreeBuilder>. Can use the
|
||||||
|
// same type name for RenderHandle.Render's arg.
|
||||||
|
builder.OpenElement(100, "p");
|
||||||
|
builder.AddAttribute(101, "name", "region-element");
|
||||||
|
builder.AddAttribute(102, "style", "color: red");
|
||||||
|
builder.AddText(103, "This is from the region");
|
||||||
|
builder.CloseElement();
|
||||||
|
};
|
||||||
|
|
||||||
|
public void Init(RenderHandle renderHandle)
|
||||||
|
=> _renderHandle = renderHandle;
|
||||||
|
|
||||||
|
public void SetParameters(ParameterCollection parameters)
|
||||||
|
=> Render();
|
||||||
|
|
||||||
|
public void HandleEvent(UIEventHandler handler, UIEventArgs args)
|
||||||
|
{
|
||||||
|
// TODO: Remove the necessity to implement IHandleEvent if you just want
|
||||||
|
// the event handler to be called. Then call Render from inside the handler.
|
||||||
|
handler(args);
|
||||||
|
Render();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Render() => _renderHandle.Render(builder =>
|
||||||
|
{
|
||||||
|
builder.OpenElement(0, "div"); // Container so we can see that passing through regions is OK
|
||||||
|
builder.OpenRegion(1);
|
||||||
|
builder.AddText(2, "Region will be toggled below ");
|
||||||
|
|
||||||
|
if (_showRegion)
|
||||||
|
{
|
||||||
|
builder.OpenRegion(3);
|
||||||
|
_exampleRegion(builder);
|
||||||
|
builder.CloseRegion();
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.OpenElement(4, "button");
|
||||||
|
builder.AddAttribute(5, "onclick", ToggleRegion);
|
||||||
|
builder.AddText(6, "Toggle");
|
||||||
|
builder.CloseElement();
|
||||||
|
|
||||||
|
builder.CloseRegion();
|
||||||
|
builder.OpenElement(7, "p");
|
||||||
|
builder.AddText(8, "The end");
|
||||||
|
builder.CloseElement();
|
||||||
|
builder.CloseElement();
|
||||||
|
});
|
||||||
|
|
||||||
|
private void ToggleRegion(UIEventArgs eventArgs)
|
||||||
|
=> _showRegion = !_showRegion;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue