diff --git a/src/Microsoft.Blazor.Browser.JS/src/Platform/Mono/MonoPlatform.ts b/src/Microsoft.Blazor.Browser.JS/src/Platform/Mono/MonoPlatform.ts
index 8624ef60ba..428d88a59c 100644
--- a/src/Microsoft.Blazor.Browser.JS/src/Platform/Mono/MonoPlatform.ts
+++ b/src/Microsoft.Blazor.Browser.JS/src/Platform/Mono/MonoPlatform.ts
@@ -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');
},
diff --git a/src/Microsoft.Blazor.Browser.JS/src/Platform/Platform.ts b/src/Microsoft.Blazor.Browser.JS/src/Platform/Platform.ts
index 6d03f66b28..958f023bc5 100644
--- a/src/Microsoft.Blazor.Browser.JS/src/Platform/Platform.ts
+++ b/src/Microsoft.Blazor.Browser.JS/src/Platform/Platform.ts
@@ -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;
}
diff --git a/src/Microsoft.Blazor.Browser.JS/src/Rendering/Renderer.ts b/src/Microsoft.Blazor.Browser.JS/src/Rendering/Renderer.ts
index afb96e8ab7..d1fdccac40 100644
--- a/src/Microsoft.Blazor.Browser.JS/src/Rendering/Renderer.ts
+++ b/src/Microsoft.Blazor.Browser.JS/src/Rendering/Renderer.ts
@@ -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);
diff --git a/src/Microsoft.Blazor.Browser.JS/src/Rendering/UITreeNode.ts b/src/Microsoft.Blazor.Browser.JS/src/Rendering/UITreeNode.ts
index 25b3fb9849..7860fd6002 100644
--- a/src/Microsoft.Blazor.Browser.JS/src/Rendering/UITreeNode.ts
+++ b/src/Microsoft.Blazor.Browser.JS/src/Rendering/UITreeNode.ts
@@ -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) {
diff --git a/src/Microsoft.Blazor.Browser/DOMComponentRenderState.cs b/src/Microsoft.Blazor.Browser/DOMComponentRenderState.cs
index 67720c73c6..94d489dda8 100644
--- a/src/Microsoft.Blazor.Browser/DOMComponentRenderState.cs
+++ b/src/Microsoft.Blazor.Browser/DOMComponentRenderState.cs
@@ -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;
+ }
}
}
diff --git a/src/Microsoft.Blazor/UITree/UITreeBuilder.cs b/src/Microsoft.Blazor/UITree/UITreeBuilder.cs
index 32f2fdae4d..16d8a05ae1 100644
--- a/src/Microsoft.Blazor/UITree/UITreeBuilder.cs
+++ b/src/Microsoft.Blazor/UITree/UITreeBuilder.cs
@@ -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));
}
+ ///
+ /// Appends a node representing a child component.
+ ///
+ /// The type of the child component.
+ public void AddComponent() 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();
+ 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}");
}
}
diff --git a/src/Microsoft.Blazor/UITree/UITreeNode.cs b/src/Microsoft.Blazor/UITree/UITreeNode.cs
index 54ac7edfcc..e44932f2a4 100644
--- a/src/Microsoft.Blazor/UITree/UITreeNode.cs
+++ b/src/Microsoft.Blazor/UITree/UITreeNode.cs
@@ -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
///
public UIEventHandler AttributeEventHandlerValue { get; private set; }
+ ///
+ /// If the property equals ,
+ /// gets the child component instance. Otherwise, the value is .
+ ///
+ 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;
diff --git a/src/Microsoft.Blazor/UITree/UITreeNodeType.cs b/src/Microsoft.Blazor/UITree/UITreeNodeType.cs
index 2ab72502ab..9998ae3273 100644
--- a/src/Microsoft.Blazor/UITree/UITreeNodeType.cs
+++ b/src/Microsoft.Blazor/UITree/UITreeNodeType.cs
@@ -22,5 +22,10 @@ namespace Microsoft.Blazor.UITree
/// Represents a key-value pair associated with another .
///
Attribute = 3,
+
+ ///
+ /// Represents a child component.
+ ///
+ Component = 4,
}
}
diff --git a/test/Microsoft.Blazor.E2ETest/Tests/ComponentRenderingTest.cs b/test/Microsoft.Blazor.E2ETest/Tests/ComponentRenderingTest.cs
index 597da422c7..45dae498ac 100644
--- a/test/Microsoft.Blazor.E2ETest/Tests/ComponentRenderingTest.cs
+++ b/test/Microsoft.Blazor.E2ETest/Tests/ComponentRenderingTest.cs
@@ -77,6 +77,19 @@ namespace Microsoft.Blazor.E2ETest.Tests
li => Assert.Equal("b", li.Text));
}
+ [Fact]
+ public void CanRenderChildComponents()
+ {
+ var appElement = MountTestComponent();
+ 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