This is the second blog post in a series of posts that covers how I’ve set up an ASP.NET MVC Core project with Aurelia in a way that will allow me to render razor views and make use of MVC features such as model binding and form validation while at the same time using Aurelia to give the UI that SPA touch that modern web applications of today so often require. The goal is a setup where most experienced ASP.NET MVC Developers can go on and develop their views sort of as always, while the designated front end developer (=me) can leverage Aurelia to enhance the user experience with all sorts of client side features. All the code can be found in this Github repo.
Let’s continue right where we left in the previous blog post and enable the use of Aurelia attributes and elements in the server side rendered razor views. Follow along starting at this tag.
To prepare I’ve changed the markup in the About.cshtml view to this:
<section>
<h2>@ViewData[“Title”]</h2>
<p>Use this area to provide additional information.</p>
</section>
<section>
Take me to the <a route-href=”route: MvcRoute; params.bind: { mvcController: ‘Home’, mvcAction: ‘Contact’ }”>Contact page</a>
</section>
</div>
As you can see, the last section element contains an a element with the aurelia specific route-href attribute. When loading the About page at this point the link will not be clickable. Obviously this is because the server side rendered markup has no knowledge about Aurelia and Aurelia has no knowledge about this markup.
My strategy to fix this consists of creating an “aurelia-enhancer” typescript module and an asp.net mvc TagHelper that we can use to “aurelia enhance” selected parts of our markup.
Let’s start with the typescript module:
let aurelia: Aurelia;
export function init(au: Aurelia): void {
aurelia = au;
}
export function enhance(element: HTMLElement): void {
let enhanceInstruction: EnhanceInstruction = {
container: aurelia.container,
resources: aurelia.resources,
bindingContext: {},
element: element
};
let templatingEngine: TemplatingEngine = aurelia.container.get(TemplatingEngine);
let view: View = templatingEngine.enhance(enhanceInstruction);
}
The module exports two functions. The first one, init, is used to receive the Aurelia instance, which we will need to access the Aurelia TemplatingEngine. We call this init function from the main.ts file.
The second function, enhance, creates an EnhanceInstruction object, uses the aurelia dependency injection container to retrieve the TemplatingEngine instance and sends the enhanceInstruction to the enhance function of the templatingEngine.
Our exported enhance function will be called from script blocks emitted by the AureliaEnhanceTagHelper:
public class AureliaEnhanceTagHelper : TagHelper
{
private const string HtmlIdAttributeName = “id”;
private const string AuEnhanceAttributeName = “th-aurelia-enhance”;
[HtmlAttributeName(AuEnhanceAttributeName)]
public bool Enhance { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (!Enhance)
{
return;
}
string elementId = Convert.ToString(output.Attributes[“id”].Value);
output.PostElement.AppendHtml($@”
<script>
SystemJS.import(‘app/core/aurelia-enhancer’).then(function(enhancer) {{
enhancer.enhance(document.getElementById(‘{elementId}‘));
}});
</script>”);
}
}
The first (simple) iteration of this TagHelper targets any element that has an id attribute and the custom attribute th-aurelia-enhance.
If you’re not up to speed about asp.net mvc core TagHelpers I recommend you take a look here.
The custom attribute is connected to a boolean Enhance property through the HtmlAttributeName attribute. In the Process method we start of by returning if this property is not set to true.
We then retrieve the id of the element and using output.PostElement.AppendHtml we emit a small script block that will import the aurelia-enhancer module, and call the enhance function, sending the html element to enhance.
With all this in place the next step is to add “@addTagHelper *, FooBar.Web” to the _ViewImports.cshtml file and th-aurelia-enhance=”true” to the container div of the About.cshtml view;
One might think this is all there is to eat, but we have on more caveat to resolve. Our current implementation of the MvcRoute route handler uses an innerhtml.bind construction to append the server side rendered markup to the DOM. For some reason (if you have any insight to this, do let me know in comments section below) this means that script blocks included in the html will not be executed. This is a problem since that’s exactly what the TagHelper we just created is adding to the output. So what we need to do is to change the approach a bit so that the html is appended to the DOM from code.
I’ll use jQuery for this and I should probably point out that jQuery is globally included in the page through a normal “script src” element in the Home/Index.cshtml view. This is due to my upcoming desire to make use of jQuery Validation which the MVC Validation attributes has a seamless and nice integration with through the jQuery Unobtrusive Validation library. Thanks to this I’ll be able to use the $ jQuery function from any .ts file without importing it.
First of we’ll change the mvc-route.html template to use a ref binding instead, this will populate our model with a reference to the html element.
<div ref=”placeholder”></div>
</template>
And here are the changes/additions we’ll need to do in mvc-route.ts:
/…/
export class MvcRoute {
@observable html: string;
private placeholder: HTMLElement;
/…/
attached() {
this.performAppend();
}
/…/
private htmlChanged(newValue, oldValue) {
if (newValue === oldValue) {
return;
}
this.performAppend();
}
private performAppend() {
$(this.placeholder).empty().append(this.html);
}
}
First we’ll import the observable decorator and add it to the html property. This means we can implement an htmlChanged function that will be called whenever the value of html is changed (which happens in the already existing activate function).
We declare a placeholder member of type HTMLElement, this is the member that will be assigned through the ref binding in the html template.
In addition to the htmlChanged method we also implement the attached function that Aurelia will call as soon as the markup has been attached to the DOM. From both these functions we call the private function performAppend.
In performAppend we use jQuery to reference the placeholder element, empty it and then append the current html.
With all this in place we can now run the site and click our route-href link in the About view. This also means that any custom element you register in the main.ts startup also can be used within this aurelia enhanced element. An example can be found in the Aurelia docs section Leveraging Progressive Enhancement.
In future blog posts I’ll cover how to extend the aurelia-enhancer module and the ArueliaEnhanceTagHelper so that the markup actually gets bound to a client side model that can contain functions and data that the server side rendered markup can be bound to using Aurelia constructs such as click.delegate and value.bind.
The diff for changes covered in this blog post can be found here. To finalize the steps taken in this blog post I’ve made two additional commits which involves improving the AureliaEnhanceTagHelper so that an existing id attribute is not necessary and refactored out the html append management from the mvc-route module to a separate html-placeholder custom element. Click each link to see the diffs for this.
If you liked this post, please click thumbs up and don’t hesitate to make any comments, questions or other remarks.
Update 2017-06-29: In the first version of this blog post I made a mistake in the AureliaEnhanceTagHelper, using the new lambda function syntax in the script block the TagHelper emits to the browser. This does not work in IE11. The fix is to replace “enhancer =>” with “function (enhancer)” and I’ve done so in the code block above and also made a commit fixing this in the current version of AureliaEnhanceTagHelper. (It’s worth pointing out that using the lambda syntax in any .ts file is OK, as the TypeScript will compile to cross browser friendly javascript.)