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
+ Assert.Equal("Child component", + appElement.FindElement(By.CssSelector("fieldset > blazor-component")).Text); + } + private IWebElement MountTestComponent() where TComponent: IComponent { var componentTypeName = typeof(TComponent).FullName; diff --git a/test/Microsoft.Blazor.Test/UITreeBuilderTest.cs b/test/Microsoft.Blazor.Test/UITreeBuilderTest.cs index 92e5833abd..50158db59b 100644 --- a/test/Microsoft.Blazor.Test/UITreeBuilderTest.cs +++ b/test/Microsoft.Blazor.Test/UITreeBuilderTest.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 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: + builder.AddComponent(); // 1: + builder.AddComponent(); // 4: + builder.CloseElement(); // + + // Assert + Assert.Collection(builder.GetNodes(), + node => AssertElement(node, "parent", 5), + node => AssertComponent(node), + node => AssertAttribute(node, "child1attribute1", "A"), + node => AssertAttribute(node, "child1attribute2", "B"), + node => AssertComponent(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(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(node.Component); + } + + private class TestComponent : IComponent + { + public void BuildUITree(UITreeBuilder builder) + => throw new NotImplementedException(); + } } } diff --git a/test/testapps/BasicTestApp/ParentChildComponent.cs b/test/testapps/BasicTestApp/ParentChildComponent.cs new file mode 100644 index 0000000000..6c42802f1e --- /dev/null +++ b/test/testapps/BasicTestApp/ParentChildComponent.cs @@ -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(); + builder.CloseElement(); + } + + private class ChildComponent : IComponent + { + public void BuildUITree(UITreeBuilder builder) + { + builder.AddText("Child component"); + } + } + } +}