15 ms·
I agree about the nuisance of creating DOM elements. innerHTML is OK if you’re doing static content, but for anything that needs to be dynamic (untrusted input,
by wolfgang42 6y ago
I agree about the nuisance of creating DOM elements. innerHTML is OK if you’re doing static content, but for anything that needs to be dynamic (untrusted input, event handlers, etc.) I have a little tiny helper library that I carry around in my head and write into projects that need it:
const $T = text => document.createTextNode(text)
function $E(tag, props, kids) {
const elem = document.createElement(tag)
for (const k in props) {
elem[k] = props[k]
}
for (const kid of kids) {
elem.appendChild(kid)
}
return elem
}
(Add flourishes as necessary for things like optional arguments and allowing falsey kids.)
This isn’t as nice as JSX, but it makes a reasonably ergonomic API for quick little projects. Here’s what the example in the article would look like (admittedly it’s a bit easier to follow with syntax highlighting):
const button = $E('button', {}, [
$E('i', {className: 'icon-lightbulb'}, []),
$T('I learned something!'),
$E('object', {data: '/confetti.svg', width: 30, height: 30}, []),
]);
- ihuman 6y ago$E is pretty close to React.createElement, which JSX's tags compile to https://reactjs.org/docs/react-without-jsx.html https://reactjs.org/docs/react-without-jsx.html
- wolfgang42 6y agoI’m not surprised by the convergent evolution—it’s the obvious API for creating elements. (My earliest versions of $E had arguments (tag, kids, fn) and would call fn(elem) so it could perform arbitrary modifications on the node, but I eventually realized that all I ever did was set properties and it was silly to have an entire lambda just for that.)
- hn_throwaway_99 6y agoThis is fairly similar to hyperscript: https://github.com/hyperhype/hyperscript/blob/master/README.md https://github.com/hyperhype/hyperscript/blob/master/README....
- fiddlerwoaroof 6y agoUsing tagged template literals the way lit-html does is the nicest JSX-substitute I’ve seen: https://github.com/Polymer/lit-html https://github.com/Polymer/lit-html
- mbrock 6y agoIt's pretty nice but doesn't play so nicely with editor indenting modes and stuff like that, so there are some reasons to use normal JavaScript function calls instead.
- fiddlerwoaroof 6y agoIn emacs, with evil mode, I can do: <esc>vi`:edit-indirect-region<ret>:html-mode And edit the string contents as HTML.
- spankalee 6y agoThere are plugins for many editors that give syntax highlighting, code completion, type-checking, etc. The experience is great.
- jsf01 6y agoThe downside there is you’re heavily relying on strings, which feels a bit weird for things like event handlers, which would either have to inline the function as a string or do some magic behind the scenes. The editor is also going to be less helpful in figuring out your intent when using a string-only templating system.
- fiddlerwoaroof 6y agoI don’t think it’s actually strings: the tag gets the value of the expression in ${}s and can return whatever sort of object it wants. As I understand it, it basically creates HtmlTemplate objects or some other kind of Fragment: the strings are only there for specifying the tags and static attributes. See “tagged templates” here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_template_literals https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
- ShamelessC 6y agoWould you mind explaining what this does?
- warent 6y agoIts just a helper function to create an element, assign it some properties, and append children elements. Not very much unlike React in the API (emphasis on "interface") but of course the inner workings is as minimal as possible.
- areoform 6y agoCould you explain how it does this?
- hexaga 6y agoI don't know much js, but it seems fairly straightforward. Here's some comments: // create an element with 'tag' const elem = document.createElement(tag) // copy properties into the new element one at a time for (const k in props) { elem[k] = props[k] } // append child elements one at a time for (const kid of kids) { elem.appendChild(kid) } // and that's it return elem Applied recursively, you can make dom elements with very little boilerplate.
- mbrock 6y agoFunny to see because I do the exact same thing down to the parameter names, almost token-by-token identical. I usually just name the function "tag" though.
- akira2501 6y agoInstead of the loop, you can just use: Object.assign(elem, props). I found I wanted a more data-driven style that matched the element types themselves, so I use the somewhat more cumbersome: // data driven HTMLElement creation var $element = function(type, p={}) { let h, elem = document.createElement(type); if (!p || (typeof(p) !== "object")) { elem.innerHTML = p || ''; return(elem); } h = p.attributes; delete p.attributes; if (h) for (let e of Object.entries(h)) { elem.setAttribute(e[0],e[1]) } h = p.classList; delete p.classList; if (h) for (let c of h) { elem.classList.add(c) } h = p.dataset; delete p.dataset; if (h) Object.assign(elem.dataset, h); h = p.style; delete p.style; if (h) Object.assign(elem.style, h); h = p.innerHTML; delete p.innerHTML; if (h) elem.innerHTML = h; h = p.children; delete p.children; if (h) for (let ch of h) { if (ch) elem.appendChild(ch) } h = p.parentNode; delete p.parentNode; if (h) h.appendChild(elem); h = p.event; delete p.event; if (h) for (let e of Object.entries(h)) { elem.addEventListener(e[0],e[1]) } return(Object.assign(elem, p)); };
- ryanbrunner 6y agoNeat solution - out of curiosity though, why remove the properties from the passed options? If I called that function, I wouldn't normally expect it to mutate my arguments like that.
- hombre_fatal 6y agoAt a glance, they do it so that at the end they can merge in any remaining props they didn't handle after all their work building `elem` is done. This can be accomplished non-destructively with destructuring. const { attributes, classList, style, ...rest } = p // elem = ...consume attributes, classList, style... return Object.assign(elem, rest)
- 6y ago
- austincheney 6y agoI also have custom DOM helpers I carry around with me: * getNodesByType - allows me to get things like attributes, text, and comments directly * getAncestor - allows me to get a specified element somewhere between a target element and the document.documentElement
- felixfbecker 6y agogetAncestor is built-in: element.closest() :)
- austincheney 6y agoSlow. It uses a selector as an argument, which requires a parse step. It doesn’t take much effort to write something substantially better.
- pcr910303 6y agoA bit more minified/modern version of this that I'm using: function $e(t='div',p={},c=[]){ let el=document.createElement(t); Object.assign(el,p); el.append(...c); return el; } var $t=document.createTextNode.bind(document); That's 173 bytes not minified, might be useful for someone. Interestingly, the function names are exactly the same - I guess people think similarly :-)
- PetahNZ 6y agoIsn't clarity better than bytes. We can always running through a minifier at build time.
- pcr910303 6y agoA minifier is better, but I find that snippet of code clear enough for personal use (el is element, t is tag, p is props, and c is children). The small bytes also mean that for simple sites I can just copy paste it before transitioning to a real setup.
- yunruse 6y agoClarity is only useful when the variable scope is complicated, which it isn’t here. The problem COULD be that the ultra-short variable names make an unclear external API… but given the function is internal, the context of its use would make it rather obvious it’s constructing HTML elements. You’re not wrong about clarity; it wouldn’t hurt here at all, but it wouldn’t hinder without, so I imagine it’s to reduce cognitive load and make it abundantly clear the function is logicless glue.
- wolfgang42 6y agoI like how short yours is—I really must use Object.assign() more. I hadn’t heard about append(); since my original version supported IE it had to use appendChild() in a loop and I never realized there was a better option. It looks like append() also accepts strings directly. This eliminates the need for $T, which makes things even nicer.
- bschwindHN 6y agoNot nearly as minimal as your example, but I like the "no build tools route" of using preact. You don't need to build your code, and you can get JSX-esque syntax and some of the niceness of React without messing with npm or webpack or any of that. https://preactjs.com/guide/v10/getting-started#no-build-tools-route https://preactjs.com/guide/v10/getting-started#no-build-tool...
- kingdomcome50 6y agoI am failing to understand why someone would choose this over React. The page you linked to, I kid you not, has a section about how you build a Preact application from the command line...
- jayshua 6y agoThat is the getting started page. It lists multiple ways to get started. One of them is simply including the pre-built preact library with a <script> tag. After that you can use h() to build elements - no build step required. I personally use preact over react because it's tiny - 3kb for the entire library.
- bschwindHN 6y agoI linked to the "no build tools route": > Preact is packaged to be used directly in the browser, and doesn't require any build or tools
- kingdomcome50 6y agoOh I gotcha! That quote applies to react as well though (I've done it many times!). I suppose I could rephrase my question to something like "What is the point of this library?". Every other page on that site that includes examples uses JSX... which means there needs to be a build step. I guess if someone really just wants to include a 3rd party library to programatically create elements, but get none of the benefits of using them this would be great! Then again there is like 3 examples in this thread of tiny functions (< 10 LoC) that appear to do the same thing. I guess 3.5kb is about 10x smaller than React + React DOM, but 35kb isn't exactly breaking the bank either. I can't tell specifically by glancing over their examples, but I also suspect feature parity is not quite there. Specifically, I develop with Typescript so being able to type hint `React.ChangeEvent<HTMLInputElement>` etc. is sometimes necessary. It's not clear if Preact exposes a sufficient API. Maybe you know?
- ktzar 6y agoTiny templating libraries like mustache are ideal for that. https://mustache.github.io/ https://mustache.github.io/
- hombre_fatal 6y agoCould never get over Mustache using {{foo}} for safe auto-escaped interpolation but {{{foo}}} for dangerous non-escaped interpolation. I wonder how many XSS vulns this decision has caused in the wild.
- onei 6y agoI'd say that's the more sensible decision if you want to maintain syntax. You can grep for {{{ without false positives matching {{. Conversely, it's harder to find places where == was used when === should have been used (although I'll concede it's not much harder). In general I'd prefer to go the extra mile to be unsafe than accidentally miss something out.
- hombre_fatal 6y agoMaybe my point wasn't clear, but {{{ and {{ look too similar. It should be {{ and something else. Like {{Dangerous=username}}. You shouldn't have to squint at your templating code to see if there's an XSS vector or not, or defensively/neurotically grep for "{{{" just in case you didn't trust your team to squint sufficiently. For comparison, here's JSX: <div dangerouslySetInnerHTML={{__html: username}} />. vs. Mustache: {{{username}}} in a file of 1000 other { and } glyphs. Mustache's hey-dey is long over thankfully.
- onei 6y agoI agree that something possibly dangerous should be harder to mis-use. The world is full of small oversights. Having said that, surely this is the job of a linter - does one exist for mustache? I've never seen JSX before (I haven't learned anything new in front-end later than ES5). In your example, it looks more like an attribute than an inner tag, the former of which I'd almost always escape. Is that how you set inner HTML as well?
- toshk 6y agoTo not need many custom helper functions like this I still prefer to throw in jQuery for "small" more serverside oriented projects. It gives a well documented and standardized API.
- wruza 6y agoYou may be interested in re:dom then. It does no “vdom”, “mount/components” part is optional, and its functionality is aligned with $E, with some extensions. I did not use it in my projects yet, but it worked pretty well in experiments. https://redom.js.org/#elements https://redom.js.org/#elements
- pull_my_finger 6y agoI had actually just made a thing to do basically this.lfn[1] provides an l function that uses the same API as hyperscript/h. [1] - https://github.com/ryanford-dev/lfn/ https://github.com/ryanford-dev/lfn/
- stockstreaks 6y ago> I have a little tiny helper library that I carry around in my head and write into projects that need it May I suggest a repo. Github is great, I hear. :)
- EvilTerran 6y agoOthers have already mentioned that you can replace "for (const k in props) elem[k] = props[k]" with "Object.assign(elem, props)"; further to that, in modern browsers, you can replace "for (const kid of kids) elem.appendChild(kid)" with just "elem.append(...kids)" - with the added benefit that plain strings passed to .append() get turned into text nodes automatically, so you probably wouldn't need $T any more: https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/append https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/...
- wolfgang42 6y agoYeah, this evolved in my head from an early version that had to support IE, so it turns out there’s a bunch of cruft in it that I hadn’t realized wasn’t necessary any more. pcr910303 posted a version which uses the same APIs you mentioned, and I think I’m going to use that one in the future.
- tambourine_man 6y agoAnd here we are in 2020 with everyone writing their own poorly documented and tested partial version jQuery. Instead of everybody working on the same better, faster, probably already cached library. When will we ever get that jQuery’s thing wasn’t only cross-browser compatibility. It’s that the DOM’s native API is terrible and jQuery is a much terser, powerful, chainable alternative.