I wanted to debug lit decorators in chrom inspect. I am using webpack plugin.
I have followed steps https://lit.dev/docs/tools/development/#development-and-production-builds and enabled the dev mode. Still I am getting minimized file version in chrom inspect, source code is not hitting. Please help on this.
`
import {LitElement, html} from 'lit';
import {customElement, property, queryAssignedNodes} from 'lit/decorators.js';
#customElement("my-element")
class MyElement extends LitElement {
#queryAssignedNodes({slot: 'list'})
listItems!: Array<HTMLElement>;
#property({type: String})
name = 'World';
#property({ type: Number })
count = 0;
#property({attribute: false})
article = {
title: 'My Nifty Article',
text: 'Some witty text.',
};
headerTemplate() {
console.log(this.renderRoot);
let shadowRoot = this.renderRoot;
console.log(shadowRoot.querySelector('slot[name="list"]'));
return html`<header>${this.article.title}</header>`;
}
articleTemplate() {
return html`<article>${this.article.text}</article>`;
}
footerTemplate() {
return html`<footer>Your footer here.</footer>`;
}
_onClick() {
this.count++;
const nodes = [...this.listItems];
console.log(nodes);
}
foo() {
return html` <h1>Hello, ${this.name}!</h1>
<button id = "1" onclick="${this._onClick}">
Click Count: ${this.count}
</button>
<slot name="list"></slot>`;
}
render() {
return html`
${this.headerTemplate()}
${this.articleTemplate()}
${this.footerTemplate()}
${this.foo()}
`;
}
}
`
Note: I am trying to debug #queryAssignedNodes decorator.
I am getting minimized file version in chrom inspect, source code is not hitting.
Related
I have created a Web Component for a requirement and I have received a The best way to handle this is by not injecting untrusted strings in this way. Instead, use node.innerText or node.textContent to inject the string- the browser will not parse this string at all, preventing an XSS attack. code review comment. I am still thinking about how to replace innerHTML to innerText or textContent.
Would the community have an input?
import RealBase from '../real-base';
import productCardTitleCss from '../../../css/_product-card-title.scss?inline';
import baseCss from '../../../css/_base.scss?inline';
const ESAPI = require('node-esapi');
class RealProductCardTitle extends RealBase {
constructor() {
super();
this.defaultClass = 'real-product-card-title';
}
connectedCallback() {
super.connectedCallback();
this.render();
}
static get observedAttributes() {
return ['heading', 'form'];
}
get heading() {
return this.getAttribute('heading') || '';
}
set heading(value) {
this.setAttribute('heading', value ? ESAPI.encoder().encodeForHTML(value) : value);
}
get form() {
return this.getAttribute('form') || '';
}
set form(value) {
this.setAttribute('form', value ? ESAPI.encoder().encodeForHTML(value) : value);
}
attributeChangedCallback() {
this.render();
}
render() {
const {
heading,
form,
} = this;
this.classList.add('real-block');
this.classList.add(this.defaultClass);
if (!this.shadowRoot) {
this.attachShadow({ mode: 'open' });
}
//Recommendation for this line below
this.shadowRoot.innerHTML = `
<style>
${productCardTitleCss}
${baseCss}
</style>
<real-heading
class="real-product-card-title-heading"
input="${heading}">
</real-heading>
<div
class="real-product-card-title-attributes real-inline-block">
${form}
</div>`;
}
}
window.customElements.define('real-product-card-title', RealProductCardTitle);
I can't seem to get theme working with LitElement. I can set the other props of setup no problem, but the theme doesn't get recognized by Twind. It's also worth mentioning that I get no error when compiling. Anyone have a quick solution?
import {LitElement, html} from 'lit';
import {setup, warn, create, cssomSheet} from 'twind';
setup({
mode: warn,
theme: {
colors: {
purple: '#2013',
},
},
});
const sheet = cssomSheet({target: new CSSStyleSheet()});
const {tw} = create({sheet});
export class MyApp extends LitElement {
static styles = [sheet.target];
static get properties() {
return {
name: {type: String},
};
}
constructor() {
super();
this.name = 'World';
}
render() {
return html` <h1 class="${tw`text(3xl purple)`}">${this.name}!</h1> `;
}
}
window.customElements.define('my-app', MyApp);
Probably, the problem is the shadow-dom.
If you can use Twind, you can try to render to light-dom, instead shadow-dom.
To use light-dom add in your web-component class this method:
createRenderRoot() {
return this;
}
In other hand, I don't sure it works without Lit...
I want to test the content of a slot in one of my custom components.
If I use my component in an html-file and open it in an browser, everything works as intended. However if I want to automate my test with jest, it fails. Below is an minimal working example with the output form jest:
placeholder.js:
const template = document.createElement("template");
template.innerHTML = `
<p>
<slot></slot>
</p>
`;
class Placeholder extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
get name() {
return this.shadowRoot.querySelector("slot").innerText;
}
}
window.customElements.define("place-holder", Placeholder);
export default Placeholder;
placeholder.test.js:
import Placeholder from "../src/placeholder.js";
describe("name is 'Lorem Ipsum'", () => {
let ph;
beforeAll(() => {
ph = new Placeholder();
const textNode = document.createTextNode("Lorem Ipsum");
ph.appendChild(textNode);
});
test("if the name is 'Lorem Ipsum'", () => {
expect(ph.name).toBe("Lorem Ipsum");
});
});
output:
name is 'Lorem Ipsum' › if the name is 'Lorem Ipsum'
expect(received).toBe(expected) // Object.is equality
Expected: "Lorem Ipsum"
Received: undefined
11 |
12 | test("if the name is 'Lorem Ipsum'", () => {
> 13 | expect(ph.name).toBe("Lorem Ipsum");
| ^
14 | });
15 | });
at Object.<anonymous> (test/placeholder.test.js:13:25)
at TestScheduler.scheduleTests (node_modules/#jest/core/build/TestScheduler.js:333:13)
at runJest (node_modules/#jest/core/build/runJest.js:387:19)
at _run10000 (node_modules/#jest/core/build/cli/index.js:408:7)
at runCLI (node_modules/#jest/core/build/cli/index.js:261:3)
As you can see jest somehow fails to get the slotted text and returns undefined. How can I solve this problem?
The text node will not be a part of the <slot> element's internals. It is only a wrapper to the text node. To get the nodes that are placed inside of the slot you have to use HTMLSlotElement.assignedNodes() method.
The assignedNodes() method of the HTMLSlotElement interface returns a sequence of the nodes assigned to this slot.
With this you get an array of nodes that reside in the slot. The added text node will be in this array.
I've modified your name getter to get the first node out of the assigned nodes array and return the textContent value of the node.
const template = document.createElement("template");
template.innerHTML = `
<p>
<slot></slot>
</p>
`;
class Placeholder extends HTMLElement {
constructor() {
super();
this.attachShadow({
mode: "open"
});
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
get name() {
const slot = this.shadowRoot.querySelector("slot");
const [name] = slot.assignedNodes();
if (!name) {
return ''
}
return name.textContent
}
connectedCallback() {
console.log(this.name)
}
}
window.customElements.define("place-holder", Placeholder);
<place-holder>Hello</place-holder>
Sidenote: The <slot> element will have a innerText whenever you add text inside the slot in the template as a placeholder.
class ExampleElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<slot>Placeholder text</slot>
`;
}
get placeholder() {
const slot = this.shadowRoot.querySelector('slot');
return slot.innerText;
}
connectedCallback() {
console.log(this.placeholder)
}
}
customElements.define('example-element', ExampleElement);
<example-element></example-element>
Building off of the accepted answer (and thank you for that helpful info):
Just in case someone else wanted to test more complex slotted elements, here is my test for slotting an SVG icon component into a button named slot:
it(`should render icons when passed to the button`, async () => {
element = await fixture(html`<my-button text=${'click me'}>
<my-icon name="article" slot="iconLeft"></my-icon>
</my-button>`);
const slot = element.shadowRoot!.querySelector('slot');
const slotContent = slot!.assignedNodes()[0] as HTMLSlotElement;
expect(slotContent.shadowRoot!.querySelector('svg')).toBeInTheDocument();
});
We can have stencilJS element with slot as below
<my-component>123</my-component>
I'm trying to get the value of 123 from my render method itself, wondering if that is possible?
#Component({ tag: 'my-component' })
export class MyComponent {
render() {
return (
<div><slot /></div>
)
}
}
I would like to do some string formatting on 123 instead of rendering slot directly
import { Element } from '#stencil/core';
#Component({ tag: 'my-component' })
export class MyComponent {
/**
* Reference to host element
*/
#Element() host: HTMLElement;
componentWillRender() {
console.log(this.host.innerHTML)
}
render() {
return (
<div><slot /></div>
)
}
}
In web components, the content inside of it is part of the main DOM, too. This content is not going to show if you don't use slots; but, the content is going to project next to the #shadow-root anyway (check it using the chrome developer tools in the "elements" section).
So, if you do not want to show the content using default slots, you can use the property decorator #Element() and declare a property of type HTMLElement:
Then, you can access to the content via innerHTML or innerText.
Finally, you can format the content. Check the code snippet bellow:
import { Component, Element, h } from "#stencil/core";
#Component({
tag: 'my-component',
styleUrl: 'my-component.css',
shadow: true
})
export class MyComponent {
#Element() element: HTMLElement;
formatContent(content: any) {
if ( isNaN(content)){
// Your format here
return content;
} else {
return content + '.00';
}
}
render() {
return [
// Commented slot tag
// <slot></slot>,
<div> {this.formatContent(this.element.innerHTML)} </div>
];
}
}
Using three times the web component with 2 strings and a number as a entry data, the result should be:
My text
My text 2
123.00
I Want to know how to create nested dynamic components and maintains its parent child relationship.
For example, I have data like this,
- A
--A.1
--A.2
-B
--B.1
-C
I wanted to create the component like this,
<A>
<A1></A1>
<A2></A2>
</A>
<B>
<B1></B1>
</B>
<C></C>
But with my code I could only create parent component or child component. But not both.
Below is my code,
setRootViewContainerRef(view: ViewContainerRef): void {
this.rootViewContainer = view;
}
createComponent(content: any, type: any) {
console.log(content);
if (content.child && content.child.length > 0) {
content.child.forEach(type => {
const typeP = this.contentMappings[type.type];
this.createComponent(type, typeP);
});
} else {
this.renderComp(content,type)
}
}
renderComp(content,type) {
if (!type) {
return
}
this.componentFactory = this.componentFactoryResolver.resolveComponentFactory(type);
this.componentReference = this.rootViewContainer.createComponent(this.componentFactory);
if (this.componentReference.instance.contentOnCreate) {
this.componentReference.instance.contentOnCreate(content);
}
}
With this code, I get this output.
Link to working example, StackBlitz
Please help me to resolve this issue.
Updated.
Even after adding the viewChild, It still throws the viewchild not defined.
Refer this image, In the component.instance I'm not seeing the view child element.
Updated stackblitz link https://stackblitz.com/edit/angular-dynamic-new-mepwch?file=src/app/content/a/a.component.ts
You should create ViewContainer on each level that is going to render child components:
a.component.html
<p>
a works!
</p>
<ng-container #container></ng-container>
a.component.ts
export class AComponent implements OnInit {
#ViewChild('container', { read: ViewContainerRef, static: true }) embeddedContainer: ViewContainerRef;
And then render component to dedicated container:
create-dynamic-component.service.ts
#Injectable()
export class CreateDynamicComponentService {
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
#Inject(CONTENT_MAPPINGS) private contentMappings: any,
private inlineService: InlineService
) { }
createComponent(content: any, type: any, vcRef) {
const componentRef = this.renderComp(content, type, vcRef)
if (content.child && content.child.length) {
if (!componentRef.instance.embeddedContainer) {
const cmpName = componentRef.instance.constructor.name;
throw new TypeError(`Trying to render embedded content. ${cmpName} must have #ViewChild() embeddedContainer defined`);
}
content.child.forEach(type => {
const typeP = this.contentMappings[type.type];
this.createComponent(type, typeP, componentRef.instance.embeddedContainer);
});
}
}
renderComp(content,type, vcRef: ViewContainerRef) {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(type);
const componentRef = vcRef.createComponent<any>(componentFactory);
if (componentRef.instance.contentOnCreate) {
componentRef.instance.contentOnCreate(content);
}
return componentRef;
}
}
Note how renderComp method takes ViewContainerRef from the component with children:
this.createComponent(type, typeP, componentRef.instance.embeddedContainer);
Forked Stackblitz