Replace @bind with bind-...
This change introduces a 'tag helper' that replaces @bind with custom
code generation that accomplishes roughly the same thing.
This feature lights up by dynamically generating tag helpers that are
visible to tooling and affect the code generation based on:
- pattern recognition of component properties
- attributes that create definitions for elements
- a 'fallback' case for elements
'bind' also supports format strings (currently only for DateTime) via
a separate attribute.
This change introduces the basic framework for bind and tooling support.
We know that we'll have to do more work to define the set of default
'bind' cases for the DOM and to flesh out the conversion/formatting
infrastructure.
This change gets us far enough to replace all of the cases we currently
have tests for :) with the new features. The old @bind technique still
works for now.
Examples:
@* bind an input element to an expression *@
<input bind="@SelectedDate" format="mm/dd/yyyy" />
@functions {
public DateTime SelectedDate { get; set; }
}
@* bind an arbitrary expression to an arbitrary set of attributes *@
<div bind-myvalue-myevent="@SomeExpression">...</div>
@* write a component that supports bind *@
@* in Counter.cshtml *@
<div>...html omitted for brevity...</div>
@functions {
public int Value { get; set; } = 1;
public Action<int> ValueChanged { get; set; }
}
@* in another file *@
<Counter bind-Value="@CurrentValue" />
@functions {
public int CurrentValue { get; set; }
}
This commit is contained in:
parent
b53d39e16f
commit
5b658c80a1
|
|
@ -0,0 +1,418 @@
|
|||
// 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;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Extensions;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
{
|
||||
internal class BindLoweringPass : IntermediateNodePassBase, IRazorOptimizationPass
|
||||
{
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
var @namespace = documentNode.FindPrimaryNamespace();
|
||||
var @class = documentNode.FindPrimaryClass();
|
||||
if (@namespace == null || @class == null)
|
||||
{
|
||||
// Nothing to do, bail. We can't function without the standard structure.
|
||||
return;
|
||||
}
|
||||
|
||||
// For each bind *usage* we need to rewrite the tag helper node to map to basic contstructs.
|
||||
var nodes = documentNode.FindDescendantNodes<TagHelperIntermediateNode>();
|
||||
for (var i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
var node = nodes[i];
|
||||
|
||||
ProcessDuplicates(node);
|
||||
|
||||
for (var j = node.Children.Count - 1; j >= 0; j--)
|
||||
{
|
||||
var attributeNode = node.Children[j] as ComponentAttributeExtensionNode;
|
||||
if (attributeNode != null &&
|
||||
attributeNode.TagHelper != null &&
|
||||
attributeNode.TagHelper.IsBindTagHelper())
|
||||
{
|
||||
RewriteUsage(node, j, attributeNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessDuplicates(TagHelperIntermediateNode node)
|
||||
{
|
||||
// Reverse order because we will remove nodes.
|
||||
//
|
||||
// Each 'property' node could be duplicated if there are multiple tag helpers that match that
|
||||
// particular attribute. This is common in our approach, which relies on 'fallback' tag helpers
|
||||
// that overlap with more specific ones.
|
||||
for (var i = node.Children.Count - 1; i >= 0; i--)
|
||||
{
|
||||
// For each usage of the general 'fallback' bind tag helper, it could duplicate
|
||||
// the usage of a more specific one. Look for duplicates and remove the fallback.
|
||||
var attributeNode = node.Children[i] as ComponentAttributeExtensionNode;
|
||||
if (attributeNode != null &&
|
||||
attributeNode.TagHelper != null &&
|
||||
attributeNode.TagHelper.IsFallbackBindTagHelper())
|
||||
{
|
||||
for (var j = 0; j < node.Children.Count; j++)
|
||||
{
|
||||
var duplicate = node.Children[j] as ComponentAttributeExtensionNode;
|
||||
if (duplicate != null &&
|
||||
duplicate.TagHelper != null &&
|
||||
duplicate.TagHelper.IsBindTagHelper() &&
|
||||
duplicate.AttributeName == attributeNode.AttributeName &&
|
||||
!object.ReferenceEquals(attributeNode, duplicate))
|
||||
{
|
||||
// Found a duplicate - remove the 'fallback' in favor of the
|
||||
// more specific tag helper.
|
||||
node.Children.RemoveAt(i);
|
||||
node.TagHelpers.Remove(attributeNode.TagHelper);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also treat the general <input bind="..." /> as a 'fallback' for that case and remove it.
|
||||
// This is a workaround for a limitation where you can't write a tag helper that binds only
|
||||
// when a specific attribute is **not** present.
|
||||
if (attributeNode != null &&
|
||||
attributeNode.TagHelper != null &&
|
||||
attributeNode.TagHelper.IsInputElementFallbackBindTagHelper())
|
||||
{
|
||||
for (var j = 0; j < node.Children.Count; j++)
|
||||
{
|
||||
var duplicate = node.Children[j] as ComponentAttributeExtensionNode;
|
||||
if (duplicate != null &&
|
||||
duplicate.TagHelper != null &&
|
||||
duplicate.TagHelper.IsInputElementBindTagHelper() &&
|
||||
duplicate.AttributeName == attributeNode.AttributeName &&
|
||||
!object.ReferenceEquals(attributeNode, duplicate))
|
||||
{
|
||||
// Found a duplicate - remove the 'fallback' input tag helper in favor of the
|
||||
// more specific tag helper.
|
||||
node.Children.RemoveAt(i);
|
||||
node.TagHelpers.Remove(attributeNode.TagHelper);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we still have duplicates at this point then they are genuine conflicts.
|
||||
var duplicates = node.Children
|
||||
.OfType<ComponentAttributeExtensionNode>()
|
||||
.GroupBy(p => p.AttributeName)
|
||||
.Where(g => g.Count() > 1);
|
||||
|
||||
foreach (var duplicate in duplicates)
|
||||
{
|
||||
node.Diagnostics.Add(BlazorDiagnosticFactory.CreateBindAttribute_Duplicates(
|
||||
node.Source,
|
||||
duplicate.Key,
|
||||
duplicate.ToArray()));
|
||||
foreach (var property in duplicate)
|
||||
{
|
||||
node.Children.Remove(property);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RewriteUsage(TagHelperIntermediateNode node, int index, ComponentAttributeExtensionNode attributeNode)
|
||||
{
|
||||
// Bind works similarly to a macro, it always expands to code that the user could have written.
|
||||
//
|
||||
// For the nodes that are related to the bind-attribute rewrite them to look like a pair of
|
||||
// 'normal' HTML attributes similar to the following transformation.
|
||||
//
|
||||
// Input: <MyComponent bind-Value="@currentCount" />
|
||||
// Output: <MyComponent Value ="...<get the value>..." ValueChanged ="... <set the value>..." />
|
||||
//
|
||||
// This means that the expression that appears inside of 'bind' must be an LValue or else
|
||||
// there will be errors. In general the errors that come from C# in this case are good enough
|
||||
// to understand the problem.
|
||||
//
|
||||
// The BindMethods calls are required in this case because to give us a good experience. They
|
||||
// use overloading to ensure that can get an Action<object> that will convert and set an arbitrary
|
||||
// value.
|
||||
//
|
||||
// We also assume that the element will be treated as a component for now because
|
||||
// multiple passes handle 'special' tag helpers. We have another pass that translates
|
||||
// a tag helper node back into 'regular' element when it doesn't have an associated component
|
||||
if (!TryComputeAttributeNames(
|
||||
node,
|
||||
attributeNode.AttributeName,
|
||||
out var valueAttributeName,
|
||||
out var changeAttributeName,
|
||||
out var valueAttribute,
|
||||
out var changeAttribute))
|
||||
{
|
||||
// Skip anything we can't understand. It's important that we don't crash, that will bring down
|
||||
// the build.
|
||||
return;
|
||||
}
|
||||
|
||||
var originalContent = GetAttributeContent(attributeNode);
|
||||
if (string.IsNullOrEmpty(originalContent))
|
||||
{
|
||||
// This can happen in error cases, the parser will already have flagged this
|
||||
// as an error, so ignore it.
|
||||
return;
|
||||
}
|
||||
|
||||
// Look for a matching format node. If we find one then we need to pass the format into the
|
||||
// two nodes we generate.
|
||||
string format = null;
|
||||
if (TryGetFormatNode(node,
|
||||
attributeNode,
|
||||
valueAttributeName,
|
||||
out var formatNode))
|
||||
{
|
||||
// Don't write the format out as its own attribute;
|
||||
node.Children.Remove(formatNode);
|
||||
format = GetAttributeContent(formatNode);
|
||||
}
|
||||
|
||||
var valueAttributeNode = new ComponentAttributeExtensionNode(attributeNode)
|
||||
{
|
||||
AttributeName = valueAttributeName,
|
||||
BoundAttribute = valueAttribute, // Might be null if it doesn't match a component attribute
|
||||
PropertyName = valueAttribute?.GetPropertyName(),
|
||||
TagHelper = valueAttribute == null ? null : attributeNode.TagHelper,
|
||||
};
|
||||
node.Children.Insert(index, valueAttributeNode);
|
||||
|
||||
// Now rewrite the content of the value node to look like:
|
||||
//
|
||||
// BindMethods.GetValue(<code>) OR
|
||||
// BindMethods.GetValue(<code>, <format>)
|
||||
//
|
||||
// For now, the way this is done isn't debuggable. But since the expression
|
||||
// passed here must be an LValue, it's probably not important.
|
||||
var valueNodeContent = format == null ?
|
||||
$"{BlazorApi.BindMethods.GetValue}({originalContent})" :
|
||||
$"{BlazorApi.BindMethods.GetValue}({originalContent}, {format})";
|
||||
valueAttributeNode.Children.Clear();
|
||||
valueAttributeNode.Children.Add(new CSharpExpressionIntermediateNode()
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new IntermediateToken()
|
||||
{
|
||||
Content = valueNodeContent,
|
||||
Kind = TokenKind.CSharp
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var changeAttributeNode = new ComponentAttributeExtensionNode(attributeNode)
|
||||
{
|
||||
AttributeName = changeAttributeName,
|
||||
BoundAttribute = changeAttribute, // Might be null if it doesn't match a component attribute
|
||||
PropertyName = changeAttribute?.GetPropertyName(),
|
||||
TagHelper = changeAttribute == null ? null : attributeNode.TagHelper,
|
||||
};
|
||||
node.Children[index + 1] = changeAttributeNode;
|
||||
|
||||
// Now rewrite the content of the change-handler node. There are two cases we care about
|
||||
// here. If it's a component attribute, then don't use the 'BindMethods wrapper. We expect
|
||||
// component attributes to always 'match' on type.
|
||||
//
|
||||
// __value => <code> = __value
|
||||
//
|
||||
// For general DOM attributes, we need to be able to create a delegate that accepts UIEventArgs
|
||||
// so we use BindMethods.SetValueHandler
|
||||
//
|
||||
// BindMethods.SetValueHandler(__value => <code> = __value, <code>) OR
|
||||
// BindMethods.SetValueHandler(__value => <code> = __value, <code>, <format>)
|
||||
//
|
||||
// For now, the way this is done isn't debuggable. But since the expression
|
||||
// passed here must be an LValue, it's probably not important.
|
||||
string changeAttributeContent = null;
|
||||
if (changeAttributeNode.BoundAttribute == null && format == null)
|
||||
{
|
||||
changeAttributeContent = $"{BlazorApi.BindMethods.SetValueHandler}(__value => {originalContent} = __value, {originalContent})";
|
||||
}
|
||||
else if (changeAttributeNode.BoundAttribute == null && format != null)
|
||||
{
|
||||
changeAttributeContent = $"{BlazorApi.BindMethods.SetValueHandler}(__value => {originalContent} = __value, {originalContent}, {format})";
|
||||
}
|
||||
else
|
||||
{
|
||||
changeAttributeContent = $"__value => {originalContent} = __value";
|
||||
}
|
||||
|
||||
changeAttributeNode.Children.Clear();
|
||||
changeAttributeNode.Children.Add(new CSharpExpressionIntermediateNode()
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new IntermediateToken()
|
||||
{
|
||||
Content = changeAttributeContent,
|
||||
Kind = TokenKind.CSharp
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private bool TryParseBindAttribute(
|
||||
string attributeName,
|
||||
out string valueAttributeName,
|
||||
out string changeAttributeName)
|
||||
{
|
||||
valueAttributeName = null;
|
||||
changeAttributeName = null;
|
||||
|
||||
if (!attributeName.StartsWith("bind"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (attributeName == "bind")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var segments = attributeName.Split('-');
|
||||
for (var i = 0; i < segments.Length; i++)
|
||||
{
|
||||
if (string.IsNullOrEmpty(segments[0]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
switch (segments.Length)
|
||||
{
|
||||
case 2:
|
||||
valueAttributeName = segments[1];
|
||||
return true;
|
||||
|
||||
case 3:
|
||||
changeAttributeName = segments[2];
|
||||
valueAttributeName = segments[1];
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Attempts to compute the attribute names that should be used for an instance of 'bind'.
|
||||
private bool TryComputeAttributeNames(
|
||||
TagHelperIntermediateNode node,
|
||||
string attributeName,
|
||||
out string valueAttributeName,
|
||||
out string changeAttributeName,
|
||||
out BoundAttributeDescriptor valueAttribute,
|
||||
out BoundAttributeDescriptor changeAttribute)
|
||||
{
|
||||
valueAttribute = null;
|
||||
changeAttribute = null;
|
||||
|
||||
// Even though some of our 'bind' tag helpers specify the attribute names, they
|
||||
// should still satisfy one of the valid syntaxes.
|
||||
if (!TryParseBindAttribute(attributeName, out valueAttributeName, out changeAttributeName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// The the tag helper specifies attribute names, they should win.
|
||||
//
|
||||
// This handles cases like <input type="text" bind="@Foo" /> where the tag helper is
|
||||
// generated to match a specific tag and has metadata that identify the attributes.
|
||||
//
|
||||
// We expect 1 bind tag helper per-node.
|
||||
var bindTagHelper = node.TagHelpers.Single(t => t.IsBindTagHelper());
|
||||
valueAttributeName = bindTagHelper.GetValueAttributeName() ?? valueAttributeName;
|
||||
changeAttributeName = bindTagHelper.GetChangeAttributeName() ?? changeAttributeName;
|
||||
|
||||
// We expect 0-1 components per-node.
|
||||
var componentTagHelper = node.TagHelpers.FirstOrDefault(t => t.IsComponentTagHelper());
|
||||
if (componentTagHelper == null)
|
||||
{
|
||||
// If it's not a component node then there isn't too much else to figure out.
|
||||
return attributeName != null && changeAttributeName != null;
|
||||
}
|
||||
|
||||
// If this is a component, we need an attribute name for the value.
|
||||
if (attributeName == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If this is a component, then we can infer '<PropertyName>Changed' as the name
|
||||
// of the change event.
|
||||
if (changeAttributeName == null)
|
||||
{
|
||||
changeAttributeName = valueAttributeName + "Changed";
|
||||
}
|
||||
|
||||
for (var i = 0; i < componentTagHelper.BoundAttributes.Count; i++)
|
||||
{
|
||||
var attribute = componentTagHelper.BoundAttributes[i];
|
||||
|
||||
if (string.Equals(valueAttributeName, attribute.Name))
|
||||
{
|
||||
valueAttribute = attribute;
|
||||
}
|
||||
|
||||
if (string.Equals(changeAttributeName, attribute.Name))
|
||||
{
|
||||
changeAttribute = attribute;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryGetFormatNode(
|
||||
TagHelperIntermediateNode node,
|
||||
ComponentAttributeExtensionNode attributeNode,
|
||||
string valueAttributeName,
|
||||
out ComponentAttributeExtensionNode formatNode)
|
||||
{
|
||||
for (var i = 0; i < node.Children.Count; i++)
|
||||
{
|
||||
var child = node.Children[i] as ComponentAttributeExtensionNode;
|
||||
if (child != null &&
|
||||
child.TagHelper != null &&
|
||||
child.TagHelper == attributeNode.TagHelper &&
|
||||
child.AttributeName == "format-" + valueAttributeName)
|
||||
{
|
||||
formatNode = child;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
formatNode = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string GetAttributeContent(ComponentAttributeExtensionNode node)
|
||||
{
|
||||
if (node.Children[0] is HtmlContentIntermediateNode htmlContentNode)
|
||||
{
|
||||
// This case can be hit for a 'string' attribute. We want to turn it into
|
||||
// an expression.
|
||||
return "\"" + ((IntermediateToken)htmlContentNode.Children.Single()).Content + "\"";
|
||||
}
|
||||
else if (node.Children[0] is CSharpExpressionIntermediateNode cSharpNode)
|
||||
{
|
||||
// This case can be hit when the attribute has an explicit @ inside, which
|
||||
// 'escapes' any special sugar we provide for codegen.
|
||||
return ((IntermediateToken)cSharpNode.Children.Single()).Content;
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is the common case for 'mixed' content
|
||||
return ((IntermediateToken)node.Children.Single()).Content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,491 @@
|
|||
// 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;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Razor;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
{
|
||||
internal class BindTagHelperDescriptorProvider : ITagHelperDescriptorProvider
|
||||
{
|
||||
// Run after the component tag helper provider, because we need to see the results.
|
||||
public int Order { get; set; } = 1000;
|
||||
|
||||
public RazorEngine Engine { get; set; }
|
||||
|
||||
public void Execute(TagHelperDescriptorProviderContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
// This provider returns tag helper information for 'bind' which doesn't necessarily
|
||||
// map to any real component. Bind behaviors more like a macro, which can map a single LValue to
|
||||
// both a 'value' attribute and a 'value changed' attribute.
|
||||
//
|
||||
// User types:
|
||||
// <input type="text" bind="@FirstName"/>
|
||||
//
|
||||
// We generate:
|
||||
// <input type="text"
|
||||
// value="@BindMethods.GetValue(FirstName)"
|
||||
// onchange="@BindMethods.SetValue(__value => FirstName = __value, FirstName)"/>
|
||||
//
|
||||
// This isn't very different from code the user could write themselves - thus the pronouncement
|
||||
// that bind is very much like a macro.
|
||||
//
|
||||
// A lot of the value that provide in this case is that the associations between the
|
||||
// elements, and the attributes aren't straightforward.
|
||||
//
|
||||
// For instance on <input type="text" /> we need to listen to 'value' and 'onchange',
|
||||
// but on <input type="checked" we need to listen to 'checked' and 'onchange'.
|
||||
//
|
||||
// We handle a few different cases here:
|
||||
//
|
||||
// 1. When given an attribute like **anywhere**'bind-value-onchange="@FirstName"' we will
|
||||
// generate the 'value' attribute and 'onchange' attribute.
|
||||
//
|
||||
// We don't do any transformation or inference for this case, because the developer has
|
||||
// told us exactly what to do. This is the *full* form of bind, and should support any
|
||||
// combination of elemement, component, and attributes.
|
||||
//
|
||||
// This is the most general case, and is implemented with a built-in tag helper that applies
|
||||
// to everything, and binds to a dictionary of attributes that start with bind-.
|
||||
//
|
||||
// 2. We also support cases like 'bind-value="@FirstName"' where we will generate the 'value'
|
||||
// attribute and and another attribute based for a changed handler based on the metadata.
|
||||
//
|
||||
// These mappings are provided by attributes that tell us what attributes, suffixes, and
|
||||
// elements to map.
|
||||
//
|
||||
// 3. When given an attribute like 'bind="@FirstName"' we will generate a value and change
|
||||
// attribute solely based on the context. We need the context of an HTML tag to know
|
||||
// what attributes to generate.
|
||||
//
|
||||
// Similar to case #2, this should 'just work' from the users point of view. We expect
|
||||
// using this syntax most frequently with input elements.
|
||||
//
|
||||
// These mappings are also provided by attributes. Primarily these are used by <input />
|
||||
// and so we have a special case for input elements and their type attributes.
|
||||
//
|
||||
// 4. For components, we have a bit of a special case. We can infer a syntax that matches
|
||||
// case #2 based on property names. So if a component provides both 'Value' and 'ValueChanged'
|
||||
// we will turn that into an instance of bind.
|
||||
//
|
||||
// So case #1 here is the most general case. Case #2 and #3 are data-driven based on attribute data
|
||||
// we have. Case #4 is data-driven based on component definitions.
|
||||
//
|
||||
// We provide a good set of attributes that map to the HTML dom. This set is user extensible.
|
||||
var compilation = context.GetCompilation();
|
||||
if (compilation == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bindMethods = compilation.GetTypeByMetadataName(BlazorApi.BindMethods.FullTypeName);
|
||||
if (bindMethods == null)
|
||||
{
|
||||
// If we can't find BindMethods, then just bail. We won't be able to compile the
|
||||
// generated code anyway.
|
||||
return;
|
||||
}
|
||||
|
||||
// Tag Helper defintion for case #1. This is the most general case.
|
||||
context.Results.Add(CreateFallbackBindTagHelper());
|
||||
|
||||
// For case #2 & #3 we have a whole bunch of attribute entries on BindMethods that we can use
|
||||
// to data-drive the definitions of these tag helpers.
|
||||
var elementBindData = GetElementBindData(compilation);
|
||||
|
||||
// Case #2 & #3
|
||||
foreach (var tagHelper in CreateElementBindTagHelpers(elementBindData))
|
||||
{
|
||||
context.Results.Add(tagHelper);
|
||||
}
|
||||
|
||||
// For case #4 we look at the tag helpers that were already created corresponding to components
|
||||
// and pattern match on properties.
|
||||
foreach (var tagHelper in CreateComponentBindTagHelpers(context.Results))
|
||||
{
|
||||
context.Results.Add(tagHelper);
|
||||
}
|
||||
}
|
||||
|
||||
private TagHelperDescriptor CreateFallbackBindTagHelper()
|
||||
{
|
||||
var builder = TagHelperDescriptorBuilder.Create(BlazorMetadata.Bind.TagHelperKind, "Bind", BlazorApi.AssemblyName);
|
||||
builder.Documentation = Resources.BindTagHelper_Fallback_Documentation;
|
||||
|
||||
builder.Metadata.Add(BlazorMetadata.SpecialKindKey, BlazorMetadata.Bind.TagHelperKind);
|
||||
builder.Metadata[TagHelperMetadata.Runtime.Name] = BlazorMetadata.Bind.RuntimeName;
|
||||
builder.Metadata[BlazorMetadata.Bind.FallbackKey] = bool.TrueString;
|
||||
|
||||
// WTE has a bug in 15.7p1 where a Tag Helper without a display-name that looks like
|
||||
// a C# property will crash trying to create the toolips.
|
||||
builder.SetTypeName("Microsoft.AspNetCore.Blazor.Components.Bind");
|
||||
|
||||
builder.TagMatchingRule(rule =>
|
||||
{
|
||||
rule.TagName = "*";
|
||||
rule.Attribute(attribute =>
|
||||
{
|
||||
attribute.Name = "bind-";
|
||||
attribute.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.PrefixMatch;
|
||||
});
|
||||
});
|
||||
|
||||
builder.BindAttribute(attribute =>
|
||||
{
|
||||
attribute.Documentation = Resources.BindTagHelper_Fallback_Documentation;
|
||||
|
||||
attribute.Name = "bind-...";
|
||||
attribute.AsDictionary("bind-", typeof(object).FullName);
|
||||
|
||||
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
|
||||
// a C# property will crash trying to create the toolips.
|
||||
attribute.SetPropertyName("Bind");
|
||||
attribute.TypeName = "System.Collections.Generic.Dictionary<string, object>";
|
||||
});
|
||||
|
||||
builder.BindAttribute(attribute =>
|
||||
{
|
||||
attribute.Documentation = Resources.BindTagHelper_Fallback_Format_Documentation;
|
||||
|
||||
attribute.Name = "format-...";
|
||||
attribute.AsDictionary("format-", typeof(string).FullName);
|
||||
|
||||
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
|
||||
// a C# property will crash trying to create the toolips.
|
||||
attribute.SetPropertyName("Format");
|
||||
attribute.TypeName = "System.Collections.Generic.Dictionary<string, string>";
|
||||
});
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
private List<ElementBindData> GetElementBindData(Compilation compilation)
|
||||
{
|
||||
var bindElement = compilation.GetTypeByMetadataName(BlazorApi.BindElementAttribute.FullTypeName);
|
||||
var bindInputElement = compilation.GetTypeByMetadataName(BlazorApi.BindInputElementAttribute.FullTypeName);
|
||||
|
||||
if (bindElement == null || bindInputElement == null)
|
||||
{
|
||||
// This won't likely happen, but just in case.
|
||||
return new List<ElementBindData>();
|
||||
}
|
||||
|
||||
var types = new List<INamedTypeSymbol>();
|
||||
var visitor = new BindElementDataVisitor(types);
|
||||
|
||||
// Visit the primary output of this compilation, as well as all references.
|
||||
visitor.Visit(compilation.Assembly);
|
||||
foreach (var reference in compilation.References)
|
||||
{
|
||||
// We ignore .netmodules here - there really isn't a case where they are used by user code
|
||||
// even though the Roslyn APIs all support them.
|
||||
if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly)
|
||||
{
|
||||
visitor.Visit(assembly);
|
||||
}
|
||||
}
|
||||
|
||||
var results = new List<ElementBindData>();
|
||||
|
||||
for (var i = 0; i < types.Count; i++)
|
||||
{
|
||||
var type = types[i];
|
||||
var attributes = type.GetAttributes();
|
||||
|
||||
// Not handling duplicates here for now since we're the primary ones extending this.
|
||||
// If we see users adding to the set of 'bind' constructs we will want to add deduplication
|
||||
// and potentially diagnostics.
|
||||
for (var j = 0; j < attributes.Length; j++)
|
||||
{
|
||||
var attribute = attributes[j];
|
||||
|
||||
if (attribute.AttributeClass == bindElement)
|
||||
{
|
||||
results.Add(new ElementBindData(
|
||||
type.ContainingAssembly.Name,
|
||||
type.ToDisplayString(),
|
||||
(string)attribute.ConstructorArguments[0].Value,
|
||||
null,
|
||||
(string)attribute.ConstructorArguments[1].Value,
|
||||
(string)attribute.ConstructorArguments[2].Value,
|
||||
(string)attribute.ConstructorArguments[3].Value));
|
||||
}
|
||||
else if (attribute.AttributeClass == bindInputElement)
|
||||
{
|
||||
results.Add(new ElementBindData(
|
||||
type.ContainingAssembly.Name,
|
||||
type.ToDisplayString(),
|
||||
"input",
|
||||
(string)attribute.ConstructorArguments[0].Value,
|
||||
(string)attribute.ConstructorArguments[1].Value,
|
||||
(string)attribute.ConstructorArguments[2].Value,
|
||||
(string)attribute.ConstructorArguments[3].Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
private List<TagHelperDescriptor> CreateElementBindTagHelpers(List<ElementBindData> data)
|
||||
{
|
||||
var results = new List<TagHelperDescriptor>();
|
||||
|
||||
for (var i = 0; i < data.Count; i++)
|
||||
{
|
||||
var entry = data[i];
|
||||
|
||||
var name = entry.Suffix == null ? "Bind" : "Bind_" + entry.Suffix;
|
||||
var attributeName = entry.Suffix == null ? "bind" : "bind-" + entry.Suffix;
|
||||
|
||||
var formatName = entry.Suffix == null ? "Format_" + entry.ValueAttribute : "Format_" + entry.Suffix;
|
||||
var formatAttributeName = entry.Suffix == null ? "format-" + entry.ValueAttribute : "format-" + entry.Suffix;
|
||||
|
||||
var builder = TagHelperDescriptorBuilder.Create(BlazorMetadata.Bind.TagHelperKind, name, entry.Assembly);
|
||||
builder.Documentation = string.Format(
|
||||
Resources.BindTagHelper_Element_Documentation,
|
||||
entry.ValueAttribute,
|
||||
entry.ChangeAttribute);
|
||||
|
||||
builder.Metadata.Add(BlazorMetadata.SpecialKindKey, BlazorMetadata.Bind.TagHelperKind);
|
||||
builder.Metadata[TagHelperMetadata.Runtime.Name] = BlazorMetadata.Bind.RuntimeName;
|
||||
builder.Metadata[BlazorMetadata.Bind.ValueAttribute] = entry.ValueAttribute;
|
||||
builder.Metadata[BlazorMetadata.Bind.ChangeAttribute] = entry.ChangeAttribute;
|
||||
|
||||
if (entry.TypeAttribute != null)
|
||||
{
|
||||
// For entries that map to the <input /> element, we need to be able to know
|
||||
// the difference between <input /> and <input type="text" .../> for which we
|
||||
// want to use the same attributes.
|
||||
//
|
||||
// We provide a tag helper for <input /> that should match all input elements,
|
||||
// but we only want it to be used when a more specific one is used.
|
||||
//
|
||||
// Therefore we use this metadata to know which one is more specific when two
|
||||
// tag helpers match.
|
||||
builder.Metadata[BlazorMetadata.Bind.TypeAttribute] = entry.TypeAttribute;
|
||||
}
|
||||
|
||||
// WTE has a bug in 15.7p1 where a Tag Helper without a display-name that looks like
|
||||
// a C# property will crash trying to create the toolips.
|
||||
builder.SetTypeName(entry.TypeName);
|
||||
|
||||
builder.TagMatchingRule(rule =>
|
||||
{
|
||||
rule.TagName = entry.Element;
|
||||
if (entry.TypeAttribute != null)
|
||||
{
|
||||
rule.Attribute(a =>
|
||||
{
|
||||
a.Name = "type";
|
||||
a.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.FullMatch;
|
||||
a.Value = entry.TypeAttribute;
|
||||
a.ValueComparisonMode = RequiredAttributeDescriptor.ValueComparisonMode.FullMatch;
|
||||
});
|
||||
}
|
||||
|
||||
rule.Attribute(a =>
|
||||
{
|
||||
a.Name = attributeName;
|
||||
a.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.FullMatch;
|
||||
});
|
||||
});
|
||||
|
||||
builder.BindAttribute(a =>
|
||||
{
|
||||
a.Documentation = string.Format(
|
||||
Resources.BindTagHelper_Element_Documentation,
|
||||
entry.ValueAttribute,
|
||||
entry.ChangeAttribute);
|
||||
|
||||
a.Name = attributeName;
|
||||
a.TypeName = typeof(object).FullName;
|
||||
|
||||
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
|
||||
// a C# property will crash trying to create the toolips.
|
||||
a.SetPropertyName(name);
|
||||
});
|
||||
|
||||
builder.BindAttribute(attribute =>
|
||||
{
|
||||
attribute.Documentation = string.Format(Resources.BindTagHelper_Element_Format_Documentation, attributeName);
|
||||
|
||||
attribute.Name = formatAttributeName;
|
||||
attribute.TypeName = "System.String";
|
||||
|
||||
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
|
||||
// a C# property will crash trying to create the toolips.
|
||||
attribute.SetPropertyName(formatName);
|
||||
});
|
||||
|
||||
results.Add(builder.Build());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private List<TagHelperDescriptor> CreateComponentBindTagHelpers(ICollection<TagHelperDescriptor> tagHelpers)
|
||||
{
|
||||
var results = new List<TagHelperDescriptor>();
|
||||
|
||||
foreach (var tagHelper in tagHelpers)
|
||||
{
|
||||
if (!tagHelper.IsComponentTagHelper())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// We want to create a 'bind' tag helper everywhere we see a pair of properties like `Foo`, `FooChanged`
|
||||
// where `FooChanged` is a delegate and `Foo` is not.
|
||||
//
|
||||
// The easiest way to figure this out without a lot of backtracking is to look for `FooChanged` and then
|
||||
// try to find a matching "Foo".
|
||||
for (var i = 0; i < tagHelper.BoundAttributes.Count; i++)
|
||||
{
|
||||
var changeAttribute = tagHelper.BoundAttributes[i];
|
||||
if (!changeAttribute.Name.EndsWith("Changed") || !changeAttribute.IsDelegateProperty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
BoundAttributeDescriptor valueAttribute = null;
|
||||
var valueAttributeName = changeAttribute.Name.Substring(0, changeAttribute.Name.Length - "Changed".Length);
|
||||
for (var j = 0; j < tagHelper.BoundAttributes.Count; j++)
|
||||
{
|
||||
if (tagHelper.BoundAttributes[j].Name == valueAttributeName && !tagHelper.BoundAttributes[j].IsDelegateProperty())
|
||||
{
|
||||
valueAttribute = tagHelper.BoundAttributes[j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (valueAttribute == null)
|
||||
{
|
||||
// No matching attribute found.
|
||||
continue;
|
||||
}
|
||||
|
||||
var builder = TagHelperDescriptorBuilder.Create(BlazorMetadata.Bind.TagHelperKind, tagHelper.Name, tagHelper.AssemblyName);
|
||||
builder.DisplayName = tagHelper.DisplayName;
|
||||
builder.Documentation = string.Format(
|
||||
Resources.BindTagHelper_Component_Documentation,
|
||||
valueAttribute.Name,
|
||||
changeAttribute.Name);
|
||||
|
||||
builder.Metadata.Add(BlazorMetadata.SpecialKindKey, BlazorMetadata.Bind.TagHelperKind);
|
||||
builder.Metadata[TagHelperMetadata.Runtime.Name] = BlazorMetadata.Bind.RuntimeName;
|
||||
builder.Metadata[BlazorMetadata.Bind.ValueAttribute] = valueAttribute.Name;
|
||||
builder.Metadata[BlazorMetadata.Bind.ChangeAttribute] = changeAttribute.Name;
|
||||
|
||||
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
|
||||
// a C# property will crash trying to create the toolips.
|
||||
builder.SetTypeName(tagHelper.GetTypeName());
|
||||
|
||||
// Match the component and attribute name
|
||||
builder.TagMatchingRule(rule =>
|
||||
{
|
||||
rule.TagName = tagHelper.TagMatchingRules.Single().TagName;
|
||||
rule.Attribute(attribute =>
|
||||
{
|
||||
attribute.Name = "bind-" + valueAttribute.Name;
|
||||
attribute.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.FullMatch;
|
||||
});
|
||||
});
|
||||
|
||||
builder.BindAttribute(attribute =>
|
||||
{
|
||||
attribute.Documentation = string.Format(
|
||||
Resources.BindTagHelper_Component_Documentation,
|
||||
valueAttribute.Name,
|
||||
changeAttribute.Name);
|
||||
|
||||
attribute.Name = "bind-" + valueAttribute.Name;
|
||||
attribute.TypeName = valueAttribute.TypeName;
|
||||
attribute.IsEnum = valueAttribute.IsEnum;
|
||||
|
||||
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
|
||||
// a C# property will crash trying to create the toolips.
|
||||
attribute.SetPropertyName(valueAttribute.GetPropertyName());
|
||||
});
|
||||
|
||||
results.Add(builder.Build());
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private struct ElementBindData
|
||||
{
|
||||
public ElementBindData(
|
||||
string assembly,
|
||||
string typeName,
|
||||
string element,
|
||||
string typeAttribute,
|
||||
string suffix,
|
||||
string valueAttribute,
|
||||
string changeAttribute)
|
||||
{
|
||||
Assembly = assembly;
|
||||
TypeName = typeName;
|
||||
Element = element;
|
||||
TypeAttribute = typeAttribute;
|
||||
Suffix = suffix;
|
||||
ValueAttribute = valueAttribute;
|
||||
ChangeAttribute = changeAttribute;
|
||||
}
|
||||
|
||||
public string Assembly { get; }
|
||||
public string TypeName { get; }
|
||||
public string Element { get; }
|
||||
public string TypeAttribute { get; }
|
||||
public string Suffix { get; }
|
||||
public string ValueAttribute { get; }
|
||||
public string ChangeAttribute { get; }
|
||||
}
|
||||
|
||||
private class BindElementDataVisitor : SymbolVisitor
|
||||
{
|
||||
private List<INamedTypeSymbol> _results;
|
||||
|
||||
public BindElementDataVisitor(List<INamedTypeSymbol> results)
|
||||
{
|
||||
_results = results;
|
||||
}
|
||||
|
||||
public override void VisitNamedType(INamedTypeSymbol symbol)
|
||||
{
|
||||
if (symbol.Name == "BindAttributes" && symbol.DeclaredAccessibility == Accessibility.Public)
|
||||
{
|
||||
_results.Add(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitNamespace(INamespaceSymbol symbol)
|
||||
{
|
||||
foreach (var member in symbol.GetMembers())
|
||||
{
|
||||
Visit(member);
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitAssembly(IAssemblySymbol symbol)
|
||||
{
|
||||
// This as a simple yet high-value optimization that excludes the vast majority of
|
||||
// assemblies that (by definition) can't contain a component.
|
||||
if (symbol.Name != null && !symbol.Name.StartsWith("System.", StringComparison.Ordinal))
|
||||
{
|
||||
Visit(symbol.GlobalNamespace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
// Keep these in sync with the actual definitions
|
||||
internal static class BlazorApi
|
||||
{
|
||||
public static readonly string AssemblyName = "Microsoft.AspNetCore.Blazor";
|
||||
|
||||
public static class BlazorComponent
|
||||
{
|
||||
public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.Components.BlazorComponent";
|
||||
|
|
@ -64,13 +66,27 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.Components.RouteAttribute";
|
||||
}
|
||||
|
||||
public static class BindElementAttribute
|
||||
{
|
||||
public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.Components.BindElementAttribute";
|
||||
}
|
||||
|
||||
public static class BindInputElementAttribute
|
||||
{
|
||||
public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.Components.BindInputElementAttribute";
|
||||
}
|
||||
|
||||
public static class BindMethods
|
||||
{
|
||||
public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.Components.BindMethods";
|
||||
|
||||
public static readonly string GetValue = "Microsoft.AspNetCore.Blazor.Components.BindMethods.GetValue";
|
||||
|
||||
public static readonly string SetValue = "Microsoft.AspNetCore.Blazor.Components.BindMethods.SetValue";
|
||||
|
||||
public static readonly string SetValueHandler = "Microsoft.AspNetCore.Blazor.Components.BindMethods.SetValueHandler";
|
||||
}
|
||||
|
||||
|
||||
public static class UIEventHandler
|
||||
{
|
||||
public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.UIEventHandler";
|
||||
|
|
|
|||
|
|
@ -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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
|
@ -88,5 +89,21 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
var diagnostic = RazorDiagnostic.Create(PageDirective_MustSpecifyRoute, source ?? SourceSpan.Undefined);
|
||||
return diagnostic;
|
||||
}
|
||||
|
||||
public static readonly RazorDiagnosticDescriptor BindAttribute_Duplicates =
|
||||
new RazorDiagnosticDescriptor(
|
||||
"BL9989",
|
||||
() => "The attribute '{0}' was matched by multiple bind attributes. Duplicates:{1}",
|
||||
RazorDiagnosticSeverity.Error);
|
||||
|
||||
public static RazorDiagnostic CreateBindAttribute_Duplicates(SourceSpan? source, string attribute, ComponentAttributeExtensionNode[] attributes)
|
||||
{
|
||||
var diagnostic = RazorDiagnostic.Create(
|
||||
BindAttribute_Duplicates,
|
||||
source ?? SourceSpan.Undefined,
|
||||
attribute,
|
||||
Environment.NewLine + string.Join(Environment.NewLine, attributes.Select(p => p.TagHelper.DisplayName)));
|
||||
return diagnostic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,12 +65,17 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
|
||||
builder.Features.Add(new ConfigureBlazorCodeGenerationOptions());
|
||||
|
||||
// Implementation of components
|
||||
builder.Features.Add(new ComponentDocumentClassifierPass());
|
||||
builder.Features.Add(new ComplexAttributeContentPass());
|
||||
builder.Features.Add(new ComponentLoweringPass());
|
||||
|
||||
builder.Features.Add(new ComponentTagHelperDescriptorProvider());
|
||||
|
||||
// Implementation of bind
|
||||
builder.Features.Add(new BindLoweringPass());
|
||||
builder.Features.Add(new BindTagHelperDescriptorProvider());
|
||||
builder.Features.Add(new OrphanTagHelperLoweringPass());
|
||||
|
||||
if (builder.Configuration.ConfigurationName == DeclarationConfiguration.ConfigurationName)
|
||||
{
|
||||
// This is for 'declaration only' processing. We don't want to try and emit any method bodies during
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
// 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.AspNetCore.Blazor.Razor
|
||||
{
|
||||
// Metadata used for Blazor's interations with the tag helper system
|
||||
internal static class BlazorMetadata
|
||||
{
|
||||
// There's a bug in the 15.7 preview 1 Razor that prevents 'Kind' from being serialized
|
||||
// this affects both tooling and build. For now our workaround is to ignore 'Kind' and
|
||||
// use our own metadata entry to denote non-Component tag helpers.
|
||||
public static readonly string SpecialKindKey = "Blazor.IsSpecialKind";
|
||||
|
||||
public static class Bind
|
||||
{
|
||||
public static readonly string RuntimeName = "Blazor.None";
|
||||
|
||||
public readonly static string TagHelperKind = "Blazor.Bind-0.1";
|
||||
|
||||
public readonly static string FallbackKey = "Blazor.Bind.Fallback";
|
||||
|
||||
public readonly static string TypeAttribute = "Blazor.Bind.TypeAttribute";
|
||||
|
||||
public readonly static string ValueAttribute = "Blazor.Bind.ValueAttribute";
|
||||
|
||||
public readonly static string ChangeAttribute = "Blazor.Bind.ChangeAttribute";
|
||||
}
|
||||
|
||||
public static class Component
|
||||
{
|
||||
public static readonly string DelegateSignatureKey = "Blazor.DelegateSignature";
|
||||
|
||||
public static readonly string RuntimeName = "Blazor.IComponent";
|
||||
|
||||
public readonly static string TagHelperKind = "Blazor.Component-0.1";
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -61,6 +61,31 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
}
|
||||
}
|
||||
|
||||
public ComponentAttributeExtensionNode(ComponentAttributeExtensionNode attributeNode)
|
||||
{
|
||||
if (attributeNode == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(attributeNode));
|
||||
}
|
||||
|
||||
AttributeName = attributeNode.AttributeName;
|
||||
AttributeStructure = attributeNode.AttributeStructure;
|
||||
BoundAttribute = attributeNode.BoundAttribute;
|
||||
PropertyName = attributeNode.BoundAttribute.GetPropertyName();
|
||||
Source = attributeNode.Source;
|
||||
TagHelper = attributeNode.TagHelper;
|
||||
|
||||
for (var i = 0; i < attributeNode.Children.Count; i++)
|
||||
{
|
||||
Children.Add(attributeNode.Children[i]);
|
||||
}
|
||||
|
||||
for (var i = 0; i < attributeNode.Diagnostics.Count; i++)
|
||||
{
|
||||
Diagnostics.Add(attributeNode.Diagnostics[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public override IntermediateNodeCollection Children { get; } = new IntermediateNodeCollection();
|
||||
|
||||
public string AttributeName { get; set; }
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Extensions;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
{
|
||||
internal class ComponentLoweringPass : IntermediateNodePassBase, IRazorOptimizationPass
|
||||
{
|
||||
// Run after our *special* tag helpers get lowered.
|
||||
public override int Order => 1000;
|
||||
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
var @namespace = documentNode.FindPrimaryNamespace();
|
||||
|
|
@ -26,20 +27,28 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
var nodes = documentNode.FindDescendantNodes<TagHelperIntermediateNode>();
|
||||
for (var i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
var count = 0;
|
||||
var node = nodes[i];
|
||||
if (node.TagHelpers.Count > 1)
|
||||
for (var j = 0; j < node.TagHelpers.Count; j++)
|
||||
{
|
||||
node.Diagnostics.Add(BlazorDiagnosticFactory.Create_MultipleComponents(node.Source, node.TagName, node.TagHelpers));
|
||||
}
|
||||
if (node.TagHelpers[j].IsComponentTagHelper())
|
||||
{
|
||||
// Only allow a single component tag helper per element. We also have some *special* tag helpers
|
||||
// and they should have already been processed by now.
|
||||
if (count++ > 1)
|
||||
{
|
||||
node.Diagnostics.Add(BlazorDiagnosticFactory.Create_MultipleComponents(node.Source, node.TagName, node.TagHelpers));
|
||||
break;
|
||||
}
|
||||
|
||||
RewriteUsage(node, node.TagHelpers[0]);
|
||||
RewriteUsage(node, node.TagHelpers[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RewriteUsage(TagHelperIntermediateNode node, TagHelperDescriptor tagHelper)
|
||||
{
|
||||
// Ignore Kind here. Some versions of Razor have a bug in the serializer that ignores it.
|
||||
|
||||
// We need to surround the contents of the node with open and close nodes to ensure the component
|
||||
// is scoped correctly.
|
||||
node.Children.Insert(0, new ComponentOpenExtensionNode()
|
||||
|
|
|
|||
|
|
@ -12,10 +12,6 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
{
|
||||
internal class ComponentTagHelperDescriptorProvider : RazorEngineFeatureBase, ITagHelperDescriptorProvider
|
||||
{
|
||||
public static readonly string DelegateSignatureMetadata = "Blazor.DelegateSignature";
|
||||
|
||||
public readonly static string ComponentTagHelperKind = ComponentDocumentClassifierPass.ComponentDocumentKind;
|
||||
|
||||
private static readonly SymbolDisplayFormat FullNameTypeDisplayFormat =
|
||||
SymbolDisplayFormat.FullyQualifiedFormat
|
||||
.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)
|
||||
|
|
@ -78,12 +74,12 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
var typeName = type.ToDisplayString(FullNameTypeDisplayFormat);
|
||||
var assemblyName = type.ContainingAssembly.Identity.Name;
|
||||
|
||||
var builder = TagHelperDescriptorBuilder.Create(ComponentTagHelperKind, typeName, assemblyName);
|
||||
var builder = TagHelperDescriptorBuilder.Create(BlazorMetadata.Component.TagHelperKind, typeName, assemblyName);
|
||||
builder.SetTypeName(typeName);
|
||||
|
||||
// This opts out this 'component' tag helper for any processing that's specific to the default
|
||||
// Razor ITagHelper runtime.
|
||||
builder.Metadata[TagHelperMetadata.Runtime.Name] = "Blazor.IComponent";
|
||||
builder.Metadata[TagHelperMetadata.Runtime.Name] = BlazorMetadata.Component.RuntimeName;
|
||||
|
||||
var xml = type.GetDocumentationCommentXml();
|
||||
if (!string.IsNullOrEmpty(xml))
|
||||
|
|
@ -114,7 +110,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
|
||||
if (property.kind == PropertyKind.Delegate)
|
||||
{
|
||||
pb.Metadata.Add(DelegateSignatureMetadata, bool.TrueString);
|
||||
pb.Metadata.Add(BlazorMetadata.Component.DelegateSignatureKey, bool.TrueString);
|
||||
}
|
||||
|
||||
xml = property.property.GetDocumentationCommentXml();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,283 @@
|
|||
// 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;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
{
|
||||
// We use some tag helpers that can be applied directly to HTML elements. When
|
||||
// that happens, the default lowering pass will map the whole element as a tag helper.
|
||||
//
|
||||
// This phase exists to turn these 'orphan' tag helpers back into HTML elements so that
|
||||
// go down the proper path for rendering.
|
||||
internal class OrphanTagHelperLoweringPass : IntermediateNodePassBase, IRazorOptimizationPass
|
||||
{
|
||||
// Run after our other passes
|
||||
public override int Order => 1000;
|
||||
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
if (codeDocument == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(codeDocument));
|
||||
}
|
||||
|
||||
if (documentNode == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(documentNode));
|
||||
}
|
||||
|
||||
var visitor = new Visitor();
|
||||
visitor.Visit(documentNode);
|
||||
|
||||
for (var i = 0; i < visitor.References.Count; i++)
|
||||
{
|
||||
var reference = visitor.References[i];
|
||||
var tagHelperNode = (TagHelperIntermediateNode)reference.Node;
|
||||
|
||||
// Since this is converted from a tag helper to a regular old HTMl element, we need to
|
||||
// flatten out the structure
|
||||
var insert = new List<IntermediateNode>();
|
||||
insert.Add(new HtmlContentIntermediateNode()
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new IntermediateToken()
|
||||
{
|
||||
Content = "<" + tagHelperNode.TagName + " ",
|
||||
Kind = TokenKind.Html,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
for (var j = 0; j < tagHelperNode.Diagnostics.Count; j++)
|
||||
{
|
||||
insert[0].Diagnostics.Add(tagHelperNode.Diagnostics[j]);
|
||||
}
|
||||
|
||||
// We expect to see a body node, followed by a series of property/attribute nodes
|
||||
// This isn't really the order we want, so skip over the body for now, and we'll do another
|
||||
// pass that merges it in.
|
||||
for (var j = 0; j < tagHelperNode.Children.Count; j++)
|
||||
{
|
||||
if (tagHelperNode.Children[j] is TagHelperBodyIntermediateNode)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if (tagHelperNode.Children[j] is TagHelperHtmlAttributeIntermediateNode htmlAttribute)
|
||||
{
|
||||
if (htmlAttribute.Children.Count == 0)
|
||||
{
|
||||
RewriteEmptyAttributeContent(insert, htmlAttribute);
|
||||
}
|
||||
else if (htmlAttribute.Children[0] is HtmlContentIntermediateNode)
|
||||
{
|
||||
RewriteHtmlAttributeContent(insert, htmlAttribute);
|
||||
}
|
||||
else if (htmlAttribute.Children[0] is CSharpExpressionAttributeValueIntermediateNode csharpContent)
|
||||
{
|
||||
RewriteCSharpAttributeContent(insert, htmlAttribute);
|
||||
}
|
||||
}
|
||||
else if (tagHelperNode.Children[j] is ComponentAttributeExtensionNode attributeNode)
|
||||
{
|
||||
RewriteComponentAttributeContent(insert, attributeNode);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We shouldn't see anything else here, but just in case, add the content as-is.
|
||||
insert.Add(tagHelperNode.Children[j]);
|
||||
}
|
||||
}
|
||||
|
||||
if (tagHelperNode.TagMode == TagMode.SelfClosing)
|
||||
{
|
||||
insert.Add(new HtmlContentIntermediateNode()
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new IntermediateToken()
|
||||
{
|
||||
Content = "/>",
|
||||
Kind = TokenKind.Html,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (tagHelperNode.TagMode == TagMode.StartTagOnly)
|
||||
{
|
||||
insert.Add(new HtmlContentIntermediateNode()
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new IntermediateToken()
|
||||
{
|
||||
Content = ">",
|
||||
Kind = TokenKind.Html,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
insert.Add(new HtmlContentIntermediateNode()
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new IntermediateToken()
|
||||
{
|
||||
Content = ">",
|
||||
Kind = TokenKind.Html,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (var j = 0; j < tagHelperNode.Children.Count; j++)
|
||||
{
|
||||
if (tagHelperNode.Children[j] is TagHelperBodyIntermediateNode bodyNode)
|
||||
{
|
||||
insert.AddRange(bodyNode.Children);
|
||||
}
|
||||
}
|
||||
|
||||
insert.Add(new HtmlContentIntermediateNode()
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new IntermediateToken()
|
||||
{
|
||||
Content = "</" + tagHelperNode.TagName + ">",
|
||||
Kind = TokenKind.Html,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reference.InsertAfter(insert);
|
||||
reference.Remove();
|
||||
}
|
||||
}
|
||||
private static void RewriteEmptyAttributeContent(List<IntermediateNode> nodes, TagHelperHtmlAttributeIntermediateNode node)
|
||||
{
|
||||
nodes.Add(new HtmlContentIntermediateNode()
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new IntermediateToken()
|
||||
{
|
||||
Content = node.AttributeName + " ",
|
||||
Kind = TokenKind.Html,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void RewriteHtmlAttributeContent(List<IntermediateNode> nodes, TagHelperHtmlAttributeIntermediateNode node)
|
||||
{
|
||||
switch (node.AttributeStructure)
|
||||
{
|
||||
case AttributeStructure.Minimized:
|
||||
nodes.Add(new HtmlContentIntermediateNode()
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new IntermediateToken()
|
||||
{
|
||||
Content = node.AttributeName + " ",
|
||||
Kind = TokenKind.Html,
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
// Blazor doesn't really care about preserving the fidelity of the attributes.
|
||||
case AttributeStructure.NoQuotes:
|
||||
case AttributeStructure.SingleQuotes:
|
||||
case AttributeStructure.DoubleQuotes:
|
||||
|
||||
var htmlNode = new HtmlContentIntermediateNode();
|
||||
nodes.Add(htmlNode);
|
||||
|
||||
htmlNode.Children.Add(new IntermediateToken()
|
||||
{
|
||||
Content = node.AttributeName + "=\"",
|
||||
Kind = TokenKind.Html,
|
||||
});
|
||||
|
||||
for (var i = 0; i < node.Children[0].Children.Count; i++)
|
||||
{
|
||||
htmlNode.Children.Add(node.Children[0].Children[i]);
|
||||
}
|
||||
|
||||
htmlNode.Children.Add(new IntermediateToken()
|
||||
{
|
||||
Content = "\" ",
|
||||
Kind = TokenKind.Html,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void RewriteCSharpAttributeContent(List<IntermediateNode> nodes, TagHelperHtmlAttributeIntermediateNode node)
|
||||
{
|
||||
var attributeNode = new HtmlAttributeIntermediateNode()
|
||||
{
|
||||
AttributeName = node.AttributeName,
|
||||
Prefix = "=\"",
|
||||
Suffix = "\"",
|
||||
};
|
||||
nodes.Add(attributeNode);
|
||||
|
||||
var valueNode = new CSharpExpressionAttributeValueIntermediateNode();
|
||||
attributeNode.Children.Add(valueNode);
|
||||
|
||||
for (var i = 0; i < node.Children[0].Children.Count; i++)
|
||||
{
|
||||
valueNode.Children.Add(node.Children[0].Children[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private void RewriteComponentAttributeContent(List<IntermediateNode> nodes, ComponentAttributeExtensionNode node)
|
||||
{
|
||||
var attributeNode = new HtmlAttributeIntermediateNode()
|
||||
{
|
||||
AttributeName = node.AttributeName,
|
||||
Prefix = "=\"",
|
||||
Suffix = "\"",
|
||||
};
|
||||
nodes.Add(attributeNode);
|
||||
|
||||
var valueNode = new CSharpExpressionAttributeValueIntermediateNode();
|
||||
attributeNode.Children.Add(valueNode);
|
||||
|
||||
for (var i = 0; i < node.Children[0].Children.Count; i++)
|
||||
{
|
||||
valueNode.Children.Add(node.Children[0].Children[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private class Visitor : IntermediateNodeWalker
|
||||
{
|
||||
public List<IntermediateNodeReference> References = new List<IntermediateNodeReference>();
|
||||
|
||||
public override void VisitTagHelper(TagHelperIntermediateNode node)
|
||||
{
|
||||
base.VisitTagHelper(node);
|
||||
|
||||
// Use a post-order traversal because we're going to rewite tag helper nodes, and thus
|
||||
// change the parent nodes.
|
||||
//
|
||||
// This ensures that we operate the leaf nodes first.
|
||||
if (!node.TagHelpers.Any(t => t.IsComponentTagHelper()))
|
||||
{
|
||||
References.Add(new IntermediateNodeReference(Parent, node));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -60,6 +60,51 @@ namespace Microsoft.AspNetCore.Blazor.Razor {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Binds the provided expression to the '{0}' property and a change event delegate to the '{1}' property of the component..
|
||||
/// </summary>
|
||||
internal static string BindTagHelper_Component_Documentation {
|
||||
get {
|
||||
return ResourceManager.GetString("BindTagHelper_Component_Documentation", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Binds the provided expression to the '{0}' attribute and a change event delegate to the '{1}' attribute..
|
||||
/// </summary>
|
||||
internal static string BindTagHelper_Element_Documentation {
|
||||
get {
|
||||
return ResourceManager.GetString("BindTagHelper_Element_Documentation", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Specifies a format to convert the value specified by the '{0}' attribute. The format string can currently only be used with expressions of type <code>DateTime</code>..
|
||||
/// </summary>
|
||||
internal static string BindTagHelper_Element_Format_Documentation {
|
||||
get {
|
||||
return ResourceManager.GetString("BindTagHelper_Element_Format_Documentation", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Binds the provided expression to an attribute and a change event, based on the naming of the bind attribute. For example: <code>bind-value-onchange="..."</code> will assign the current value of the expression to the 'value' attribute, and assign a delegate that attempts to set the value to the 'onchange' attribute..
|
||||
/// </summary>
|
||||
internal static string BindTagHelper_Fallback_Documentation {
|
||||
get {
|
||||
return ResourceManager.GetString("BindTagHelper_Fallback_Documentation", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Specifies a format to convert the value specified by the corresponding bind attribute. For example: <code>format-value="..."</code> will apply a format string to the value specified in <code>bind-value-...</code>. The format string can currently only be used with expressions of type <code>DateTime</code>..
|
||||
/// </summary>
|
||||
internal static string BindTagHelper_Fallback_Format_Documentation {
|
||||
get {
|
||||
return ResourceManager.GetString("BindTagHelper_Fallback_Format_Documentation", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Declares an interface implementation for the current document..
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -117,6 +117,21 @@
|
|||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="BindTagHelper_Component_Documentation" xml:space="preserve">
|
||||
<value>Binds the provided expression to the '{0}' property and a change event delegate to the '{1}' property of the component.</value>
|
||||
</data>
|
||||
<data name="BindTagHelper_Element_Documentation" xml:space="preserve">
|
||||
<value>Binds the provided expression to the '{0}' attribute and a change event delegate to the '{1}' attribute.</value>
|
||||
</data>
|
||||
<data name="BindTagHelper_Element_Format_Documentation" xml:space="preserve">
|
||||
<value>Specifies a format to convert the value specified by the '{0}' attribute. The format string can currently only be used with expressions of type <code>DateTime</code>.</value>
|
||||
</data>
|
||||
<data name="BindTagHelper_Fallback_Documentation" xml:space="preserve">
|
||||
<value>Binds the provided expression to an attribute and a change event, based on the naming of the bind attribute. For example: <code>bind-value-onchange="..."</code> will assign the current value of the expression to the 'value' attribute, and assign a delegate that attempts to set the value to the 'onchange' attribute.</value>
|
||||
</data>
|
||||
<data name="BindTagHelper_Fallback_Format_Documentation" xml:space="preserve">
|
||||
<value>Specifies a format to convert the value specified by the corresponding bind attribute. For example: <code>format-value="..."</code> will apply a format string to the value specified in <code>bind-value-...</code>. The format string can currently only be used with expressions of type <code>DateTime</code>.</value>
|
||||
</data>
|
||||
<data name="ImplementsDirective_Description" xml:space="preserve">
|
||||
<value>Declares an interface implementation for the current document.</value>
|
||||
</data>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
throw new ArgumentNullException(nameof(attribute));
|
||||
}
|
||||
|
||||
var key = ComponentTagHelperDescriptorProvider.DelegateSignatureMetadata;
|
||||
var key = BlazorMetadata.Component.DelegateSignatureKey;
|
||||
return
|
||||
attribute.Metadata.TryGetValue(key, out var value) &&
|
||||
string.Equals(value, bool.TrueString);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
// 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 Microsoft.AspNetCore.Razor.Language;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
{
|
||||
internal static class TagHelperDescriptorExtensions
|
||||
{
|
||||
public static bool IsBindTagHelper(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
return
|
||||
tagHelper.Metadata.TryGetValue(BlazorMetadata.SpecialKindKey, out var kind) &&
|
||||
string.Equals(BlazorMetadata.Bind.TagHelperKind, kind);
|
||||
}
|
||||
|
||||
public static bool IsFallbackBindTagHelper(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
return
|
||||
tagHelper.IsBindTagHelper() &&
|
||||
tagHelper.Metadata.TryGetValue(BlazorMetadata.Bind.FallbackKey, out var fallback) &&
|
||||
string.Equals(bool.TrueString, fallback);
|
||||
}
|
||||
|
||||
public static bool IsInputElementBindTagHelper(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
return
|
||||
tagHelper.IsBindTagHelper() &&
|
||||
tagHelper.TagMatchingRules.Count == 1 &&
|
||||
string.Equals("input", tagHelper.TagMatchingRules[0].TagName);
|
||||
}
|
||||
|
||||
public static bool IsInputElementFallbackBindTagHelper(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
return
|
||||
tagHelper.IsInputElementBindTagHelper() &&
|
||||
!tagHelper.Metadata.ContainsKey(BlazorMetadata.Bind.TypeAttribute);
|
||||
}
|
||||
|
||||
public static string GetValueAttributeName(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
tagHelper.Metadata.TryGetValue(BlazorMetadata.Bind.ValueAttribute, out var result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static string GetChangeAttributeName(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
tagHelper.Metadata.TryGetValue(BlazorMetadata.Bind.ChangeAttribute, out var result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static bool IsComponentTagHelper(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
return !tagHelper.Metadata.ContainsKey(BlazorMetadata.SpecialKindKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
// 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.AspNetCore.Blazor.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Infrastructure for the discovery of <c>bind</c> attributes for markup elements.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To extend the set of <c>bind</c> attributes, define a public class named
|
||||
/// <c>BindAttributes</c> and annotate it with the appropriate attributes.
|
||||
/// </remarks>
|
||||
|
||||
// Handles cases like <input bind="..." /> - this is a fallback and will be ignored
|
||||
// when a specific type attribute is applied.
|
||||
[BindInputElement(null, null, "value", "onchange")]
|
||||
|
||||
// For right now, the BrowserRenderer translates the value attribute to the checked attribute.
|
||||
[BindInputElement("checkbox", null, "value", "onchange")]
|
||||
[BindInputElement("text", null, "value", "onchange")]
|
||||
|
||||
[BindElement("select", null, "value", "onchange")]
|
||||
public static class BindAttributes
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
// 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.AspNetCore.Blazor.Components
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||
public sealed class BindElementAttribute : Attribute
|
||||
{
|
||||
public BindElementAttribute(string element, string suffix, string valueAttribute, string changeAttribute)
|
||||
{
|
||||
if (element == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(element));
|
||||
}
|
||||
|
||||
if (valueAttribute == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(valueAttribute));
|
||||
}
|
||||
|
||||
if (changeAttribute == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(changeAttribute));
|
||||
}
|
||||
|
||||
Element = element;
|
||||
ValueAttribute = valueAttribute;
|
||||
ChangeAttribute = changeAttribute;
|
||||
}
|
||||
|
||||
public string Element { get; }
|
||||
|
||||
// Set this to `value` for `bind-value` - set this to null for `bind`
|
||||
public string Suffix { get; }
|
||||
|
||||
public string ValueAttribute { get; }
|
||||
|
||||
public string ChangeAttribute { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// 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.AspNetCore.Blazor.Components
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||
public sealed class BindInputElementAttribute : Attribute
|
||||
{
|
||||
public BindInputElementAttribute(string type, string suffix, string valueAttribute, string changeAttribute)
|
||||
{
|
||||
if (valueAttribute == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(valueAttribute));
|
||||
}
|
||||
|
||||
if (changeAttribute == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(changeAttribute));
|
||||
}
|
||||
|
||||
Type = type;
|
||||
Suffix = suffix;
|
||||
ValueAttribute = valueAttribute;
|
||||
ChangeAttribute = changeAttribute;
|
||||
}
|
||||
|
||||
public string Type { get; }
|
||||
|
||||
public string Suffix { get; }
|
||||
|
||||
public string ValueAttribute { get; }
|
||||
|
||||
public string ChangeAttribute { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,64 @@ namespace Microsoft.AspNetCore.Blazor.Components
|
|||
value == default ? null
|
||||
: (format == null ? value.ToString() : value.ToString(format));
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static UIEventHandler SetValueHandler(Action<string> setter, string existingValue)
|
||||
{
|
||||
return _ => setter((string)((UIChangeEventArgs)_).Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static UIEventHandler SetValueHandler(Action<bool> setter, bool existingValue)
|
||||
{
|
||||
return _ => setter((bool)((UIChangeEventArgs)_).Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static UIEventHandler SetValueHandler(Action<int> setter, int existingValue)
|
||||
{
|
||||
return _ => setter(int.Parse((string)((UIChangeEventArgs)_).Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static UIEventHandler SetValueHandler(Action<DateTime> setter, DateTime existingValue)
|
||||
{
|
||||
return _ => SetDateTimeValue(setter, (object)((UIChangeEventArgs)_).Value, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static UIEventHandler SetValueHandler(Action<DateTime> setter, DateTime existingValue, string format)
|
||||
{
|
||||
return _ => SetDateTimeValue(setter, (object)((UIChangeEventArgs)_).Value, format);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static UIEventHandler SetValueHandler<T>(Action<T> setter, T existingValue)
|
||||
{
|
||||
if (!typeof(T).IsEnum)
|
||||
{
|
||||
throw new ArgumentException($"'bind' does not accept values of type {typeof(T).FullName}. To read and write this value type, wrap it in a property of type string with suitable getters and setters.");
|
||||
}
|
||||
|
||||
return _ =>
|
||||
{
|
||||
var value = (string)((UIChangeEventArgs)_).Value;
|
||||
var parsed = (T)Enum.Parse(typeof(T), value);
|
||||
setter(parsed);
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
|
|
@ -35,6 +93,12 @@ namespace Microsoft.AspNetCore.Blazor.Components
|
|||
public static Action<object> SetValue(Action<bool> setter, bool existingValue)
|
||||
=> objValue => setter((bool)objValue);
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static Action<object> SetValue(Action<int> setter, int existingValue)
|
||||
=> objValue => setter(int.Parse((string)objValue));
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,491 @@
|
|||
// 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 Microsoft.AspNetCore.Blazor.Test.Helpers;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Build.Test
|
||||
{
|
||||
public class BindRazorIntegrationTest : RazorIntegrationTestBase
|
||||
{
|
||||
internal override bool UseTwoPhaseCompilation => true;
|
||||
|
||||
[Fact]
|
||||
public void Render_BindToComponent_SpecifiesValue_WithMatchingProperties()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
public int Value { get; set; }
|
||||
|
||||
public Action<int> ValueChanged { get; set; }
|
||||
}
|
||||
}"));
|
||||
|
||||
var component = CompileToComponent(@"
|
||||
@addTagHelper *, TestAssembly
|
||||
<MyComponent bind-Value=""ParentValue"" />
|
||||
@functions {
|
||||
public int ParentValue { get; set; } = 42;
|
||||
}");
|
||||
|
||||
// Act
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
frames,
|
||||
frame => AssertFrame.Component(frame, "Test.MyComponent", 3, 0),
|
||||
frame => AssertFrame.Attribute(frame, "Value", 42, 1),
|
||||
frame => AssertFrame.Attribute(frame, "ValueChanged", typeof(Action<int>), 2),
|
||||
frame => AssertFrame.Whitespace(frame, 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_BindToComponent_SpecifiesValue_WithoutMatchingProperties()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
public class MyComponent : BlazorComponent, IComponent
|
||||
{
|
||||
void IComponent.SetParameters(ParameterCollection parameters)
|
||||
{
|
||||
}
|
||||
}
|
||||
}"));
|
||||
|
||||
var component = CompileToComponent(@"
|
||||
@addTagHelper *, TestAssembly
|
||||
<MyComponent bind-Value=""ParentValue"" />
|
||||
@functions {
|
||||
public int ParentValue { get; set; } = 42;
|
||||
}");
|
||||
|
||||
// Act
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
frames,
|
||||
frame => AssertFrame.Component(frame, "Test.MyComponent", 3, 0),
|
||||
frame => AssertFrame.Attribute(frame, "Value", 42, 1),
|
||||
frame => AssertFrame.Attribute(frame, "ValueChanged", typeof(UIEventHandler), 2),
|
||||
frame => AssertFrame.Whitespace(frame, 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_BindToComponent_SpecifiesValueAndChangeEvent_WithMatchingProperties()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
public int Value { get; set; }
|
||||
|
||||
public Action<int> OnChanged { get; set; }
|
||||
}
|
||||
}"));
|
||||
|
||||
var component = CompileToComponent(@"
|
||||
@addTagHelper *, TestAssembly
|
||||
<MyComponent bind-Value-OnChanged=""ParentValue"" />
|
||||
@functions {
|
||||
public int ParentValue { get; set; } = 42;
|
||||
}");
|
||||
|
||||
// Act
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
frames,
|
||||
frame => AssertFrame.Component(frame, "Test.MyComponent", 3, 0),
|
||||
frame => AssertFrame.Attribute(frame, "Value", 42, 1),
|
||||
frame => AssertFrame.Attribute(frame, "OnChanged", typeof(Action<int>), 2),
|
||||
frame => AssertFrame.Whitespace(frame, 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_BindToComponent_SpecifiesValueAndChangeEvent_WithoutMatchingProperties()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
public class MyComponent : BlazorComponent, IComponent
|
||||
{
|
||||
void IComponent.SetParameters(ParameterCollection parameters)
|
||||
{
|
||||
}
|
||||
}
|
||||
}"));
|
||||
|
||||
var component = CompileToComponent(@"
|
||||
@addTagHelper *, TestAssembly
|
||||
<MyComponent bind-Value-OnChanged=""ParentValue"" />
|
||||
@functions {
|
||||
public int ParentValue { get; set; } = 42;
|
||||
}");
|
||||
|
||||
// Act
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
frames,
|
||||
frame => AssertFrame.Component(frame, "Test.MyComponent", 3, 0),
|
||||
frame => AssertFrame.Attribute(frame, "Value", 42, 1),
|
||||
frame => AssertFrame.Attribute(frame, "OnChanged", typeof(UIEventHandler), 2),
|
||||
frame => AssertFrame.Whitespace(frame, 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_BindToElement_WritesAttributes()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
[BindElement(""div"", null, ""myvalue"", ""myevent"")]
|
||||
public static class BindAttributes
|
||||
{
|
||||
}
|
||||
}"));
|
||||
|
||||
var component = CompileToComponent(@"
|
||||
@addTagHelper *, TestAssembly
|
||||
<div bind=""@ParentValue"" />
|
||||
@functions {
|
||||
public string ParentValue { get; set; } = ""hi"";
|
||||
}");
|
||||
|
||||
// Act
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
frames,
|
||||
frame => AssertFrame.Element(frame, "div", 3, 0),
|
||||
frame => AssertFrame.Attribute(frame, "myvalue", "hi", 1),
|
||||
frame => AssertFrame.Attribute(frame, "myevent", typeof(UIEventHandler), 2),
|
||||
frame => AssertFrame.Whitespace(frame, 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_BindToElementWithSuffix_WritesAttributes()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
[BindElement(""div"", ""value"", ""myvalue"", ""myevent"")]
|
||||
public static class BindAttributes
|
||||
{
|
||||
}
|
||||
}"));
|
||||
|
||||
var component = CompileToComponent(@"
|
||||
@addTagHelper *, TestAssembly
|
||||
<div bind-value=""@ParentValue"" />
|
||||
@functions {
|
||||
public string ParentValue { get; set; } = ""hi"";
|
||||
}");
|
||||
|
||||
// Act
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
frames,
|
||||
frame => AssertFrame.Element(frame, "div", 3, 0),
|
||||
frame => AssertFrame.Attribute(frame, "myvalue", "hi", 1),
|
||||
frame => AssertFrame.Attribute(frame, "myevent", typeof(UIEventHandler), 2),
|
||||
frame => AssertFrame.Whitespace(frame, 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_BindDuplicates_ReportsDiagnostic()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
[BindElement(""div"", ""value"", ""myvalue2"", ""myevent2"")]
|
||||
[BindElement(""div"", ""value"", ""myvalue"", ""myevent"")]
|
||||
public static class BindAttributes
|
||||
{
|
||||
}
|
||||
}"));
|
||||
|
||||
// Act
|
||||
var result = CompileToCSharp(@"
|
||||
@addTagHelper *, TestAssembly
|
||||
<div bind-value=""@ParentValue"" />
|
||||
@functions {
|
||||
public string ParentValue { get; set; } = ""hi"";
|
||||
}");
|
||||
|
||||
// Assert
|
||||
var diagnostic = Assert.Single(result.Diagnostics);
|
||||
Assert.Equal("BL9989", diagnostic.Id);
|
||||
Assert.Equal(
|
||||
"The attribute 'bind-value' was matched by multiple bind attributes. Duplicates:" + Environment.NewLine +
|
||||
"Test.BindAttributes" + Environment.NewLine +
|
||||
"Test.BindAttributes",
|
||||
diagnostic.GetMessage());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_BuiltIn_BindToInputWithoutType_WritesAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var component = CompileToComponent(@"
|
||||
@addTagHelper *, TestAssembly
|
||||
<input bind=""@ParentValue"" />
|
||||
@functions {
|
||||
public int ParentValue { get; set; } = 42;
|
||||
}");
|
||||
|
||||
// Act
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
frames,
|
||||
frame => AssertFrame.Element(frame, "input", 3, 0),
|
||||
frame => AssertFrame.Attribute(frame, "value", "42", 1),
|
||||
frame => AssertFrame.Attribute(frame, "onchange", typeof(UIEventHandler), 2),
|
||||
frame => AssertFrame.Whitespace(frame, 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_BuiltIn_BindToInputText_WithFormat_WritesAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var component = CompileToComponent(@"
|
||||
@addTagHelper *, TestAssembly
|
||||
<input type=""text"" bind=""@CurrentDate"" format-value=""MM/dd/yyyy""/>
|
||||
@functions {
|
||||
public DateTime CurrentDate { get; set; } = new DateTime(2018, 1, 1);
|
||||
}");
|
||||
|
||||
// Act
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
frames,
|
||||
frame => AssertFrame.Element(frame, "input", 4, 0),
|
||||
frame => AssertFrame.Attribute(frame, "type", "text", 1),
|
||||
frame => AssertFrame.Attribute(frame, "value", "01/01/2018", 2),
|
||||
frame => AssertFrame.Attribute(frame, "onchange", typeof(UIEventHandler), 3),
|
||||
frame => AssertFrame.Whitespace(frame, 4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_BuiltIn_BindToInputText_WithFormatFromProperty_WritesAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var component = CompileToComponent(@"
|
||||
@addTagHelper *, TestAssembly
|
||||
<input type=""text"" bind=""@CurrentDate"" format-value=""@Format""/>
|
||||
@functions {
|
||||
public DateTime CurrentDate { get; set; } = new DateTime(2018, 1, 1);
|
||||
|
||||
public string Format { get; set; } = ""MM/dd/yyyy"";
|
||||
}");
|
||||
|
||||
// Act
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
frames,
|
||||
frame => AssertFrame.Element(frame, "input", 4, 0),
|
||||
frame => AssertFrame.Attribute(frame, "type", "text", 1),
|
||||
frame => AssertFrame.Attribute(frame, "value", "01/01/2018", 2),
|
||||
frame => AssertFrame.Attribute(frame, "onchange", typeof(UIEventHandler), 3),
|
||||
frame => AssertFrame.Whitespace(frame, 4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_BuiltIn_BindToInputText_WritesAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var component = CompileToComponent(@"
|
||||
@addTagHelper *, TestAssembly
|
||||
<input type=""text"" bind=""@ParentValue"" />
|
||||
@functions {
|
||||
public int ParentValue { get; set; } = 42;
|
||||
}");
|
||||
|
||||
// Act
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
frames,
|
||||
frame => AssertFrame.Element(frame, "input", 4, 0),
|
||||
frame => AssertFrame.Attribute(frame, "type", "text", 1),
|
||||
frame => AssertFrame.Attribute(frame, "value", "42", 2),
|
||||
frame => AssertFrame.Attribute(frame, "onchange", typeof(UIEventHandler), 3),
|
||||
frame => AssertFrame.Whitespace(frame, 4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_BuiltIn_BindToInputCheckbox_WritesAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var component = CompileToComponent(@"
|
||||
@addTagHelper *, TestAssembly
|
||||
<input type=""checkbox"" bind=""@Enabled"" />
|
||||
@functions {
|
||||
public bool Enabled { get; set; }
|
||||
}");
|
||||
|
||||
// Act
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
frames,
|
||||
frame => AssertFrame.Element(frame, "input", 4, 0),
|
||||
frame => AssertFrame.Attribute(frame, "type", "checkbox", 1),
|
||||
frame => AssertFrame.Attribute(frame, "value", "False", 2),
|
||||
frame => AssertFrame.Attribute(frame, "onchange", typeof(UIEventHandler), 3),
|
||||
frame => AssertFrame.Whitespace(frame, 4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_BindToElementFallback_WritesAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var component = CompileToComponent(@"
|
||||
@addTagHelper *, TestAssembly
|
||||
<input type=""text"" bind-value-onchange=""@ParentValue"" />
|
||||
@functions {
|
||||
public int ParentValue { get; set; } = 42;
|
||||
}");
|
||||
|
||||
// Act
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
frames,
|
||||
frame => AssertFrame.Element(frame, "input", 4, 0),
|
||||
frame => AssertFrame.Attribute(frame, "type", "text", 1),
|
||||
frame => AssertFrame.Attribute(frame, "value", "42", 2),
|
||||
frame => AssertFrame.Attribute(frame, "onchange", typeof(UIEventHandler), 3),
|
||||
frame => AssertFrame.Whitespace(frame, 4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_BindToElementFallback_WithFormat_WritesAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var component = CompileToComponent(@"
|
||||
@addTagHelper *, TestAssembly
|
||||
<input type=""text"" bind-value-onchange=""@CurrentDate"" format-value=""MM/dd"" />
|
||||
@functions {
|
||||
public DateTime CurrentDate { get; set; } = new DateTime(2018, 1, 1);
|
||||
}");
|
||||
|
||||
// Act
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
frames,
|
||||
frame => AssertFrame.Element(frame, "input", 4, 0),
|
||||
frame => AssertFrame.Attribute(frame, "type", "text", 1),
|
||||
frame => AssertFrame.Attribute(frame, "value", "01/01", 2),
|
||||
frame => AssertFrame.Attribute(frame, "onchange", typeof(UIEventHandler), 3),
|
||||
frame => AssertFrame.Whitespace(frame, 4));
|
||||
}
|
||||
|
||||
[Fact] // Additional coverage of OrphanTagHelperLoweringPass
|
||||
public void Render_BindToElementFallback_SpecifiesValueAndChangeEvent_WithCSharpAttribute()
|
||||
{
|
||||
// Arrange
|
||||
var component = CompileToComponent(@"
|
||||
@addTagHelper *, TestAssembly
|
||||
<input type=""@(""text"")"" bind-value-onchange=""@ParentValue"" visible />
|
||||
@functions {
|
||||
public int ParentValue { get; set; } = 42;
|
||||
}");
|
||||
|
||||
// Act
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
frames,
|
||||
frame => AssertFrame.Element(frame, "input", 5, 0),
|
||||
frame => AssertFrame.Attribute(frame, "visible", 1), // This gets reordered in the node writer
|
||||
frame => AssertFrame.Attribute(frame, "type", "text", 2),
|
||||
frame => AssertFrame.Attribute(frame, "value", "42", 3),
|
||||
frame => AssertFrame.Attribute(frame, "onchange", typeof(UIEventHandler), 4),
|
||||
frame => AssertFrame.Whitespace(frame, 5));
|
||||
}
|
||||
|
||||
[Fact] // Additional coverage of OrphanTagHelperLoweringPass
|
||||
public void Render_BindToElementFallback_SpecifiesValueAndChangeEvent_BodyContent()
|
||||
{
|
||||
// Arrange
|
||||
var component = CompileToComponent(@"
|
||||
@addTagHelper *, TestAssembly
|
||||
<div bind-value-onchange=""@ParentValue"">
|
||||
<span>@(42.ToString())</span>
|
||||
</div>
|
||||
@functions {
|
||||
public int ParentValue { get; set; } = 42;
|
||||
}");
|
||||
|
||||
// Act
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
frames,
|
||||
frame => AssertFrame.Element(frame, "div", 7, 0),
|
||||
frame => AssertFrame.Attribute(frame, "value", "42", 1),
|
||||
frame => AssertFrame.Attribute(frame, "onchange", typeof(UIEventHandler), 2),
|
||||
frame => AssertFrame.Whitespace(frame, 3),
|
||||
frame => AssertFrame.Element(frame, "span", 2, 4),
|
||||
frame => AssertFrame.Text(frame, "42", 5),
|
||||
frame => AssertFrame.Whitespace(frame, 6),
|
||||
frame => AssertFrame.Whitespace(frame, 7));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 System;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Blazor.RenderTree;
|
||||
using Microsoft.AspNetCore.Blazor.Test.Helpers;
|
||||
|
|
@ -293,13 +294,11 @@ namespace Test
|
|||
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
|
||||
using Microsoft.AspNetCore.Blazor;
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
public string MyAttr { get; set; }
|
||||
|
||||
public RenderFragment ChildContent { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent(
|
||||
@"<input @bind(MyValue) />
|
||||
@"<input bind=""MyValue"" />
|
||||
@functions {
|
||||
public string MyValue { get; set; } = ""Initial value"";
|
||||
}");
|
||||
|
|
@ -394,7 +394,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent(
|
||||
@"<input @bind(MyDate) />
|
||||
@"<input bind=""MyDate"" />
|
||||
@functions {
|
||||
public DateTime MyDate { get; set; } = new DateTime(2018, 3, 4, 1, 2, 3);
|
||||
}");
|
||||
|
|
@ -426,10 +426,10 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
// Arrange/Act
|
||||
var testDateFormat = "ddd yyyy-MM-dd";
|
||||
var component = CompileToComponent(
|
||||
@"<input @bind(MyDate, """ + testDateFormat + @""") />
|
||||
@functions {
|
||||
public DateTime MyDate { get; set; } = new DateTime(2018, 3, 4);
|
||||
}");
|
||||
$@"<input bind=""@MyDate"" format-value=""{testDateFormat}"" />
|
||||
@functions {{
|
||||
public DateTime MyDate {{ get; set; }} = new DateTime(2018, 3, 4);
|
||||
}}");
|
||||
var myDateProperty = component.GetType().GetProperty("MyDate");
|
||||
|
||||
// Assert
|
||||
|
|
@ -456,7 +456,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent(
|
||||
@"<input @bind(MyValue) />
|
||||
@"<input bind=""MyValue"" />
|
||||
@functions {
|
||||
public bool MyValue { get; set; } = true;
|
||||
}");
|
||||
|
|
@ -487,7 +487,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
// Arrange/Act
|
||||
var myEnumType = FullTypeName<MyEnum>();
|
||||
var component = CompileToComponent(
|
||||
$@"<input @bind(MyValue) />
|
||||
$@"<input bind=""MyValue"" />
|
||||
@functions {{
|
||||
public {myEnumType} MyValue {{ get; set; }} = {myEnumType}.{nameof(MyEnum.FirstValue)};
|
||||
}}");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
// 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.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.Extensions.DependencyModel;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor.Extensions
|
||||
{
|
||||
public abstract class BaseTagHelperDescriptorProviderTest
|
||||
{
|
||||
static BaseTagHelperDescriptorProviderTest()
|
||||
{
|
||||
var dependencyContext = DependencyContext.Load(typeof(ComponentTagHelperDescriptorProviderTest).Assembly);
|
||||
|
||||
var metadataReferences = dependencyContext.CompileLibraries
|
||||
.SelectMany(l => l.ResolveReferencePaths())
|
||||
.Select(assemblyPath => MetadataReference.CreateFromFile(assemblyPath))
|
||||
.ToArray();
|
||||
|
||||
BaseCompilation = CSharpCompilation.Create(
|
||||
"TestAssembly",
|
||||
Array.Empty<SyntaxTree>(),
|
||||
metadataReferences,
|
||||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
||||
}
|
||||
|
||||
protected static Compilation BaseCompilation { get; }
|
||||
|
||||
// For simplicity in testing, exclude the built-in components. We'll add more and we
|
||||
// don't want to update the tests when that happens.
|
||||
protected static TagHelperDescriptor[] ExcludeBuiltInComponents(TagHelperDescriptorProviderContext context)
|
||||
{
|
||||
return context.Results
|
||||
.Where(c => c.AssemblyName == "TestAssembly")
|
||||
.OrderBy(c => c.Name)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,682 @@
|
|||
// 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.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Razor;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor.Extensions
|
||||
{
|
||||
public class BindTagHelperDescriptorProviderTest : BaseTagHelperDescriptorProviderTest
|
||||
{
|
||||
[Fact]
|
||||
public void Excecute_FindsBindTagHelperOnComponentType_CreatesDescriptor()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = BaseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(@"
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
public class MyComponent : IComponent
|
||||
{
|
||||
public void Init(RenderHandle renderHandle) { }
|
||||
|
||||
public void SetParameters(ParameterCollection parameters) { }
|
||||
|
||||
public string MyProperty { get; set; }
|
||||
|
||||
public Action<string> MyPropertyChanged { get; set; }
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
||||
Assert.Empty(compilation.GetDiagnostics());
|
||||
|
||||
var context = TagHelperDescriptorProviderContext.Create();
|
||||
context.SetCompilation(compilation);
|
||||
|
||||
// We run after component discovery and depend on the results.
|
||||
var componentProvider = new ComponentTagHelperDescriptorProvider();
|
||||
componentProvider.Execute(context);
|
||||
|
||||
var provider = new BindTagHelperDescriptorProvider();
|
||||
|
||||
// Act
|
||||
provider.Execute(context);
|
||||
|
||||
// Assert
|
||||
var matches = GetBindTagHelpers(context);
|
||||
var bind = Assert.Single(matches);
|
||||
|
||||
// These are features Bind Tags Helpers don't use. Verifying them once here and
|
||||
// then ignoring them.
|
||||
Assert.Empty(bind.AllowedChildTags);
|
||||
Assert.Null(bind.TagOutputHint);
|
||||
|
||||
// These are features that are invariants of all Bind Tag Helpers. Verifying them once
|
||||
// here and then ignoring them.
|
||||
Assert.Empty(bind.Diagnostics);
|
||||
Assert.False(bind.HasErrors);
|
||||
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, bind.Kind);
|
||||
Assert.Equal(BlazorMetadata.Bind.RuntimeName, bind.Metadata[TagHelperMetadata.Runtime.Name]);
|
||||
Assert.False(bind.IsDefaultKind());
|
||||
Assert.False(bind.KindUsesDefaultTagHelperRuntime());
|
||||
|
||||
Assert.Equal("MyProperty", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]);
|
||||
Assert.Equal("MyPropertyChanged", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]);
|
||||
|
||||
Assert.Equal(
|
||||
"Binds the provided expression to the 'MyProperty' property and a change event " +
|
||||
"delegate to the 'MyPropertyChanged' property of the component.",
|
||||
bind.Documentation);
|
||||
|
||||
// These are all trivally derived from the assembly/namespace/type name
|
||||
Assert.Equal("TestAssembly", bind.AssemblyName);
|
||||
Assert.Equal("Test.MyComponent", bind.Name);
|
||||
Assert.Equal("Test.MyComponent", bind.DisplayName);
|
||||
Assert.Equal("Test.MyComponent", bind.GetTypeName());
|
||||
|
||||
var rule = Assert.Single(bind.TagMatchingRules);
|
||||
Assert.Empty(rule.Diagnostics);
|
||||
Assert.False(rule.HasErrors);
|
||||
Assert.Null(rule.ParentTag);
|
||||
Assert.Equal("MyComponent", rule.TagName);
|
||||
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
|
||||
|
||||
var requiredAttribute = Assert.Single(rule.Attributes);
|
||||
Assert.Empty(requiredAttribute.Diagnostics);
|
||||
Assert.Equal("bind-MyProperty", requiredAttribute.DisplayName);
|
||||
Assert.Equal("bind-MyProperty", requiredAttribute.Name);
|
||||
Assert.Equal(RequiredAttributeDescriptor.NameComparisonMode.FullMatch, requiredAttribute.NameComparison);
|
||||
Assert.Null(requiredAttribute.Value);
|
||||
Assert.Equal(RequiredAttributeDescriptor.ValueComparisonMode.None, requiredAttribute.ValueComparison);
|
||||
|
||||
var attribute = Assert.Single(bind.BoundAttributes);
|
||||
|
||||
// Invariants
|
||||
Assert.Empty(attribute.Diagnostics);
|
||||
Assert.False(attribute.HasErrors);
|
||||
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, attribute.Kind);
|
||||
Assert.False(attribute.IsDefaultKind());
|
||||
Assert.False(attribute.HasIndexer);
|
||||
Assert.Null(attribute.IndexerNamePrefix);
|
||||
Assert.Null(attribute.IndexerTypeName);
|
||||
Assert.False(attribute.IsIndexerBooleanProperty);
|
||||
Assert.False(attribute.IsIndexerStringProperty);
|
||||
|
||||
Assert.Equal(
|
||||
"Binds the provided expression to the 'MyProperty' property and a change event " +
|
||||
"delegate to the 'MyPropertyChanged' property of the component.",
|
||||
attribute.Documentation);
|
||||
|
||||
Assert.Equal("bind-MyProperty", attribute.Name);
|
||||
Assert.Equal("MyProperty", attribute.GetPropertyName());
|
||||
Assert.Equal("string Test.MyComponent.MyProperty", attribute.DisplayName);
|
||||
|
||||
// Defined from the property type
|
||||
Assert.Equal("System.String", attribute.TypeName);
|
||||
Assert.True(attribute.IsStringProperty);
|
||||
Assert.False(attribute.IsBooleanProperty);
|
||||
Assert.False(attribute.IsEnum);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Excecute_NoMatchedPropertiesOnComponent_IgnoresComponent()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = BaseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(@"
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
public class MyComponent : IComponent
|
||||
{
|
||||
public void Init(RenderHandle renderHandle) { }
|
||||
|
||||
public void SetParameters(ParameterCollection parameters) { }
|
||||
|
||||
public string MyProperty { get; set; }
|
||||
|
||||
public Action<string> MyPropertyChangedNotMatch { get; set; }
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
||||
Assert.Empty(compilation.GetDiagnostics());
|
||||
|
||||
var context = TagHelperDescriptorProviderContext.Create();
|
||||
context.SetCompilation(compilation);
|
||||
|
||||
// We run after component discovery and depend on the results.
|
||||
var componentProvider = new ComponentTagHelperDescriptorProvider();
|
||||
componentProvider.Execute(context);
|
||||
|
||||
var provider = new BindTagHelperDescriptorProvider();
|
||||
|
||||
// Act
|
||||
provider.Execute(context);
|
||||
|
||||
// Assert
|
||||
var matches = GetBindTagHelpers(context);
|
||||
Assert.Empty(matches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Excecute_BindOnElement_CreatesDescriptor()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = BaseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(@"
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
[BindElement(""div"", null, ""myprop"", ""myevent"")]
|
||||
public class BindAttributes
|
||||
{
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
||||
Assert.Empty(compilation.GetDiagnostics());
|
||||
|
||||
var context = TagHelperDescriptorProviderContext.Create();
|
||||
context.SetCompilation(compilation);
|
||||
|
||||
var provider = new BindTagHelperDescriptorProvider();
|
||||
|
||||
// Act
|
||||
provider.Execute(context);
|
||||
|
||||
// Assert
|
||||
var matches = GetBindTagHelpers(context);
|
||||
var bind = Assert.Single(matches);
|
||||
|
||||
// These are features Bind Tags Helpers don't use. Verifying them once here and
|
||||
// then ignoring them.
|
||||
Assert.Empty(bind.AllowedChildTags);
|
||||
Assert.Null(bind.TagOutputHint);
|
||||
|
||||
// These are features that are invariants of all Bind Tag Helpers. Verifying them once
|
||||
// here and then ignoring them.
|
||||
Assert.Empty(bind.Diagnostics);
|
||||
Assert.False(bind.HasErrors);
|
||||
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, bind.Kind);
|
||||
Assert.Equal(BlazorMetadata.Bind.RuntimeName, bind.Metadata[TagHelperMetadata.Runtime.Name]);
|
||||
Assert.False(bind.IsDefaultKind());
|
||||
Assert.False(bind.KindUsesDefaultTagHelperRuntime());
|
||||
|
||||
Assert.Equal("myprop", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]);
|
||||
Assert.Equal("myevent", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]);
|
||||
Assert.False(bind.IsInputElementBindTagHelper());
|
||||
Assert.False(bind.IsInputElementFallbackBindTagHelper());
|
||||
|
||||
Assert.Equal(
|
||||
"Binds the provided expression to the 'myprop' attribute and a change event " +
|
||||
"delegate to the 'myevent' attribute.",
|
||||
bind.Documentation);
|
||||
|
||||
// These are all trivally derived from the assembly/namespace/type name
|
||||
Assert.Equal("TestAssembly", bind.AssemblyName);
|
||||
Assert.Equal("Bind", bind.Name);
|
||||
Assert.Equal("Test.BindAttributes", bind.DisplayName);
|
||||
Assert.Equal("Test.BindAttributes", bind.GetTypeName());
|
||||
|
||||
// The tag matching rule for a bind-Component is always the component name + the attribute name
|
||||
var rule = Assert.Single(bind.TagMatchingRules);
|
||||
Assert.Empty(rule.Diagnostics);
|
||||
Assert.False(rule.HasErrors);
|
||||
Assert.Null(rule.ParentTag);
|
||||
Assert.Equal("div", rule.TagName);
|
||||
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
|
||||
|
||||
var requiredAttribute = Assert.Single(rule.Attributes);
|
||||
Assert.Empty(requiredAttribute.Diagnostics);
|
||||
Assert.Equal("bind", requiredAttribute.DisplayName);
|
||||
Assert.Equal("bind", requiredAttribute.Name);
|
||||
Assert.Equal(RequiredAttributeDescriptor.NameComparisonMode.FullMatch, requiredAttribute.NameComparison);
|
||||
Assert.Null(requiredAttribute.Value);
|
||||
Assert.Equal(RequiredAttributeDescriptor.ValueComparisonMode.None, requiredAttribute.ValueComparison);
|
||||
|
||||
var attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("bind"));
|
||||
|
||||
// Invariants
|
||||
Assert.Empty(attribute.Diagnostics);
|
||||
Assert.False(attribute.HasErrors);
|
||||
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, attribute.Kind);
|
||||
Assert.False(attribute.IsDefaultKind());
|
||||
Assert.False(attribute.HasIndexer);
|
||||
Assert.Null(attribute.IndexerNamePrefix);
|
||||
Assert.Null(attribute.IndexerTypeName);
|
||||
Assert.False(attribute.IsIndexerBooleanProperty);
|
||||
Assert.False(attribute.IsIndexerStringProperty);
|
||||
|
||||
Assert.Equal(
|
||||
"Binds the provided expression to the 'myprop' attribute and a change event " +
|
||||
"delegate to the 'myevent' attribute.",
|
||||
attribute.Documentation);
|
||||
|
||||
Assert.Equal("bind", attribute.Name);
|
||||
Assert.Equal("Bind", attribute.GetPropertyName());
|
||||
Assert.Equal("object Test.BindAttributes.Bind", attribute.DisplayName);
|
||||
|
||||
// Defined from the property type
|
||||
Assert.Equal("System.Object", attribute.TypeName);
|
||||
Assert.False(attribute.IsStringProperty);
|
||||
Assert.False(attribute.IsBooleanProperty);
|
||||
Assert.False(attribute.IsEnum);
|
||||
|
||||
attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("format"));
|
||||
|
||||
// Invariants
|
||||
Assert.Empty(attribute.Diagnostics);
|
||||
Assert.False(attribute.HasErrors);
|
||||
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, attribute.Kind);
|
||||
Assert.False(attribute.IsDefaultKind());
|
||||
Assert.False(attribute.HasIndexer);
|
||||
Assert.Null(attribute.IndexerNamePrefix);
|
||||
Assert.Null(attribute.IndexerTypeName);
|
||||
Assert.False(attribute.IsIndexerBooleanProperty);
|
||||
Assert.False(attribute.IsIndexerStringProperty);
|
||||
|
||||
Assert.Equal(
|
||||
"Specifies a format to convert the value specified by the 'bind' attribute. " +
|
||||
"The format string can currently only be used with expressions of type <code>DateTime</code>.",
|
||||
attribute.Documentation);
|
||||
|
||||
Assert.Equal("format-myprop", attribute.Name);
|
||||
Assert.Equal("Format_myprop", attribute.GetPropertyName());
|
||||
Assert.Equal("string Test.BindAttributes.Format_myprop", attribute.DisplayName);
|
||||
|
||||
// Defined from the property type
|
||||
Assert.Equal("System.String", attribute.TypeName);
|
||||
Assert.True(attribute.IsStringProperty);
|
||||
Assert.False(attribute.IsBooleanProperty);
|
||||
Assert.False(attribute.IsEnum);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_BindOnElementWithSuffix_CreatesDescriptor()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = BaseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(@"
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
[BindElement(""div"", ""myprop"", ""myprop"", ""myevent"")]
|
||||
public class BindAttributes
|
||||
{
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
||||
Assert.Empty(compilation.GetDiagnostics());
|
||||
|
||||
var context = TagHelperDescriptorProviderContext.Create();
|
||||
context.SetCompilation(compilation);
|
||||
|
||||
var provider = new BindTagHelperDescriptorProvider();
|
||||
|
||||
// Act
|
||||
provider.Execute(context);
|
||||
|
||||
// Assert
|
||||
var matches = GetBindTagHelpers(context);
|
||||
var bind = Assert.Single(matches);
|
||||
|
||||
Assert.Equal("myprop", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]);
|
||||
Assert.Equal("myevent", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]);
|
||||
Assert.False(bind.IsInputElementBindTagHelper());
|
||||
Assert.False(bind.IsInputElementFallbackBindTagHelper());
|
||||
|
||||
var rule = Assert.Single(bind.TagMatchingRules);
|
||||
Assert.Equal("div", rule.TagName);
|
||||
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
|
||||
|
||||
var requiredAttribute = Assert.Single(rule.Attributes);
|
||||
Assert.Equal("bind-myprop", requiredAttribute.DisplayName);
|
||||
Assert.Equal("bind-myprop", requiredAttribute.Name);
|
||||
|
||||
var attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("bind"));
|
||||
Assert.Equal("bind-myprop", attribute.Name);
|
||||
Assert.Equal("Bind_myprop", attribute.GetPropertyName());
|
||||
Assert.Equal("object Test.BindAttributes.Bind_myprop", attribute.DisplayName);
|
||||
|
||||
attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("format"));
|
||||
Assert.Equal("format-myprop", attribute.Name);
|
||||
Assert.Equal("Format_myprop", attribute.GetPropertyName());
|
||||
Assert.Equal("string Test.BindAttributes.Format_myprop", attribute.DisplayName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_BindOnInputElementWithoutTypeAttribute_CreatesDescriptor()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = BaseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(@"
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
[BindInputElement(null, null, ""myprop"", ""myevent"")]
|
||||
public class BindAttributes
|
||||
{
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
||||
Assert.Empty(compilation.GetDiagnostics());
|
||||
|
||||
var context = TagHelperDescriptorProviderContext.Create();
|
||||
context.SetCompilation(compilation);
|
||||
|
||||
var provider = new BindTagHelperDescriptorProvider();
|
||||
|
||||
// Act
|
||||
provider.Execute(context);
|
||||
|
||||
// Assert
|
||||
var matches = GetBindTagHelpers(context);
|
||||
var bind = Assert.Single(matches);
|
||||
|
||||
Assert.Equal("myprop", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]);
|
||||
Assert.Equal("myevent", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]);
|
||||
Assert.False(bind.Metadata.ContainsKey(BlazorMetadata.Bind.TypeAttribute));
|
||||
Assert.True(bind.IsInputElementBindTagHelper());
|
||||
Assert.True(bind.IsInputElementFallbackBindTagHelper());
|
||||
|
||||
var rule = Assert.Single(bind.TagMatchingRules);
|
||||
Assert.Equal("input", rule.TagName);
|
||||
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
|
||||
|
||||
var requiredAttribute = Assert.Single(rule.Attributes);
|
||||
Assert.Equal("bind", requiredAttribute.DisplayName);
|
||||
Assert.Equal("bind", requiredAttribute.Name);
|
||||
|
||||
var attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("bind"));
|
||||
Assert.Equal("bind", attribute.Name);
|
||||
Assert.Equal("Bind", attribute.GetPropertyName());
|
||||
Assert.Equal("object Test.BindAttributes.Bind", attribute.DisplayName);
|
||||
|
||||
attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("format"));
|
||||
Assert.Equal("format-myprop", attribute.Name);
|
||||
Assert.Equal("Format_myprop", attribute.GetPropertyName());
|
||||
Assert.Equal("string Test.BindAttributes.Format_myprop", attribute.DisplayName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_BindOnInputElementWithTypeAttribute_CreatesDescriptor()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = BaseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(@"
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
[BindInputElement(""checkbox"", null, ""myprop"", ""myevent"")]
|
||||
public class BindAttributes
|
||||
{
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
||||
Assert.Empty(compilation.GetDiagnostics());
|
||||
|
||||
var context = TagHelperDescriptorProviderContext.Create();
|
||||
context.SetCompilation(compilation);
|
||||
|
||||
var provider = new BindTagHelperDescriptorProvider();
|
||||
|
||||
// Act
|
||||
provider.Execute(context);
|
||||
|
||||
// Assert
|
||||
var matches = GetBindTagHelpers(context);
|
||||
var bind = Assert.Single(matches);
|
||||
|
||||
Assert.Equal("myprop", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]);
|
||||
Assert.Equal("myevent", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]);
|
||||
Assert.Equal("checkbox", bind.Metadata[BlazorMetadata.Bind.TypeAttribute]);
|
||||
Assert.True(bind.IsInputElementBindTagHelper());
|
||||
Assert.False(bind.IsInputElementFallbackBindTagHelper());
|
||||
|
||||
var rule = Assert.Single(bind.TagMatchingRules);
|
||||
Assert.Equal("input", rule.TagName);
|
||||
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
|
||||
|
||||
Assert.Collection(
|
||||
rule.Attributes,
|
||||
a =>
|
||||
{
|
||||
Assert.Equal("type", a.DisplayName);
|
||||
Assert.Equal("type", a.Name);
|
||||
Assert.Equal(RequiredAttributeDescriptor.NameComparisonMode.FullMatch, a.NameComparison);
|
||||
Assert.Equal("checkbox", a.Value);
|
||||
Assert.Equal(RequiredAttributeDescriptor.ValueComparisonMode.FullMatch, a.ValueComparison);
|
||||
},
|
||||
a =>
|
||||
{
|
||||
Assert.Equal("bind", a.DisplayName);
|
||||
Assert.Equal("bind", a.Name);
|
||||
});
|
||||
|
||||
var attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("bind"));
|
||||
Assert.Equal("bind", attribute.Name);
|
||||
Assert.Equal("Bind", attribute.GetPropertyName());
|
||||
Assert.Equal("object Test.BindAttributes.Bind", attribute.DisplayName);
|
||||
|
||||
attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("format"));
|
||||
Assert.Equal("format-myprop", attribute.Name);
|
||||
Assert.Equal("Format_myprop", attribute.GetPropertyName());
|
||||
Assert.Equal("string Test.BindAttributes.Format_myprop", attribute.DisplayName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_BindOnInputElementWithTypeAttributeAndSuffix_CreatesDescriptor()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = BaseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(@"
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
[BindInputElement(""checkbox"", ""somevalue"", ""myprop"", ""myevent"")]
|
||||
public class BindAttributes
|
||||
{
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
||||
Assert.Empty(compilation.GetDiagnostics());
|
||||
|
||||
var context = TagHelperDescriptorProviderContext.Create();
|
||||
context.SetCompilation(compilation);
|
||||
|
||||
var provider = new BindTagHelperDescriptorProvider();
|
||||
|
||||
// Act
|
||||
provider.Execute(context);
|
||||
|
||||
// Assert
|
||||
var matches = GetBindTagHelpers(context);
|
||||
var bind = Assert.Single(matches);
|
||||
|
||||
Assert.Equal("myprop", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]);
|
||||
Assert.Equal("myevent", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]);
|
||||
Assert.Equal("checkbox", bind.Metadata[BlazorMetadata.Bind.TypeAttribute]);
|
||||
Assert.True(bind.IsInputElementBindTagHelper());
|
||||
Assert.False(bind.IsInputElementFallbackBindTagHelper());
|
||||
|
||||
var rule = Assert.Single(bind.TagMatchingRules);
|
||||
Assert.Equal("input", rule.TagName);
|
||||
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
|
||||
|
||||
Assert.Collection(
|
||||
rule.Attributes,
|
||||
a =>
|
||||
{
|
||||
Assert.Equal("type", a.DisplayName);
|
||||
Assert.Equal("type", a.Name);
|
||||
Assert.Equal(RequiredAttributeDescriptor.NameComparisonMode.FullMatch, a.NameComparison);
|
||||
Assert.Equal("checkbox", a.Value);
|
||||
Assert.Equal(RequiredAttributeDescriptor.ValueComparisonMode.FullMatch, a.ValueComparison);
|
||||
},
|
||||
a =>
|
||||
{
|
||||
Assert.Equal("bind-somevalue", a.DisplayName);
|
||||
Assert.Equal("bind-somevalue", a.Name);
|
||||
});
|
||||
|
||||
var attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("bind"));
|
||||
Assert.Equal("bind-somevalue", attribute.Name);
|
||||
Assert.Equal("Bind_somevalue", attribute.GetPropertyName());
|
||||
Assert.Equal("object Test.BindAttributes.Bind_somevalue", attribute.DisplayName);
|
||||
|
||||
attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("format"));
|
||||
Assert.Equal("format-somevalue", attribute.Name);
|
||||
Assert.Equal("Format_somevalue", attribute.GetPropertyName());
|
||||
Assert.Equal("string Test.BindAttributes.Format_somevalue", attribute.DisplayName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Excecute_BindFallback_CreatesDescriptor()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = BaseCompilation;
|
||||
Assert.Empty(compilation.GetDiagnostics());
|
||||
|
||||
var context = TagHelperDescriptorProviderContext.Create();
|
||||
context.SetCompilation(compilation);
|
||||
|
||||
var provider = new BindTagHelperDescriptorProvider();
|
||||
|
||||
// Act
|
||||
provider.Execute(context);
|
||||
|
||||
// Assert
|
||||
var bind = Assert.Single(context.Results, r => r.IsFallbackBindTagHelper());
|
||||
|
||||
// These are features Bind Tags Helpers don't use. Verifying them once here and
|
||||
// then ignoring them.
|
||||
Assert.Empty(bind.AllowedChildTags);
|
||||
Assert.Null(bind.TagOutputHint);
|
||||
|
||||
// These are features that are invariants of all Bind Tag Helpers. Verifying them once
|
||||
// here and then ignoring them.
|
||||
Assert.Empty(bind.Diagnostics);
|
||||
Assert.False(bind.HasErrors);
|
||||
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, bind.Kind);
|
||||
Assert.Equal(BlazorMetadata.Bind.RuntimeName, bind.Metadata[TagHelperMetadata.Runtime.Name]);
|
||||
Assert.False(bind.IsDefaultKind());
|
||||
Assert.False(bind.KindUsesDefaultTagHelperRuntime());
|
||||
|
||||
Assert.False(bind.Metadata.ContainsKey(BlazorMetadata.Bind.ValueAttribute));
|
||||
Assert.False(bind.Metadata.ContainsKey(BlazorMetadata.Bind.ChangeAttribute));
|
||||
Assert.True(bind.IsFallbackBindTagHelper());
|
||||
|
||||
Assert.Equal(
|
||||
"Binds the provided expression to an attribute and a change event, based on the naming of " +
|
||||
"the bind attribute. For example: <code>bind-value-onchange=\"...\"</code> will assign the " +
|
||||
"current value of the expression to the 'value' attribute, and assign a delegate that attempts " +
|
||||
"to set the value to the 'onchange' attribute.",
|
||||
bind.Documentation);
|
||||
|
||||
// These are all trivally derived from the assembly/namespace/type name
|
||||
Assert.Equal("Microsoft.AspNetCore.Blazor", bind.AssemblyName);
|
||||
Assert.Equal("Bind", bind.Name);
|
||||
Assert.Equal("Microsoft.AspNetCore.Blazor.Components.Bind", bind.DisplayName);
|
||||
Assert.Equal("Microsoft.AspNetCore.Blazor.Components.Bind", bind.GetTypeName());
|
||||
|
||||
// The tag matching rule for a bind-Component is always the component name + the attribute name
|
||||
var rule = Assert.Single(bind.TagMatchingRules);
|
||||
Assert.Empty(rule.Diagnostics);
|
||||
Assert.False(rule.HasErrors);
|
||||
Assert.Null(rule.ParentTag);
|
||||
Assert.Equal("*", rule.TagName);
|
||||
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
|
||||
|
||||
var requiredAttribute = Assert.Single(rule.Attributes);
|
||||
Assert.Empty(requiredAttribute.Diagnostics);
|
||||
Assert.Equal("bind-...", requiredAttribute.DisplayName);
|
||||
Assert.Equal("bind-", requiredAttribute.Name);
|
||||
Assert.Equal(RequiredAttributeDescriptor.NameComparisonMode.PrefixMatch, requiredAttribute.NameComparison);
|
||||
Assert.Null(requiredAttribute.Value);
|
||||
Assert.Equal(RequiredAttributeDescriptor.ValueComparisonMode.None, requiredAttribute.ValueComparison);
|
||||
|
||||
var attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("bind"));
|
||||
|
||||
// Invariants
|
||||
Assert.Empty(attribute.Diagnostics);
|
||||
Assert.False(attribute.HasErrors);
|
||||
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, attribute.Kind);
|
||||
Assert.False(attribute.IsDefaultKind());
|
||||
Assert.False(attribute.IsIndexerBooleanProperty);
|
||||
Assert.False(attribute.IsIndexerStringProperty);
|
||||
|
||||
Assert.True(attribute.HasIndexer);
|
||||
Assert.Equal("bind-", attribute.IndexerNamePrefix);
|
||||
Assert.Equal("System.Object", attribute.IndexerTypeName);
|
||||
|
||||
Assert.Equal(
|
||||
"Binds the provided expression to an attribute and a change event, based on the naming of " +
|
||||
"the bind attribute. For example: <code>bind-value-onchange=\"...\"</code> will assign the " +
|
||||
"current value of the expression to the 'value' attribute, and assign a delegate that attempts " +
|
||||
"to set the value to the 'onchange' attribute.",
|
||||
attribute.Documentation);
|
||||
|
||||
Assert.Equal("bind-...", attribute.Name);
|
||||
Assert.Equal("Bind", attribute.GetPropertyName());
|
||||
Assert.Equal(
|
||||
"System.Collections.Generic.Dictionary<string, object> Microsoft.AspNetCore.Blazor.Components.Bind.Bind",
|
||||
attribute.DisplayName);
|
||||
|
||||
// Defined from the property type
|
||||
Assert.Equal("System.Collections.Generic.Dictionary<string, object>", attribute.TypeName);
|
||||
Assert.False(attribute.IsStringProperty);
|
||||
Assert.False(attribute.IsBooleanProperty);
|
||||
Assert.False(attribute.IsEnum);
|
||||
|
||||
attribute = Assert.Single(bind.BoundAttributes, a => a.Name.StartsWith("format"));
|
||||
|
||||
// Invariants
|
||||
Assert.Empty(attribute.Diagnostics);
|
||||
Assert.False(attribute.HasErrors);
|
||||
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, attribute.Kind);
|
||||
Assert.False(attribute.IsDefaultKind());
|
||||
Assert.True(attribute.HasIndexer);
|
||||
Assert.Equal("format-", attribute.IndexerNamePrefix);
|
||||
Assert.Equal("System.String", attribute.IndexerTypeName);
|
||||
Assert.False(attribute.IsIndexerBooleanProperty);
|
||||
Assert.True(attribute.IsIndexerStringProperty);
|
||||
|
||||
Assert.Equal(
|
||||
"Specifies a format to convert the value specified by the corresponding bind attribute. " +
|
||||
"For example: <code>format-value=\"...\"</code> will apply a format string to the value " +
|
||||
"specified in <code>bind-value-...</code>. The format string can currently only be used with " +
|
||||
"expressions of type <code>DateTime</code>.",
|
||||
attribute.Documentation);
|
||||
|
||||
Assert.Equal("format-...", attribute.Name);
|
||||
Assert.Equal("Format", attribute.GetPropertyName());
|
||||
Assert.Equal(
|
||||
"System.Collections.Generic.Dictionary<string, string> Microsoft.AspNetCore.Blazor.Components.Bind.Format",
|
||||
attribute.DisplayName);
|
||||
|
||||
// Defined from the property type
|
||||
Assert.Equal("System.Collections.Generic.Dictionary<string, string>", attribute.TypeName);
|
||||
Assert.False(attribute.IsStringProperty);
|
||||
Assert.False(attribute.IsBooleanProperty);
|
||||
Assert.False(attribute.IsEnum);
|
||||
}
|
||||
|
||||
|
||||
private static TagHelperDescriptor[] GetBindTagHelpers(TagHelperDescriptorProviderContext context)
|
||||
{
|
||||
return ExcludeBuiltInComponents(context).Where(t => t.IsBindTagHelper()).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +1,16 @@
|
|||
// 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.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Razor;
|
||||
using Microsoft.Extensions.DependencyModel;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor.Extensions
|
||||
{
|
||||
public class ComponentTagHelperDescriptorProviderTest
|
||||
public class ComponentTagHelperDescriptorProviderTest : BaseTagHelperDescriptorProviderTest
|
||||
{
|
||||
static ComponentTagHelperDescriptorProviderTest()
|
||||
{
|
||||
var dependencyContext = DependencyContext.Load(typeof(ComponentTagHelperDescriptorProviderTest).Assembly);
|
||||
|
||||
var metadataReferences = dependencyContext.CompileLibraries
|
||||
.SelectMany(l => l.ResolveReferencePaths())
|
||||
.Select(assemblyPath => MetadataReference.CreateFromFile(assemblyPath))
|
||||
.ToArray();
|
||||
|
||||
BaseCompilation = CSharpCompilation.Create(
|
||||
"TestAssembly",
|
||||
Array.Empty<SyntaxTree>(),
|
||||
metadataReferences,
|
||||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
||||
}
|
||||
|
||||
private static Compilation BaseCompilation { get; }
|
||||
|
||||
[Fact]
|
||||
public void Excecute_FindsIComponentType_CreatesDescriptor()
|
||||
{
|
||||
|
|
@ -77,8 +56,7 @@ namespace Test
|
|||
// here and then ignoring them.
|
||||
Assert.Empty(component.Diagnostics);
|
||||
Assert.False(component.HasErrors);
|
||||
Assert.Equal(ComponentTagHelperDescriptorProvider.ComponentTagHelperKind, component.Kind);
|
||||
Assert.Equal("Blazor.Component-0.1", component.Kind);
|
||||
Assert.Equal(BlazorMetadata.Component.TagHelperKind, component.Kind);
|
||||
Assert.False(component.IsDefaultKind());
|
||||
Assert.False(component.KindUsesDefaultTagHelperRuntime());
|
||||
|
||||
|
|
@ -328,15 +306,5 @@ namespace Test
|
|||
Assert.False(attribute.IsStringProperty);
|
||||
Assert.True(attribute.IsDelegateProperty());
|
||||
}
|
||||
|
||||
// For simplicity in testing, exclude the built-in components. We'll add more and we
|
||||
// don't want to update the tests when that happens.
|
||||
private TagHelperDescriptor[] ExcludeBuiltInComponents(TagHelperDescriptorProviderContext context)
|
||||
{
|
||||
return context.Results
|
||||
.Where(c => c.AssemblyName == "TestAssembly")
|
||||
.OrderBy(c => c.Name)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,12 @@ namespace Microsoft.AspNetCore.Blazor.Test.Helpers
|
|||
Assert.Equal(attributeValue, frame.AttributeValue);
|
||||
}
|
||||
|
||||
public static void Attribute(RenderTreeFrame frame, string attributeName, Type valueType, int? sequence = null)
|
||||
{
|
||||
AssertFrame.Attribute(frame, attributeName, sequence);
|
||||
Assert.IsType(valueType, frame.AttributeValue);
|
||||
}
|
||||
|
||||
public static void Attribute(RenderTreeFrame frame, string attributeName, Action<object> attributeValidator, int? sequence = null)
|
||||
{
|
||||
AssertFrame.Attribute(frame, attributeName, sequence);
|
||||
|
|
|
|||
|
|
@ -1,30 +1,30 @@
|
|||
<h2>Textbox</h2>
|
||||
<p>
|
||||
Initially blank:
|
||||
<input id="textbox-initially-blank" @bind(textboxInitiallyBlankValue) />
|
||||
<input id="textbox-initially-blank" bind="textboxInitiallyBlankValue" />
|
||||
<span id="textbox-initially-blank-value">@textboxInitiallyBlankValue</span>
|
||||
</p>
|
||||
<p>
|
||||
Initially populated:
|
||||
<input id="textbox-initially-populated" @bind(textboxInitiallyPopulatedValue) />
|
||||
<input id="textbox-initially-populated" bind="textboxInitiallyPopulatedValue" />
|
||||
<span id="textbox-initially-populated-value">@textboxInitiallyPopulatedValue</span>
|
||||
</p>
|
||||
|
||||
<h2>Checkbox</h2>
|
||||
<p>
|
||||
Initially unchecked:
|
||||
<input id="checkbox-initially-unchecked" @bind(checkboxInitiallyUncheckedValue) type="checkbox" />
|
||||
<input id="checkbox-initially-unchecked" bind="checkboxInitiallyUncheckedValue" type="checkbox" />
|
||||
<span id="checkbox-initially-unchecked-value">@checkboxInitiallyUncheckedValue</span>
|
||||
</p>
|
||||
<p>
|
||||
Initially checked:
|
||||
<input id="checkbox-initially-checked" @bind(checkboxInitiallyCheckedValue) type="checkbox" />
|
||||
<input id="checkbox-initially-checked" bind="checkboxInitiallyCheckedValue" type="checkbox" />
|
||||
<span id="checkbox-initially-checked-value">@checkboxInitiallyCheckedValue</span>
|
||||
</p>
|
||||
|
||||
<h2>Select</h2>
|
||||
<p>
|
||||
<select id="select-box" @bind(selectValue)>
|
||||
<select id="select-box" bind="@selectValue">
|
||||
<option value=@SelectableValue.First>First choice</option>
|
||||
<option value=@SelectableValue.Second>Second choice</option>
|
||||
<option value=@SelectableValue.Third>Third choice</option>
|
||||
|
|
|
|||
Loading…
Reference in New Issue