How to enhance a server side generated page with Aurelia.io?
Asked Answered
L

3

7

I'm writing an app with some parts as SPA and some pages generated on server side for SEO. I've chosen Aurelia.io framework and I use enhance method to enable custom elements on my pages. But I can't find the best way to use aurelia specific template directives and interpolation on my server side page. Let's start with an exemple.

All of my pages contains a dynamic header. This header will be a custom element named my-cool-header. This header will load authentified user and display its name, or, if no user is currently authentified, a link to the signin will be displayed. The body of the page will be generated on server side and cached. So, we'll have something like that :

<html>
<body>
    <my-cool-header>
        <img src="logo.png">
        <div
            show.bind="user">${user.name}</div>
        <div
            show.bind="!user"><a href="/signin">Sign-in</a></div>
    </my-cool-header>
    <div>Cachabled content</div>
</body>
</html>

Then, my header will by defined by :

import {UserService} from './user';
import {inject} from 'aurelia-framework';

@inject(UserService)
export class MyCoolHeader {
    constructor(userService) {
        this.userService = userService;
    }

    async attached() {
        this.user = await this.userService.get();
    }
}

With the following template :

<template>
    <content></content>
</template>

And this bootstrap script :

export function configure(aurelia) {
  aurelia.use
    .standardConfiguration()
    .developmentLogging()
    .globalResources('my-cool-header');

  aurelia.start().then(a => a.enhance(document.body));
}

In this configuration, the custom element is well loaded and instanciated. But, I can't access the viewModel of the node inside the <content> node. So, all the interpolation (${user.name}) and attributes (show.bind) are ignored. If I include a custom-element in my content template, it will be loaded only if it is declared as global in the bootstrap : the` tag is ignored.

I've found a workaround to be able to change the viewModel after reading the doc by setting a custom viewModel to enhance method and then, injecting it to my custom element class. Something like :

import {MainData} from './main-data';
export function configure(aurelia) {
  const mainData = aurelia.container.get(MainData);
  aurelia.use
    .standardConfiguration()
    .developmentLogging()
    .globalResources('my-cool-header');

  aurelia.start().then(a => a.enhance(mainData, document.body));
}

Custom element:

import {UserService} from './user';
import {inject} from 'aurelia-framework';
import {MainData} from './main-data';

@inject(UserService, MainData)
export class MyCustomElement {
    constructor(userService, mainData) {
        this.userService = userService;
        this.mainData = mainData;
    }

    async attached() {
        this.mainData.user = await this.userService.get();
    }
}

And finally, if I change my template like that, it will work :

<html>
<body>
    <my-cool-header
        user.bind="user">
        <img src="logo.png">
        <div
            show.bind="user">${user.name}</div>
        <div
            show.bind="!user"><a href="/signin">Sign-in</a></div>
    </my-cool-header>
    <div>Cachabled content</div>
</body>
</html>

I can't believe it is the right way to do because it's ugly and it does not resolve the problem of <require> tag. So my question is : What is the best way to do ?

Lyda answered 28/1, 2016 at 11:29 Comment(2)
How do you develop "some parts as SPA and some pages generated on server side"? Why you can't only use client-side rendering? and why you use server-side for seo?Neuromuscular
I have some pages which need to be referenced and to be enhanced with some javascript. In my example, I have this cool header, which is a common custom element between the backoffice as a SPA and the referenced pages, but I'll have some more.Lyda
L
3

Thanks to your clues, I found the solution!

Custom element need to construct its own template:

import {processContent, noView} from 'aurelia-framework';

@processContent(function(viewCompiler, viewResources, element, instruction) {
  instruction.viewFactory = viewCompiler.compile(`<template>${element.innerHTML}</template>`, viewResources, instruction);
  element.innerHTML = '';
  return false;
})
@noView
export class MyCustomElement {
  attached() {
    this.world = 'World!';
    this.display = true;
  }  
}

Then, in my view from server, we can interpolate and require custom elements!

<body>
  <my-custom-element>
    <require="./other-custom-element"></require>
    <p
      if.bind="display">Hello ${world}</p>
    <other-custom-element></other-custom-element>
  </my-custom-element>
</body>

I've wrote a decorator to help creating this kind of enhanced custom elements : https://github.com/hadrienl/aurelia-enhanced-template

Plus de détails en français sur mon blog : https://blog.hadrien.eu/2016/02/04/amelioration-progressive-avec-aurelia-io/

EDIT: <require> is not really working with this solution. I have to dig again :(

Lyda answered 4/2, 2016 at 11:5 Comment(0)
R
2

Change your MyCoolHeader's template from:

<template>
  <content></content>
</template>

to:

<template>
  <img src="logo.png">
  <div show.bind="user">${user.name}</div>
  <div show.bind="!user"><a href="/signin">Sign-in</a></div>
</template>

then change your server-generated page to something like this:

<html>
<body>
  <my-cool-header></my-cool-header>
  <div>Cachabled content</div>
</body>
</html>

Hope that helps. If this doesn't solve the problem or is not an acceptable solution, let me know.

Edit

After reading your reply and thinking about this a bit more I'm leaning towards removing the <my-cool-header> element. It's not providing any behavior, it only acts as a data loader, it's template is provided by the server-side rendering process and it's expected to be rendered outside of the aurelia templating system, there's no real need to re-render it. Here's what this approach would look like, let me know if it seems like a better fit:

<html>
<body>
  <div class="my-cool-header">
    <img src="logo.png">
    <div show.bind="user">${user.name}</div>
    <div show.bind="!user"><a href="/signin">Sign-in</a></div>
  </div>

  <div>Cachabled content</div>
</body>
</html>
import {MainData} from './main-data';
import {UserService} from './user';

export function configure(aurelia) {
  const mainData = aurelia.container.get(MainData);
  const userService = aurelia.container.get(UserService);

  aurelia.use
    .standardConfiguration()
    .developmentLogging();

  Promise.all([
    this.userService.get(),
    aurelia.start()    
  ]).then(([user, a]) => {
    mainData.user = user;
    a.enhance(mainData, document.body);
  });
}
Revell answered 28/1, 2016 at 12:23 Comment(4)
Nope, because I want some markup in the content node to be visible without javascript. My exemple would be more unsderstandable with : <my-cool-header> <h1>My Web App Title to be crawled by Google</h1> <div>${someInterpolation}</div> </my-cool-header>Lyda
Offhand, I think the problem here is viewmodel scope. Stuff inside the <content> tag will belong to the containing viewmodel (the parent), not the custom element's viewmodel.Melessa
I agree ! Is there a way to access the scope of the <content> tag?Lyda
The fact is my exemple is very simple, so I understand you want to refactor in the easiest way. But in reality, my-cool-header has a lot of logic, not just loading a user and send it to the view. So I really need it to be wrapped in a dedicated custom element.Lyda
W
0

To supplement Jeremy's answer, if you did change the template to:

<template>
  <img src="logo.png">
  <div show.bind="user">${user.name}</div>
  <div show.bind="!user"><a href="/signin">Sign-in</a></div>
</template>

This content would be present when Aurelia processed the element and in the absence of a content selector, anything inside the custom element tags will be replaced by the template

If you then put your non-javascript content inside the custom element tags:

<my-cool-header>
    <div>This stuff will be visible when JS is turned off</div>
</my-cool-header>

In the example above, in the absence of JS the div should still be there as Aurelia won't remove it from the DOM.

(This is of course assuming your server side tech doesn't mangle/fix the unknown HTML tags in the DOM for some reason when serving pages - which it probably won't since it would break Aurelia anyway)

EDIT:

The alternative you may be looking for is the @processContent decorator.

This allows you to pass a callback function that runs before Aurelia inspects the element.

At this point you could just lift the content between the custom element tags and add it as a child of the template element. The content should then be in scope of your viewmodel.

This way you can have the same markup in between the custom element tags with no javascript, and inside your template in the correct scope when Aurelia is running

import {processContent, TargetInstruction, inject} from 'aurelia-framework';

@inject(Element, TargetInstruction)
@processContent(function(viewCompiler, viewResources, element, instruction) {
    // Do stuff
    instruction.templateContent = element;

    return true;
})
class MyViewModel {
    constructor(element, targetInstruction) {
        var behavior = targetInstruction.behaviorInstructions[0];
        var userTemplate = behavior.templateContent;

        element.addChild(userTemplate);
    }
}

Disclaimer: the above code hasn't been tested and I pulled it from my grid which is several releases old - you may need to tweak

Williamsen answered 1/2, 2016 at 15:48 Comment(6)
I want <img src="logo.png"> which may become a bigger markup because it is the site header with title and things to be visible without js, and with js.Lyda
Not sure what you mean - the above approach will result in the template markup being present when Aurelia is active, and the div when Aurelia is inactive (js off)Williamsen
With your exemple, <img src="logo.png"> is in the custom element template. So it will be displayed only when aurelia will be loaded. I want <img src="logo.png"> to be visible inside the <my-cool-header> element before and after javascript is executed. And I don't want to duplicate markup in servers template and client template.Lyda
Then use @processContent decorator and lift the content into your template element inside your custom element. This way it will be in the correct viewmodel scope.Williamsen
I've edited - not tested but that should do exactly what you wantWilliamsen
I suppose you would have type appendChild instead of addChild ? With your solution, the content is cloned, but it is not interpolated : ${foo} and if.bind are not replaced by processed values.Lyda

© 2022 - 2024 — McMap. All rights reserved.