Organizing multiple web components with seperation of concerns - javascript

I'm building a small application, and planning to use web components (rather than use a UI library). I don't plan to use any bundlers etc., as this is going to be a small personal site.
I would like to store each web component in a separate ES6 JS module file, and here is an example of my setup:
hello-planet.mjs
export class HelloPlanet extends HTMLElement {
constructor() {
super();
}
connectedCallback(){
// Attach a shadow root to the element.
let shadowRoot = this.attachShadow({mode: 'open'});
let planet = this.getAttribute('planet')
shadowRoot.innerHTML = `<p>hello ${planet}</p>`;
}
}
hello-world.mjs
export class HelloWorld extends HTMLElement {
constructor(){
super()
}
connectedCallback(){
// Attach a shadow root to the element.
let shadowRoot = this.attachShadow({mode: 'open'});
['Mercury','Venus','Earth','Mars','Jupiter','Saturn','Uranus','Neptune'].forEach(planet=>{
let el = document.createElement('hello-planet')
el.setAttribute('planet', planet);
shadowRoot.appendChild(el)
})
}
}
main.mjs
// ordering of imports does not matter
import {HelloPlanet} from './hello-planet.mjs';
import {HelloWorld} from './hello-world.mjs';
customElements.define('hello-world', HelloWorld);
customElements.define('hello-planet', HelloPlanet);
// this will typically be handled by a client side router (e.g. Navigo)
let el = document.createElement('hello-world');
let body = document.querySelector('body');
body.appendChild(el);
index.html
(only calls main.mjs, browser will download the rest of the scripts)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="main.mjs" type="module"></script>
<title>Web components test</title>
</head>
<body>
</body>
</html>
Questions:
I've not seen this approach in any examples I've encountered so far, so wondering if
is a good approach? Or there is a better approach than this in terms
of organizing web components.
When I need template+styles, how can
this approach be extended to read those from different files i.e. html and css
in separate files (so we have separation of concerns)?
I've seen this but not sure how to adapt to my kind of setup. I've also gone
through this - but it seems too complex already, and not even sure
if it can handle complex scripting.
Thank you!

I've not seen this approach in any examples I've encountered so far, so wondering if is a good approach? Or there is a better approach than this in terms of organizing web components.
It's perfectly fine. Creating your elements programmatically has many advantages, mainly there is no need to query your own shadow root to get access to child elements/components. If need be, you can directly hold references or even create those in class properties, e.g.:
export class HelloWorld extends HTMLElement {
planets = ['Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune']
.map(
planet => Object.assign(document.createElement('hello-planet'), { planet })
)
)
constructor() {
super().attachShadow({ mode: 'open' }).append(...this.planets);
}
}
Sidenote: Creating the shadow root can and should safely be done in the constructor.
When I need template+styles, how can this approach be extended to read those from different files i.e. html and css in separate files (so we have separation of concerns)?
For CSS, we have CSS module scripts:
import styles from './hello-world.css' assert { type: 'css' }
then, in your constructor, do
constructor() {
// ...
this.shadowRoot.adoptedStylesheets.push(styles);
}
For HTML, this importing feature unfortunately is still work in progress.

Using import assert to load the component's HTML and CSS may be the generic approach moving forward, however, browser support is still limited. To create web components with a separation of concerns and limited build tools (like bundlers) you can use standard fetch with top level await to ensure the component's bootstrapped appropriately before using.
I similarly wanted to separate concerns and minimize tooling while developing modern web components as ES modules. I've started documentation (WIP) on web component best practices that I wanted to follow while developing tts-element.
This is the relevant setup work to bootstrap the component during its import:
const setup = async () => {
const url = new URL(import.meta.url)
const directory = url.pathname.substring(0, url.pathname.lastIndexOf('/'))
const baseUrl = `${url.origin}${directory}`
const [html, css] = await Promise.all([
fetch(`${baseUrl}/template.html`).then((resp) => resp.text()),
fetch(`${baseUrl}/styles.css`).then((resp) => resp.text())
])
const parser = new DOMParser()
const template = parser
.parseFromString(html, 'text/html')
.querySelector('template') as HTMLTemplateElement
const style = document.createElement('style')
style.textContent = css
template.content.prepend(style)
return class TextToSpeech extends HTMLElement {/* implementation */}
}
Here is how the component will be exported to allow top level await to be used:
export default await setup()
Here is an example of how tts-element can be loaded in various ways (all using top level await) to control the timing of when the component is defined.
Loading of defined.js will automatically register the component under the default name text-to-speech.
Using the name query parameter while loading defined.js?name=custom-name will register the component with the provided custom name.
Loading of element.js will require manual registration of the components definition, i.e. the consumer will be responsible for calling define().
Check the network tab in the dev console to see how the HTML/CSS/JS is loaded:
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>tts-element combined example</title>
<style>
text-to-speech:not(:defined), my-tts:not(:defined), speech-synth:not(:defined) {
display: none;
}
</style>
<script type="module" src="https://unpkg.com/tts-element/dist/text-to-speech/defined.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/tts-element#0.0.3-beta/dist/text-to-speech/defined.js?name=my-tts"></script>
<script type="module">
import ctor from 'https://unpkg.com/tts-element/dist/text-to-speech/element.js'
customElements.define('speech-synth', ctor)
</script>
</head>
<body>
<text-to-speech>Your run-of-the-mill text-to-speech example.</text-to-speech>
<my-tts>Example using the "name" query parameter.</my-tts>
<speech-synth>Example using element.js.</speech-synth>
</body>
</html>

The way they do it in react is you import all your components into one file and then you just insert that component into a statice div with an id of root in your HTML page.
So your index file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="main.mjs" type="module"></script>
<title>Web components test</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
Than anything you want to display in the DOM you insert into that div via JS.
I believe this might help your problem, honestly wasn't a 100% clear on your isssue

I had similar questions regarding separating HTML and CSS, but had a hard time finding a solution I wanted.
The "CSS Module Script" that connexo suggest sounded great, but doesn't seem to be supported by Safari.
Here's how I solved this - in case this might resonate with you as well.
I built a CLI tool to use with PHPStorm and VSCode. The IDE will automatically detect changes in HTML and CSS and compile it automatically to JS/TS modules. No need for Webpack, Rollup or similar tools.
I have shared it here: https://www.npmjs.com/package/csshtml-module
It can compile a single CSS or HTML file, or it can bundle both in a HTMLTemplate module.
Example: this (button.css) 👇🏻
button {
background-color: red;
}
automatically becomes this (button.style.ts) when css file has changed 👇🏻
// language=css
export const css: string = `button {
background-color: red;
}`;
And I can use plain JS/TS to get it:
import {css} from './button.style.js'
const style = document.createElement('style');
style.innerHTML = css;
And since the IDE compile on save, it is almost instant changes between CSS/HTML edits and result.

Related

Write HTML tags Inside <script>

I am making a React Online compiler. I know that using HTML and JavaScript could possibly build a online react compiler, as React provides a online CDN!
But, I am new to react. Well, the output from the code will be rendered somelike below:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body> ${htmlCode}
<script src="https://unpkg.com/react#18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#18/umd/react-dom.development.js" crossorigin></script>
<script>${ jsCode }</script>
</body>
</html>
Here, JavaScript will replace the ${htmlCode} with the value of the variable htmlCode. Same, ${jsCode} will replace the provided ReactJS code. Above the tag, you can see the CDN that I am loading to the page.
Now on, if the html code is the below:
<div id="like_button_container"></div>
and...below JavaScript code:
'use strict';
const e = React.createElement;
class LikeButton extends React.Component {
constructor(props) {
super(props);
this.state = { liked: false };
}
render() {
if (this.state.liked) {
return 'You liked this.';
}
return e(
'button',
{ onClick: () => this.setState({ liked: true }) },
'Like'
);
}
}
When the ${jsCode} will be replaced with the JavaScript code, the special symbols included in the Script inherits the HTML. < and > mostly interhit the HTML.
This is the problem!
I created a new JS file and then loaded it in the HTML. But, as in the compiler, the compiler can't create a new file for the JavaScript. So, how can I stop the ReactJS code inheriting the HTML Code?
I also tried to create blob URLS, blob URLS are created but the blob URL is not able to be loaded into the HTML page.
Help, please!
Do you render the LikeButton component to the main DOM? React needs to render virtual DOM.
You could see more here: https://reactjs.org/docs/add-react-to-a-website.html#step-3-create-a-react-component

Polymer 1.x: Incompatible with native web-components?

(tl;dr - yes. However Polymer 2.0 is not and is an easy upgrade.)
I am trying to migrate an old front-end code base away from polymer 1.11.3 (webcomponentsjs 0.7.24) to native web-components.
However, I'm running into the following issue:
When Polymer is included in a page, even trivial native web-components are effectively broken. Web-components that have neither custom methods nor properties seem work. But, include a custom method or property and try to access it, and you'll encounter an error indicating that the property or method does not exist.
For example, given the following component:
customElements.define( 'x-hello', class J extends HTMLElement {
hello() { console.log( 'hello called' ) }
})
document.createElement( 'x-hello' ).hello()
If I run this on a page that includes Polymer,
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<meta name="application-name" content="re">
<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<link rel="import" href="bower_components/polymer/polymer.html">
</head>
<body>
<h1>Here</h1>
</body>
<script>
customElements.define( 'x-hello', class J extends HTMLElement {
hello() { console.log( 'hello called' ) }
})
document.createElement( 'x-hello' ).hello()
</script>
</html>
Then I get the following error:
Uncaught TypeError: document.createElement(...).hello is not a function
<anonymous> debugger eval code:5
However, run it in a page that doesn't include Polymer and it will run just fine.
The few components that I've migrated away from Polymer so far fail in the same way when included in the Polymer-based app I pulled them from. Similarly, running them in a test project that doesn't contain Polymer and they function fine.
I get that both the Polymer lib and webcomponentjs were intended to provide webcomponent support and functionality at a time when browsers didn't support it, but it seems like Polymer should at least be compatible with native web-components.
Is there a work-around for this? Is there some way to incorporate native web-components into an application that is using Polymer 1.x?
I'd really like to be able to migrate components away from Polymer and to native one at a time rather than all at once.

How to fetch HTML and CSS files for web components when hosted on a different domain than the application itself?

I am looking for a clean solution to split web components into JS, HTML and CSS files and host them on a CDN. I try to avoid the webpack html and css-loader as they dont allow me to export my web component as a plain ES module.
The goal is to use a web component from any frontend app just by importing it from a spcified URL. Thereby seperation of concerns should be preserved. Individual files for style, markup and logic also allow for syntax highlighting.
In a local dev environment I found the following to work great:
WebComponent.js:
export default class WebComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
const style = new CSSStyleSheet();
const template = document.createElement("template");
fetch("./WebComponent.css").then((res) =>
res.text().then((css) => {
style.replaceSync(css);
this.shadowRoot.adoptedStyleSheets = [style];
})
);
fetch("./WebComponent.html").then((res) =>
res.text().then((html) => {
template.innerHTML = html;
this.shadowRoot.appendChild(template.content.cloneNode(true));
})
);
}
}
WebComponent.css:
button {
/* Some styling */
}
WebComponent.html:
<button>Custom buttom</button>
I can import the component by using browser native ES module imports:
index.html:
<!DOCTYPE html>
<html>
<body>
<web-component></web-component>
<script type="module">
import WebComponent from "./WebComponent";
customElements.define("web-component", WebComponent);
</script>
</body>
</html>
This works until I move the web component files to a different location (a google cloud storage bucket) than my index.html and import WebComponent.js from there.
<!DOCTYPE html>
<html>
<body>
<web-component></web-component>
<script type="module">
import WebComponent from "https://storage.googleapis.com/storage-bucket/WebComponent.js";
customElements.define("web-component", WebComponent);
</script>
</body>
</html>
WebComponent.js gets imported correctly but it then tries to fetch WebComponent.css and WebComponent.html from a URL relative to localhost where index.html is served. However it should fetch from a URL relative to where it is hosted (https://storage.googleapis.com/storage-bucket/).
Any ideas how something like that can be achieved? Without hard coding the url into both fetch calls. That's not an option as the url can change automatically from time to time.
You are having issue with linking resources in the JS web page for which :
local component is working
import WebComponent from "./WebComponent";
remote component is failing
import WebComponent from "URL";
It might be that for this to work you should try this :
<script type="module" src="https://storage.googleapis.com/storage-bucket/WebComponent.js">
customElements.define("web-component", WebComponent);
</script>
References :
https://cloud.google.com/storage/docs/hosting-static-website
https://www.npmjs.com/package/webcomponent?activeTab=readme
https://lit-element.polymer-project.org/guide/use
JavaScript file paths are relative to the displayed page. So the behavior you are observing is expected.
You can use a JavaScript variable with a simple js declaration like below and use this variable across whenever you assign URLs dynamically:
<script type="text/javascript">
var webComponentPath = 'https://storage.googleapis.com/storage-bucket/';
</script>

How to import external html file to React app?

I have a project in a middle of development. And I need to use external landing page as a home page. Therefore, I need to import landings index.html, but it has its own folders with css and js(mainly Jquery code).
I wanted to import it as <iframe src={html}></iframe> into my project but my app doesn't seem to load htmls.
What are best ways to import html files that use own jquery code to react?
This is a bit tricky, and there might be other ways (perhaps better) to achieve the same result. Also, I would consider the performance impact of loading multiple libraries into an existing React app.
With that humble disclaimer out of the way, one way to do this would be to include jQuery directly into React's main index.html page using <script> tags, this will make $ globally available across the app:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
</body>
</html>
Once this is done, place the landing page project folder (along with its dependencies) inside the public directory:
Then, from the main app component load the desired landing page using fetch, then use .text() to transform the retrieved page into regular text.
Use setState to set the retrieved HTML into the app state, then inside render() use a regular <div> container to store the landing page and use the React attribute dangerouslySetInnerHTML to set HTML inside that target container.
Finally, I pass an anonymous function (as a second parameter to setState) and use jQuery's getScript() to load and execute the required JS libraries that the landing page depends on.
In the example, I loaded Bootstrap's JS, which is needed to power the Carousel.
Bootstrap's CSS is loaded directly from the landing page's HTML file using a standard <link> tag.
import React from "react";
class App extends React.Component {
state = {
page: null
};
componentDidMount() {
fetch("landing-page-one/index.html")
.then(result => {
return result.text();
})
.then(page => {
this.setState(
{
page: { __html: page }
},
() => {
window.$.getScript("landing-page-one/js/index.js");
}
);
});
}
render() {
const { page } = this.state;
return (
<>
<h2>
<span>Inserting project using React's </span>
<code>dangerouslySetInnerHTML</code>:
</h2>
<div
dangerouslySetInnerHTML={page && page}
/>
</>
);
}
}
export default App;
Working example here.
I'm just going to through this out there as a spitball... I've never tried it, but why not directly replace index.html and not render anything on that page? You would have to either adjust the build somehow... I have no idea how to, or manually put it after the react app finishes building. I'm sure it would be a bug nightmare, but it honestly sounds a little less of a hastle than an iframe which might give you some weird UI behavior. React-Server, even though it says server, really just takes stringified JSX and I think html translates it back to workable code. You could also try that approach, but this does sound like a nightmare project.
I was also trying to find out a solution and figured out two ways for different use cases:
If your HTML file contains <div> and other child tags (that can be nested under other <div> or <body> tags) then this approach will work for you:
First you need to configure webpack to be able to parse HTML files.
You can either do that with npm eject or use another module
react-app-rewired - and then add a HTML loader in overrides.
Then import the HTML file and use a parser. I believe it's better than using dangerouslySetInnerHTML.
Example with Method 1:
const parse = require("html-react-parser");
const docs = require("../../public/redoc-static.html").default;
const ApiDocs = (props: any) => {
return (
<Box sx={{ display: "flex", height: "100vh" }}>
<Box>{parse(docs)}</Box>
</Box>
);
};
If your HTML file contains a <html> tag (is a complete file) following the above approach would give the very obvious error <html> cannot appear as a child of <div>. In that case, the workaround I found is to open the whole HTML file in a different tab of the browser with _blank:
Example with Method 2:
window.open(filename, "_blank")
Note: The HTML file has to reside in the public folder of your project.

Is it possible to create a web component anyone can use without installation?

I'm just getting started with web components, and if I understand correctly, the point is for them to be reusable by anyone. Is it possible to create a component that can be used by anyone simply by adding a piece of html to their static site (similar to how JavaScript widgets are added, simply by copy-pasting a few lines of code), or does it need to be installed by someone? Or is this not an intended use case of web components?
Yes. A Web Component is a kind of "Javascript widget".
Typicially, you define a Web Component in a Javascript file.
You can then include it in any HTML with a <script> element.
Example with a minimal custom element called <hello-world>:
In hello-world.js:
customElements.define( 'hello-world', class extends HTMLElement {
constructor() {
super()
this.attachShadow( {mode: 'open' })
.innerHTML = 'Hello World!'
}
} )
In your main page index.html:
<html>
<head>
<!-- import the web component -->
<script src="hello-world.js">
</head>
<body>
<!-- use the new element -->
<hello-world></hello-world>
</body>
</html>
Note: alternately, one could also copy-paste the Javascript code that defines the custom element to its main HTML page.

Categories

Resources