createElement on Custom Element breaks template - web-component

I am creating two custom elements, both are added to index.html using link rel="import". One is a container with slots and the other is something to put a number in the slots. Both elements each have an HTML file with the template and a link to a js file that defines them as custom elements. To link the custom element class declaration to the HTML template I am using:
class PuzzlePiece extends HTMLElement{
constructor(){
super();
console.dir(document.currentScript);
const t = document.currentScript.ownerDocument.getElementById('piece-puzzle');
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(t.content.cloneNode(true));
}
This puzzle-piece element and the container render properly and it all works when you manually put them in the index.html light dom
<special-puzzle id="grid">
<puzzle-piece id="hello"></puzzle-piece>
</special-puzzle>
However, once I try and create and append a puzzle-piece using JS in index.html:
<script>
const addToGrid = document.createElement("puzzle-piece");
document.getElementById("grid").appendChild(addToGrid);
</script>
I see a new puzzle-piece in the special-puzzle light dom but it is not taking up a slot, doesn't render, and the console has the error:
Uncaught TypeError: Cannot read property 'content' of null
at new PuzzlePiece (puzzle-piece.ts:8)
at HTMLDocument.createElement (:3:492)
at (index):37
As far as I can tell the problem is when using document.createElement the browser is getting to the class define but the document.currentScript.ownerDocument is different from when just manually using HTML tags. I believe because of this, the component can't find it's template. This is my first Stack Overflow question so any feedback/help would be appreciated!

Solved thanks to the awesome #Supersharp and their Stack Overflow post
Basically, in order to preserve the correct document.currentScript.ownerDocument I need to declare it in a var before the class then use that var in the class.
Old:
class PuzzlePiece extends HTMLElement{
constructor(){
super();
const t = document.currentScript.ownerDocument.getElementById('piece-puzzle');
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(t.content.cloneNode(true));}
New:
var importedDoc = document.currentScript.ownerDocument;
class PuzzlePiece extends HTMLElement{
constructor(){
super();
const t = importedDoc.getElementById('piece-puzzle');
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(t.content.cloneNode(true));}

Related

Vue - using props on custom elements fails using createApp and mount?

We would like to pass props to custom elements that uses createApp
// index.html
<div id="my-root">
<my-element prop1="abc"></my-element>
</div>
// my-element.vue
<script lang="ts" setup>
const props = defineProps<{ prop1: number }>();
</script>
<template>
{{props.prop1}}
</template>
This works fine, but as our custome element get bigger we would like to register components and use e.g pinia and other tools. Do use those we need to add createApp and mount it. But then prop1 is always undefined
// main.ts
import ...<lots of imports>
import AppCe from "./AppWebComponent.ce.vue";
import { createPinia } from "pinia";
// Adding code below is causing prop1 to be undefined - if we skip this part, prop1 works fine
const pinia = createPinia();
const app = createApp(App);
app.use(pinia).use(ConfirmDialog);
app.component(...<lots of components>);
app.mount("#my-root");
const ceApp = defineCustomElement(AppCe);
customElements.define("my-element", ceApp);
update:
Here's a sample without: https://stackblitz.com/edit/vue3-script-setup-with-vite-56rizn?file=src/my-element/my-element-main.js
And here's a sample with the createApp: https://stackblitz.com/edit/vue3-script-setup-with-vite-gtkbaq?file=index.html
Any idea on how we could solve this?
We have a fallback, that is to do a getElementById and read the attribute value in the mounted callback - but that is not an optimal solution.
Thanks for any ideas!
update2:
Here's an attempt using #duannex suggestion. We're getting closer, the app is availible, components registered, but still no sigar. : https://stackblitz.com/edit/vue3-script-setup-with-vite-ofwcjt?file=src/my-element/defineCustomElementWrapped.js
Based on update2 with the wrapped defineCustomElement; Just pass the props to the render function:
render() {
return h(component, this.$props)
},
https://stackblitz.com/edit/vue3-script-setup-with-vite-vfdnvg?file=src/my-element/defineCustomElementWrapped.js

Extending React Types for CSS custom attributes

I'm converting my React project to TypeScript, but can't figure out how to extend React.HTMLPorps to accept custom CSS data-attributes.
A common pattern that I use in my project is:
const MyComponent = ( ) => <div mycustomattr="true"/>;
I've tried creating an interface like so:
interface ExtendedDiv extends HTMLProps<HTMLDivElement> {
mycutomattr: "true" | "false";
};
However, I'm not sure how to apply this to JSX div element.
I can see 2 ways:
easiest way is to rename atribut - custom => data-custom. But this will results in:
<div data-custom="true" />
extend react interface HTMLAttributes but it will allow this attribute to all elements
interface HTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
// extends React's HTMLAttributes
custom?: boolean;
}

Change css variables dynamically in angular

In my angular project, I have some css variables defined in top level styles.scss file like this. I use these variable at many places to keep the whole theme consistent.
:root {
--theme-color-1: #f7f7f7;
--theme-color-2: #ec4d3b;
--theme-color-3: #ffc107;
--theme-color-4: #686250;
--font-weight: 300
}
How can I update values of these variables dynamically from app.component.ts ? And What is the clean way to do this in angular ?
You can update them using
document.documentElement.style.setProperty('--theme-color-1', '#fff');
If u want to update many values, then create a object
this.styles = [
{ name: 'primary-dark-5', value: "#111" },
{ name: 'primary-dark-7_5', value: "#fff" },
];
this.styles.forEach(data => {
document.documentElement.style.setProperty(`--${data.name}`, data.value);
});
The main thing here is document.documentElement.style.setProperty. This line allows you to access the root element (HTML tag) and assigns/overrides the style values.
Note that the names of the variables should match at both places(css and js files)
if you don't want to use document API, then you can use inline styles on HTML tag directly
const styleObject = {};
this.styles.forEach(data => {
styleObject[`--${data.name}`] = data.value;
});
Then In your template file using ngStyle (https://angular.io/api/common/NgStyle)
Set a collection of style values using an expression that returns
key-value pairs.
<some-element [ngStyle]="objExp">...</some-element>
<html [ngStyle]="styleObject" >...</html> //not sure about quotes syntax
Above methods do the same thing, "Update root element values" but in a different way.
When you used :root, the styles automatically got attached to HTML tag
Starting with Angular v9 you can use the style binding to change a value of a custom property
<app-component-name [style.--theme-color-1="'#CCC'"></app-component-name>
Some examples add variables directly to html tag and it seem in the element source as a long list. I hope this helps to you,
class AppComponent {
private variables=['--my-var: 123;', '--my-second-var: 345;'];
private addAsLink(): void {
const cssVariables = `:root{ ${this.variables.join('')}};
const blob = new Blob([cssVariables]);
const url = URL.createObjectURL(blob);
const cssElement = document.createElement('link');
cssElement.setAttribute('rel', 'stylesheet');
cssElement.setAttribute('type', 'text/css');
cssElement.setAttribute('href', url);
document.head.appendChild(cssElement);
}
}

How to import CSS file content into a Javascript variable

Consider a very simply custom element using shadow DOM:
customElements.define('shadow-element', class ShadowElement extends HTMLElement {
constructor() {
super();
this.styleTag = document.createElement('style');
this.styleTag.textContent= `
.root::before {
content: "root here!";
color: green;
}
`
this.shadow = this.attachShadow({mode: 'closed'});
this.root = null;
}
connectedCallback() {
this.root = document.createElement('div');
this.root.className = 'root';
this.shadow.append(this.root, this.styleTag);
}
})
<shadow-element></shadow-element>
To get the CSS into the shadow DOM, I create a style tag, which I append into the shadow root. This is all working fine so far.
Now for more complex CSS I would like to author it in a file shadow-element.css which is in the same folder as shadow-element.js. Besides seperation of concerns I also want IDE syntax highlighting and -completion for CSS authoring, so I really want the CSS in a separate, dedicated file.
I want to import the contents of that CSS file into a Javascript variable, like
import styles from './shadow-element.css'; // obviously doesn't work
On the project where this is being used we have a working webpack stack that allows importing CSS (and even SCSS), but unfortunately that imported CSS then becomes part of bundle.css - which obviously is not at all useful, because the element uses shadow DOM.
Does anyone have a solution to this? I'm also open to alternative solutions, as long it won't require me to author my CSS in a .js file.
Edit: I am aware of the option of using #import './shadow-elements.css'; inside the style tag, but I would much prefer a solution that bundles the imported CSS into my Javascript bundle (as part of the component code).
As you are using webpack, you can use raw-loader to import a text file (CSS in your case) into a string:
npm install raw-loader --save-dev
And you can use it inline in each file:
import css from 'raw-loader!./shadow-element.css';
customElements.define('shadow-element', class ShadowElement extends HTMLElement {
constructor() {
super();
this.styleTag = document.createElement('style');
this.styleTag.innerText = css;
this.shadow = this.attachShadow({mode: 'closed'});
this.root = null;
}
connectedCallback() {
this.root = document.createElement('div');
this.root.className = 'root';
this.shadow.append(this.root, this.styleTag);
}
})

Web component template filling multiple named slots

Given the following html template:
<div class="Page">
Hello <slot name="personName"></slot>. Your name is <slot name="personName"></slot>.
</div>
How is it possible (if at all) to fill both slots with one value using custom elements?
The below demo code will produce:
Hello Bob, Your name is .
Is this intended? Is this the wrong way of displaying a single value in multiple locations of a template?
let tmpl = document.createElement("template");
tmpl.innerHTML = `
<div>
Hello <slot name="personName"></slot>. Your name is <slot name="personName"></slot>.
</div>
`;
class MyElement extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.appendChild(tmpl.content.cloneNode(true));
}
}
customElements.define("x-myelement", MyElement);
<x-myelement>
<span slot="personName">Bob</span>
</x-myelement>
It's the normal behavior.
If you want to reuse a variable multiple times, a good solution is to use template literals with placeholders.
In a template literal string (that is a string delimited with back-ticks), placeholders are replaced by the value of the matching variable.
let name = "Bob"
let template = `Hello ${name}. Your name is ${name}`
// template will contain "Hello Bob. Your name is Bob"
You can get the content of the light DOM using querySelector() on the slot attribute, or on any other attribute you prefer. To get the value, use property textContent, innerHTML, or even outerHTML if you wan to keep the surrounding <span> element.
class MyElement extends HTMLElement {
connectedCallback() {
var person_name = this.querySelector( '[slot=personName]' ).outerHTML
this.attachShadow({ mode: "open" })
.innerHTML = `Hello ${person_name}. Your name is ${person_name}`
}
}
customElements.define("x-myelement", MyElement)
<x-myelement>
<span slot="personName">Bob</span>
</x-myelement>
take a look at LitHtml and MDN
make name as an attribute of element, then inject it into the template.
const tmpl = document.createElement('template');
tmpl.innerHTML = `
<style>span{font-weight:bold}</style>
<p>Hello <span id="name"></span> Your name is <span id="name2"></span></p>`;
class MyElement extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: "open" });
shadow.appendChild(tmpl.content.cloneNode(true));
const name = this.getAttribute('personName');
shadow.querySelector('#name').innerText = name;
shadow.querySelector('#name2').innerText = name;
}
}
customElements.define("x-myelement", MyElement);
<x-myelement personName="Bob"></x-myelement>
slots are not supported by all browser, especially named slots.
second, slotted content are not within the scope of the component (shadow dom), unless that was what you where going for?

Resources