How to separate template out of stencil components (lazy load template)

1.1k Views Asked by At

Interesting problem, so please read till the end. what I want to achieve is separating template in another js file and lazyload it when required. This same thing when done in React ecosystem works, but stencil doesn't! Triage Repo https://github.com/pranav-js/triage-repo

I am having my tsx template in another .js file say

template-three.js has simple onClick which only alerts

import { h } from '@stencil/core';
export const template_three = () => {
  return <button onClick={() => alert()}>Template three here</button>;
};

when I try to call this method by importing in component-two.tsx like this

import { Component, Fragment, h, Host, State } from '@stencil/core';

@Component({
  tag: 'component-two',
  styleUrl: 'component-two.css',
  shadow: true,
})
export class ComponentTwo {
  @State() showComponentOne: any;
  method: any;
  template: string = '';

  constructor() {}

  componentWillRender() {
    this.fetchComp(2);
  }

// lazy load template when needed based on type passed

  fetchComp(type) {
    let this_ = this;
    switch (type) {
      case 0:
        import('./template').then(module => {
          this.showComponentOne = module.template_one;
        });
        break;
      case 1:
        import('./template-two').then(module => {
          this.showComponentOne = module.template_two;
        });
        break;
      case 2:
          import('./template-three').then(module => {
            this.showComponentOne = module.template_three;
          );
        break;
      default:
        break;
    }
  }

  clicked() {
    alert();
  }

  methodHere() {}

// check if template received then simply call that method with this attached

  render() {
    let this_ = this;
    return this.showComponentOne ? this.showComponentOne.apply(this) : <div></div>;
  }
}

View renders, but event listners are not working :/, not even a simple alert :(. When I inspect, I don’t see any event attached to button. however if same function I keep inside component class, it works :( !!!

check two different objects when template defined inside and outside component. $elem is null when template is in outside js

Can you tell me what I am doing wrong here.

I can’t keep templates in component only cause I have many UI’s for same logic. So far, I didn't get any way on internet this answer doesn't help either Passing custom template to stencil component

2

There are 2 best solutions below

1
On

I think it should be

@Component({
  tag: 'component-two',
  styleUrl: 'component-two.css',
  shadow: true,
})
export class ComponentTwo {
  // omitting all code that didn't change ...

  render() {
    // probably not needed
    let this_ = this;

    /* ---------------------------------------------------- */
    /* Note the change in the following line:               */
    /* showComponentOne must be the template function here. */
    /* ---------------------------------------------------- */
    return this.showComponentOne ? <this.showComponentOne onclick={this.onClick.bind(this)} /> : <div></div>;
  }
}
6
On

I think the problem is a combination of Stencil's tree-shakable API and a problem with its build-time analysis of dynamic imports.

Stencil tries to ship as little "Stencil" code as possible, so it will analyze your project to find out which features you're actually using and only include those in the final bundle. If you check the ./build/index-{hash}.js (in a dev www build) on line 2 you'll find a list of which features it detected.

I created my own quick reproduction and compared this file when using a dynamic and static import. Here are the differences:

Dynamic Import

{ vdomAttribute: false, vdomListener: false }

Static Import

{ vdomAttribute: true, vdomListener: true }

So it seems that Stencil isn't aware of the features you're only using in the dynamically imported template and therefore doesn't include it in the build. But if you use the same features in any component (or file that is statically imported into a component), Stencil should include it.

So a simple work around would be to attach any listener to any element in any component of your project. And you'd have to do that for every single Stencil feature you're currently only using in a dynamically loaded template.

Another option is to create a Stencil component that statically includes all your dynamic templates. AFAIK this would detect all used features and enable them for the project even without having to use this new component anywhere.

Example:

import { Component, Host, h } from '@stencil/core';
import { Template1 } from "../../templates/template1";
import { Template2 } from "../../templates/template2";
import { Template3 } from "../../templates/template3";

@Component({
  tag: 'template-imports',
})
export class TemplateImports {
  render() {
    return (
      <Host>
        <Template1></Template1>
        <Template2></Template2>
        <Template3></Template3>
      </Host>
    );
  }
}