Separating HTML and CSS from Javascript Lit Element
Asked Answered
A

2

7

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);
    }
}
Abash answered 29/11, 2018 at 14:33 Comment(0)
C
1

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
    )
}
Countryandwestern answered 29/11, 2018 at 16:48 Comment(6)
The link tag for CSS worked perfectly but adding HTML is proving more difficult. I am retrieving the HTML template by using an XMLHttpRequest I am assigning the file to a variable in the constructor, and then I am just calling the variable in the return html within the render method. When I load the page I can see the html markup on my page, but it is in a string format. You can see the code as I am updating my post.Abash
@FilipSkukan did you try return this.htmlTemplate in render()?Countryandwestern
Yep, it's not rendering at all when I do thatAbash
sorry you should use return html( [this.htmlTemplate] ), given the this.htmlTemplates contains the string (not the promise). Also you shoul set the this.shadowRoot.innerHTML directly. See the updated answer.Countryandwestern
I just tested passing a property in the html like ${this._text} but it just renders the component with ${this._text} text rendered instead of binding it to the property. Is there a workaround for this? There is little use to separating the html if I cannot bind the properties to it. You can see the updated code above.Abash
${} notation will work only in template literals, you cannot use it in external HTML. As a workaround, you could import template literals in a javascript module, or parse the HTML file. It's a limitation a template literals, which are not objects but only a notation for creating strings.Countryandwestern
F
3

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.

Frailty answered 13/12, 2018 at 9:18 Comment(2)
Great advice, although sometimes it is not so simple to split components into more modules. We are currently working on migrating a project from Polymer 2 to Polymer 3 and one of the requirements is to keep everything separate since the team wants to keep the same workflow. We are looking into Webpack as we speak and exploring the possibility of adding the html code after building the project.Abash
@FilipSkukan I recently went through the same thing, wrote a utility to switch from JS in HTML to HTML in JS and swap from Polymer 2 to 3. For Polymer 3's bindings you can have an external HTML file that bundlers put into the static get template and it works ok, but if you're planning to migrate on to LitElement that really doesn't work well. We've relied on extensions like bierner.lit-html instead.Frailty
C
1

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
    )
}
Countryandwestern answered 29/11, 2018 at 16:48 Comment(6)
The link tag for CSS worked perfectly but adding HTML is proving more difficult. I am retrieving the HTML template by using an XMLHttpRequest I am assigning the file to a variable in the constructor, and then I am just calling the variable in the return html within the render method. When I load the page I can see the html markup on my page, but it is in a string format. You can see the code as I am updating my post.Abash
@FilipSkukan did you try return this.htmlTemplate in render()?Countryandwestern
Yep, it's not rendering at all when I do thatAbash
sorry you should use return html( [this.htmlTemplate] ), given the this.htmlTemplates contains the string (not the promise). Also you shoul set the this.shadowRoot.innerHTML directly. See the updated answer.Countryandwestern
I just tested passing a property in the html like ${this._text} but it just renders the component with ${this._text} text rendered instead of binding it to the property. Is there a workaround for this? There is little use to separating the html if I cannot bind the properties to it. You can see the updated code above.Abash
${} notation will work only in template literals, you cannot use it in external HTML. As a workaround, you could import template literals in a javascript module, or parse the HTML file. It's a limitation a template literals, which are not objects but only a notation for creating strings.Countryandwestern

© 2022 - 2024 — McMap. All rights reserved.