Separating HTML and CSS from Javascript Lit Element

6.3k Views Asked by At

We are working with Lit Element and our component files are getting rather large because we have to write styling and html within the .js file. Is there a way to split them in separate files and then import them into our components? What would be the best approach to doing so? HTML and CSS especially are making the workflow difficult for us since our files are getting too bloated.

Update: Supersharp's advice helped me separate the CSS, and the html template but I am having issue binding the html template with specified properties. I have a service file which is making an XML request for a file I specified, and them I am importing it in my component file. Here is my component:

import { LitElement, html} from '@polymer/lit-element';

import HtmlLoader from './Service/HtmlLoader';

class TestAnimatedButtonElement extends LitElement {

  static get properties() {
    return {
      _text: {
        type: String
      }
    }
  }

  constructor() {
    super();
    this.htmlTemplate = HtmlLoader.prototype.getHtmlTemplate('/src/Components/ExampleElements/test-animated-button-element.html');
  }

  render(){
    this.htmlTemplate.then( template => this.shadowRoot.innerHTML = template)
    return html( [this.htmlTemplate] );
  }

  handleButton(e) {
    //e.preventDefault();
    console.log('Button pressed');
  }
}
customElements.define('test-animated-button-element', TestAnimatedButtonElement);

And this is my html file:

<link rel="stylesheet" href="src/Components/animatedButton/animated-button-element.css"/>
<a href="#" class="btn btn--white btn--animated" @click="${(e) => this.handleButton(e)}">${this._text}</a>

And this is the service file I am using for making XMLHttpRequest:

export default class HtmlLoader {

    xhrCall(url){
      return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open("GET", url);
        xhr.onload = () => resolve(xhr.responseText);
        xhr.onerror = () => reject(xhr.statusText);
        xhr.send();
      });
    }

    getHtmlTemplate(url) {
       return this.xhrCall(url);
    }
}
2

There are 2 best solutions below

6
On BEST ANSWER

You can import CSS and HTML parts separately.

CSS

Use a <link rel="stylesheet"> element in the HTML code.

HTML

You can use HTML Imports (deprecated), or XMLHttpRequest, or fetch(), as explain in this post about importing templates for custom elements.

Update

With XMLHtpRequest you'll get the answer in a Promise.

render(){
    this.htmlTemplate.then( template =>
        this.shadowRoot.innerHTML = template
    )
}
2
On

This is the downside of ES Modules, and it's why Google pushed for HTML Imports for so long despite other browser vendors being dead against them.

You can seperate out your CSS and JS, but you're adding additional round trips to the browser if you get them at run time. In particular your templates are retrieved asynchronously, if you wait for them you'll be blocking the main JS thread while you do, and if you await them you'll break lit-element as it expects the root template to be synchronous.

You could (but shouldn't) use until:

import { until } from 'lit-html/directives/until';

...

render(){
    return html`until(
        this.loadTemplate(
            '/src/Components/ExampleElements/test-animated-button-element.html',
            this.foo, 
            this.bar), html`Loading...`)`;
}

async loadTemplate(path, ...parts) {
    // You're using templates you can use fetch, instead of XMLHttpRequest
    const r = await fetch(path, { method: 'GET', credentials: 'same-origin' });
    if(r.ok)
         return html(await r.text(), parts);

    return html`Not Found: ${path}`
}

However, I notice that you're using bare modules (i.e. without a . at the start and .js at the end) so you can't be serving this file direct to browsers (none can handle bare modules yet). In short: you must have something that turns:

import { LitElement, html} from '@polymer/lit-element';

Into:

import { LitElement, html } from '../../node_modules/@polymer/lit-element/lit-element.js';

Or bundles those together into a single file. Given you're doing that why not make your bundler (Webpack, RollupJS, Browserify, whatever) import your CSS and HTML into the file at build time?

Finally, with any of these solutions you lose write-time parts<->html relationship, which doesn't matter for CSS but will make the HTML parts much harder to develop/maintain. With a library like lit you want your HTML in your JS, for the same reasons that JSX/React/Preact works so well. If your files are too large then I don't think the solution is to split them by JS/CSS/HTML - instead split them into more components.

So, for instance, suppose you have a massive <product-detail> control...

Don't

  • product-detail.js
  • product-detail.html
  • product-detail.css

Do

  • product-detail.js
  • product-detail-dimensions.js
  • product-detail-specs.js
  • product-detail-parts.js
  • product-detail-preview.js
  • etc

Break each component down to do a specific task. The whole point of lit-element is to make all these as simple as possible to create, and you want lots of small, self-contained components.