dotnet / razor

Compiler and tooling experience for Razor ASP.NET Core apps in Visual Studio, Visual Studio for Mac, and VS Code.
https://asp.net
MIT License
492 stars 191 forks source link

Better syntax for methods the return RenderFragment #9959

Open RdJNL opened 7 months ago

RdJNL commented 7 months ago

Feature request

What

Writing methods that return RenderFragment is a way to extract parts of a component's template and potentially allow them to be virtual/abstract. I've tried multiple ways to write such a method (see below), but every way comes with its own disadvantages. It would be nice if there's a syntax for these methods that has none of these disadvantages.

Why

As a C#/OO programmer, I've always been annoyed by the way templates interact with component inheritance in frontend code. When inheriting a base component, there's typically two choices: either keep the base component's template entirely unchanged or create a new template from scratch.

In my opinion, the solution to this is the use of virtual/abstract methods to create part of the template. A subtype can then replace parts of the template without having to rewrite the whole template. Blazor allows this by creating methods that return RenderFragment.

Implementing those methods is best done in a .razor file, because in a C# file, you need to work with RenderTreeBuilder directly.

The current options

I've tried multiple options, but none of them are satisfying.

Option 1 (one tag)

This option is pretty nice if the RenderFragment consists of only one HTML tag. For some reason Visual Studio indents the HTML by only 4 spaces, despite the return being at 8 spaces.

@ButtonWithNumber(3)

@code {
    protected virtual RenderFragment ButtonWithNumber(int x)
    {
        return
    @<button>
        <b>@x</b>
    </button>;
    }
}

Option 2 (<text>)

This option allows multiple lines of HTML mixed with C# code like a normal Razor template. In my opinion, having to put the <text> tag around the HTML is ugly and it also means that whitespace is preserved in the final HTML. Visual Studio messes up the formatting even more this time.

@MultipleParagraphs(2)

@code {
    protected virtual RenderFragment MultipleParagraphs(int x)
    {
        return
    @<text>
        This is some text.
        @for(int i = 0; i < x; i++)
    {
        <p>@i</p>
    })
    </text>;
    }
}

Option 3 (lambda with __builder)

This option is my favorite from the ones I've found, but it still has disadvantages. For starters, you need to write a lambda, making this more verbose than the previous options. Also, the parameter for the lambda must be named __builder. The code within the body of the lambda is treated as a Razor code block (i.e. @{ }). That means that plain text must be prefixed by @: and e.g. for loops don't need to be prefixed by @.

@LambdaFragment(4)

@code {
    protected override RenderFragment LambdaFragment(int x)
    {
        return __builder =>
        {
            <p>Multi line :)</p>
            <p>Second line...</p>
            for (int i = 0; i < x; ++i)
            {
                <p>Test</p>
            }
            <button>Button 1</button>
            <button>Button 2</button>
            @:text without parent
        };
    }
}

Option 4 (the method itself is a RenderFragment)

I'll add this last option, even though I think it's very impractical. Here, we don't return a RenderFragment from the method, but the method is a RenderFragment (RenderFragment is a delegate). The first 3 options could handle zero or more parameters. This option cannot handle any parameters. Also, when called without cast, there's a CS8974 warning, which isn't actually there in the generated C# file. Like option 3, the body of the method is treated like a Razor code block. Once again the parameter must be named __builder.

@Test @* Warning *@
@((RenderFragment)Test) @* No warning *@

@code {
    protected virtual void Test(RenderTreeBuilder __builder)
    {
        <p>Hello from test method</p>
        <p>Second paragraph</p>
        @:text without parent
    }
}

Remarks

Example components

Demo of super and sub class that use this mechanism (using option 3) LoaderBase.razor: ```razor @if(!HasLoaded) { @:Loading... } else { @Content() } @code { protected abstract RenderFragment Content(); } ``` LoaderBase.razor.cs: ```csharp public abstract partial class LoaderBase { protected bool HasLoaded { get; private set; } protected sealed override async Task OnParametersSetAsync() { HasLoaded = false; await LoadAsync(); HasLoaded = true; } protected abstract Task LoadAsync(); } ``` SomeComponent.razor: ```razor @inherits LoaderBase @{ base.BuildRenderTree(__builder); } @code { protected override Task LoadAsync() { return Task.Delay(5000); } protected override RenderFragment Content() { return __builder => { @:Done loading! }; } } ```
Fronix commented 3 months ago

I am wondering why it was named <text>, it doesn't make any sense and in Blazor it stops us from having a <Text> component. Something like <fragment>, <root> or even <razorfragment> would have been way more understanable and non-invasive.

In React it's <> or <React.Fragment> In Vue it's <template> In Angular it's <ng-container>