Webpack how to import html template inside decorator just like Angular

374 Views Asked by At

I am writing a decorator for custom elements,the primary code is:

interface CustomElementOptions {
  selector: string,
  template?: { default: string };
}

function CustomElement(options: CustomElementOptions) {
  return (target: CustomElementConstructor) => {
    if(options.template) {
      const template = document.createElement('template');
      template.innerHTML = options.template.default;
      Object.defineProperty(target.prototype, 'template', {
        value: template,
      });
    }
    window.customElements.define(options.selector, target as any);
  };
}

@CustomElement({
  selector: 'test-span',
  template: require('./template.html'),        // <----------------------   I want to change here with `templateUrl`
})
export class SpanElement extends HTMLElement {

  public template?: HTMLTemplateElement;

  constructor() {
    super();
    if(this.template) {
      this.appendChild(this.template.content.cloneNode(true));
    }
  }

}

I am now using raw-loader to load html template, but this is not elegant enough, I want to be like Angular:

@CustomElement({
  selector: 'test-span',
  templateUrl: './template.html',        // <----------------------  templateUrl
})
export class SpanElement extends HTMLElement { ... }

But I can not use require or import inside decorator, the error message like this:

Uncaught Error: Cannot find module './template.html'
    at webpackEmptyContext (app|sync:2)
    at application.ts:10
    at __webpack_modules__../src/app/application.ts.__decorate (index.js:6)
    at Object../src/app/application.ts (application.ts:20)
    at __webpack_require__ (bootstrap:18)
    at Object../src/main.ts (main.ts:1)
    at __webpack_require__ (bootstrap:18)
    at startup:3
    at startup:5

What's the magic inside angular-cli? I tried my best, thanks for your help!

1

There are 1 best solutions below

0
On

Finally, Finally! I found the solution.

The short answer is update the typescript ast.

The next are my detailed steps.

I had no idea at first, I want to achieve automated require('./template.html'), then I thought of AST, I am using ts-loader for compile typescript, ts-loader provider getCustomTransformers to custom typescript transformer, and this is my transformer, please ignore my messy code, this is my first time working with AST:

{
  loader: 'ts-loader',
  options: {
    configFile: 'tsconfig.json',
    getCustomTransformers: program => {
      return {
        before: [
          context => {
            const isIncludeTemplate = node => {
              return !!node.arguments[0].properties.find(property => property.name.escapedText === 'template');
            };
            const updateCustomElement = node => {
              if(isIncludeTemplate(node)) {
                return node;
              } else {
                const objectLiteralExpression = node.arguments[0];
                objectLiteralExpression.properties.push(
                  factory.createPropertyAssignment(
                    factory.createIdentifier('template'),
                    factory.createPropertyAccessExpression(
                      factory.createCallExpression(
                        factory.createIdentifier('require'),
                        undefined,
                        [objectLiteralExpression.properties.find(property => property.name.escapedText === 'templateUrl').initializer]
                      ),
                      factory.createIdentifier('default')
                    ),
                  ),
                );
                return factory.updateCallExpression(node, node.expression, undefined, [objectLiteralExpression]);
              }
            };
            const visit = node => {
              if(ts.isCallExpression(node) && node.expression.escapedText === 'CustomElement') {
                const newNode = updateCustomElement(node);
                return newNode;
              }
              return ts.visitEachChild(node, child => visit(child), context);
            };
            return node => ts.visitNode(node, visit);
          },
        ],
      };
    },
  },
}