web component - accept no child elements - html

How can I define a web component that works like <img> in that it accepts no child elements?
<script>
const QA = (q, d) => Array.prototype.slice.call((d||document).querySelectorAll(q), 0);
const QS = (q, d) => (d||document).querySelector(q);
</script>
<template id="push-message">
<style>
message { display: grid; font-family: sans-serif; background: #eee; outline: 1px solid; }
.badge { }
</style>
<message>
<img class="badge">
<img class="icon">
<img class="image">
</message>
</template>
<script>
const wpm = 'push-message';
customElements.define(wpm,
class extends HTMLElement {
constructor() {
super();
const l = QS(`#${wpm}`).content.cloneNode(true);
const s = this.attachShadow({ mode: 'open' }); s.appendChild(l);
}
QS(q) { return QS(q, this.shadowRoot); }
QA(q) { return QA(q, this.shadowRoot); }
static get observedAttributes() { return [ "badge", "icon", "image" ]; }
attributeChangedCallback(a, o, n) {
if (/^(badge|icon|image)$/.test(a))
this.QS(`.${a}`).src = n;
}
});
</script>
<push-message
badge="//google.com/favicon.ico"
icon="//google.com/favicon.ico"
image="//google.com/favicon.ico">
<p>ok</p>
DOM should be
<push-message></push-message>
<p></p>
not
<push-message><p></p></push-message>
and ok should display in the result.
Is there a way to change customElements.define to avoid having to explicitly close <push-message></push-message> and just use <push-message> but have it implicitly self-close?

Autonomous Custom Elements require a closing tag: Do custom elements require a close tag?
You can create a Customized Built-In Element extended from HTMLImageElement
to get a a self-closing IMG tag:
<img is="push-message" badge="//google.com/favicon.ico">
<img is="push-message" icon="//google.com/favicon.ico">
<img is="push-message" image="//google.com/favicon.ico">
<p>ok</p>
But an IMG can only have one src, so you might as well create 3 elements and use
<img is="message-badge">
<img is="message-icon">
<img is="message-image">

Self-closing tags as known as void elements.
AFAIK, it is not possible to create custom void elements. In summary, it needs changing browser HTML parsers which is not an easy thing to pull by the web community. Changes are required because of the way the browser implements a tag-soup algorithm.
Thus you will need a closing tag. You can read more about this here:
Specs discussion
Lit-html discussion
On a side note, if you have your own template compiler/parser like vue-compiler and ng-compiler, you can probably instruct it to understand self-closing custom elements at a build time. However, the benefits of achieving this are virtually non-existent.

Related

Is it possible to programmatically slot elements in web components?

Is it possible to automatically or programmatically slot nested web components or elements of a specific type without having to specify the slot attribute on them?
Consider some structure like this:
<parent-element>
<child-element>Child 1</child-element>
<child-element>Child 2</child-element>
<p>Content</p>
</parent-element>
With the <parent-element> having a Shadow DOM like this:
<div id="child-elements">
<slot name="child-elements">
<child-element>Default child</child-element>
</slot>
</div>
<div id="content">
<slot></slot>
</div>
The expected result is:
<parent-element>
<#shadow-root>
<div id="child-elements">
<slot name="child-elements">
<child-element>Child 1</child-element>
<child-element>Child 2</child-element>
</slot>
</div>
<div id="content">
<slot>
<p>Content</p>
</slot>
</div>
</parent-element>
In words, I want to enforce that <child-element>s are only allowed within a <parent-element> similar to <td> elements only being allowed within a <tr> element. And I want them to be placed within the <slot name="child-elements"> element. Having to specify a slot attribute on each of them to place them within a specific slot of the <parent-element> seems redundant.
At the same time, the rest of the content within the <parent-element> should automatically be slotted into the second <slot> element.
I've first searched for a way to define this when registering the parent element, though CustomElementRegistry.define() currently only supports extends as option.
Then I thought, maybe there's a function allowing to slot the elements manually, i.e. something like childElement.slot('child-elements'), but that doesn't seem to exist.
I've then tried to achive this programmatically in the constructor of the parent element like this:
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.appendChild(template.content.cloneNode(true));
const childElements = this.getElementsByTagName('child-element');
const childElementSlot = this.shadowRoot.querySelector('[name="child-elements"]');
for (let i = 0; i < childElements.length; i++) {
childElementSlot.appendChild(childElements[i]);
}
}
Though this doesn't move the child elements to the <slot name="child-elements">, so all of them still get slotted in the second <slot> element.
Your unnamed default <slot></slot> will capture all elements not assigned to a named slot;
so a slotchange Event can capture those and force child-element into the correct slot:
customElements.define('parent-element', class extends HTMLElement {
constructor() {
super().attachShadow({mode:'open'})
.append(document.getElementById(this.nodeName).content.cloneNode(true));
this.shadowRoot.addEventListener("slotchange", (evt) => {
if (evt.target.name == "") {// <slot></slot> captures
[...evt.target.assignedElements()]
.filter(el => el.nodeName == 'CHILD-ELEMENT') //process child-elements
.map(el => el.slot = "child-elements"); // force them to their own slot
} else console.log(`SLOT: ${evt.target.name} got:`,evt.target.assignedNodes())
})}});
customElements.define('child-element', class extends HTMLElement {
connectedCallback(parent = this.closest("parent-element")) {
// or check and force slot name here
if (this.parentNode != parent) {
if (parent) parent.append(this); // Child 3 !!!
else console.error(this.innerHTML, "wants a PARENT-ELEMENT!");
}}});
child-element { color: red; display: block; } /* style lightDOM in global CSS! */
<template id=PARENT-ELEMENT>
<style>
:host { display: inline-block; border: 2px solid red; }
::slotted(child-element) { background: lightgreen }
div { border:3px dashed rebeccapurple }
</style>
<div><slot name=child-elements></slot></div>
<slot></slot>
</template>
<parent-element>
<child-element>Child 1</child-element>
<child-element>Child 2</child-element>
<b>Content</b>
<div><child-element>Child 3 !!!</child-element></div>
</parent-element>
<child-element>Child 4 !!!</child-element>
Note the logic for processing <child-element> not being a direct child of <parent-element>, you probably want to rewrite this to your own needs
As of recently, yes, you can, by using the assign() method of slot elements. Sadly, Safari doesn't support it yet, but there is a polyfill.

Couldn't use the same classname across components. CSS is not being scoped inside a style component

The problem I am having seems to defeat the very purpose of CSS in JS. I am using styled-compomnents. And when I tried to use a classname that is being used somewhere up in the react tree inside a styled component. The upper component classname styles somehow get applied to the classname I used down (very) the tree.
Steps to reproduce
Render UpperComponent anywhere in a react project.
const StyledContainer = styled.div`
.title {
color: red;
margin-bottom: 32px;
}
`;
const UpperComponent = () => {
return (
<StyledContainer>
<FirstComponent />
<h4 className="title"> text inside upper component </h4>
</StyledContainer>
);
};
const FirstStyledContainer = styled.div``;
const FirstComponent = () => {
return (
<FirstStyledContainer>
<h4 className="title">text inside first component</h4>
<SecondComponent />
</FirstStyledContainer>
);
};
const SecondComponent = () => {
return (
<div>
<h4 className="title">text inside second component</h4>
<ThirdComponent />
</div>
);
};
const ThirdComponent = () => {
return (
<div>
<h4 className="title">text inside second component </h4>
</div>
);
};
Expected Behavior
title classname in the UpperComponent should not affect it's descendants' elements with the same classname. It should be scoped only to <h4 className="title"> text inside upper component </h4>
Actual Behavior
.title { color: red; margin-bottom: 32px; } class styles get applied to all the components inside UpperComponent. title somehow makes it down to ThirdCompoent which is nested inide two components.
Is this expected behavior or am I missing something basic (best practice)?
If you want enforce the scoping - You can remove the class names and/or let "styled component" name them (generates a random hash class name) by creating a TitleStyle and attach to the title div (class name "title" can be removed). This should scope to that title only then. Right ?
Another alternative
Yes the FirstComponent and SecondComponent (etc) will catch the css rule from the top. This is the expected result for me. Its not like when we do this below !
<div style = {{color:"red"}}>Test</div>
This would apply the css inline to that div only.
I would slightly change the names of the title classes like so
const StyledContainer = styled.div`
.title {
color: red;
margin-bottom: 32px;
&.secondary { color: pink; }
&.thirdly { color: yellow; }
}
`;
const UpperComponent = () => {
return (
<StyledContainer>
<FirstComponent />
<h4 className="title"> text inside upper component </h4>
</StyledContainer>
);
};
const SecondComponent = () => {
return (
<div>
<h4 className="title secondary">text inside second component</h4>
<ThirdComponent />
</div>
);
};
const ThirdComponent = () => {
return (
<div>
<h4 className="title thirdly">text inside second component </h4>
</div>
);
};
The & is a SCSS operator and works fine with styled components.
CSS is more benifical to behave this way as passing css rules down is more effecient. Work with this effeciency ! You want to create site wide CSS patterns, try avoid specific targeting unless your sure its required (Which should be not too common).
What I do moslty is, created a styled component for the react component, so one per react components to handle all css/scss in that react component.
Daniel
This is working as it should. You're selecting all the .titles in that styled-component.
In the end, styled-components just generate a unique class name for every styled-component you made. So the rules of CSS still work there.
You can
You can select only the direct descendant .title.
const StyledContainer = styled.div`
>.title {
// rules...
}
`
Change the class name to something more specific.
Nest the CSS rule on the parent. So instead of this,
const StyledContainer = styled.div`
.title {
// rules...
}
`
Wrap your h4 with another element and do this
const StyledContainer = styled.div`
.wrapperClassName {
.title {
// rules...
}
}
`

Prettier: Keeping empty lines separating attributes in HTML/JSX HTML

So I've been trying to find a way to tell Prettier to keep line breaks between attributes in HTML, for keeping code clear, but I'm coming up empty. I'm working in TypeScript React with Styled Components, don't know if that makes a difference for the answer. I've been wondering if this is a job for Beautify, but since I'm using prettier to auto-format files on commit I'm worried using both might get very messy.
Here is what I'm talking about:
<CustomInput
value={whatever}
width="100px"
height="32px"
background="#333"
borderColor="sandybrown"
onFocus={handleFocus}
onBlur={handleBlur}
onChange={evt => setWhatever(evt.target.value)}
/>
Notice the empty lines separating the different "categories" of attributes. Prettier automatically removes these. Is this possible to achieve?
Prettifiers aught to remove empty lines in my opinion
Perhaps you can use HTML comments
<CustomInput
value="{whatever}"
<!-- formatting -->
width="100px"
height="32px"
background="#333"
borderColor="sandybrown"
<!-- event handlers-->
onFocus={handleFocus}
onBlur={handleBlur}
onChange={evt => setWhatever(evt.target.value)}
/>
Or just do what is recommended:
const handleFocus = () => {}
const handleBlur = () => {}
window.addEventListener("load", function() {
const inp = document.querySelector("CustomInput");
inp.addEventListener("focus", handleFocus);
inp.addEventListener("blur", handleBlur);
inp.addEventListener("change", evt => setWhatever(evt.target.value));
});
CustomInput {
display: block;
width: 100px;
height: 32px;
background: #333;
border-color: sandybrown;
color: white;
}
<CustomInput value="{whatever}">Text</CustomInput>
It's indeed supposed to work the way you expect (just like it works in object literals). This is simply not implemented yet. There is an open pull request, but it seems to be abandoned.

Is it possible to import typescript into html ans a color value or edit a color value from html in typescript

I have been working on this piece of code for like two days now. I am using Angular to create a web app, and I need some numbers to change color when they reach a certain value. (EXAMPLE: if num > 45 color = green else color = red) I would like to be able to pass the value of color between my typescript and HTML, but I'm having trouble with that. The color value passes to HTML just fine, but I can't put the color value into any type of style.
Here is my code. Thanks for the help!
Typescript:
colorOption=''
if(this.Classyaverage > 45){
console.log('red')
this.colorOption='#FF0000'
}
else{
console.log('green')
this.colorOption='#00FF00'
}
HTML:
<body>
<div class="pagecolor">
<div class="box">
<canvas class="offset"
id="lineChart"
width="240"
height="180"
>
</canvas>
//This is what I want...
<style>
h1{ color:{{colorOption}};}
</style>
<h1>
{{Classyaverage}}
</h1>
</div>
</div>
</body>
So is I can't import a Typescript like that, is it possible to export HTML or CSS into typescript?
Create a css class where you will apply the color:
.myColor {
color: var(--colorVariable);
}
Assign the value of the css variable from the typescript according to what you need in this way:
document.documentElement.style.setProperty( '--colorVariable', '#00FF00' );
You can modify it from any calculation or event of the application.
To achieve expected result, use ngStyle on h1 tag (https://angular.io/api/common/NgStyle)
<h1 [ngStyle]="{'color': colorOption}">
{{Classyaverage}}
</h1>
I think what you are looking for is the ngClass directive.
Here is the documentation from Angular. https://campuslabs.visualstudio.com/Student%20Assessment/_workitems/edit/59235
// .css
.red{
color: #f00;
}
.green: {
color: #0f0
}
-
// .html
<h1 [ngClass]="{'red': Classyaverage > 45, 'green': Classyaverage <= 45}"></h1>
This might be more than you need, but if you wanted to style multiple elements (such as all of a class or tag name) you could do the following.
HTML:
<h1 class="color-change">
{{Classyaverage}}
</h1>
<h2 class="color-change" *ngFor"let el of arr">
test
</h2>
TypeScript:
#ViewChildren(".color-change") private colorChange: QueryList<ElementRef>;
constructor(private renderer: Renderer2) { }
ngAfterViewInit(): void {
this.colorChange.subscribe(() =>
this.colorChange.forEach((element: ElementRef) => {
this.renderer.setStyle(
element.nativeElement,
'color',
this.colorOption
})
})
);
}
...
This would take care of multiple elements as well as asynchronous elements

How to make links clickable in a chat

I have a chat on my website that reads from a JSON file and grabs each message and then displays it using Vue.js. However, my problem is that when a user posts a link, it is not contained in an anchor tag <a href=""/>. Therefore it is not clickable.
I saw this post, and I think something like this would work, however, I am not allowed to add any more dependencies to the site. Would there be a way for me to do something similar to this without adding more dependencies?
Code for displaying the message.
<p v-for="msg in messages">
<em class="plebe">
<b> [ {{msg.platform.toUpperCase()}} ]
<span style="color: red" v-if="msg.isadmin">{{msg.user.toUpperCase()}}</span>
<span style="color: #afd6f8" v-else="">{{msg.user.toUpperCase()}}</span>
</b>
</em>:
{{msg.message}}
</p>
In a situation like this, its preferred to write a custom functional component.
The reason for this is the fact that we are required to emit a complex html structure, but we have to make sure to properly protect against xss attacks (so v-html + http regex is out of the picture)
We are also going to use render functions, because render functions have the advantage to allow for javascript that generates the html, having more freedom.
<!-- chatLine.vue -->
<script>
export default {
functional: true,
render: function (createElement, context) {
// ...
},
props: {
line: {
type: String,
required: true,
},
},
};
</script>
<style>
</style>
We now need to think about how to parse the actual chat message, for this purpose, I'm going to use a regex that splits on any length of whitespace (requiring our chat urls to be surrounded with spaces, or that they are at the start or end of line).
I'm now going to make the code in the following way:
Make a list for child componenets
Use a regex to find url's inside the target string
For every url found, do:
If the match isn't at the start, place the text leading from the previous match/start inside the children
place the url inside the list of children as an <a> tag, with the proper href attribute
At the end, if we still have characters left, at them to the list of children too
return our list wrapped inside a P element
Vue.component('chat-line', {
functional: true,
// To compensate for the lack of an instance,
// we are now provided a 2nd context argument.
// https://vuejs.org/v2/guide/render-function.html#createElement-Arguments
render: function (createElement, context) {
const children = [];
let lastMatchEnd = 0;
// Todo, maybe use a better url regex, this one is made up from my head
const urlRegex = /https?:\/\/([a-zA-Z0-9.-]+(?:\/[a-zA-Z0-9.%:_()+=-]*)*(?:\?[a-zA-Z0-9.%:_+&/()=-]*)?(?:#[a-zA-Z0-9.%:()_+=-]*)?)/g;
const line = context.props.line;
let match;
while(match = urlRegex.exec(line)) {
if(match.index - lastMatchEnd > 0) {
children.push(line.substring(lastMatchEnd, match.index));
}
children.push(createElement('a', {
attrs:{
href: match[0],
}
}, match[1])); // Using capture group 1 instead of 0 to demonstrate that we can alter the text
lastMatchEnd = urlRegex.lastIndex;
}
if(lastMatchEnd < line.length) {
// line.length - lastMatchEnd
children.push(line.substring(lastMatchEnd, line.length));
}
return createElement('p', {class: 'chat-line'}, children)
},
// Props are optional
props: {
line: {
required: true,
type: String,
},
},
});
var app = new Vue({
el: '#app',
data: {
message: 'Hello <script>, visit me at http://stackoverflow.com! Also see http://example.com/?celebrate=true'
},
});
.chat-line {
/* Support enters in our demo, propably not needed in production */
white-space: pre;
}
<script src="https://unpkg.com/vue#2.0.1/dist/vue.js"></script>
<div id="app">
<p>Message:</p>
<textarea v-model="message" style="display: block; min-width: 100%;"></textarea>
<p>Output:</p>
<chat-line :line="message"></chat-line>
</div>
You can watch or write computed method for the variable having url and manupulate it to html content and then use v-html to show html content on the page
v-html