Photo by Clint Patterson on Unsplash
Alpine.js allows you set javascript objects in the scope of a HTML tag using the x-data
attribute:
<div x-data="{ open: false }">
<button @click="open = ! open">Toggle Content</button>
<div x-show="open">
Content...
</div>
</div>
The data is a plain javascript object whose properties are available to all child elements of the outer div
.
Clicking the button will show/hide the content of the inner div
.
In a typical SPA like setup you might use fetch
together with Alpine's json
function to include server side data:
<div
x-data="{ posts: [] }"
x-init="posts = await (await fetch('/posts')).json()"
>...</div>
But what to do if you want to render the Razor view and let it include the server side object directly into Alpine's x-data
attribute? The closest you can get using out of the box serialization is this:
<div x-data='@Html.Raw(Json.Serialize(new { Open = false}))'></div>
This will render the expected output, and you can toggle the content using the button.
But as you may have noticed, the x-data
attribute in the last sample is using single quotes. When using double quotes, the code will break at runtime. Your single quotes might get replaced by double quotes due to IDE settings on when saving the cshtml
file.
A better solution would be to use single quotes for the javascript object string and date properties.
One approach could be to replace the instance of IJsonHelper
which is being called by Json.Serialize
inside the view.
Yet, the implementation is using System.Text.Json
which neither allows to use non-quoted property names nor use a different quote char.
So, back to Newtonsoft.Json
- here's a possible solution (we could also create a new implementation of IJSonHelper
, of course):
public static class JavaScriptConverter
{
public static IHtmlContent SerializeObject(
object value
)
{
using var stringWriter = new StringWriter();
using var jsonWriter = new JsonTextWriter(stringWriter);
var serializer = new JsonSerializer
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
jsonWriter.QuoteName = false;
jsonWriter.QuoteChar = '\'';
serializer.Serialize(jsonWriter, value);
return new HtmlString(stringWriter.ToString());
}
}
Using the SerializeObject
method, we solve three tasks:
.NET objects get serialized
Usage inside the view changed to following:
<div x-data="@JavaScriptSerializer.SerializeObject(new { Open = false})"></div>
This is less error-prone but still feels a bit cumbersome.
So let's wrap the JavaScriptSerializer
call using a Tag Helper:
[HtmlTargetElement("*", Attributes = "alpine-data")]
public class AlpineTagHelper : TagHelper
{
public override void Process(
TagHelperContext context,
TagHelperOutput output
)
{
output.Attributes.Add("x-data", JavaScriptConverter.SerializeObject(Data));
base.Process(context, output);
}
[HtmlAttributeName("alpine-data")]
public object Data { get; set; } = null!;
}
Now lets see how we can use this one.
First, register the Tag Helper in _ViewImports.cshtml
:
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Alpine.TagHelpers
And just use the Tag Helper:
<div alpine-data="new { Open = false }">
<button x-on:click="open = !open">Show</button>
<div x-show="open" x-cloak>Some details...</div>
</div>
The rendered result:
<div x-data="{open:false}">
<button x-on:click="open = !open">Show</button>
<div x-show="open">Some details...</div>
</div>
The Tag Helper is available as a NuGet package:
dotnet add package Alpine.TagHelpers