Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 ?

like image 342
Hadrien.eu Avatar asked Jan 28 '16 11:01

Hadrien.eu


2 Answers

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

like image 99
Hadrien.eu Avatar answered Sep 28 '22 07:09

Hadrien.eu


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);
  });
}
like image 30
Jeremy Danyow Avatar answered Sep 28 '22 05:09

Jeremy Danyow