Webfundamentals, CustomElement with shadowDOM and HTML Templates with HTML import - web-component

I have various questions about the webfundamentals implementations, i have read that a true web component must have shadowDOM for css encapsulation, customElements for the logic of the component which i really love, and HTML Temapltes and import, so im trying to do it all in a customElement component and i have encounter with many and many issues that i find really hard to debug, i will enlist them all.
Does i have to insert html template into the document to actually get it? cant i get its content from js only? and in case i have to, when i intent to replace a shadowHost content hows is it works, i mean i got the template (the link) inside the shadowRoot, my actuall problem is that when i do querySelector(link[rel="import"]).import.querySelector("template") its null after the .import function tag and when i insert that function into the document, it actually gets the template content, heres the doc.
Watching that screenshot i got 2 more questions
Should i use shadowHost.innerHTML = file.querySelector(link[rel="import"]).import.querySelector("template")
to use the tag and copy its content inside the shadowRoot element? i mean how can i implement that approach? im using Angular as first example, they use an HTML file (which im guessing its a template or slots tag) and then they add it into the component as parameters on the constructor, so how with HTMLTemplates and HTMLImport i can implement that behaviour, i have used the documented functions but it doesnt works in the final phase.
Should i keep <link rel="import"> inside the shadowRoot or inside the document.head? can i implement the template without the need of adding it into the document?
I have been trying for days to do a simple customElement with shadowDOM that works completly fine, the problem is when i try to add an external to make it more robust.
Any helps? suggestions? i can show some functions that i use on the components to have an idea.
class EgHeader extends HTMLElement {
constructor() {
super();
this.shadowHost = shadowHost.bind(this);
this.shadowStyle = shadowStyle.bind(this);
this.shadowTemplate = shadowTemplate.bind(this);
this.host = this.shadowHost();
}
connectedCallback() {
this.defaultProperties();
let importSelector = this.host.querySelector(`link[rel="import"]`);
console.log(importSelector);
// this.host.appendChild(importSelector.cloneNode(true));
this.host.innerHTML = importSelector.import.querySelector(
"template"
).content;
}
defaultProperties() {
this.getAttributeNames().forEach(key => {
console.log(key);
if (key === "css") {
return this.shadowStyle(this.getAttribute(key));
}
if (key === "template") {
return this.shadowTemplate(this.getAttribute(key));
}
});
}
}
customElements.define("eg-header", EgHeader);
function shadowHost() {
let root = this.attachShadow({
mode: "open"
});
return root;
}
function shadowStyle(stylesheet) {
let link = document.createElement("link");
link.rel = "stylesheet";
link.href = stylesheet + ".css";
this.host.appendChild(link.cloneNode(true));
return link;
}
function shadowTemplate(link) {
var template = document.createElement("link");
template.rel = "import";
template.id = `${link}-template`;
template.href = link + ".html";
document.head.appendChild(template);
this.host.appendChild(template);
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Page Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="./Header.js"></script>
<script src="./index.js"></script>
</head>
<body>
<eg-header css="./Header" template="./Header">
</eg-header>
</body>
</html>
// Separated file called Header.html
<template>
<nav>This is X element</nav>
<script>
console.warn("Executed when the template is activated.");
</script>
</template>

i have read that a true web component must have shadowDOM for css encapsulation, customElements for the logic of the component which i really love, and HTML Temapltes and import
What you've read is quite outdated:
HTML Imports are deprecated so you should use another method to load templates.
Because of #1, HTML templates (aka <template> elements) are often replaced par template literals.
Templates literals can be define in Javascript. This way they can be defined in a classical Javascript file or in the ES6 module.
By the way, if you still want to use HTML Imports (not recommanded) , you'll need to use a polyfill.
<link rel="import"> should be put in the <head> element of the main document, not in the Shadow DOM.
If you want to use <template> you don't need to append it to the main document.
var template = document.createElement( 'template' )
template.innerHTML = `
<h1>Title</h1>
<div>Content</div>
`
...
this.shadowRoot.appendChild( template.content.clone( true ) )

Related

Vue conditional style tag in single file component

I have started development on a vue web component library. Members of my team asked for the potential to remove default styles via an HTML attribute on the web component. I know that I could use CSS class bindings on the template elements, however, I was wondering if there is a way to conditionally include the style tag itself so that I would not need to change the class names in order to include the base styles or not.
Example of a component's structure
<template>
<section class="default-class" />
</template>
<script>
export default {
props: {
useDefault: Boolean
}
}
</script>
<style>
// Default styles included here
// Ideally the style tag or it's content could be included based off useDefault prop
</style>
Potential implementation
<web-component use-default="false"></web-component>
As I read your question; you want to keep <style> both affecting Global DOM and shadowDOM
One way is to clone those <style> elements into shadowDOM
But maybe ::parts works better for you; see: https://meowni.ca/posts/part-theme-explainer/
customElements.define("web-component", class extends HTMLElement {
constructor() {
super()
.attachShadow({mode:"open"})
.innerHTML = "<div>Inside Web Component</div>";
}
connectedCallback() {
// get all styles from global DOM and clone them inside the Web Component
let includeStyles = this.getAttribute("clone-styles");
let globalStyles = document.querySelectorAll(`[${includeStyles}]`);
let clonedStyles = [...globalStyles].map(style => style.cloneNode(true));
this.shadowRoot.prepend(...clonedStyles);
}
});
<style mystyles>
div {
background: gold
}
</style>
<style mystyles>
div {
color: blue
}
</style>
<div>I am Global</div>
<web-component clone-styles="mystyles"></web-component>

Unload/remove dynamically loaded css files

After loading a css file like this:
const themes = ['dark-theme.css', 'light-theme.css'];
async function loadcss(file) {
return await import(file);
}
loadcss(themes[0]).then(console.log)
The console output is an empty object for me and a new annonymous < style> tag sits in the < head> of my index.html. So far so good, but what if I (in this example) want to change the theme to light-theme.css. That would merge both themes as dark-theme.css is already loaded.
Is there a way to remove the < style> tag from the DOM?
To furthermore specify my question, the provided example shows an abstracted behaviour and I am only interested in removing the dynamically loaded css from the DOM.
I don't know vue.js but here is simple example in React hope it helps somehow :) perhaps some ideas at least :)
class TodoApp extends React.Component {
static themes = {
dark: 'dark-theme.css',
light: 'light-theme.css',
};
render() {
return ReactDOM.createPortal(
(<link rel="stylesheet" href={TodoApp.themes.dark} type="text/css"></link>),
document.head
);
}
}
ReactDOM.render(<TodoApp />, document.querySelector("#app"))
http://jsfiddle.net/3y4hw2ox/
Thanks to OZZIE, I questioned my methodology and found, that importing the css files, like my question shows (through ES6 import, or require.context(...)), is not usefull, as we can't identify it, we dont get access to the <style> element, leaving us with no entry to the DOM and no way to manipulate it.
Instead we will link the css files manually in the <head>, as we know their name and path.
const themes = ['dark-theme.css', 'light-theme.css'];
const head = document.body.parentElement.firstElementChild;
const link = document.createElement('link');
link.setAttribute('href', process.env.BASE_URL + themes[0]);
link.setAttribute('id', themes[0]); // set id so we can remove it later
head.appendChild(link);

How does the asset schema property work with img assets?

I am trying to pass an image as an asset property type to some other component (so that either a #selector or a url(url) can be passed) but it seems to take in the entire html component, instead of just the url.
<!DOCTYPE html>
<html>
<head>
<title>Hello, WebVR! - A-Frame</title>
<meta name='description' content='Hello, WebVR! - A-Frame'>
<script src='../../global/js/aframe-v0.8.0.min.js'></script>
<script>
AFRAME.registerComponent('some-component', {
schema: {
image: {type:'asset', default:''},
model: {type:'asset', default:''}
},
init: function() {
console.log(this.data.image); //prints out <img id="SomeImage" src="../../someDir/someFile.jpg">
console.log(this.data.model); //prints out '../../someDir/someModel.gltf'
}
});
</script>
</head>
<body>
<a-scene >
<a-assets timeout='3000'>
<!-- this works as an asset no problem -->
<a-asset-item id='SomeModel' src='../../global/assets/models/gltf/UserHead/UserHead.gltf'></a-asset-item>
<!-- this does not pass as an asset but rather an html element -->
<img id='SomeImage' src='../../global/assets/textures/equirectangular/CloudySky.jpg'>
</a-assets>
<a-entity some-component='image:#SomeImage; model:#SomeModel;'></a-entity>
</a-scene>
</body>
</html>
Thought I might look to see how A-Frame handles this in the material component can't see where does the 'src' property on material come from?
<a-entity id='skyBox'
geometry='primitive:sphere; radius:50; segments-height:6; segments-width:6;'
material='shader:flat; src:#skyMap; side:back; height:2048; width:2048'>
</a-entity>
Material component (can't see src): https://github.com/aframevr/aframe/blob/master/src/components/material.js
Thanks!
EDIT:
As per Piotr's discoveries below it looks like images are handed as a special case, as can be in the src code here with frame 0.8.0 where an image source is handed like this:
hash: function (data) {
if (data.src.tagName) {
// Since `data.src` can be an element, parse out the string if necessary for the hash.
data = utils.extendDeep({}, data);
data.src = data.src.src;
}
return JSON.stringify(data);
},
So basically if an image we the asset property will not handle image's properly and an additional step of grabbing the url from it via
data.src.src
OR
data.src.getAttribute('src');
Please correct me if i'm wrong, but i think it's not in the material schema.
I think the devil's in the component.js, which not only seems to allow you to assign a value to any given property, but also has the constructor for any component, and parses the schema.
That being said, the material does not need a src in the schema, as it seems to be a part of every component. Furthermore there are multiple parsers like the assetParse, or src-loader, checking whether an attribute is a html element, or even a video / image asset.
As for the material part, check out the dist source code, where
I think what you're looking for is:
module.exports.updateMapMaterialFromData
where you can see a-frame team uses the data.src for the material, and updates the texture with it. Just give it a ctrl+F (only 3 hits).
So when
module.exports.updateMapMaterialFromData('map', 'src', shader, data);
is called, with the given definition:
module.exports.updateMapMaterialFromData = function (materialName, dataName, shader, data) {
var el = shader.el;
var material = shader.material;
var src = data[dataName];
.......
makes src = data[src] => they make updates using a local variable src.
Also you can see the src-loader in action where the material system is registered, and when a src attribute is found out, the validateSrc function fires one of the two callbacks:
utils.srcLoader.validateSrc(src, loadImageCb, loadVideoCb);

Use a remote stylesheet inside a template tag (with shadow dom)

I am trying to make a semi-resuseable widget but I am running into a problem. I am trying to encapsulate a some CSS code inside a shadow root so that it does not affect the rest of the webpage but this CSS is used across multiple widgets so I am trying to include a remote stylesheet. None of the examples I have found use a remote style sheet and I was wondering if this was possible.
EX:
<template id="templateContent">
<head>
<link rel="stylesheet" href="css/generalStyle1.css">
</head>
<body>
<div class="affectedByGeneralStyle1"></div>
</body>
</template>
script to include template:
<div id="host"></div>
<script>
var importedData = (html_import_element).import.getElementById("templateContent");
var shadow = document.querySelector('#host').createShadowRoot();
var clone = document.importNode(importedData.content, true);
shadow.appendChild(clone);
</script>
I came across the same problem recently. What I ended up doing was using:
<template id="templateContent">
<style> #import "css/generalStyle.css"; </style>
</template>
Additional info: This worked just fine except that now I'm having some cache issues as Chrome does not seem to reload those resources after a hard reload.
Let add to the answer . Now direct tag is supported in shadow dom.
You can directly use
<link rel="stylesheet" href="yourcss1.css">
<link href="yourcss2.css" rel="stylesheet" type="text/css">
Check they has been update by whatwg and W3C
Useful link for using css in shadow dom.
https://w3c.github.io/webcomponents/spec/shadow/#inertness-of-html-elements-in-a-shadow-tree https://github.com/whatwg/html/commit/43c57866c2bbc20dc0deb15a721a28cbaad2140c
https://github.com/w3c/webcomponents/issues/628
Direct css link can be use in shadow dom
Thanks.
I added the stylesheet's link element directly to the shadow root this way:
let link = document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('href', 'whatever.css');
this.shadowRoot.appendChild(link);
It seems to work fine. (I called this from the constructor of the component.)
actually polymer has an internal utility to load css links, i have implemented a javascript function that is using polymer internal css processor,so if you want to add css links at runtime you can use it:
Polymer('my-element', {
ready: function () {
this.importCss("path/myfile.css");
},
importCss: function (path) {
var $shadow = $(this.shadowRoot);
var $head = $("<div></div>");
var $link = $("<link rel='stylesheet' type='text/css'>");
$link.attr("href", path);
$head.append($link);
var head = $head[0];
this.copySheetAttributes = Polymer.api.declaration.styles.copySheetAttributes;
Polymer.api.declaration.styles.convertSheetsToStyles.call(this, head);
var styles = Polymer.api.declaration.styles.findLoadableStyles(head);
if (styles.length) {
var templateUrl = this.baseURI;
Polymer.styleResolver.loadStyles(styles, templateUrl, function () {
var $style = $shadow.find("style");
if ($style.length > 0){
$shadow.find("style").append($head.find("style").html());
}else{
$shadow.append($head.html());
}
});
}
}
});
Note: this code needs jquery to run

Add Content To Dialog Box DOJO

writing an app for opensocial brings up the following problem:
I create a dialog box (css is tundra)
myDialog = new dijit.Dialog({
title: "My Dialog",
content: "test content",
style: "width: 300px"
});
How can I change the properties "overflow" and /or "height" of the
"dijitDialogPaneContent"
contained in myDialog after creating this object?
Thank you
Subin
There are several approaches you can use, depending on how generic the solution should be.
Apply to all dialogs
If you want to apply the same style to all dialogs, you can "extend" a theme, for example, normally you would use the tundra theme like this:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="location/to/tundra.css" />
</head>
<body class="tundra">
<!-- Your content comes here -->
</body>
</html>
If you're going to apply it to all dialogs, you could do the following:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="location/to/tundra.css" />
<link rel="stylesheet" href="custom/style.css" />
</head>
<body class="tundra custom">
<!-- Your content comes here -->
</body>
</html>
And then write a custom stylesheet like this:
.custom .dijitDialogPaneContent {
overflow: hidden; /** Custom styles */
}
This will guarantee that it will override the general Tundra style for all dialogs. If you don't use a class like .custom, you cannot override the Tundra stylesheet because .tundra .dijitDialogPaneContent will be more specific (which means it has a higher priority).
Of course, you could write .tundra .dijitDialogPaneContent in your custom stylesheet as well.
Apply to a single dialog through stylesheet
If you want to apply it to a single dialog, then give an ID to the dialog, for example:
myDialog = new dijit.Dialog({
title: "My Dialog",
content: "test content",
style: "width: 300px",
id: "myDialogId"
});
Then you could write a stylesheet like this:
#myDialogId .dijitDialogPaneContent {
overflow: hidden; /** Custom styles */
}
Apply to a single dialog (using JavaScript)
Seperate stylesheets may improve readability because you seperate logic from design. If you don't need the seperate stylesheet you could do something like this:
require([ "dojo/query", "dojo/NodeList-dom" ], function(query) {
// Your code
query(".dijitDialogPaneContent", myDialog.domNode).style("overflow", "hidden");
});
This will use the domNode property of the dialog to query the content pane and then apply the style.
Apply to multiple dialogs
If you want to apply the same style to multiple dialogs (but not all dialogs), then your best approach would be to create a custom dialog by extending the default dialog. Considering the length of my answer atm I'm not going to explain that into detail, but I recommend reading guides about writing your own widget.

Resources