To give you a better idea of how Razor Forms actually does what it does, let's take a look under the hood at the classes that make RazorForms work.
RazorForms.TagHelpers.TagHelperBase
This class is inherited indirectly by every single tag helper in RazorForms, so it's the perfect place to start. You can view the source code on GitHub.
IHtmlGenerator HtmlGenerator
An IHtmlGenerator
used to create new tag helpers in codeIHtmlHelper HtmlHelper
An IHtmlHelper
used to render the template fileTOptions Options
The configuration options for this specific type of tag helperFunc<bool, HtmlEncoder, Task<TagHelperContent>> DefaultTagHelperContent
A function that returns an empty TagHelperContent
, used to create TagHelperOutput
instances with empty outputbool LabelReceivesChildContent
Whether the <label>
should receive the child content of the tag helper or notstring ContainerTag = string.Empty
The HTML element to wrap the entire tag helper output withTagMode ContainerTagMode = TagMode.StartTagAndEndTag
The TagMode
to use for the tag helper outputstring InputTag = "input"
The HTML element to use for the input tag helper outputTagMode InputTagMode = TagMode.SelfClosing
The TagMode
to use for the input tag helper outputViewContext ViewContext
The Razor ViewContext
ModelExpression For
(usable in markup as asp-for
) The ModelExpression
that returns the model member this tag helper representsstring? TemplatePath
(usable in markup as template-path
) A path string indicating which .cshtml
template to use for this tag helper. If not specified, defaults to the value of FormComponentOptions.TemplatePathProcessAsync()
Like all tag helpers, everything starts at ProcessAsync()
. None of the RazorForms tag helpers override ProcessAsync()
, so the implementation in TagHelperBase
is what runs in every tag helper. It only has 7 lines of code; these 7 statements hook into extensible parts of RazorForms to generate all the different tag helpers available. ProcessAsync()
is broken down into 3 logical tasks: configure the tag helper output, set up the model for the .cshtml
template, and render the .cshtml
template to the tag helper output.
Configuring the tag helper output is simple. All we do is set the TagName
and TagMode
based on TagHelperBase.ContainerTag
and TagHelperBase.ContainerTagMode
. We use protected properties because these are implementation details of the tag helpers themselves, and don't change based on your RazorForms configuration - so it's not appropriate to give end users control over these properties or store them in configuration.
ContainerTag
defaults to an empty string, so by default, there is no HTML wrapper around the tag helper output.
ContainerTagMode
defaults to TagMode.StartTagAndEndTag
, which is what you want if you're going to render child input inside.
All RazorForms .cshtml
templates have a POCO model, so we need to set that up here.
The GenerateHtmlModel()
method creates the model. This method is one of only two methods on TagHelperBase that aren't virtual
, but this method calls several other methods that are virtual, so you can still customize this process.
The ProcessModel()
method allows you to arbitrarily modify the model in whatever way you want. So this would be a prime opportunity to do custom processing for your model (for example, deciding what icon to render based on the validity of the data model).
Now that the model is created and processed, we can render the template and write its output.
First, we need to prepare the HtmlHelper
, which is a class instance of IHtmlHelper
. It's not ready to use out of the box - it needs to be contextualized first, which gives it access to the ViewContext
that it needs to do its job. It doesn't come pre-contextualized because the underlying class, HtmlHelper
, is registered in DI as a transient service, so since you're setting this one up yourself, you have to manually prepare it for use.
Next, we render the template using our contextualized HtmlHelper
. TagHelperBase
will use the TemplatePath
value if provided, or else it will fall back to the TemplatePath
on the configuration options for that tag helper (see the custom templates guide for more information).
Then, all we need to do is set the content of the tag helper output to our rendered template!
GenerateHtmlModel()
It seems like we got a lot for just a little in ProcessAsync()
, doesn't it? Sure, a lot of the markup comes from the template, but most of the logic happens in the tag helper, so where is it?
Well, as it turns out, most of this happens in GenerateHtmlModel()
and the methods it calls. Each line of the source is a different logical step, so we'll go over it sequentially.
<label>
outputThe <label>
and the <input>
tags aren't created in the Razor template. Instead, they're created here, in C# code. But why?
The <label>
and <input>
tag helpers use the asp-for
parameter to decide which model member they're rendering for. asp-for
is a ModelExpression, which is a little different from most other expressions. For starters, it isn't representable as a lambda: when you use a ModelExpression
in a Razor template, it infers that the member you reference is coming off your template's model. There's no straightforward way to deconstruct a ModelExpression
into its underlying model reference, so there's also no straightforward way to create a reusable Razor template when form inputs are involved. If you try, what happens is your ModelExpression
will no longer represent your Razor Page model member - instead, in your reusable Razor template, your ModelExpression
will point to the ModelExpression
property on your ViewComponent, and none of it will work as expected.
RazorForms sidesteps this problem by creating the <label>
and <input>
tag helpers manually in C# code. By working in C# instead of Razor, the ModelExpression
resolves as expected, and everything just works. So for this reason, the rendered <label>
and <input>
are stored on the model passed to the Razor template, and the Razor template just injects those values directly into the template from the model properties.
TOptions
The TOptions
here represents the type of the configuration options for this tag helper. For the built-in tag helpers, validity-aware tag helpers use ValidityAwareFormComponentOptions and validity-unaware tag helpers use FormComponentOptions. Instead of passing the global configuration options to the template model, however, we're going to copy the options so that we can modify them if we need to (more on that later).
<input>
outputWe manually create and render the <input>
for the same reason as the <label>
. However, if the <input>
is supposed to render inside the <label>
, it was created when we created the <label>
so we don't need to create it here.
We created a new TOptions
a couple lines ago because we may want to modify the options (again, more on that later). Here, we need to copy over any options our template needs that we don't want to modify.
We don't apply CSS classes directly to the output. Rather, we pass CSS classes to the TOptions
we created a few lines ago, and then let the template render those CSS classes as necessary.
ProcessModel()
Here, we can do any additional processing on the model that needs to happen. However, the base implementation doesn't do anything.
Custom tag helpers that extend TagHelperBase can do additional processing here if they use a subclass of MarkupModel that adds new properties. For example, ValidityAwareTagHelperBase
overrides this method to add validity-related information to the model.
CreateLabel()
As discussed previously, we create the <label>
in code. We could just hand-render a <label>
in our template, but the LabelTagHelper
built into ASP.NET does some handy work for us (like setting the for
attribute), so we decided to just use the built-in tag helper.
The source code can be broken down into 6 logical tasks: create the LabelTagHelper
, create a TagHelperOutput
to store the generated output, add custom attributes to the <label>
, render the child content, apply CSS classes to the <label>
, and process the LabelTaghelper
.
LabelTagHelper
Since we're doing everything manually, there's a lot of work we need to do to generate a <label>
. We start by creating the LabelTagHelper
and setting up its ViewContext
and For
properties. We already have those on the TagHelperBase
instance, so this is simple.
TagHelperOutput
Tag helpers require a TagHelperOutput
to actually render anything. This is typically handled by the framework when you call tag helpers from a Razor template, but when you're manually rendering a tag helper in C# code, you need to manually create a TagHelperOutput
for it to render to.
<label>
Some tag helpers need to add additional attributes to the label. We do that here, once the TagHelperOutput
has been created.
The <label>
has several different possible configurations of its child content, so this logical portion is more complex than the others.
The <label>
can either render the <input>
or not. Sometimes the <label>
receives the child content of the tag helper, and sometimes it doesn't. If it does, the <label>
can either render the <input>
or the child content first. The <label>
text can either be surrounded by an HTML tag or not. The next 60 or so lines represent the logic of figuring out how to render the <label>
's child content, given those possible configurations.
<label>
Now that the <label>
's child content has been generated, we need to apply CSS classes to the <label>
itself.
LabelTaghelper
Finally, the <label>
is ready to go! The framework will typically call labelHelper.ProcessAsync()
itself - but again, we're doing this all manually, so we need to call labelHelper.ProcessAsync()
manually too. Once the LabelTagHelper
has been processed, the labelOutput
is populated with content, so we can return it.
CreateInput()
Just like the <label>
, we create the input in code. The built-in InputTagHelper
, TextareaTagHelper
, and SelectTagHelper
add HTML attributes used by JavaScript error handling on the client.
The source code can be broken down into 7 logical tasks: create the tag helper, create a TagHelperOutput
to store the generated output, add custom attributes to the output, render the child content, initialize the tag helper, apply CSS classes to the output, and process the output.
To create the tag helper, we're going to call a different method, CreateInputTagHelper()
. CreateInput()
is how RazorForms defines the logic of creating the input's rendered content, but CreateInputTagHelper()
is how RazorForms creates the actual tag helper.
TagHelperOutput
As with the <label>
, we can't render anything if we don't have a TagHelperOutput
. In this case, which tag to use is defined by the TagHelperBase.InputTag
property. This defaults to input
, but needs to be changed if a different tag is expected. For example, the SelectInputTagHelper
changes this property to select
.
It's also possible to add custom attributes to the input tag helper output. We do that here, once the TagHelperOutput
has been created.
If the label doesn't receive the child content, the input does. For example, the <select-input>
allows you to pass your <option>
s as child content, so the underlying <select>
will need to receive the child content.
Some tag helpers need initialized. Since we don't know what kind of tag helper we're dealing with, we go ahead and initialize the tag helper here because initializing a tag helper that doesn't need it won't hurt anything. (We didn't initialize the LabelTagHelper
in CreateLabel()
because the LabelTagHelper
doesn't have any initialization - its Init()
method is empty. But some tag helpers, such as SelectTagHelper
, do have required initialization, so we need to make sure to call it just in case.)
Now that the output's child content has been generated, we can apply CSS classes to the underlying input.
As with the <label>
, we have to call tagHelper.ProccessAsync()
manually because we're calling the tag helper from code. We do this conditionally here because it's perfectly valid for the tag helper to be null
. Once the tag helper has been processed, the output is populated with its final content, so we can return it.
SetupModelOptions()
Instead of passing the configuration object from dependency injection into the model, we decided to create a new TOptions
and pass it into the model. In order to do this, we need to copy configuration settings that won't get re-calculated during tag helper execution.
If we already have the options object passed into the tag helper, why are we creating a new one here?
In order to keep the Razor template logic as minimal as possible, we want as much processing as possible to happen in the C# code of the tag helper. Later, we're going to calculate the final CSS classes to send to the model, but for now, we need to copy over options that aren't calculated.
AddCssClasses()
Here, we calculate the CSS classes that need to be applied to the output in the tag helper, then pass those to the base TOptions
CSS-related properties.
The TagHelperAttributeList
comes from the base TagHelperOutput
that contains the entire tag helper output. We want to strip the class
attribute off, if it exists, and pass its value to the component wrapper. Then we simply copy over the input wrapper and label wrapper classes.
Overrides of this method can do even more work. For example, in ValidityAwareTagHelperBase
, this method adds CSS classes based on the validation state of the model.
AddCustomLabelAttributes()
Some tag helpers need to add custom attributes to the <label>
. This method allows tag helpers to do that. However, the base implementation is empty because most tag helpers don't need to add anything special.
Some built-in tag helpers override this method. For example, <check-input>
and <radio-input>
don't use asp-for
to create the for
attribute on their <label>
; instead, they create a Guid
and pass its value to a manually-created for
attribute to ensure that the <label>
and <input>
are linked in the HTML code.
ApplyCssClassesToLabel()
This method is used to add CSS classes to the <label>
tag. The <label>
is represented by a TagHelperOutput
, so it's fairly straightforward to add CSS classes to it.
There is an AddClassesToOutput()
utility method that simplifies the process of adding multiple CSS classes at once (because TagHelperOutputExtensions.AddClass()
explicitly disallows adding multiple CSS classes in a space-separated list, but RazorForms explicitly expects space-separated CSS class lists).
By default, only the FormComponentOptions.LabelClasses value is added to the output, but this method is virtual
so more can be added by inheritors. For example, the ValidityAwareTagHelperBase
overrides this method. In its implementation, the ValidityAwareFormComponentOptions.LabelValidClasses and ValidityAwareFormComponentOptions.LabelInvalidClasses are also added, depending on the validation state of the model.
CreateInputTagHelper()
This method is called by CreateInput()
. It should return the TagHelper
that represents the tag helper's input, or null if the tag helper doesn't need an input.
This is a separate virtual method because different types of inputs may have different underlying tag helpers that they use. For example, the <select-input>
is backed by a SelectTagHelper
, while the <text-area-input>
is backed by a TextareaTagHelper
. The <radio-input-group>
doesn't have an input, so it uses the base implementation that returns null
.
AddCustomInputAttributes()
Some tag helpers need to add custom attributes to the <input>
. Like AddCustomLabelAttributes()
, the base implementation of this method is empty.
Some built-in tag helpers override this method. For example, when binding multiple checkboxes to a list, each checkbox needs a unique ID to link it to its <label>
, so we need to pass the same Guid
from the <label>
as the id
attribute of the <input>
.
ApplyCssClassesToInput()
This method is used to add CSS classes to the <input>
tag. The <input>
is represented by a TagHelperOutput
, so it works the same way as ApplyCssClassesToLabel()
.
Now that we've seen the base class that makes the magic happen, let's take a look at the simplest tag helper implementations: the validity-unaware tag helpers.
The two validity-unaware tag helpers (<check-input>
and <radio-input>
) inherit from RazorForms.TagHelpers.CheckRadioTagHelperBase
, which inherits from TagHelperBase
. There's very little custom functionality added on top of TagHelperBase
, so the validity-unaware tag helpers are a good next step.
RazorForms.TagHelpers.CheckRadioTagHelperBase
The source code for CheckRadioTagHelperBase
is available on GitHub.
string Type
The HTML type
attribute to use for the <input>
ctor()
The constructor only makes one change over the base constructor: the LabelReceivesChildContent
property is set to true
because <check-input>
and <radio-input>
use the inner HTML as the label content.
override CreateInputTagHelper()
This override returns an InputTagHelper
because <check-input>
and <radio-input>
are both represented by an <input>
.
override AddCustomLabelAttributes()
This override adds the HTML for
attribute to link the <label>
to its <input>
.
override AddCustomInputAttributes()
This override does 3 things: Adds the HTML id
attribute to the <input>
to link it to its <label>
, adds the Type
property as the value of the HTML type
attribute, and conditionally adds the HTML checked
attribute.
AddCheckedAttribute()
Checkboxes and radio buttons both use the HTML checked
attribute to determine if they should be ticked on page load. However, the logic of whether to add the checked
attribute is different for <check-input>
and <radio-input>
, so AddCheckedAttribute()
is an abstract method.
RazorForms.TagHelpers.Inputs.CheckInputTagHelper
This tag helper represents the <check-input>
used in markup. Its source code is available on GitHub.
ctor()
This constructor only sets the Type
property to checkbox
, ensuring that the created input will have its type
property set to checkbox
.
override AddCheckedAttribute()
This override only needs to manually add the checked
attribute if the model member is a List
because ASP.NET still correctly adds the checked
attribute if the model member is a bool
.
In this override, the checked
attribute is added only if all of the following checks pass:
TagHelperAttributeList
passed into AddcheckedAttribute()
has a value
attribute<check-input>
is not null
<check-input>
is a List<T>
<check-input>
can be cast to IList
IList
contains a value equal to the value
attribute of this <check-input>
RazorForms.TagHelpers.Inputs.RadioInputTagHelper
This tag helper represents the <radio-input>
used in markup. Its source code is available on GitHub.
ctor()
This constructor only sets the Type
property to radio
, ensuring that the created input will have its type
property set to radio
.
override AddCheckedAttribute()
This override is simpler than CheckInputTagHelper
's implementation because it only needs to compare the values directly, since radio buttons can't bind to lists.
Three of the validity-aware tag helpers(<text-input>
, <text-area-input>
, and <select-input>
) inherit from RazorForms.TagHelpers.ValidityAwareTagHelperBase
, which inherits from TagHelperBase
. The other two validity-aware tag helpers (<check-input-group>
and <radio-input-group>
) inherit from RazorForms.TagHelpers.InputGroupTagHelperBase
, which inherits from ValidityAwareTagHelperBase
.
RazorForms.TagHelpers.ValidityAwareTagHelperBase
This class is the direct base for <text-input>
, <text-area-input>
, and <select-input>
. It's also the indirect base class for <check-input-group>
and <radio-input-group>
. This class has validation-related functionality added on top of TagHelperBase
.
bool IsValid
represents whether the model member this tag helper represents is explicitly valid, i.e. not simply "not invalid"bool IsInvalid
represents whether the model member this tag helper represents is explicitly invalid, i.e. not simply "not valid"IEnumerable<string> Errors
represents the validation error messages for the model member this tag helper representsoverride ProcessModel()
This override adds the IsValid
, IsInvalid
, and Errors
properties to the markup model. This override doesn't call base.ProcessModel()
since the base implementation doesn't do anything.
override SetupModelOptions()
This override calls base.SetupModelOptions()
to transfer the default model options to the TOptions
instance for the markup model. It also copies the ValidityAwareFormComponentOptions.AlwaysRenderErrorContainer option.
override AddCssClasses()
This override adds CSS classes to the TOptions
instance on the markup model based on the validity of the model member this tag helper represents.
The markup sections added here are the component wrapper, input block wrapper, input wrapper, label wrapper, error wrapper, and error. The explicitly valid and explicitly invalid CSS classes are also added if the model member is explicitly valid or explicitly invalid, respectively.
The error wrapper classes and error classes are added as-is without respect to validation state, since those sections of the markup are intended only to convey error information.
Because the CSS classes are pre-calculated and added to the TOptions
instance on the _____WrapperClasses
property, the template doesn't need to calculate whether or not to include CSS classes in the markup. This simplifies the template and keeps logic in C# code where it belongs.
override ApplyCssClassesToLabel()
This override adds CSS classes to the underlying <label>
, with explicitly valid and explicitly invalid CSS classes added based on the validity of the model member this tag helper represents.
override ApplyCssClassesToInput()
This override adds CSS classes to the underlying <input>
, with explicitly valid and explicitly invalid CSS classes added based on the validity of the model member this tag helper represents.
CreateValidityAwareClasses()
This new method returns a single string
containing all CSS classes for a single section of the markup. This method appends the explicitly valid or explicitly invalid CSS class names as appropriate.
This method is used to create the CSS classes for markup sections, such as component wrappers, label wrappers, etc. Its only usages are in AddCssClasses()
overrides.
AddValidityAwareClasses()
This new method applies validity-aware CSS classes to a TagHelperOutput
. Its only usages are in overrides of ApplyCssClassesToInput()
and ApplyCssClassesToLabel()
.
RazorForms.TagHelpers.Inputs.TextInputTagHelper
This tag helper represents the <text-input>
used in markup. Its source code is available on GitHub.
string? Format
(usable in markup as asp-format
) A pass-through property for the asp-format
value used by InputTagHelper
ctor()
This constructor sets LabelReceivesChildContent
to true
.
override CreateInputTagHelper()
This override creates a new InputTagHelper
, setting the ViewContext
, For
, and Format
properties to their respective values.
RazorForms.TagHelpers.Input.TextAreaInputTagHelper
This tag helper represents the <text-area-input>
used in markup. Its source code is available on GitHub.
ctor()
This constructor sets three properties:
InputTag
is set to textarea
InputTagMode
is set to TagMode.StartTagAndEndTag
LabelReceivesChildContent
is set to true
override CreateInputTagHelper()
This override creates a new TextAreaTagHelper
, setting the ViewContext
and For
properties to their respective values.
RazorForms.TagHelpers.Input.SelectInputTagHelper
This tag helper represents the <select-input>
used in markup. Its source code is available on GitHub.
IEnumerable<SelectListItem>? Items
(usable in markup as asp-items
) Allows passing an array of SelectListItem
s to automatically render your <option>
sctor()
This constructor sets two properties:
InputTag
is set to select
InputTagMode
is set to TagMode.StartTagAndEndTag
override CreateInputTagHelper()
This override creates a new SelectTagHelper
, setting the ViewContext
, For
, and Items
properties to their respective values.
RazorForms.TagHelpers.InputGroupTagHelperBase
This tag helper is the base class for the <check-input-group>
and <radio-input-group>
used in markup. (The CheckInputGroupTagHelper
and RadioInputGroupTagHelper
inherit directly from this tag helper with no modificiations, so they are identical to this class.) Its source is available on GitHub.
ctor()
This constructor sets two properties:
InputTag
is set to an empty string (this allows the tag helper to render its child content in place of an <input>
tag)InputTagMode
is set to TagMode.StartTagAndEndTag
(this is a just-in-case setting, as it has no effect on the generated markup; however, if an inheritor changes InputTag
to something else, such as a <div>
, this property will need to be TagMode.StartTagAndEndTag
in order to render child content)RazorForms.TagHelpers.Elements.CheckInputGroupTagHelper
This tag helper represents the <check-input-group>
used in markup. It is an unmodified subclass of InputGroupTagHelperBase
, so it is identical to that tag helper. This tag helper exists as a separate tag helper in the event that a developer wants to customize <check-input-group>
separately from <radio-input-group>
. Its source is available on GitHub.
RazorForms.TagHelpers.Elements.RadioInputGroupTagHelper
This tag helper represents the <radio-input-group>
used in markup. It is an unmodified subclass of InputGroupTagHelperBase
, so it is identical to that tag helper. This tag helper exists as a separate tag helper in the event that a developer wants to customize <radio-input-group>
separately from <check-input-group>
. Its source is available on GitHub.