Add ability to render child components
This commit is contained in:
parent
04c582647a
commit
5793bf700a
|
|
@ -105,6 +105,11 @@ export const monoPlatform: Platform = {
|
|||
return address as any as Pointer;
|
||||
},
|
||||
|
||||
getHeapObjectFieldsPtr: function getHeapObjectFieldsPtr(heapObject: System_Object): Pointer {
|
||||
// The first two int32 values are internal Mono data
|
||||
return (heapObject as any as number + 8) as any as Pointer;
|
||||
},
|
||||
|
||||
readHeapInt32: function readHeapInt32(address: Pointer, offset?: number): number {
|
||||
return Module.getValue((address as any as number) + (offset || 0), 'i32');
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@
|
|||
getArrayLength(array: System_Array): number;
|
||||
getArrayEntryPtr(array: System_Array, index: number, itemSize: number): Pointer;
|
||||
|
||||
getHeapObjectFieldsPtr(heapObject: System_Object): Pointer;
|
||||
|
||||
readHeapInt32(address: Pointer, offset?: number): number;
|
||||
readHeapObject(address: Pointer, offset?: number): System_Object;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { System_Object, System_String, System_Array, MethodHandle } from '../Pla
|
|||
import { platform } from '../Environment';
|
||||
import { getTreeNodePtr, uiTreeNode, NodeType, UITreeNodePointer } from './UITreeNode';
|
||||
let raiseEventMethod: MethodHandle;
|
||||
let getComponentRenderInfoMethod: MethodHandle;
|
||||
|
||||
// TODO: Instead of associating components to parent elements, associate them with a
|
||||
// start/end node, so that components don't have to be enclosed in a wrapper
|
||||
|
|
@ -60,12 +61,39 @@ function insertNode(componentId: string, intoDomElement: Element, tree: System_A
|
|||
break;
|
||||
case NodeType.attribute:
|
||||
throw new Error('Attribute nodes should only be present as leading children of element nodes.');
|
||||
case NodeType.component:
|
||||
insertComponent(intoDomElement, componentId, nodeIndex);
|
||||
break;
|
||||
default:
|
||||
const unknownType: never = nodeType; // Compile-time verification that the switch was exhaustive
|
||||
throw new Error(`Unknown node type: ${ unknownType }`);
|
||||
}
|
||||
}
|
||||
|
||||
function insertComponent(intoDomElement: Element, parentComponentId: string, componentNodeIndex: number) {
|
||||
if (!getComponentRenderInfoMethod) {
|
||||
getComponentRenderInfoMethod = platform.findMethod(
|
||||
'Microsoft.Blazor.Browser', 'Microsoft.Blazor.Browser', 'DOMComponentRenderState', 'GetComponentRenderInfo'
|
||||
);
|
||||
}
|
||||
|
||||
// Currently, platform.callMethod always returns a heap object. If the target method
|
||||
// tries to return a value, it gets boxed before return.
|
||||
const renderInfoBoxed = platform.callMethod(getComponentRenderInfoMethod, null, [
|
||||
platform.toDotNetString(parentComponentId),
|
||||
platform.toDotNetString(componentNodeIndex.toString())
|
||||
]);
|
||||
const renderInfoFields = platform.getHeapObjectFieldsPtr(renderInfoBoxed);
|
||||
const componentId = platform.toJavaScriptString(platform.readHeapObject(renderInfoFields, 0) as System_String);
|
||||
const componentTree = platform.readHeapObject(renderInfoFields, 4) as System_Array;
|
||||
const componentTreeLength = platform.readHeapInt32(renderInfoFields, 8);
|
||||
|
||||
const containerElement = document.createElement('blazor-component');
|
||||
intoDomElement.appendChild(containerElement);
|
||||
componentIdToParentElement[componentId] = containerElement;
|
||||
insertNodeRange(componentId, containerElement, componentTree, 0, componentTreeLength - 1);
|
||||
}
|
||||
|
||||
function insertElement(componentId: string, intoDomElement: Element, tree: System_Array, elementNode: UITreeNodePointer, elementNodeIndex: number) {
|
||||
const tagName = uiTreeNode.elementName(elementNode);
|
||||
const newDomElement = document.createElement(tagName);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { System_String, System_Array, Pointer } from '../Platform/Platform';
|
||||
import { platform } from '../Environment';
|
||||
const uiTreeNodeStructLength = 28;
|
||||
const uiTreeNodeStructLength = 32;
|
||||
|
||||
// To minimise GC pressure, instead of instantiating a JS object to represent each tree node,
|
||||
// we work in terms of pointers to the structs on the .NET heap, and use static functions that
|
||||
|
|
@ -25,6 +25,7 @@ export enum NodeType {
|
|||
element = 1,
|
||||
text = 2,
|
||||
attribute = 3,
|
||||
component = 4,
|
||||
}
|
||||
|
||||
function _readInt32Property(baseAddress: Pointer, offsetBytes: number) {
|
||||
|
|
|
|||
|
|
@ -95,5 +95,37 @@ namespace Microsoft.Blazor.Browser
|
|||
tree.Array,
|
||||
tree.Count);
|
||||
}
|
||||
|
||||
public static ComponentRenderInfo GetComponentRenderInfo(string parentComponentId, string componentNodeIndexString)
|
||||
{
|
||||
var parentComponentRenderState = FindByDOMComponentID(parentComponentId);
|
||||
var parentComponentNodes = parentComponentRenderState._uITreeBuilder.GetNodes();
|
||||
var component = parentComponentNodes
|
||||
.Array[int.Parse(componentNodeIndexString)]
|
||||
.Component;
|
||||
if (component == null )
|
||||
{
|
||||
throw new ArgumentException($"The tree entry at position {componentNodeIndexString} does not refer to a component.");
|
||||
}
|
||||
|
||||
var componentRenderState = GetOrCreate(component);
|
||||
|
||||
// Don't necessarily need to re-render the child at this point. Review when the
|
||||
// component lifecycle has more details (e.g., async init method)
|
||||
var componentNodes = componentRenderState.UpdateRender();
|
||||
return new ComponentRenderInfo
|
||||
{
|
||||
ComponentId = componentRenderState.DOMComponentId,
|
||||
UITree = componentNodes.Array,
|
||||
UITreeLength = componentNodes.Count
|
||||
};
|
||||
}
|
||||
|
||||
public struct ComponentRenderInfo
|
||||
{
|
||||
public string ComponentId;
|
||||
public UITreeNode[] UITree;
|
||||
public int UITreeLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// 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 Microsoft.Blazor.Components;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
|
|
@ -71,11 +72,27 @@ namespace Microsoft.Blazor.UITree
|
|||
Append(UITreeNode.Attribute(name, value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a node representing a child component.
|
||||
/// </summary>
|
||||
/// <typeparam name="TComponent">The type of the child component.</typeparam>
|
||||
public void AddComponent<TComponent>() where TComponent: IComponent
|
||||
{
|
||||
// Later, instead of instantiating the child component here, we'll instead
|
||||
// store a descriptor of the component (type, parameters) on the attributes
|
||||
// of the appended nodes. Then after the tree is diffed against the
|
||||
// previous tree, we'll either instantiate a new component or reuse the
|
||||
// existing instance (and notify it about changes to parameters).
|
||||
var instance = Activator.CreateInstance<TComponent>();
|
||||
Append(UITreeNode.ChildComponent(instance));
|
||||
}
|
||||
|
||||
private void AssertCanAddAttribute()
|
||||
{
|
||||
if (_lastNonAttributeNodeType != UITreeNodeType.Element)
|
||||
if (_lastNonAttributeNodeType != UITreeNodeType.Element
|
||||
&& _lastNonAttributeNodeType != UITreeNodeType.Component)
|
||||
{
|
||||
throw new InvalidOperationException($"Attributes may only be added immediately after nodes of type {UITreeNodeType.Element}");
|
||||
throw new InvalidOperationException($"Attributes may only be added immediately after nodes of type {UITreeNodeType.Element} or {UITreeNodeType.Component}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// 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 Microsoft.Blazor.Components;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Blazor.UITree
|
||||
|
|
@ -56,6 +57,12 @@ namespace Microsoft.Blazor.UITree
|
|||
/// </summary>
|
||||
public UIEventHandler AttributeEventHandlerValue { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="NodeType"/> property equals <see cref="UITreeNodeType.Component"/>,
|
||||
/// gets the child component instance. Otherwise, the value is <see langword="null"/>.
|
||||
/// </summary>
|
||||
public IComponent Component { get; private set; }
|
||||
|
||||
internal static UITreeNode Element(string elementName) => new UITreeNode
|
||||
{
|
||||
NodeType = UITreeNodeType.Element,
|
||||
|
|
@ -82,6 +89,12 @@ namespace Microsoft.Blazor.UITree
|
|||
AttributeEventHandlerValue = value
|
||||
};
|
||||
|
||||
internal static UITreeNode ChildComponent(IComponent component) => new UITreeNode
|
||||
{
|
||||
NodeType = UITreeNodeType.Component,
|
||||
Component = component
|
||||
};
|
||||
|
||||
internal void CloseElement(int descendantsEndIndex)
|
||||
{
|
||||
ElementDescendantsEndIndex = descendantsEndIndex;
|
||||
|
|
|
|||
|
|
@ -22,5 +22,10 @@ namespace Microsoft.Blazor.UITree
|
|||
/// Represents a key-value pair associated with another <see cref="UITreeNode"/>.
|
||||
/// </summary>
|
||||
Attribute = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Represents a child component.
|
||||
/// </summary>
|
||||
Component = 4,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,19 @@ namespace Microsoft.Blazor.E2ETest.Tests
|
|||
li => Assert.Equal("b", li.Text));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRenderChildComponents()
|
||||
{
|
||||
var appElement = MountTestComponent<ParentChildComponent>();
|
||||
Assert.Equal("Parent component",
|
||||
appElement.FindElement(By.CssSelector("fieldset > legend")).Text);
|
||||
|
||||
// TODO: Once we remove the wrapper elements from around child components,
|
||||
// assert that the child component text node is directly inside the <fieldset>
|
||||
Assert.Equal("Child component",
|
||||
appElement.FindElement(By.CssSelector("fieldset > blazor-component")).Text);
|
||||
}
|
||||
|
||||
private IWebElement MountTestComponent<TComponent>() where TComponent: IComponent
|
||||
{
|
||||
var componentTypeName = typeof(TComponent).FullName;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// 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 Microsoft.Blazor.Components;
|
||||
using Microsoft.Blazor.UITree;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
|
@ -214,6 +215,31 @@ namespace Microsoft.Blazor.Test
|
|||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanAddChildComponents()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new UITreeBuilder();
|
||||
|
||||
// Act
|
||||
builder.OpenElement("parent"); // 0: <parent>
|
||||
builder.AddComponent<TestComponent>(); // 1: <testcomponent
|
||||
builder.AddAttribute("child1attribute1", "A"); // 2: child1attribute1="A"
|
||||
builder.AddAttribute("child1attribute2", "B"); // 3: child1attribute2="B" />
|
||||
builder.AddComponent<TestComponent>(); // 4: <testcomponent
|
||||
builder.AddAttribute("child2attribute", "C"); // 5: child2attribute="C" />
|
||||
builder.CloseElement(); // </parent>
|
||||
|
||||
// Assert
|
||||
Assert.Collection(builder.GetNodes(),
|
||||
node => AssertElement(node, "parent", 5),
|
||||
node => AssertComponent<TestComponent>(node),
|
||||
node => AssertAttribute(node, "child1attribute1", "A"),
|
||||
node => AssertAttribute(node, "child1attribute2", "B"),
|
||||
node => AssertComponent<TestComponent>(node),
|
||||
node => AssertAttribute(node, "child2attribute", "C"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanClear()
|
||||
{
|
||||
|
|
@ -262,5 +288,23 @@ namespace Microsoft.Blazor.Test
|
|||
AssertAttribute(node, attributeName);
|
||||
Assert.Equal(attributeEventHandlerValue, node.AttributeEventHandlerValue);
|
||||
}
|
||||
|
||||
private void AssertComponent<T>(UITreeNode node) where T: IComponent
|
||||
{
|
||||
Assert.Equal(UITreeNodeType.Component, node.NodeType);
|
||||
|
||||
// Currently, we instantiate child components during the tree building phase.
|
||||
// Later this will change so it happens during the tree diffing phase, so this
|
||||
// logic will need to change. It will need to verify that we're tracking the
|
||||
// information needed to instantiate the component.
|
||||
Assert.NotNull(node.Component);
|
||||
Assert.IsType<T>(node.Component);
|
||||
}
|
||||
|
||||
private class TestComponent : IComponent
|
||||
{
|
||||
public void BuildUITree(UITreeBuilder builder)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
// 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 Microsoft.Blazor.Components;
|
||||
using Microsoft.Blazor.UITree;
|
||||
|
||||
namespace BasicTestApp
|
||||
{
|
||||
public class ParentChildComponent : IComponent
|
||||
{
|
||||
public void BuildUITree(UITreeBuilder builder)
|
||||
{
|
||||
builder.OpenElement("fieldset");
|
||||
builder.OpenElement("legend");
|
||||
builder.AddText("Parent component");
|
||||
builder.CloseElement();
|
||||
builder.AddComponent<ChildComponent>();
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
private class ChildComponent : IComponent
|
||||
{
|
||||
public void BuildUITree(UITreeBuilder builder)
|
||||
{
|
||||
builder.AddText("Child component");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue