Basic parameterless events support (e.g., button click)
This commit is contained in:
parent
f7cb54121b
commit
23f7120b75
|
|
@ -1,7 +1,8 @@
|
|||
import { registerFunction } from '../RegisteredFunction';
|
||||
import { System_String, System_Array } from '../Platform/Platform';
|
||||
import { System_Object, System_String, System_Array, MethodHandle } from '../Platform/Platform';
|
||||
import { platform } from '../Environment';
|
||||
import { getTreeNodePtr, uiTreeNode, NodeType, UITreeNodePointer } from './UITreeNode';
|
||||
let raiseEventMethod: 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
|
||||
|
|
@ -20,8 +21,6 @@ function attachComponentToElement(elementSelector: System_String, componentId: S
|
|||
throw new Error(`Could not find any element matching selector '${elementSelectorJs}'.`);
|
||||
}
|
||||
|
||||
clearElement(element);
|
||||
|
||||
const componentIdJs = platform.toJavaScriptString(componentId);
|
||||
componentIdToParentElement[componentIdJs] = element;
|
||||
}
|
||||
|
|
@ -33,13 +32,14 @@ function renderUITree(componentId: System_String, tree: System_Array, treeLength
|
|||
throw new Error(`No element is currently associated with component ${componentIdJs}`);
|
||||
}
|
||||
|
||||
insertNodeRange(element, tree, 0, treeLength - 1);
|
||||
clearElement(element);
|
||||
insertNodeRange(componentIdJs, element, tree, 0, treeLength - 1);
|
||||
}
|
||||
|
||||
function insertNodeRange(intoDomElement: Element, tree: System_Array, startIndex: number, endIndex: number) {
|
||||
function insertNodeRange(componentId: string, intoDomElement: Element, tree: System_Array, startIndex: number, endIndex: number) {
|
||||
for (let index = startIndex; index <= endIndex; index++) {
|
||||
const node = getTreeNodePtr(tree, index);
|
||||
insertNode(intoDomElement, tree, node, index);
|
||||
insertNode(componentId, intoDomElement, tree, node, index);
|
||||
|
||||
// Skip over any descendants, since they are already dealt with recursively
|
||||
const descendantsEndIndex = uiTreeNode.descendantsEndIndex(node);
|
||||
|
|
@ -49,11 +49,11 @@ function insertNodeRange(intoDomElement: Element, tree: System_Array, startIndex
|
|||
}
|
||||
}
|
||||
|
||||
function insertNode(intoDomElement: Element, tree: System_Array, node: UITreeNodePointer, nodeIndex: number) {
|
||||
function insertNode(componentId: string, intoDomElement: Element, tree: System_Array, node: UITreeNodePointer, nodeIndex: number) {
|
||||
const nodeType = uiTreeNode.nodeType(node);
|
||||
switch (nodeType) {
|
||||
case NodeType.element:
|
||||
insertElement(intoDomElement, tree, node, nodeIndex);
|
||||
insertElement(componentId, intoDomElement, tree, node, nodeIndex);
|
||||
break;
|
||||
case NodeType.text:
|
||||
insertText(intoDomElement, node);
|
||||
|
|
@ -66,7 +66,7 @@ function insertNode(intoDomElement: Element, tree: System_Array, node: UITreeNod
|
|||
}
|
||||
}
|
||||
|
||||
function insertElement(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 newDomElement = document.createElement(tagName);
|
||||
intoDomElement.appendChild(newDomElement);
|
||||
|
|
@ -76,21 +76,46 @@ function insertElement(intoDomElement: Element, tree: System_Array, elementNode:
|
|||
for (let descendantIndex = elementNodeIndex + 1; descendantIndex <= descendantsEndIndex; descendantIndex++) {
|
||||
const descendantNode = getTreeNodePtr(tree, descendantIndex);
|
||||
if (uiTreeNode.nodeType(descendantNode) === NodeType.attribute) {
|
||||
applyAttribute(newDomElement, descendantNode);
|
||||
applyAttribute(componentId, newDomElement, descendantNode, descendantIndex);
|
||||
} else {
|
||||
// As soon as we see a non-attribute child, all the subsequent child nodes are
|
||||
// not attributes, so bail out and insert the remnants recursively
|
||||
insertNodeRange(newDomElement, tree, descendantIndex, descendantsEndIndex);
|
||||
insertNodeRange(componentId, newDomElement, tree, descendantIndex, descendantsEndIndex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyAttribute(toDomElement: Element, attributeNode: UITreeNodePointer) {
|
||||
toDomElement.setAttribute(
|
||||
uiTreeNode.attributeName(attributeNode),
|
||||
uiTreeNode.attributeValue(attributeNode)
|
||||
);
|
||||
function applyAttribute(componentId: string, toDomElement: Element, attributeNode: UITreeNodePointer, attributeNodeIndex: number) {
|
||||
const attributeName = uiTreeNode.attributeName(attributeNode);
|
||||
|
||||
switch (attributeName) {
|
||||
case 'onclick':
|
||||
toDomElement.addEventListener('click', () => raiseEvent(componentId, attributeNodeIndex, 'click'));
|
||||
break;
|
||||
default:
|
||||
// Treat as a regular string-valued attribute
|
||||
toDomElement.setAttribute(
|
||||
attributeName,
|
||||
uiTreeNode.attributeValue(attributeNode)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function raiseEvent(componentId: string, uiTreeNodeIndex: number, eventName: string) {
|
||||
if (!raiseEventMethod) {
|
||||
raiseEventMethod = platform.findMethod(
|
||||
'Microsoft.Blazor.Browser', 'Microsoft.Blazor.Browser', 'Events', 'RaiseEvent'
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Find a way of passing the uiTreeNodeIndex as a System.Int32, possibly boxing
|
||||
// it first if necessary. Until then we have to send it as a string.
|
||||
platform.callMethod(raiseEventMethod, null, [
|
||||
platform.toDotNetString(componentId),
|
||||
platform.toDotNetString(uiTreeNodeIndex.toString())
|
||||
]);
|
||||
}
|
||||
|
||||
function insertText(intoDomElement: Element, textNode: UITreeNodePointer) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { System_String, System_Array, Pointer } from '../Platform/Platform';
|
||||
import { platform } from '../Environment';
|
||||
const uiTreeNodeStructLength = 24;
|
||||
const uiTreeNodeStructLength = 28;
|
||||
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
using Microsoft.Blazor.Browser.Interop;
|
||||
using Microsoft.Blazor.Components;
|
||||
using Microsoft.Blazor.UITree;
|
||||
|
||||
namespace Microsoft.Blazor.Browser
|
||||
{
|
||||
|
|
@ -26,17 +25,7 @@ namespace Microsoft.Blazor.Browser
|
|||
RegisteredFunction.InvokeUnmarshalled<string, string, object>(
|
||||
"_blazorAttachComponentToElement", elementSelector, renderState.DOMComponentId);
|
||||
|
||||
RefreshComponentInDOM(renderState);
|
||||
}
|
||||
|
||||
private static void RefreshComponentInDOM(DOMComponentRenderState renderState)
|
||||
{
|
||||
var tree = renderState.UpdateRender();
|
||||
RegisteredFunction.InvokeUnmarshalled<string, UITreeNode[], int, object>(
|
||||
"_blazorRender",
|
||||
renderState.DOMComponentId,
|
||||
tree.Array,
|
||||
tree.Count);
|
||||
renderState.RenderToDOM();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.Browser.Interop;
|
||||
using Microsoft.Blazor.Components;
|
||||
using Microsoft.Blazor.UITree;
|
||||
using System;
|
||||
|
|
@ -14,8 +15,16 @@ namespace Microsoft.Blazor.Browser
|
|||
/// </summary>
|
||||
internal class DOMComponentRenderState
|
||||
{
|
||||
private static ConditionalWeakTable<IComponent, DOMComponentRenderState> _stateInstances
|
||||
// Track the associations between component IDs, IComponent instances, and
|
||||
// DOMComponentRenderState instances, but without pinning any IComponent instances
|
||||
// in memory.
|
||||
// TODO: Instead of storing these as statics, have some kind of RenderContext instance
|
||||
// that holds them. It can also hold a reference to the root component, since otherwise
|
||||
// there isn't anything stopping the whole hierarchy of components from being GCed.
|
||||
private static ConditionalWeakTable<IComponent, DOMComponentRenderState> _renderStatesByComponent
|
||||
= new ConditionalWeakTable<IComponent, DOMComponentRenderState>();
|
||||
private static WeakValueDictionary<string, DOMComponentRenderState> _renderStatesByComponentId
|
||||
= new WeakValueDictionary<string, DOMComponentRenderState>();
|
||||
private static long _nextDOMComponentId = 0;
|
||||
|
||||
private readonly UITreeBuilder _uITreeBuilder; // TODO: Maintain two, so we can diff successive renders
|
||||
|
|
@ -26,16 +35,16 @@ namespace Microsoft.Blazor.Browser
|
|||
|
||||
private DOMComponentRenderState(string componentId, IComponent component)
|
||||
{
|
||||
DOMComponentId = DOMComponentId;
|
||||
DOMComponentId = componentId;
|
||||
Component = component;
|
||||
_uITreeBuilder = new UITreeBuilder();
|
||||
}
|
||||
|
||||
public static DOMComponentRenderState GetOrCreate(IComponent component)
|
||||
{
|
||||
lock (_stateInstances)
|
||||
lock (_renderStatesByComponent)
|
||||
{
|
||||
if (_stateInstances.TryGetValue(component, out var existingState))
|
||||
if (_renderStatesByComponent.TryGetValue(component, out var existingState))
|
||||
{
|
||||
return existingState;
|
||||
}
|
||||
|
|
@ -43,13 +52,19 @@ namespace Microsoft.Blazor.Browser
|
|||
{
|
||||
var newId = (_nextDOMComponentId++).ToString();
|
||||
var newState = new DOMComponentRenderState(newId, component);
|
||||
_stateInstances.Add(component, newState);
|
||||
_renderStatesByComponent.Add(component, newState);
|
||||
_renderStatesByComponentId.Add(newId, newState);
|
||||
return newState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ArraySegment<UITreeNode> UpdateRender()
|
||||
public static DOMComponentRenderState FindByDOMComponentID(string id)
|
||||
=> _renderStatesByComponentId.TryGetValue(id, out var result)
|
||||
? result
|
||||
: throw new ArgumentException($"No component was found with ID {id}");
|
||||
|
||||
private ArraySegment<UITreeNode> UpdateRender()
|
||||
{
|
||||
_uITreeBuilder.Clear();
|
||||
Component.BuildUITree(_uITreeBuilder);
|
||||
|
|
@ -57,5 +72,28 @@ namespace Microsoft.Blazor.Browser
|
|||
// TODO: Change this to return a diff between the previous render result and this new one
|
||||
return _uITreeBuilder.GetNodes();
|
||||
}
|
||||
|
||||
public void RaiseEvent(int uiTreeNodeIndex)
|
||||
{
|
||||
var nodes = _uITreeBuilder.GetNodes();
|
||||
var eventHandler = nodes.Array[nodes.Offset + uiTreeNodeIndex].AttributeEventHandlerValue;
|
||||
if (eventHandler == null)
|
||||
{
|
||||
throw new ArgumentException($"Cannot raise event because the specified {nameof(UITreeNode)} at index {uiTreeNodeIndex} does not have any {nameof(UITreeNode.AttributeEventHandlerValue)}.");
|
||||
}
|
||||
|
||||
eventHandler.Invoke();
|
||||
RenderToDOM();
|
||||
}
|
||||
|
||||
public void RenderToDOM()
|
||||
{
|
||||
var tree = UpdateRender();
|
||||
RegisteredFunction.InvokeUnmarshalled<string, UITreeNode[], int, object>(
|
||||
"_blazorRender",
|
||||
DOMComponentId,
|
||||
tree.Array,
|
||||
tree.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.Blazor.Browser
|
||||
{
|
||||
// Invoked by the Microsoft.Blazor.Browser.JS code when a DOM event occurs
|
||||
internal static class Events
|
||||
{
|
||||
public static void RaiseEvent(string domComponentID, string uiTreeNodeIndex)
|
||||
{
|
||||
// We're receiving the uiTreeNodeIndex as a string only because there's not
|
||||
// yet a way to pass ints (or construct boxed ones) from JS with the current Mono
|
||||
// runtime. When there's a supported way to do that, this can be simplified.
|
||||
var renderState = DOMComponentRenderState.FindByDOMComponentID(domComponentID);
|
||||
renderState.RaiseEvent(int.Parse(uiTreeNodeIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
// 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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.Blazor.Browser
|
||||
{
|
||||
internal class WeakValueDictionary<TKey, TValue> where TValue : class
|
||||
{
|
||||
private IDictionary<TKey, WeakReference<TValue>> _store
|
||||
= new Dictionary<TKey, WeakReference<TValue>>();
|
||||
private int _cullThreshold = 10;
|
||||
|
||||
public bool TryGetValue(TKey key, out TValue value)
|
||||
{
|
||||
if (_store.TryGetValue(key, out var existingWeakRef))
|
||||
{
|
||||
if (existingWeakRef.TryGetTarget(out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Since we know it's not there, we might as well drop the entry now
|
||||
_store.Remove(key);
|
||||
}
|
||||
|
||||
value = default(TValue);
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Add(TKey key, TValue value)
|
||||
{
|
||||
if (_store.TryGetValue(key, out _))
|
||||
{
|
||||
throw new ArgumentException($"The given key was already present in the {nameof(WeakValueDictionary<TKey, TValue>)}. Key: {key}");
|
||||
}
|
||||
|
||||
_store[key] = new WeakReference<TValue>(value);
|
||||
CullIfApplicable();
|
||||
}
|
||||
|
||||
private void CullIfApplicable()
|
||||
{
|
||||
if (_store.Count > _cullThreshold)
|
||||
{
|
||||
var itemsToRemove = _store.Where(x => !x.Value.TryGetTarget(out _)).ToList();
|
||||
foreach (var itemToRemove in itemsToRemove)
|
||||
{
|
||||
_store.Remove(itemToRemove.Key);
|
||||
}
|
||||
|
||||
if (_store.Count > (_cullThreshold / 2))
|
||||
{
|
||||
_cullThreshold *= 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.Blazor.UITree
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles an event raised for a <see cref="UITreeNode"/>.
|
||||
/// </summary>
|
||||
public delegate void UIEventHandler();
|
||||
}
|
||||
|
|
@ -48,18 +48,32 @@ namespace Microsoft.Blazor.UITree
|
|||
=> Append(UITreeNode.Text(textContent));
|
||||
|
||||
/// <summary>
|
||||
/// Appends a node representing an attribute. The attribute is associated
|
||||
/// with the most recently added element.
|
||||
/// Appends a node representing a string-valued attribute.
|
||||
/// The attribute is associated with the most recently added element.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the attribute.</param>
|
||||
/// <param name="value">The value of the attribute.</param>
|
||||
public void AddAttribute(string name, string value)
|
||||
{
|
||||
if (_lastNonAttributeNodeType == UITreeNodeType.Element)
|
||||
{
|
||||
Append(UITreeNode.Attribute(name, value));
|
||||
}
|
||||
else
|
||||
AssertCanAddAttribute();
|
||||
Append(UITreeNode.Attribute(name, value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a node representing an <see cref="UIEventHandler"/>-valued attribute.
|
||||
/// The attribute is associated with the most recently added element.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the attribute.</param>
|
||||
/// <param name="value">The value of the attribute.</param>
|
||||
public void AddAttribute(string name, UIEventHandler value)
|
||||
{
|
||||
AssertCanAddAttribute();
|
||||
Append(UITreeNode.Attribute(name, value));
|
||||
}
|
||||
|
||||
private void AssertCanAddAttribute()
|
||||
{
|
||||
if (_lastNonAttributeNodeType != UITreeNodeType.Element)
|
||||
{
|
||||
throw new InvalidOperationException($"Attributes may only be added immediately after nodes of type {UITreeNodeType.Element}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// 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 System;
|
||||
|
||||
namespace Microsoft.Blazor.UITree
|
||||
{
|
||||
// TODO: Consider coalescing properties of compatible types that don't need to be
|
||||
|
|
@ -48,6 +50,12 @@ namespace Microsoft.Blazor.UITree
|
|||
/// </summary>
|
||||
public string AttributeValue { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="NodeType"/> property equals <see cref="UITreeNodeType.Attribute"/>,
|
||||
/// gets the attribute's event handle, if any. Otherwise, the value is <see langword="null"/>.
|
||||
/// </summary>
|
||||
public UIEventHandler AttributeEventHandlerValue { get; private set; }
|
||||
|
||||
internal static UITreeNode Element(string elementName) => new UITreeNode
|
||||
{
|
||||
NodeType = UITreeNodeType.Element,
|
||||
|
|
@ -67,6 +75,13 @@ namespace Microsoft.Blazor.UITree
|
|||
AttributeValue = value
|
||||
};
|
||||
|
||||
internal static UITreeNode Attribute(string name, UIEventHandler value) => new UITreeNode
|
||||
{
|
||||
NodeType = UITreeNodeType.Attribute,
|
||||
AttributeName = name,
|
||||
AttributeEventHandlerValue = value
|
||||
};
|
||||
|
||||
internal void CloseElement(int descendantsEndIndex)
|
||||
{
|
||||
ElementDescendantsEndIndex = descendantsEndIndex;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using BasicTestApp;
|
||||
using Microsoft.Blazor.Components;
|
||||
using Microsoft.Blazor.E2ETest.Infrastructure;
|
||||
using Microsoft.Blazor.E2ETest.Infrastructure.ServerFixtures;
|
||||
using OpenQA.Selenium;
|
||||
|
|
@ -17,43 +18,55 @@ namespace Microsoft.Blazor.E2ETest.Tests
|
|||
public ComponentRenderingTest(BrowserFixture browserFixture, DevHostServerFixture<Program> serverFixture)
|
||||
: base(browserFixture, serverFixture)
|
||||
{
|
||||
Navigate("/", noReload: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicTestAppCanBeServed()
|
||||
{
|
||||
Navigate("/", noReload: true);
|
||||
Assert.Equal("Basic test app", Browser.Title);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRenderTextOnlyComponent()
|
||||
{
|
||||
Navigate("/", noReload: true);
|
||||
MountTestComponent("BasicTestApp.TextOnlyComponent");
|
||||
|
||||
var appElement = Browser.FindElement(By.TagName("app"));
|
||||
var appElement = MountTestComponent<TextOnlyComponent>();
|
||||
Assert.Equal("Hello from TextOnlyComponent", appElement.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRenderComponentWithAttributes()
|
||||
{
|
||||
Navigate("/", noReload: true);
|
||||
MountTestComponent("BasicTestApp.RedTextComponent");
|
||||
|
||||
var appElement = Browser.FindElement(By.TagName("app"));
|
||||
var appElement = MountTestComponent<RedTextComponent>();
|
||||
var styledElement = appElement.FindElement(By.TagName("h1"));
|
||||
Assert.Equal("Hello, world!", styledElement.Text);
|
||||
Assert.Equal("color: red;", styledElement.GetAttribute("style"));
|
||||
Assert.Equal("somevalue", styledElement.GetAttribute("customattribute"));
|
||||
}
|
||||
|
||||
private void MountTestComponent(string componentTypeName)
|
||||
[Fact]
|
||||
public void CanTriggerEvents()
|
||||
{
|
||||
var appElement = MountTestComponent<CounterComponent>();
|
||||
|
||||
Assert.Equal(
|
||||
"Current count: 0",
|
||||
appElement.FindElement(By.TagName("p")).Text);
|
||||
|
||||
appElement.FindElement(By.TagName("button")).Click();
|
||||
|
||||
Assert.Equal(
|
||||
"Current count: 1",
|
||||
appElement.FindElement(By.TagName("p")).Text);
|
||||
}
|
||||
|
||||
private IWebElement MountTestComponent<TComponent>() where TComponent: IComponent
|
||||
{
|
||||
var componentTypeName = typeof(TComponent).FullName;
|
||||
WaitUntilDotNetRunningInBrowser();
|
||||
((IJavaScriptExecutor)Browser).ExecuteScript(
|
||||
$"mountTestComponent('{componentTypeName}')");
|
||||
return Browser.FindElement(By.TagName("app"));
|
||||
}
|
||||
|
||||
private void WaitUntilDotNetRunningInBrowser()
|
||||
|
|
|
|||
|
|
@ -136,13 +136,14 @@ namespace Microsoft.Blazor.Test
|
|||
{
|
||||
// Arrange
|
||||
var builder = new UITreeBuilder();
|
||||
UIEventHandler eventHandler = () => { };
|
||||
|
||||
// Act
|
||||
builder.OpenElement("myelement"); // 0: <myelement
|
||||
builder.AddAttribute("attribute1", "value 1"); // 1: attribute1="value 1"
|
||||
builder.AddAttribute("attribute2", "value 2"); // 2: attribute2="value 2">
|
||||
builder.OpenElement("child"); // 3: <child
|
||||
builder.AddAttribute("attribute1", "child value"); // 4: attribute1="child value">
|
||||
builder.AddAttribute("childevent", eventHandler); // 4: childevent=eventHandler>
|
||||
builder.AddText("some text"); // 5: some text
|
||||
builder.CloseElement(); // </child>
|
||||
builder.CloseElement(); // </myelement>
|
||||
|
|
@ -153,12 +154,12 @@ namespace Microsoft.Blazor.Test
|
|||
node => AssertAttribute(node, "attribute1", "value 1"),
|
||||
node => AssertAttribute(node, "attribute2", "value 2"),
|
||||
node => AssertElement(node, "child", 5),
|
||||
node => AssertAttribute(node, "attribute1", "child value"),
|
||||
node => AssertAttribute(node, "childevent", eventHandler),
|
||||
node => AssertText(node, "some text"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CannotAddAttributesAtRoot()
|
||||
public void CannotAddAttributeAtRoot()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new UITreeBuilder();
|
||||
|
|
@ -171,7 +172,20 @@ namespace Microsoft.Blazor.Test
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void CannotAddAttributesToText()
|
||||
public void CannotAddEventHandlerAttributeAtRoot()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new UITreeBuilder();
|
||||
|
||||
// Act/Assert
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
builder.AddAttribute("name", () => { });
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CannotAddAttributeToText()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new UITreeBuilder();
|
||||
|
|
@ -185,6 +199,21 @@ namespace Microsoft.Blazor.Test
|
|||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CannotAddEventHandlerAttributeToText()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new UITreeBuilder();
|
||||
|
||||
// Act/Assert
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
builder.OpenElement("some element");
|
||||
builder.AddText("hello");
|
||||
builder.AddAttribute("name", () => { });
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanClear()
|
||||
{
|
||||
|
|
@ -216,11 +245,22 @@ namespace Microsoft.Blazor.Test
|
|||
Assert.Equal(descendantsEndIndex, node.ElementDescendantsEndIndex);
|
||||
}
|
||||
|
||||
void AssertAttribute(UITreeNode node, string attributeName, string attributeValue)
|
||||
void AssertAttribute(UITreeNode node, string attributeName)
|
||||
{
|
||||
Assert.Equal(UITreeNodeType.Attribute, node.NodeType);
|
||||
Assert.Equal(attributeName, node.AttributeName);
|
||||
}
|
||||
|
||||
void AssertAttribute(UITreeNode node, string attributeName, string attributeValue)
|
||||
{
|
||||
AssertAttribute(node, attributeName);
|
||||
Assert.Equal(attributeValue, node.AttributeValue);
|
||||
}
|
||||
|
||||
void AssertAttribute(UITreeNode node, string attributeName, UIEventHandler attributeEventHandlerValue)
|
||||
{
|
||||
AssertAttribute(node, attributeName);
|
||||
Assert.Equal(attributeEventHandlerValue, node.AttributeEventHandlerValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
// 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 CounterComponent : IComponent
|
||||
{
|
||||
private int currentCount = 0;
|
||||
|
||||
public void BuildUITree(UITreeBuilder builder)
|
||||
{
|
||||
builder.OpenElement("h1");
|
||||
builder.AddText("Counter");
|
||||
builder.CloseElement();
|
||||
|
||||
builder.OpenElement("p");
|
||||
builder.AddText("Current count: ");
|
||||
builder.AddText(currentCount.ToString());
|
||||
builder.CloseElement();
|
||||
|
||||
builder.OpenElement("button");
|
||||
builder.AddAttribute("onclick", OnButtonClicked);
|
||||
builder.AddText("Click me");
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
private void OnButtonClicked()
|
||||
{
|
||||
currentCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue