The problem with standards is that it’s hard to get them right, and you want to get it right the first time because you’re stuck with them forever. With frameworks at any time you can install a new version, and that version may not be backwards compatible and that’s ok. But standards, particularly on the web, once they’re here, they’re not going away - assuming they’re adopted at all (RIP E4X). Safari/Apple have been doing a good job of blocking things like customized built-in elements, though on the bright side it looks like they’re finally addressing attachInternals.
Google should have jumped on the web components train instead of flutter and dart to make web components a new standard competing with react
Google did this through Polymer, and more recently with Lit.
But most of their web apps are not made with lit. The traction of react is due to it being used by Facebook to build Facebook itself
According to wikipedia
Polymer is used by a number of Google services and websites, including the YouTube, YouTube Gaming, the redesigned Google Earth (since 2017),[7] Google I/O 2015 and 2016 websites, Google Play Music, redesign of Google Sites[8] and Allo for web (until its shutdown in 2019).[9]
Probably not an exhaustive list.
They are fighting words…
So let’s break down what’s going on with Cory House:
-
He releases a course on Plural sight - Learn HTML5 Web Components- 2015… when React used classes.
-
2019 React Hooks come in 16.8 and now “functional” components are all the hype…
-
React has the worst support out of all the frameworks for Custom Elements because it uses “synthetic events”…
-
He has a consulting business that helps teams "transition to React
-
He blindly follows the hype because that’s how he makes his money…
Ironically the experimental version of React July 2022 has full support for Custom Elements…
I wonder if he will do a 180 on his opinion or will see it as a threat to his business model…
Custom Elements have 3 main methods that can’t be replicated in an easy and performant way by any framework:
-
connectedCallback()
function runs when a component is attached or parsed as HTML -
disconnectedCallback()
function runs when a component is removed from the DOM -
attachShadow()
- no CCS or browser event leakage outside of component, if required
Those “API’s” are way more performant than anything the “frameworks” have come up with… and they’re easy to use (even without a framework)…
I think the main issue with Web Components and the reason why Lit
exists is because people want to write components like JSX or template literals e.g.
render(obj){this.innerHTML = '<h1>${obj.title}</h1>
<p>${obj.para}</p>'}
But then you could potentially be XSS’d.
So to my knowledge Lit uses tagged template literal functions as a HTML parser/ sanitizer…
The solution to all would be if the HTML spec had a textNode element tag e.g. <textnode>some words</textnode>
that:
- was an empty element (can’t have children)
- automatically attaches to the parent tag as a property when parsed…
- all text inside the tag becomes textContent
Then you could just do:
render(obj){this.innerHTML = '<h1><textnode>${obj.title}</textnode></h1>
<p><textnode>${obj.para}</textnode></p>'}
and not have to worry…
I’ve thought of attempting to make a customElement
that could replicate that but I don’t think it’s possible…
It’s not really possible to do it with a single Custom Element
but if you are only using something like <text-node>
inside of a render()
function then you can write your own tagged template function…
This one will convert anything inside of <text-node>
to a textNode
and append to the parent:
const litDOM = function(str, ...exp){
let acc = ''
let temp = document.createElement('template')
for(let i = 0,len = str.length; i < len; i++){
let x = str[i]; acc += x;
let query = x.substring(x.lastIndexOf('<'), x.length)
if (query.includes('<text-node>')) continue
else acc += exp.splice(i,1)
}
temp.innerHTML = acc;
let slots = temp.content.querySelectorAll('text-node');
for(let i = 0, len = slots.length; i < len; i++){
let node = document.createTextNode(exp[i])
slots[i].parentNode.replaceChild(node, slots[i])
}
return temp.content
}
let obj = "some random text"
let injection = "<script>alert('wtf')<script>"
let str = litDOM`<div><text-node>${injection}</text-node>${obj}</div>`
It wont:
- work with nested template literals
- work with unwrapped expression (don’t have tags around it)
AND It only returns adocumentFragment
not a string…
You kind of have this with the <xmp>
element. Its it’s own element and doesn’t collapse into its parent like you’re describing, but it does render its contents as plain text - everything other than a closing </xmp>
which could obviously be used in a XSS attack, so some sanitizing is still needed… and its deprecated, so there’s that .
I think one of the problems of trying to replicate this is that as the document loads or as innerHTML is set you can’t stop the parser from parsing… parsers gonna parse! You need to be able to intercept user content before that happens. Otherwise, what’s to distinguish your HTML from inserted content? What is there to indicate that the </textnode>
or </xmp>
is yours or theirs?
That’s where the tagged template literal comes in. It makes that separation for you and feeds that into a function (the tag). Then, using that information you can make those calls. And this is exactly what lit does. And it does this defaulting to text without needing a text node. If you want to render as HTML you need to take some extra steps, either by making another template or using the unsafeHTML
directive.
Lit also does quite a bit more than just making sure those expressions get inserted as strings. But if you just want to be able to make the distinction between text and markup, a simple custom template tag could do that. I do wonder about what could be done in a completely standard way that would prevent anyone from even having to worry about that. Maybe there is a way to make something like a textnode work?
Yep…
Funny enough I’ve made a custom Element that uses the mutationObserver
to scan its own child nodes as its parsing and remove()
them if they aren’t textNodes…
It prevents scripts from running BUT the main issue that I have is trying to get obsever.disconnect()
to run when the CE is loaded…
Now I’ve got to try to hack an onload
function
<!DOCTYPE html>
<html lang="en">
<head>
<script>
const mutCB = function(mutations, observer) {
for (let mut of mutations) {
for(let node of mut.addedNodes){
if(node.nodeType !== 3) node.remove();
}}};
const observer = new MutationObserver(mutCB);
class textNode extends HTMLElement{
constructor(){super()}
connectedCallback(){
observer.observe(this, { childList: true })
}
}
customElements.define('text-node', textNode)
</script>
</head>
<body>
<text-node>
This is some text <script>alert('failed')</script>
</text-node>
</body>
</html>
Yeah scratch that… It works fine in Chrome not in Firefox… so annoying…
I use YouTube music on the browser everyday. It is a really good web app. I wonder how they managed to get the architecture with lit elements. I just saw vite use both react and lit projects
Frameworks are super helpful and make writing code a lot quicker in most cases, but having a strong programming foundation before that is also super important – so make sure you know the language your framework is written in well first.