Add ability to render child components

This commit is contained in:
Steve Sanderson 2018-01-08 14:21:48 +00:00
parent 04c582647a
commit 5793bf700a
11 changed files with 192 additions and 3 deletions

View File

@ -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');
},

View File

@ -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;
}

View File

@ -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);

View File

@ -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) {

View File

@ -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;
}
}
}

View File

@ -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}");
}
}

View File

@ -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;

View File

@ -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,
}
}

View File

@ -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;

View File

@ -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();
}
}
}

View File

@ -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");
}
}
}
}