Search code examples
javascriptaureliaprogressive-enhancement

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


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 ?


Solution

  • 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 :(