Pseudo Inverted Templates Techniques

This thread started a discussion of how to do inverted templates in donejs.

Inverted templates (I think they are layouts in Ruby on Rails) allow “child” components/views/etc to alter the structure of parent components/view/etc.

For example, we might have components for different parts of the page. A home, blog and product component. We’d like to build these components to include the view for the main content (<article>) of the page, the <head> element of the page, and perhaps even a sidebar <nav>.

The chat discusses a variety of ways to do it.

Not doing it by having all the configuration in the appVM

<html>
  <head>{{content.head(this)}}</head>
  <body>{{content.body(this)}}</body>
</html>
  "404": {
    head: stache(`<title>404</title>`),
    body: stache(`<h1>uh oh</h1>`)
  },
  "200": {
    head: stache(`<title>My Cool Site</title>`),
    body: stache(`<h1>Hello World</h1>`)
   }
}

DefineMap.extend({
  get content(){
     if( IS_200 ) { return contents["200"] }
     else { return contents["400"] }
  }
})

Pros: simple
Cons: doesn’t progressively load, components are not being used

Add a headView to component’s prototype and read it:

All page components would look something like:

Component.extend({
  tag: "blog-page",
  view: `<articles> .... </articles>`,
  headView: stache(`<title>Blog</title>`),
  ViewModel: {...}
})

index.stache would call out to a pageComponentHeadView with the pageComponentViewModel:

<head>
  {{ pageComponentHeadView(pageComponentViewModel) }}
</head>
<body>
  {{pageComponentPromise.value}}
</body>

The appVM would get these properties:

get pageComponentPromise() {
  return steal.import( .... ).then(()=>{ new Component })
},
pageComponent: {
  get(lastValue, resolve){
    this.pageComponentPromise.then(resolve)
  } 
}
get pageComponentHeadView() { 
  return this.pageComponent && this.pageComponent.headView
},
get pageComponentViewModel() { 
  return this.pageComponent && this.pageComponent.viewModel
},
1 Like

We talked about this some more offline and came up with a few different ideas on how to solve this problem.

For those just now reading about this, the problem we are trying to solve is projecting part of a template somewhere else.

It’s best discussed with a common case. You have various page components in your app, and you want to modify the <head> depending on which page component you are showing. With done-autorender you can do this directly in the index.stache template but it becomes a bit repetitive.

<html>
<head>
  {{#eq(page, "cart")}}
    <title>Cart</title>
  {{/eq}}
  
  {{#eq(page, "home")}}
    <title>Home page</title>
  {{/eq}}
</head>
<body>
  {{#eq(page, "cart")}}
    <app-cart />
  {{/eq}}
  
  {{#eq(page, "home")}}
    <app-home />
  {{/eq}}
</body>
</html>

Notice that we have two of what are essentially the same chunks of code in both the head and the body. The pageComponent pattern cleans up the body part a bit. And you could extend that pattern to also deal with the head. It does turn into configuration soup though.

What you really want is to colocate the template that gets projected into the head with the rest of your component. Here’s a couple of different ideas that attempt to achieve that:

Adding template to the component definition

Anything you put on the can-component definition should be available on the component’s prototype. For example if you were to do:

import { Component } from "can";

const Cart = Component.extend({
  tag: "app-cart",
  view: `
    <h1>Items: {{count}}</h1>
  `,
  
  headView: stache("<title>Cart</title>")
});

let cart = new Cart();

document.head.appendChild(cart.headView());

Notice that we are able to use headView as part of the component instance.

This means that when using the pageComponent pattern you could do this in your index.stache:

<html>
<head>
  {{pageComponent.headView(pageComponent.viewModel)}}
</head>
<body>
  <h1>My app</h1>
  
  {{pageComponent}}
  
  <script src="./node_modules/steal/steal.js" main></script>
</body>
</html>

The important line here is {{pageComponent.headView(pageComponent.viewModel)}}. This calls the headView template, which again is just something on the component’s prototype, and passes in the viewModel as the scope.

Making inline partials accessible from the outside

This is very similar to the first idea, but instead of having to define a separate template you can just do it as an inline partial For example your template might look like:

import { Component } from "can";

const Cart = Component.extend({
  tag: "app-cart",
  view: `
    {{<head}}
      <title>Cart</title>
    {{/head}}
  
    <h1>Items: {{count}}</h1>
  `
});

Notice we are using an inline partial named head here. Then to use it in your app you might do:

<html>
<head>
  {{pageComponent.view.head(pageComponent.viewModel)}}
</head>
<body>
  <h1>My app</h1>
  
  {{pageComponent}}
  
  <script src="./node_modules/steal/steal.js" main></script>
</body>
</html>

Again, this is very similar to the first idea. In this case the component instance’s view property gets a head property added to it, which is the inline partial. We call that as a function just like in the other example.

Portals

This idea comes from other frameworks which have the idea of “portals”. A portal is a way to project part of your template to another DOM node. In stache this would likely be done using a new helper:

import { Component } from "can";

const Cart = Component.extend({
  tag: "app-cart",
  
  ViewModel: {
    head: () => document.head
  }
  
  view: `
    {{#portal(head)}}
      <title>Cart</title>
    {{/portal}}
  
    <h1>Items: {{count}}</h1>
  `
});

There are two parts to this worth pointing out. First there is the new {{portal()}} helper. You call this helper and pass in a DOM node that you would like the contents to be projected to. Here we are passing in the reference to head.

Since head is defined on our ViewModel to point to document.head, this means the contents of the portal would be rendered into the head.

You could imagine that this technique (as well as the previous ones) would be useful in other cases, like modals which you project somewhere else in the DOM than the component’s own children.

1 Like

I can see something like this being useful for other things as well.

I have a preference for the {{#portal()}} idea. I think it gives more control to the component that is rendering it.

For example, say you wanted to only update the head in a certain condition:

const NewOrder = Component.extend({
  tag: "order-new",
  
  ViewModel: {
    modalElement: {
      default: () => document.querySelector('.modal')
    }
  },
  
  view: `
    {{#if ( showModal ) }}

      {{# portal( modalElement ) }}
        <app-modal text="Close" />
      {{/ portal }}

    {{/ if }}
  
    <h1>Items: {{count}}</h1>
  `
});

Using one of the other methods we would always be rendering the template within the projected element.

Another nice thing about {{portal(el)}} is that it makes it really easy to create wrappers. For example we could trivially create a can-head element or something:

Component.extend({
  tag: "new-order",

  view: `
    <can-head>
      <title>New Order</title>
    </can-head>
  `
});

Where can-head is essentially just:

Component.extend({
  tag: "can-head",
 
  ViewModel: {
    head: { default: () => document.head }
  }

  view: `{{#portal(head)}}<content />{{/portal}}`
});
1 Like