Support temporary @(Layout<MyLayoutType>()) syntax
This commit is contained in:
parent
f91d1d4803
commit
9e333e31c5
|
|
@ -22,6 +22,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
_engine = RazorEngine.Create(configure =>
|
_engine = RazorEngine.Create(configure =>
|
||||||
{
|
{
|
||||||
FunctionsDirective.Register(configure);
|
FunctionsDirective.Register(configure);
|
||||||
|
TemporaryLayoutPass.Register(configure);
|
||||||
|
|
||||||
configure.SetBaseType(BlazorComponent.FullTypeName);
|
configure.SetBaseType(BlazorComponent.FullTypeName);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Razor.Language;
|
||||||
|
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This code is temporary. It finds top-level expressions of the form
|
||||||
|
/// @Layout<SomeType>()
|
||||||
|
/// ... and converts them into [Layout(typeof(SomeType))] attributes on the class.
|
||||||
|
/// Once we're able to add Blazor-specific directives and have them show up in tooling,
|
||||||
|
/// we'll replace this with a simpler and cleaner "@Layout SomeType" directive.
|
||||||
|
/// </summary>
|
||||||
|
internal class TemporaryLayoutPass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
|
||||||
|
{
|
||||||
|
// Example: "Layout<MyApp.Namespace.SomeType<T1, T2>>()"
|
||||||
|
// Captures: MyApp.Namespace.SomeType<T1, T2>
|
||||||
|
private static readonly Regex LayoutSourceRegex
|
||||||
|
= new Regex(@"^\s*Layout\s*<(.+)\>\s*\(\s*\)\s*$");
|
||||||
|
private const string LayoutAttributeTypeName
|
||||||
|
= "Microsoft.AspNetCore.Blazor.Layouts.LayoutAttribute";
|
||||||
|
|
||||||
|
public static void Register(IRazorEngineBuilder configuration)
|
||||||
|
{
|
||||||
|
configuration.Features.Add(new TemporaryLayoutPass());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||||
|
{
|
||||||
|
var visitor = new Visitor();
|
||||||
|
visitor.Visit(documentNode);
|
||||||
|
|
||||||
|
if (visitor.DidFindLayoutDeclaration)
|
||||||
|
{
|
||||||
|
visitor.MethodNode.Children.Remove(visitor.LayoutNode);
|
||||||
|
|
||||||
|
var attributeNode = new CSharpCodeIntermediateNode();
|
||||||
|
attributeNode.Children.Add(new IntermediateToken()
|
||||||
|
{
|
||||||
|
Kind = TokenKind.CSharp,
|
||||||
|
Content = $"[{LayoutAttributeTypeName}(typeof ({visitor.LayoutType}))]" + Environment.NewLine,
|
||||||
|
});
|
||||||
|
|
||||||
|
var classNodeIndex = visitor
|
||||||
|
.NamespaceNode
|
||||||
|
.Children
|
||||||
|
.IndexOf(visitor.ClassNode);
|
||||||
|
visitor.NamespaceNode.Children.Insert(classNodeIndex, attributeNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Visitor : IntermediateNodeWalker
|
||||||
|
{
|
||||||
|
public bool DidFindLayoutDeclaration { get; private set; }
|
||||||
|
public NamespaceDeclarationIntermediateNode NamespaceNode { get; private set; }
|
||||||
|
public ClassDeclarationIntermediateNode ClassNode { get; private set; }
|
||||||
|
public MethodDeclarationIntermediateNode MethodNode { get; private set; }
|
||||||
|
public CSharpExpressionIntermediateNode LayoutNode { get; private set; }
|
||||||
|
public string LayoutType { get; private set; }
|
||||||
|
|
||||||
|
public override void VisitNamespaceDeclaration(NamespaceDeclarationIntermediateNode node)
|
||||||
|
{
|
||||||
|
NamespaceNode = node;
|
||||||
|
base.VisitNamespaceDeclaration(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node)
|
||||||
|
{
|
||||||
|
ClassNode = node;
|
||||||
|
base.VisitClassDeclaration(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void VisitMethodDeclaration(MethodDeclarationIntermediateNode methodNode)
|
||||||
|
{
|
||||||
|
var topLevelExpressions = methodNode.Children.OfType<CSharpExpressionIntermediateNode>();
|
||||||
|
foreach (var csharpExpression in topLevelExpressions)
|
||||||
|
{
|
||||||
|
if (csharpExpression.Children.Count == 1)
|
||||||
|
{
|
||||||
|
var child = csharpExpression.Children[0];
|
||||||
|
if (child is IntermediateToken intermediateToken)
|
||||||
|
{
|
||||||
|
if (TryGetLayoutType(intermediateToken.Content, out string layoutType))
|
||||||
|
{
|
||||||
|
DidFindLayoutDeclaration = true;
|
||||||
|
MethodNode = methodNode;
|
||||||
|
LayoutNode = csharpExpression;
|
||||||
|
LayoutType = layoutType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
base.VisitMethodDeclaration(methodNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetLayoutType(string sourceCode, out string layoutType)
|
||||||
|
{
|
||||||
|
var match = LayoutSourceRegex.Match(sourceCode);
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
layoutType = match.Groups[1].Value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
layoutType = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,7 +24,13 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Razor
|
namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
{
|
{
|
||||||
public class RazorPage<T>: BlazorComponent { }
|
public class RazorPage<T>: BlazorComponent
|
||||||
|
{
|
||||||
|
// This is temporary and exists only to support TemporaryLayoutPass.
|
||||||
|
// It will be removed when we can add Blazor-specific directives.
|
||||||
|
public object Layout<TLayout>() where TLayout : IComponent
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
namespace Internal
|
namespace Internal
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Blazor.Components;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Blazor.Layouts
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that the associated component type uses a specified layout.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
|
||||||
|
public class LayoutAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The type of the layout. The type always implements <see cref="IComponent"/>.
|
||||||
|
/// </summary>
|
||||||
|
public Type LayoutType { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs an instance of <see cref="LayoutAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="layoutType">The type of the layout. This must implement <see cref="IComponent"/>.</param>
|
||||||
|
public LayoutAttribute(Type layoutType)
|
||||||
|
{
|
||||||
|
LayoutType = layoutType ?? throw new ArgumentNullException(nameof(layoutType));
|
||||||
|
|
||||||
|
if (!typeof(IComponent).IsAssignableFrom(layoutType))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Invalid layout type: {layoutType.FullName} " +
|
||||||
|
$"does not implement {typeof(IComponent).FullName}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Blazor.Build.Core.RazorCompilation;
|
using Microsoft.AspNetCore.Blazor.Build.Core.RazorCompilation;
|
||||||
using Microsoft.AspNetCore.Blazor.Components;
|
using Microsoft.AspNetCore.Blazor.Components;
|
||||||
|
using Microsoft.AspNetCore.Blazor.Layouts;
|
||||||
using Microsoft.AspNetCore.Blazor.Rendering;
|
using Microsoft.AspNetCore.Blazor.Rendering;
|
||||||
using Microsoft.AspNetCore.Blazor.RenderTree;
|
using Microsoft.AspNetCore.Blazor.RenderTree;
|
||||||
using Microsoft.AspNetCore.Blazor.Test.Shared;
|
using Microsoft.AspNetCore.Blazor.Test.Shared;
|
||||||
|
|
@ -360,6 +361,34 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
||||||
frame => AssertFrame.Component<TestComponent>(frame, 0));
|
frame => AssertFrame.Component<TestComponent>(frame, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ComponentsDoNotHaveLayoutAttributeByDefault()
|
||||||
|
{
|
||||||
|
// Arrange/Act
|
||||||
|
var component = CompileToComponent($"Hello");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(component.GetType().GetCustomAttribute<LayoutAttribute>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SupportsLayoutDeclarationsViaTemporarySyntax()
|
||||||
|
{
|
||||||
|
// Arrange/Act
|
||||||
|
var testComponentTypeName = typeof(TestComponent).FullName.Replace('+', '.');
|
||||||
|
var component = CompileToComponent(
|
||||||
|
$"@(Layout<{testComponentTypeName}>())" +
|
||||||
|
$"Hello");
|
||||||
|
var frames = GetRenderTree(component);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var layoutAttribute = component.GetType().GetCustomAttribute<LayoutAttribute>();
|
||||||
|
Assert.NotNull(layoutAttribute);
|
||||||
|
Assert.Equal(typeof(TestComponent), layoutAttribute.LayoutType);
|
||||||
|
Assert.Collection(frames,
|
||||||
|
frame => AssertFrame.Text(frame, "Hello"));
|
||||||
|
}
|
||||||
|
|
||||||
private static RenderTreeFrame[] GetRenderTree(IComponent component)
|
private static RenderTreeFrame[] GetRenderTree(IComponent component)
|
||||||
{
|
{
|
||||||
var renderer = new TestRenderer();
|
var renderer = new TestRenderer();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue