I am trying to pass data from one component to another. My idea is to have a generic component with buttons that increase or decrease a value and add those specific values to another template.
Here is my code with the buttons:
import { LitElement, html } from 'lit-element';
class ButtonsCounter extends LitElement {
static get properties() {
return {
max: {type: Number},
min: {type: Number},
num: {type: Number},
value: {type: Number},
};
}
constructor() {
super();
this.value = 0;
}
createRenderRoot() {
return this;
}
increment(e) {
e.preventDefault();
if (this.value < this.maxValue) this.value++;
}
decrement(e) {
e.preventDefault();
if (this.value > this.minValue) this.value--;
}
render(){
return html`
<div class="searchCounter" max=${this.maxValue} min=${this.minValue}>
<span num=${this.value}>${this.value}</span>
</div>
`;
}
}
customElements.define('buttons-counter', ButtonsCounter);
Here is the template where I want to add the data that will go to the previous template:
import { LitElement, html } from 'lit-element';
import './buttons-counter'
class SelectOption extends LitElement {
constructor() {
super();
this.num = 0;
this.max = 5;
this.min = 0;
}
createRenderRoot() {
return this;
}
render(){
return html`
<buttons-counter .max="${this.max}" .min="${this.min}" .num="${this.num}"></buttons-counter>
`;
};
}
customElements.define('select-option', SelectOption);
I've tried different ways but none works. Any idea?
It looks like you're mixing up value with num, maxValue with max, and minValue with min.
You'll also likely want some text inside your a tags so that they're reasonably clickable. Technically, they should also be button tags (styled to your liking), as a tags are considered for linking to parts of a page or to another page (see https://stackoverflow.com/a/37148542/6090140 for more details). You then will not need e.preventDefault(); inside decrement and increment.
<button #click="${this.decrement}">-</button>
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'm developing an Entity-Component-System in TypeScript where entities contain a map of their components. CT stands for ComponentType Here is the code:
class Entity {
components: Map<CT, Component>;
constructor() {
this.components = new Map();
}
get(componentType: CT): Component {
return this.components.get(componentType);
}
}
const enum CT {
Position,
Health // etc..
}
class Component {
type: CT;
constructor(type: CT) {
this.type = type;
}
}
class HealthComponent extends Component {
amount: number;
constructor() {
super(CT.Health);
this.amount = 0;
}
}
The problem is that when I do something like:
let healthComponent = new HealthComponent();
healthComponent.amount = 100;
let entity = new Entity();
entity.components.set(healthComponent.type, healthComponent);
let position = entity.get(CT.Health);
console.log(health.amount);
I get this error: Property 'amount' does not exist on type 'Component'
If I change the Entity.get function to instead return any like this, then the error goes away:
get(componentType: CT): any {
return this.components.get(componentType);
}
But then I no longer get code-completion, which I would like.
Is it possible to have it return the proper type, so I can have the code-completion and error-detection?
class Component {
// error: 'amount' does not exist on type 'Component'
amount: number; // <-- NOTE: moved amount here from HealthComponent
type: CT;
constructor(type: CT) {
this.type = type;
}
}
class HealthComponent extends Component {
// removed amount from here
constructor() {
super(CT.Health);
this.amount = 0;
}
}
I managed to hack something together using TypeScript's conditional types. It's not ideal but it might work for you:
Let's declare the enum and component classes first:
const enum CT {
Position,
Health
}
class HealthComponent {
amount: number;
constructor() {
this.amount = 0;
}
}
class PositionComponent {
position: number;
constructor() {
this.position = 1;
}
}
Then the conditional type that maps the enum values to component classes:
type TypeMapper<T extends CT> = T extends CT.Position
? PositionComponent
: HealthComponent;
It's a generic type that based on it's type parameter T either evaluates to the PositionComponent or the HealthComponent class.
And now the entity class:
class Entity {
private components: Map<CT, TypeMapper<CT>>;
constructor() {
this.components = new Map();
}
set<T extends CT>(ct: T, component: TypeMapper<T>) {
this.components.set(ct, component);
}
get<T extends CT>(ct: T): TypeMapper<T> {
return this.components.get(ct) as any; //a little lie here to shut up the TS compiler
}
}
Now when you instantiate everything, the types should align for you correctly:
const entity = new Entity();
entity.set(CT.Health, new HealthComponent());
const health = entity.get(CT.Health);
health.amount //and the amount prop should show up in intellisense correctly.
As I said, it's not ideal because it relies on using any, also depending on the number of component classes you have, your TypeMapper type can become huge, but at least the Component base class and super() calls to it's constructor are no longer necessary so you win some lines of code there.
Here's the TS playground link.
I'm trying to create a custom textfield web component based on Material Web Components mwc-textfield:
import {LitElement, html, css} from 'lit-element';
import '#material/mwc-textfield';
export class CustomTextfield extends LitElement {
static get properties() {
return {
label: {type: String},
required: {type: Boolean},
value: {type: String}
}
};
get value_() {
return this.shadowRoot.getElementById("input").value;
};
constructor() {
super();
this.label = "";
this.value = "";
this.required = false;
}
render() {
return html`
<mwc-textfield id="input"
label="${this.label}"
value="${(this.value === undefined) ? "" : this.value}"
?required="${this.required}"
outlined
>
</mwc-textfield>
`;
};
}
customElements.define('custom-textfield', CustomTextfield);
At this moment, I can get the mwc-textfield value attribute with the custom-textfield value_ attribute. Is there any way to get the mwc-textfield value attribute with the custom-textfield value attribute?
For that to happen the value attribute in mwc-textfield should be reflecting its value, but that's not what's happening in https://github.com/material-components/material-components-web-components/blob/master/packages/textfield/src/mwc-textfield-base.ts#L123
So, at the moment it's not possible what you are looking for.
But anyway check this issue where they talk about the possible future which is implementing an internal api to mimic form elements with Form Associated Custom Elements
I am building a project with web components and vanilla javascript.
I have a component/module called meal.module - It is the parent component of the components meal-list and meal-search.
meal-list displays multiple meals from an api.
meal-search contains an input field and seachterm as attribute.
meal.module.js
export default class MealModule extends HTMLElement {
connectedCallback() {
this.innerHTML = '<mp-meal-search searchterm=""></mp-meal-search> ' +
'<mp-meal-list></mp-meal-list> ' +
}
}
if (!customElements.get('mp-meal-module')) {
customElements.define('mp-meal-module', EssenModule);
}
meal-list.component
export default class MealListComponent extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = Template.render();
// Renders the meals from api into the template
this.getMeals();
}
(....) more code
}
if (!customElements.get('mp-meal-list')) {
customElements.define('mp-meal-list', MealListComponent);
}
meal-search.component
export default class MealSearchComponent extends HTMLElement {
static get observedAttributes() {
return ['searchterm'];
}
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'searchterm') {
this.doSearch();
}
}
set searchTerm(val) {
this.setAttribute('searchterm', val)
}
get searchTerm() {
return this.getAttribute('searchterm');
}
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = '<input type="text" id="searchterm" value=""/>'
this.shadowRoot.addEventListener('change', (event) =>
this.searchTerm = event.target.value
)
}
doSearch() {
// send signal to MealListComponent for search
}
}
if (!customElements.get('mp-meal-search')) {
customElements.define('mp-meal-search', MealSearchComponent);
}
In the seach-component the SearchTerm is configured as Attribute. Everytime the input field gets changed, the attribute also changes.
Now I want to implement a "searchMeal" function, that always triggers when the attribute in MealSearchComponent changes.
I already tried to import the MealSearchComponent into the MealListComponent. But it does seem to break the rule of components, not having any dependencies.
JavaScript from the outside of the shadow DOM can access the shadow DOM via the element.ShadowRoot property.
I'm working with lit-elements via #open-wc and is currently trying to write a nested set of components where the inner component is an input field and some ancestor component has to support some arbitrary rewrite rules like 'numbers are not allowed input'.
What I'm trying to figure out is what the right way to built this is using lit-elements. In React I would use a 'controlled component' see here easily forcing all components to submit to the root component property.
The example below is what I've come up with using Lit-Elements. Is there a better way to do it?
Please note; that the challenge becomes slightly harder since I want to ignore some characters. Without the e.target.value = this.value; at level-5, the input elmement would diverge from the component state on ignored chars. I want the entire chain of components to be correctly in sync, hence the header tags to exemplify.
export class Level1 extends LitElement {
static get properties() {
return {
value: { type: String }
};
}
render() {
return html`
<div>
<h1>${this.value}</h1>
<level-2 value=${this.value} #input-changed=${this.onInput}></level-2>
</div>`;
}
onInput(e) {
this.value = e.detail.value.replace(/\d/g, '');
}
}
...
export class Level4 extends LitElement {
static get properties() {
return {
value: { type: String }
};
}
render() {
return html`
<div>
<h4>${this.value}</h4>
<level-5 value=${this.value}></level-5>
</div>`;
}
}
export class Level5 extends LitElement {
static get properties() {
return {
value: { type: String }
};
}
render() {
return html`
<div>
<h5>${this.value}</h5>
<input .value=${this.value} #input=${this.onInput}></input>
</div>`;
}
onInput(e) {
let event = new CustomEvent('input-changed', {
detail: { value: e.target.value },
bubbles: true,
composed: true
});
e.target.value = this.value;
this.dispatchEvent(event);
}
}
export class AppShell extends LitElement {
constructor() {
super();
this.value = 'initial value';
}
render() {
return html`
<level-1 value=${this.value}></level-1>
`;
}
}
Added later
An alternative approach was using the path array in the event to access the input element directly from the root component.
I think it's a worse solution because it results in a stronger coupling accross the components, i.e. by assuming the child component is an input element with a value property.
onInput(e) {
const target = e.path[0]; // origin input element
this.value = e.path[0].value.replace(/\d/g, '');
// controlling the child elements value to adhere to the colletive state
target.value = this.value;
}
Don't compose your events, handle them in the big parent with your logic there. Have the children send all needed info in the event, try not to rely on target in the parent's event handler.
To receive updates, have your components subscribe in a shared mixin, a la #mishu's suggestion, which uses some state container (here, I present some imaginary state solution)
import { subscribe } from 'some-state-solution';
export const FormMixin = superclass => class extends superclass {
static get properties() { return { value: { type: String }; } }
connectedCallback() {
super.connectedCallback();
subscribe(this);
}
}
Then any component-specific side effects you can handle in updated or the event handler (UI only - do logic in the parent or in the state container)
import { publish } from 'some-state-solution';
class Level1 extends LitElement {
// ...
onInput({ detail: { value } }) {
publish('value', value.replace(/\d/g, ''));
}
}