Elements and Decorations
BlazorCodeFirst mirrors HTML directly: every element you write in a Body expression names the
element it produces. There is no second set of widget names to learn on top of HTML, and no runtime
UI tree.
Elements#
An HTML element is a helper, named by its tag with only the first letter uppercased — Figcaption,
not FigCaption, likewise Colgroup and Textarea. Attributes chain onto it, children go in
brackets.
protected override View Body => Figure[ Img.Src("/diagram.png").Alt("Architecture"), Figcaption["The compilation pipeline"]];
<figure> <img src="/diagram.png" alt="Architecture"> <figcaption>The compilation pipeline</figcaption> </figure>
A dedicated helper exists for every element the HTML Living Standard lists as conforming.
Element covers everything else. Two things use it. The first is custom elements and Web
Components, whose tag names are never a known helper. The second is the handful of standard elements
no helper covers:
- the document and
<head>-only elements:html,head,body,title,base,meta,link - raw-text elements:
script,style,noscript - elements the render tree cannot give meaning to:
template,slot object, whose name is ambiguous with the C# keyword- the foreign elements
svgandmath, in full
private const string Widget = "my-widget"; Element(Widget).Attr("value", "42") // custom element Element("svg")[Element("circle")] // foreign element Element(_kind + "-widget") // BCF3009: not a constant Element("my widget") // BCF3009: not spelled like a tag name
The tag has to be a compile-time constant spelled like a tag name, or BCF3009 is reported.
Children#
A bare string converts to a text node, so there is no separate Text() construct. An element with
no children drops the brackets entirely.
protected override View Body => Div[ "plain text, then ", A.Href("/docs")["a link"], Br, Img.Src("/logo.png").Alt("Logo")];
<div>plain text, then <a href="/docs">a link</a><br><img src="/logo.png" alt="Logo"></div>
A Blazor RenderFragment sits in the child list like any other child:
[Parameter] public RenderFragment? ChildContent { get; set; } protected override View Body => Div["before", ChildContent];
The generator has to see each child as its own expression to give it a sequence number. Writing the children as a nested collection literal is accepted, because C# expands it to the same call:
Div[["a", "b"]] // same as Div["a", "b"], which is the form to prefer
A child list the generator cannot read through reports
BCF1003. Use ForEach for
repetition instead.
Void elements take no children#
The thirteen void elements of the HTML standard have no closing tag, so children written on one report BCF3016:
Img.Src("/logo.png")["Logo"] // BCF3016 Element("img")["Logo"] // BCF3016, same rule Img.Src("/logo.png").Alt("Logo") // what to write instead
Configure a void element with decorations and put content beside it. This is the limit of what the
surface checks about HTML, and the limit is deliberate: Table[Div["x"]] also renders differently
after hydration, and it is accepted, along with attributes an element does not define
(Div.Href("/x")). See DESIGN.md §4.1 for the full rationale.
When a name collides#
using static BlazorCodeFirst.Html; imports every conforming HTML element name, and a declaration
of your own wins simple-name lookup over an imported one. Blazor parameters named Label, Data,
Summary or Source are ordinary, so this happens:
[Parameter] public string Data { get; set; } Div[Data["Heading"]] // BCF3027 Div[Html.Data["Heading"]] // what to write instead
A type, a namespace, or a method of yours takes the name the same way, and each is BCF3027, naming what it found.
Decorations#
Decorations are chained onto the element they belong to, before its children, the way HTML writes
attributes inside the tag. They collapse into the owning element's attributes rather than
introducing wrapper nodes, and class folds: chaining .Class more than once merges the values
into a single attribute.
protected override View Body => Button .Class("btn") .Class("btn-primary") .Title("Save the current document")["Save"];
<button class="btn btn-primary" title="Save the current document">Save</button>
Available decorations are .Class, .Role, .OnClick, the general-purpose escape hatches
.Attr(name, value) and .On(eventName, handler), and 142 standard-derived shortcuts, among them
.Href, .Src, .Alt, .Type, and .Title used above. Each standard-derived shortcut covers one
HTML Living Standard attribute whose name spells directly as a C# identifier. Element
reference lists all of them, split by whether
they take string? or Blazor's conditional bool.
.On takes the full attribute name including the on prefix (.On("onmouseenter", …)); nothing is
prefixed for you, and a name without it reports BCF3019. The name given
to .Attr or .On must be a non-empty compile-time constant
(BCF3011).
.Attr takes a string? or a bool. A bool is Blazor's conditional attribute: true renders the
attribute with an empty value, which is how HTML reads disabled, checked and hidden as set, and
false leaves the attribute out entirely. Where the attribute is always present, write it bare, as HTML
does — the bool is for the conditional case.
Input.Type("checkbox").Attr("checked") // <input type="checkbox" checked> Button.Attr("disabled", _submitting)["Save"] // conditional
A null string value leaves the attribute out too, so an attribute that carries a value only sometimes
needs no branch around the element:
Span.Attr("title", _hasTip ? _tip : null)["Hover me"]
null and "" are different values at every stage — frames, prerendered HTML, and a re-render — so
"" gives you title="" and null gives you no title at all. When a re-render turns a value null,
Blazor removes the attribute from the element already in the DOM rather than replacing the element.
Every decoration that takes one value accepts null this way.
There is deliberately no object overload. A value of any other type is formatted at render time
under the formatting thread's culture rather than the one your component ran under, so write it out
yourself, where the culture is explicit:
Div.Tabindex(index.ToString(CultureInfo.InvariantCulture))
Handlers#
A handler written as Action or Func<Task> receives nothing. To read the event, give the lambda
parameter its type, and .On picks up the typed overload:
Input.Type("text").Value(_name) .On("oninput", (ChangeEventArgs e) => _name = e.Value?.ToString() ?? "")
Unlike Razor, the argument type is not inferred from the event name, so writing it on the parameter
is what selects the overload. ChangeEventArgs lives in Microsoft.AspNetCore.Components;
MouseEventArgs, KeyboardEventArgs and FocusEventArgs live in
Microsoft.AspNetCore.Components.Web, which a Blazor app already references.
The type is not inferred, but it is checked. Naming one the event does not deliver reports
BCF3028, read from the same [EventHandler] metadata Razor uses. A base
of the delivered type is accepted, because that is what the handler can actually receive:
Button.On("onclick", (MouseEventArgs e) => Zoom(e.ClientX, e.ClientY))["Zoom"] // the delivered type Button.On("onclick", (EventArgs e) => Save())["Save"] // a base of it: fine Button.On("onclick", (KeyboardEventArgs e) => Save())["Save"] // BCF3028
An event with no [EventHandler] registration has no mapping to check against, so a custom event you
have not registered is left alone. Registering one is the ordinary Blazor mechanism, and a
registration in your own project is read:
[EventHandler("onrate", typeof(RatingEventArgs))] public static class AppEventHandlers;
An attribute out and an event back is the pair .Bind writes as one
decoration.
The class channel#
Because that channel joins its values as text, class is the one name that takes a string and nothing
else. .Attr("class", flag) reports BCF3023 — as does .Attr("class"),
whose bare spelling stands for a presence and so has no text to join either. Write a conditional class
as a string, using null for the term you want gone:
Div.Class("card").Class(_selected ? "is-selected" : "")
A null term drops out of the join, so an element carrying one class decoration loses the attribute
entirely when the term is null. It still leaves the separator behind when there is another term to join
against: Div.Class("card").Class(_selected ? "is-selected" : null) renders class="card " while
_selected is false, which the browser reads as the single class card.
Every other attribute and event is a single binding, and binding one twice on the same element reports
BCF3010. style is one of those others: write it as .Attr("style", …).
.Bind("class", …) is the third way to write the name and the one that does not fold, so an element
carrying both it and a .Class reports BCF3024.
Where a decoration may go#
A decoration must target a single element. Applying one to If, ForEach, Fragment, Raw, or a
component result reports BCF3008, because none of those opens an
element. Writing the chain after the children (Div["text"].Class("card")) reports the same thing:
the brackets have already produced a View.
A decoration also has to be one this library declares. A misspelled name (Div.Clas("card")), or an
extension method of your own that takes an element and gives one back, reports
BCF3026.
Next#
Read control flow for conditionals and lists, or go back to getting started.