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;
|
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 {
|
readHeapInt32: function readHeapInt32(address: Pointer, offset?: number): number {
|
||||||
return Module.getValue((address as any as number) + (offset || 0), 'i32');
|
return Module.getValue((address as any as number) + (offset || 0), 'i32');
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@
|
||||||
getArrayLength(array: System_Array): number;
|
getArrayLength(array: System_Array): number;
|
||||||
getArrayEntryPtr(array: System_Array, index: number, itemSize: number): Pointer;
|
getArrayEntryPtr(array: System_Array, index: number, itemSize: number): Pointer;
|
||||||
|
|
||||||
|
getHeapObjectFieldsPtr(heapObject: System_Object): Pointer;
|
||||||
|
|
||||||
readHeapInt32(address: Pointer, offset?: number): number;
|
readHeapInt32(address: Pointer, offset?: number): number;
|
||||||
readHeapObject(address: Pointer, offset?: number): System_Object;
|
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 { platform } from '../Environment';
|
||||||
import { getTreeNodePtr, uiTreeNode, NodeType, UITreeNodePointer } from './UITreeNode';
|
import { getTreeNodePtr, uiTreeNode, NodeType, UITreeNodePointer } from './UITreeNode';
|
||||||
let raiseEventMethod: MethodHandle;
|
let raiseEventMethod: MethodHandle;
|
||||||
|
let getComponentRenderInfoMethod: MethodHandle;
|
||||||
|
|
||||||
// TODO: Instead of associating components to parent elements, associate them with a
|
// 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
|
// 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;
|
break;
|
||||||
case NodeType.attribute:
|
case NodeType.attribute:
|
||||||
throw new Error('Attribute nodes should only be present as leading children of element nodes.');
|
throw new Error('Attribute nodes should only be present as leading children of element nodes.');
|
||||||
|
case NodeType.component:
|
||||||
|
insertComponent(intoDomElement, componentId, nodeIndex);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
const unknownType: never = nodeType; // Compile-time verification that the switch was exhaustive
|
const unknownType: never = nodeType; // Compile-time verification that the switch was exhaustive
|
||||||
throw new Error(`Unknown node type: ${ unknownType }`);
|
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) {
|
function insertElement(componentId: string, intoDomElement: Element, tree: System_Array, elementNode: UITreeNodePointer, elementNodeIndex: number) {
|
||||||
const tagName = uiTreeNode.elementName(elementNode);
|
const tagName = uiTreeNode.elementName(elementNode);
|
||||||
const newDomElement = document.createElement(tagName);
|
const newDomElement = document.createElement(tagName);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { System_String, System_Array, Pointer } from '../Platform/Platform';
|
import { System_String, System_Array, Pointer } from '../Platform/Platform';
|
||||||
import { platform } from '../Environment';
|
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,
|
// 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
|
// 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,
|
element = 1,
|
||||||
text = 2,
|
text = 2,
|
||||||
attribute = 3,
|
attribute = 3,
|
||||||
|
component = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
function _readInt32Property(baseAddress: Pointer, offsetBytes: number) {
|
function _readInt32Property(baseAddress: Pointer, offsetBytes: number) {
|
||||||
|
|
|
||||||
|
|
@ -95,5 +95,37 @@ namespace Microsoft.Blazor.Browser
|
||||||
tree.Array,
|
tree.Array,
|
||||||
tree.Count);
|
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.
|
// 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.
|
// 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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
|
@ -71,11 +72,27 @@ namespace Microsoft.Blazor.UITree
|
||||||
Append(UITreeNode.Attribute(name, value));
|
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()
|
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.
|
// 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.
|
// 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;
|
||||||
|
|
||||||
namespace Microsoft.Blazor.UITree
|
namespace Microsoft.Blazor.UITree
|
||||||
|
|
@ -56,6 +57,12 @@ namespace Microsoft.Blazor.UITree
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public UIEventHandler AttributeEventHandlerValue { get; private set; }
|
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
|
internal static UITreeNode Element(string elementName) => new UITreeNode
|
||||||
{
|
{
|
||||||
NodeType = UITreeNodeType.Element,
|
NodeType = UITreeNodeType.Element,
|
||||||
|
|
@ -82,6 +89,12 @@ namespace Microsoft.Blazor.UITree
|
||||||
AttributeEventHandlerValue = value
|
AttributeEventHandlerValue = value
|
||||||
};
|
};
|
||||||
|
|
||||||
|
internal static UITreeNode ChildComponent(IComponent component) => new UITreeNode
|
||||||
|
{
|
||||||
|
NodeType = UITreeNodeType.Component,
|
||||||
|
Component = component
|
||||||
|
};
|
||||||
|
|
||||||
internal void CloseElement(int descendantsEndIndex)
|
internal void CloseElement(int descendantsEndIndex)
|
||||||
{
|
{
|
||||||
ElementDescendantsEndIndex = descendantsEndIndex;
|
ElementDescendantsEndIndex = descendantsEndIndex;
|
||||||
|
|
|
||||||
|
|
@ -22,5 +22,10 @@ namespace Microsoft.Blazor.UITree
|
||||||
/// Represents a key-value pair associated with another <see cref="UITreeNode"/>.
|
/// Represents a key-value pair associated with another <see cref="UITreeNode"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Attribute = 3,
|
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));
|
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
|
private IWebElement MountTestComponent<TComponent>() where TComponent: IComponent
|
||||||
{
|
{
|
||||||
var componentTypeName = typeof(TComponent).FullName;
|
var componentTypeName = typeof(TComponent).FullName;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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 Microsoft.Blazor.UITree;
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
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]
|
[Fact]
|
||||||
public void CanClear()
|
public void CanClear()
|
||||||
{
|
{
|
||||||
|
|
@ -262,5 +288,23 @@ namespace Microsoft.Blazor.Test
|
||||||
AssertAttribute(node, attributeName);
|
AssertAttribute(node, attributeName);
|
||||||
Assert.Equal(attributeEventHandlerValue, node.AttributeEventHandlerValue);
|
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