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

(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.

Related

Organizing multiple web components with seperation of concerns

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.

How to import a blazor custom element in another JS application?

I have a blazor server app, with a registered custom element as below code:
builder.Services.AddServerSideBlazor(options =>
{
options.RootComponents.RegisterAsCustomElement<Counter>("my-blazor-counter");
});
I want to import this blazor custom element in another node.js application to convert it into a lit element(web component).
I have added below scripts in my node.js app
<script src="https://localhost:7075/_framework/blazor.server.js"></script>
<script src="https://localhost:7075/_content/Microsoft.AspNetCore.Components.CustomElements/BlazorCustomElements.js"></script>
but while initializing the Blazor it still using node app port and failing while initialization.
I am not sure I am missing anything here or if there is any other way to do it.
The following describes how I resolved an issue similar to yours: trying to register a custom element, the client not rendering the component and no error message anywhere.
I followed the instructions but the there was nothing happening client-side. After inspecting the websocket's traffic using Firefox I ran into the following message from the client to the server (slightly edited for readability):
ùÀµEndInvokeJSFromDotNetÂÚÙ[
2,
false,
"Could not find 'registerBlazorCustomElement' ('registerBlazorCustomElement' was undefined).
findFunction/<#https://localhost:5001/_framework/blazor.server.js:1:497
findFunction#https://localhost:5001/_framework/blazor.server.js:1:465
E#https://localhost:5001/_framework/blazor.server.js:1:2606
attachWebRendererInterop/<#https://localhost:5001/_framework/blazor.server.js:1:33097
attachWebRendererInterop#https://localhost:5001/_framework/blazor.server.js:1:33145
beginInvokeJSFromDotNet/s<#https://localhost:5001/_framework/blazor.server.js:1:3501
beginInvokeJSFromDotNet#https://localhost:5001/_framework/blazor.server.js:1:3475
_invokeClientMethod/<#https://localhost:5001/_framework/blazor.server.js:1:71894
_invokeClientMethod#https://localhost:5001/_framework/blazor.server.js:1:71880
_processIncomingData#https://localhost:5001/_framework/blazor.server.js:1:69922
kt/this.connection.onreceive#https://localhost:5001/_framework/blazor.server.js:1:64322
connect/</o.onmessage#https://localhost:5001/_framework/blazor.server.js:1:48638
EventHandlerNonNull*connect/<#https://localhost:5001/_framework/blazor.server.js:1:48489
connect#https://localhost:5001/_framework/blazor.server.js:1:48005
_startTransport#https://localhost:5001/_framework/blazor.server.js:1:57626
_createTransport#https://localhost:5001/_framework/blazor.server.js:1:56195
_startInternal#https://localhost:5001/_framework/blazor.server.js:1:54044
async*start#https://localhost:5001/_framework/blazor.server.js:1:51309
_startInternal#https://localhost:5001/_framework/blazor.server.js:1:66198
_startWithStateTransitions#https://localhost:5001/_framework/blazor.server.js:1:65598
start#https://localhost:5001/_framework/blazor.server.js:1:65262
Gn#https://localhost:5001/_framework/blazor.server.js:1:129904
Yn#https://localhost:5001/_framework/blazor.server.js:1:127771
async*#https://localhost:5001/_framework/blazor.server.js:1:131523
#https://localhost:5001/_framework/blazor.server.js:1:131529
"
]
In my case it was that I hadn't added <script src="/_content/Microsoft.AspNetCore.Components.CustomElements/BlazorCustomElements.js"></script> to the html.
I was struggling to get this working for a Blazor serverside app. I created a test.html page in the wwwroot of the blazor project.
The fix for me was to specify the base url.
My html looks like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- my component is here -->
<blazor-counter></blazor-counter>
<base href="http://localhost:5144">
</head>
<body>
<script src="_framework/blazor.server.js"></script>
</body>
</html>

HTML import is not working in Electron app

I'm trying to build desktop app using Electron.
I'm having issue importing HTML template.
Code
<link rel="import" href="fileLocation/file.html">
file.html
<template>
<div>Template html content </div>
</template>
//script in index.html file
<script>
const links = document.querySelectorAll('link[rel="import"]')
Array.prototype.forEach.call(links, (link) => {
let template = link.import.querySelector('template')
//error: cannot read property "querySelectorAll" of undefined
//I tried to console.log(link)
//it gives output: <link rel="import" href="fileLocation/file.html">
//but in electron demo app it is giving actual template.
})
</script>
//running below code in my index.html to test html import compatibility
console.log(
"Native HTML Imports?", 'import' in document.createElement('link'),
"Native Custom Elements v0?", 'registerElement' in document,
"Native Shadow DOM v0?", 'createShadowRoot' in document.createElement('div'));
//output: Native HTML Imports? false Native Custom Elements v0? false Native Shadow DOM v0? false
//running the same code in Electron API demo app (example app given on electron github repo)
console.log(
"Native HTML Imports?", 'import' in document.createElement('link'),
"Native Custom Elements v0?", 'registerElement' in document,
"Native Shadow DOM v0?", 'createShadowRoot' in document.createElement('div'));
//output: Native HTML Imports? true Native Custom Elements v0? true Native Shadow DOM v0? true
Please tell me how can I configure my app so that my result matches with Electron demo app.
I had same issue and seem like it's related to new version of electron (10.1.5).
Revert back to version 7.2.4 works for me

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.

How to link a paper-button to another Polymer page?

I created a Polymer 2.0 app from the starter kit template in order to get to know the framework a little bit. I have a paper-button that should link to another page of the app. However, I still haven't figured out how to do so, since Polymer is loading pages dynamically via JavaScript rather than the browser just calling another one.
I also noticed something else strange: When I click a link in my app-drawer, the page changes automatically and the URL in my browser tab is being updated. However, when I hit refresh with that new URL in my address bar, I get a 404 error since the URL doesn't exist. So is there any way I can resolve this issue and link my button to another page?
This is my button:
<paper-button id="buttonStartQuiz" on-click="startQuiz">
go! <iron-icon icon="chevron-right"></iron-icon>
</paper-button>
And this is the JavaScript class that corresponds to the layout:
class MyView1 extends Polymer.Element {
static get is() { return 'my-view1'; }
/* This method is the listener for the button click */
startQuiz(e) {
// Found this on a website, but doesn't work
this.set('route.path', '/view-question');
}
}
window.customElements.define(MyView1.is, MyView1);
I don't know if it's any useful, but here are my imports in case you need to know.
<link rel="import" href="../bower_components/polymer/polymer-element.html">
<link rel="import" href="../bower_components/iron-icons/iron-icons.html">
<link rel="import" href="../bower_components/paper-input/paper-input.html">
<link rel="import" href="../bower_components/paper-button/paper-button.html">
The fact is Polymer doesn't do that, some element (app-route which implement with Polymer) do that. The Polymer itself is the library that help you work with custom element easier.
This behavior done by JavaScript and History API. See how to use it on mdn. An application like this, dynamically rewriting the current page rather than loading entire new pages its called a single-page application (SPA).
Basically application like this have only one page (index.html). When you try to load from another path the server will cannot find it.
You can resolve this by config the server to serve every path you used with index.html. For development you can easily use polymer serve command from polymer-cli see here.
To link to another page you can done by many ways:
=> Wrap your element with <a>:
<a href='/another-page'>
<paper-button>...</paper-button>
</a>
=> Change route variable from app-location: in my-app.html
<app-location route='{{route}}'></app-location>
...
<paper-button on-click='changeRoute'>...</paper-button>
class MyApp extends Polymer.Element {
...
changeRoute () {
this.set('route.path', '/another-page')
}
...
}
If you want to do this in your file just import and use app-location.
=> Use History API
window.history.pushState({}, null, '/another-page');
// Notify to `app-location`
window.dispatchEvent(new CustomEvent('location-changed'))

Categories

Resources