I'm just discovering Web Components and I'm not sure yet to have it understood well.
But here's my question. Seems it has a lot of advantages.
I would like to know if it is possible to share the same shadow DOM on several shadow hosts ?
What I want is several instances of an element on my webpage.
If the (single) shadow DOM is updated, all the instances would be updated too automatically.
Is it one of the uses of the shadow DOM stuff; and how can I achieve it ?
Thanks !
You could achieve the same effect by having a containing component that listens for an event when a property changes and passes the value down to child components. This way you don't share the shadow DOM instance itself, but you re-use the component without any code duplication.
class XContainer extends HTMLElement {
constructor() {
super();
this.total = 0;
}
connectedCallback() {
this.addEventListener('x-increment', this.onIncrement);
this.onIncrement = this.onIncrement.bind(this);
}
onIncrement(event) {
this.total = this.total + event.detail.amount;
this.querySelectorAll('x-counter').forEach((counterEl) => {
counterEl.total = this.total;
});
}
}
customElements.define('x-container', XContainer);
class XControls extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<button>+</button>
`;
}
connectedCallback() {
this.shadowRoot.querySelector('button').addEventListener('click', this.onClick);
this.onClick = this.onClick.bind(this);
}
onClick(event) {
this.dispatchEvent(new CustomEvent('x-increment', {
bubbles: true,
composed: true,
detail: {
amount: 1,
}
}));
}
}
customElements.define('x-controls', XControls);
class XCounter extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<h2>Total: <span>0</span>
`;
}
set total(value) {
this.shadowRoot.querySelector('span').innerText = value;
}
}
customElements.define('x-counter', XCounter);
<!DOCTYPE html>
<html lang="en">
<head>
<title>Web Component 101</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<x-container>
<x-counter></x-counter>
<hr>
<x-counter></x-counter>
<hr>
<x-controls></x-controls>
</x-container>
</body>
</html>
Related
front-layout.component.ts
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-front-layout',
templateUrl: './front-layout.component.html',
styleUrls: ['./front-layout.component.css']
})
export class FrontLayoutComponent implements OnInit {
host:any;
constructor() { }
ngOnInit(): void {
this.host = "http://localhost:4200";
}
}
front-layout.component.html
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0">
<title></title>
<link rel="stylesheet" href="'{{ host }}'/assets/front/css/bootstrap.min.css">
<link rel="stylesheet" href="'{{ host }}'/assets/front/plugins/select2/css/select2.min.css">
</head>
<body>
<div class="main-wrapper">
<router-outlet></router-outlet>
</div>
<script src="'{{ host }}'/assets/front/js/jquery-3.6.0.min.js"></script>
</body>
</html>
In the above code I simply declare host and try to access in my html component but it throw an error i.e. src/app/component/front/front-layout/front-layout.component.ts:5:16 - error NG2008: Could not find stylesheet file ''{{ host }}'/assets/front/css/bootstrap.min.css' linked from the template.. No idea why variable not working? Please help me.
Thank You
I believe you should move the links and scripts to index.html, and from there, use this path
./assets/front/js/jquery-3.6.0.min.js
To load the content of host
Must be [href] and [src] instead of href and src
and instead of
href="'{{ host }}'/assets/front/css/bootstrap.min.css"
must be
[href]="host + '/assets/front/css/bootstrap.min.css'"
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0">
<title></title>
<link rel="stylesheet" [href]="host + '/assets/front/css/bootstrap.min.css'">
<link rel="stylesheet" [href]="host + '/assets/front/plugins/select2/css/select2.min.css'">
</head>
<body>
<div class="main-wrapper">
</div>
<script [src]="host + '/assets/front/js/jquery-3.6.0.min.js'"></script>
</body>
</html>
But in angular all this stuff is done in index.html
This is how you dynamically load scripts in angular from any component
script.service.ts
import { Injectable , Renderer2, RendererFactory2 } from '#angular/core';
import { ScriptStore } from '../../store/store.script';
#Injectable({
providedIn: 'root'
})
export class LoadScriptService {
private scripts: any = {};
private renderer: Renderer2;
constructor(rendererFactory: RendererFactory2) {
this.renderer = rendererFactory.createRenderer(null, null);
this.getScripts();
}
private getScripts() {
ScriptStore.forEach((script: any) => {
this.scripts[script.name] = { src: script.src, loaded: script.loaded }
});
}
load(scriptName: string[]) {
scriptName.forEach((script: any) => {
this.loadScript(script);
});
}
private loadScript(name: string) {
if (this.scripts[name].loaded) {
return;
}
let script = this.renderer.createElement('script');
script.src = this.scripts[name].src;
document.getElementsByTagName('head')[0].appendChild(script);
this.scripts[name].loaded = true;
}
}
store.script.ts
import { Script } from "../interfaces/schema.script";
export const ScriptStore: Script[] = [
{ name: 'typeform', src: '//embed.typeform.com/next/embed.js', loaded: false }
* import here script info in object format. ex:
* { name: 'script1', src: 'url1', loaded: false }
* { name: 'script2', src: 'url2', loaded: false }
* { name: 'script3', src: 'url3', loaded: false }
];
interfaces/schema.script.ts
export interface Script {
name: string;
src: string;
loaded: boolean;
}
Then you import script.service in any component and call load function this.scriptService.load('script's name') or this.scriptService.load('script's name1', 'script's name2', 'script's name3')
I have a lambda service that returns html to the client (i.e browser). However, the event listener doesn't work. Any idea why? Code is as follows:
const serverless = require('serverless-http');
const express = require('express');
const fetch = require('node-fetch');
const app = express();
app.get('/', function (req, res) {
var htmlText = `
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/2.2.7/webcomponents-loader.js"></script>
<script type="module" >
import {html, render} from 'https://unpkg.com/lit-html?module';
/**
* Adapted from the Ractive.js clock example: http://www.ractivejs.org/examples/clock/
*/
export class MyElement extends HTMLElement {
// Define a template
get flag() { return this._flag; }
set flag(v) { this._flag = v }
constructor() {
super();
this.attachShadow({mode: 'open'});
setInterval(() => {
if (!this.flag) {
this.flag = true;
render(this.render(), this.shadowRoot);
}
}, 1000);
}
// Render the template to the document
render(site) {
console.log("called")
return html\`
<style>
:host {
display: block;
padding: 0 15px;
border-right: 1px solid #333333;
line-height: 12px;
}
</style>
<button #click="${() => console.log('button was clicked')}">BUTTON</button>
\`
}
}
customElements.define('my-element', MyElement);
</script>
</head>
<body>
<p>Hello World</p>
<my-element></my-element>
</body>
</html>
`;
res.send(htmlText)
});
module.exports.handler = serverless(app);
looking at the shadowroot from Chrome dev tools, the button was incorrectly rendered as <button #click="() => console.log('button was clicked')">BUTTON</button>. Any idea what am I doing wrong? Thanks.
Inside shadow dom try to import litElement instead lit-html as lit-html lets you write HTML templates in JavaScript.
import {html, render} from 'https://unpkg.com/lit-element'
class MyElement extends LitElement {...
How to use Lit-element with Object-type property?
I think defining myelement works:
static get properties() {
return {
whales:Object
}
}
constructor() {
super();
this.whales={"nb":0};
}
html, this works also:
<my-element id="mytest" whales='{"nb":2}'></my-element>
But I can't get setAttribute to work:
myElement.setAttribute("whales", {"nb":4});
EDIT:
Thank you mishu, your answer helped me to solve my problem. Here is full working example, if someone wants to know. However there is still one thing I couldn't get working: I don't know how to give a property initial value declaratively, if the value is an object (object is handled as a string).
index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lit-html test</title>
</head>
<body>
<script type="module" src="./components/my-e.js"></script>
<button id="reset" onclick="reset()">reset</button>
<p>1 works: <my-e id="mytest"></my-e></p>
<p>2 doesn't work: <my-e id="mytest2" person='{"name":"Joe", "age":32}'></my-e></p>
<script src="main.js"></script>
</body>
</html>
main.js:
function updatePerson(e) {
var myE = e.target;
myE.person = e.detail;
myE._requestRender();
}
function reset() {
var p = { "name": "Jack", "age": 20 };
var myE = document.getElementById('mytest');
myE.person = p;
myE._requestRender();
myE = document.getElementById('mytest2');
myE.person = p;
myE._requestRender();
}
document.getElementById('mytest').addEventListener('person', updatePerson);
document.getElementById('mytest2').addEventListener('person', updatePerson);
my-e.js:
import { LitElement, html } from './lit-element.js';
class MyE extends LitElement {
static get properties() {
return {
person: Object
}
}
constructor() {
super();
this.person = { "name": "Paul", "age": 40 };
this.addEventListener('click', async (e) => {
await this.renderComplete;
var p = {};
p.name = this.person.name + ", a";
p.age = this.person.age + 1;
this.dispatchEvent(new CustomEvent('person', { detail: p }))
});
}
_render({ person }) {
return html`
<p> Name: ${person.name}</p>
<p>Age: ${person.age}</p>
`;
}
}
customElements.define('my-e', MyE);
There you are trying to update a property, not really to set an attribute, and you don't have reflectToAttribute on the property. But you can try to use simply:
myElement.whales = {nb: 4};
UPDATE: I see that you changed the "scope" of the question.. so, you should be able to pass an object declaratively as long as the JSON is correct. You can see it in this demo.
<my-e id="mytest2" person='{"name":"Joe", "age":32}'></my-e>
Should have been
<my-e id="mytest2" person='${{"name":"Joe", "age":32}}'></my-e>
If you are using lit-extended. But this notation is going to be removed and is currently deprecated.
Hence the better option would be to write it as:
<my-e id="mytest2" .person={"name":"Joe", "age":32}></my-e>
You can find the details at this documentation
This example is tested in Lit 2.3.1. It should also work with lit-element.
Note: While passing object type attribute through html outer quotes must be single quotes and inner quotes must be double quotes.( like this: article='{"title": "Hello"}' ).
Index.html
<!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">
<title>Lit 2.3.1</title>
</head>
<body>
<my-page article='{"title": "Hello", "text": "World", "lines": 120}'></my-page>
</body>
</html>
my-page.js
import {LitElement, html} from 'lit';
class MyPage extends LitElement {
static properties = {
article: {
type: Object,
attribute: 'article'
}
};
render() {
return html`
<div>
<p>${this.article.title}</p>
<p>${this.article.text}</p>
<p>${this.article.lines}</p>
</div>`;
}
}
customElements.define('my-page', MyPage);
I'm trying to implement an angular2 app that has a dependency on a 3rd party js library, that is getting included in index.html script tag. Trying to implement the sales force external identity solution as outlined here.
this script is looking for some meta tags in the page in order to function properly, whether to pop-up as a model or inline form for example.
I'm using the angular platform browser's Meta functionality to set the meta tags in the page, implemented in the constructor of app.component.ts.
This seems to have created a race condition, as in certain situations (typically, when the script exists in the browser cache) it throws alerts that the required meta tags aren't available.
How can I stop the loading of the script in index.html (I've set it with the defer and async keywords btw) to not load until AFTER the app.component has set the meta tags?
app.component.ts
import {Component, OnInit} from '#angular/core';
import {TodoService} from './providers/todo.service';
import {Meta} from '#angular/platform-browser';
import {environment} from '../environments/environment';
import { isNull } from 'util';
declare var SFIDWidget: any;
declare var SFIDWidget_loginHandler: any;
declare var SFIDWidget_logoutHandler: any;
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [TodoService]
})
export class AppComponent implements OnInit {
constructor(public meta: Meta) {
//meta tag stuff
//check for meta tags and add if not exists
if (isNull(this.meta.getTag('name="salesforce-logout-handler"'))) {
this.meta.addTags(
[
{
name: "salesforce-client-id",
content: `${environment.clientId}`
},
{
name: "salesforce-community",
content: `${environment.communityURL}`
},
{
name: "salesforce-redirect-uri",
content: `${environment.redirectURL}`
},
{
name: "salesforce-mode",
content: "modal"
},
{
name: "salesforce-forgot-password-enabled",
content: "true"
},
{
name: "self-register-enabled",
content: "true"
},
{
name: "salesforce-save-access-token",
content: "true"
},
{
name: "salesforce-target",
content: "#salesforce-login"
},
{
name: "salesforce-login-handler",
content: "onLogin"
},
{
name: "salesforce-logout-handler",
content: "onLogout"
}
], true);
} else {
console.log('meta tags exist!');
}
}
ngOnInit() {
}
onLogin(identity) {
console.log('fired from the app component');
}
onLogout() {
SFIDWidget.init();
}
}
index.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Herokudos</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<script
src="https://sdo-134f326f986sdo-134f326f986.force.com/servlet/servlet.loginwidgetcontroller?type=javascript_widget&min=false&cacheMaxAge=0"
async defer></script>
<link
href="https://sdo-134f326f986sdo-134f326f986.force.com/servlet/servlet.loginwidgetcontroller?type=css"
rel="stylesheet" type="text/css"/>
</head>
<body>
<app-root>Loading...</app-root>
</body>
</html>
Remove the <script> tag that you hardcoded into index.html. If you want to load scripts that depend on Angular being ready, consider loading them dynamically in the component's ngAfterViewInit() lifecycle hook. This will guarantee that the script will load only after your AppComponent has been rendered.
AppComponent
/*executes once the component template is built*/
ngAfterViewInit(){
//Add a script tag to the DOM
let script = document.createElement('script');
document.body.appendChild(script);
//listen for the load event on the script...
Observable.fromEvent(script,'load').first().subscribe(
//...and call onScriptLoad once the script is loaded
()=>this.onScriptLoad()
);
//set script source: will trigger the download
script.src = "https://sdo...";
}
/*executes once the script has been loaded*/
onScriptLoad(){}
I try to check if the user have camera with a swf file.
But the external interface call are not executed, and callback say error :
Uncaught TypeError: Object #<HTMLObjectElement> has no method 'checkWebcam'
this is my html file :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>webcamDetector</title>
<meta name="description" content="" />
<script src="js/swfobject.js"></script>
<script>
var flashvars = {
};
var params = {
menu: "false",
scale: "noScale",
allowFullscreen: "true",
allowScriptAccess: "always",
bgcolor: "",
wmode: "direct" // can cause issues with FP settings & webcam
};
var attributes = {
id:"webcamDetector"
};
swfobject.embedSWF(
"webcamDetector.swf",
"altContent", "1", "1", "10.0.0",
"expressInstall.swf",
flashvars, params, attributes);
function alllert(test){
console.log(test);
}
</script>
<style>
html, body { height:100%; overflow:hidden; }
body { margin:0; }
</style>
</head>
<body>
<div id="altContent">
<h1>webcamDetector</h1>
<p>Get Adobe Flash player</p>
</div>
<div onclick="alert(document.getElementById('webcamDetector').checkWebcam());">test</div>
</body>
</html>
And this is my as3 main file :
package
{
import flash.display.Sprite;
import flash.events.Event;
import flash.external.*;
import flash.media.Camera;
/**
* ...
* #author
*/
public class Main extends Sprite
{
public function Main():void
{
ExternalInterface.call("alllert", "tedsf dfds fsd f");
ExternalInterface.addCallback("webcam",checkWebcam);
ExternalInterface.addCallback("checkWebcam", checkWebcam);
}
public function checkWebcam():int {
if (Camera.isSupported) {
var webcam:Array = Camera.names;
if (webcam.length > 0) {
return 58;
}else {
return 59;
}
}else {
return 60;
}
}
}
}
Anyone see my error ?
Why this doesn't work ?
thx.
This doesn't work because i try it on local.
I have upload my code in a ftp, and this work fine.
Thx for all reply.