In my last blog post, I discussed range inputs. This time, we’ll examine a tag helper that adds support for the HTML element of datalist.

In this short post, I’ll explain what a datalist is and why you may want to consider using it in your ASP.NET Core applications. Finally, we’ll implement a set of tag helpers to make using the datalist element more straightforward with your ASP.NET Core MVC and Razor Page applications.

What is datalist?

True to its name, the datalist element allows web developers to create a list of options that are permissible or recommended for form input elements. This allows users to choose from a predefined list the application developer has curated.

Let’s look at a quick HTML example pulled from the MDN web docs.

<label for="ice-cream-choice">Choose a flavor:</label>
<input list="ice-cream-flavors" id="ice-cream-choice" name="ice-cream-choice"/>

<datalist id="ice-cream-flavors">
    <option value="Chocolate"></option>
    <option value="Coconut"></option>
    <option value="Mint"></option>
    <option value="Strawberry"></option>
    <option value="Vanilla"></option>
</datalist>

The ice-cream-choice field will use the datalist options and provide users with a dropdown of potential options but still allow users to type their choices using freeform text.

The list attribute supports multiple input types, including text, date, range, and color. While the datalist is supported in most browsers, Firefox partially supports the input field and does not work with the types date, time, range, and color.

Providing a datalist can guide your users to common answers for otherwise fuzzy fields. The element might be helpful in surveys or support forms.

How do we use it in ASP.NET Core MVC and Razor Pages?

Datalist TagHelper for MVC and Razor Pages

Let’s start with our end goal of using a datalist in our Razor views. We’ll look at a Razor Pages example where we’ll see the HTML and C# Page Model.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace OutputValues.Pages;

public class IndexModel(ILogger<IndexModel> logger) : PageModel
{
    [BindProperty]
    public string? Value { get; set; }

    public string? Message { get; set; }

    public List<SelectListItem> Fruits { get; } =
    [
        new("The finest from Tokyo", "Apple"),
        new("The curviest fruit", "Banana"),
        new("The citrus is amazing", "Orange")
    ];

    public void OnGet()
    {
    }

    public void OnPost()
    {
        Message = $"You selected {Value}!";
    }
}

We’ll reuse the SelectListItem class commonly used by the select element. There’s no reason to reinvent the wheel here. Next, let’s update our view.

<form method="post" asp-page="Index">
    <div>
        <label class="form-label" asp-for="Value"></label>
        <input class="form-control" asp-for="Value" asp-list="Fruits" />
    </div>

    <button type="submit" class="mt-3 btn btn-primary">
        Add
    </button>
    
    <datalist asp-items="Fruits"></datalist>
</form>

That’s it! We now have an input field backed by our new datalist. Let’s see what the output HTML looks like.


<form method="post" action="/">
    <div>
        <label class="form-label" for="Value">Value</label>
        <input class="form-control" type="text" id="Value" name="Value" value="" list="Fruits">
    </div>

    <button type="submit" class="mt-3 btn btn-primary">
        Add
    </button>

    <datalist id="Fruits">
        <option label="The finest from Tokyo" value="Apple"></option>
        <option label="The curviest fruit" value="Banana"></option>
        <option label="The citrus is amazing" value="Orange"></option>
    </datalist>
    <input name="__RequestVerificationToken" type="hidden" value="">
</form>

The tag helpers are designed to look for asp-items on a datalist element and an asp-list attribute on any input field. Let’s see how these two tag helpers are implemented.

using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace OutputValues;

[HtmlTargetElement("datalist", Attributes = ItemsAttributeName)]
public class DataListHelper : TagHelper
{
    private const string ItemsAttributeName = "asp-items";

    /// <summary>
    /// A collection of <see cref="SelectListItem"/> objects used to populate the &lt;datalist&gt; element with
    /// &lt;option&gt; elements.
    /// </summary>
    [HtmlAttributeName(ItemsAttributeName)]
    public ModelExpression For { get; set; } = default!;

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        ArgumentNullException.ThrowIfNull(context);
        ArgumentNullException.ThrowIfNull(output);

        var items = (IEnumerable<SelectListItem>)For.Model ?? [];
        foreach (var item in items)
        {
            var tagBuilder = new TagBuilder("option")
            {
                Attributes =
                {
                    ["value"] = item.Value
                }
            };

            if (!string.IsNullOrWhiteSpace(item.Text))
            {
                tagBuilder.Attributes["label"] = item.Text;
            }

            output.PostContent.AppendHtml(tagBuilder);
        }

        // the developer may choose their own id if they choose to
        // otherwise we use the property name to generate an id
        if (!output.Attributes.ContainsName("id"))
        {
            var id = GetDatalistId(For);
            output.Attributes.SetAttribute("id", id);
        }
    }

    public static string GetDatalistId(ModelExpression @for)
    {
        // perhaps we want different ids
        return TagBuilder.CreateSanitizedId(@for.Name, "");
    }
}

[HtmlTargetElement("input", Attributes = ItemsAttributeName)]
public class DataListInputHelper : TagHelper
{
    private const string ItemsAttributeName = "asp-list";

    /// <summary>
    /// A collection of <see cref="SelectListItem"/> objects used to populate the &lt;datalist&gt; element with
    /// &lt;option&gt; elements.
    /// </summary>
    [HtmlAttributeName(ItemsAttributeName)]
    public ModelExpression For { get; set; } = default!;

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        ArgumentNullException.ThrowIfNull(context);
        ArgumentNullException.ThrowIfNull(output);

        // if it already has a list attribute then don't override it
        // not sure why it's there, but sure why not 🤷‍♂️
        if (!output.Attributes.ContainsName("list"))
        {
            var listId = DataListHelper.GetDatalistId(For);
            output.Attributes.SetAttribute("list", listId);
        }
    }
}

This straightforward implementation could easily be modified to accommodate your needs.

The most crucial implementation detail is the ModelExpression, which allows us to get both the metadata of our property and the value of the property. It’s an awesome part of the tag helper API.

Conclusion

The datalist element is an HTML-native feature that provides completion on several input types. With a few simple tag helpers, we can derive the datalist options we need to power any input, thus reducing the work required to keep the UI and application data models in sync. Please try this and let me know if you found it helpful.

As always, thanks for reading, and cheers.