0

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

minigeek
  • 2,766
  • 1
  • 25
  • 35
  • You might have more luck using [functional components](https://stenciljs.com/docs/functional-components#working-with-functional-components) for the templates. – Thomas Dec 22 '21 at 15:21
  • @Thomas functional components is also having the same issue. This is very weird. H – minigeek Dec 23 '21 at 17:07

2 Answers2

2

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>
    );
  }
}
Thomas
  • 8,426
  • 1
  • 25
  • 49
  • Nice explanation. thanks a lot! I came to same conclusion.I tried the last option of maintaining all templates in one file and using logical component as child of template component. it works but I loose lazy loading template feature. I guess I will have to stick to this.el.shadowroot.queryselector('#id')).addEventListener for now. this works. But stencil should consider functional components for analyzing, exact same thing works in react :/ – minigeek Dec 26 '21 at 13:23
  • The problem isn't that they're functional components but the dynamic import. If you import them statically they are analyzed correctly. And I didn't mean keeping all templates in one file but to have an additional component where you statically import each template. Then you can still import them dynamically in other components and as long as you don't actually use this additional component the user won't have to download more. – Thomas Dec 26 '21 at 13:46
  • could you please add an answer sample on how to statically import, I didn't quite get that part on how to exactly – minigeek Dec 27 '21 at 09:50
  • Sure, I've added an example. – Thomas Dec 27 '21 at 12:03
  • ah gotcha, but this won't be lazy load anymore and increase chunk size :( – minigeek Dec 28 '21 at 06:23
  • No, because you don't need to actually use that component anywhere, it's only so the Stencil compiler knows about the templates. It will still get compiled to the output folder but the users will never have to download it. – Thomas Dec 28 '21 at 08:14
0

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>;
  }
}
jlang
  • 929
  • 11
  • 32
  • isn't it like same thing as registering random div with event listners :(. I have to do this for id, style, placeholders, events. But I think you have used functional component approach, I will try this. for every event however i have to pass props like this. – minigeek Dec 25 '21 at 07:20