Hacker News new | past | comments | ask | show | jobs | submit login

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}, []),
    ]);



A 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 :-)


I 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.


Isn't clarity better than bytes. We can always running through a minifier at build time.


A 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.


Clarity 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.


Instead 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));
        };


Using innerHTML as default if the second argument isn't an object risks XSS vulnerabilities. I'd prefer innerText for as default.


What XSS vulnerability? Any user can set the innerHTML of any element at any time.


XSS is when other users can write javascript that executes on your machine, like if they can set their forum signature to `<script>fetch('/send/@attacker/100usd')</script>` and the client uses innerHTML to render it on your machine in the context of your authenticated session.


> What XSS vulnerability? Any user can set the innerHTML of any element at any time.

This mindset right here is exactly why XSS is still an issue.

If you pull user generated content and put it in the DOM like this, you will open your users to XSS from other users. Basing your personal use DOM APIs on setting `el.innerHTML` will lead to a slip-up. Use `textContent` by default.


Oh my, of course. My assumption was that this function would not be used with dynamic, possibly user-defined input. This is small-scale thinking, and obviously if you intend for this code to be reused then this case must be accounted for. I'm pretty terrified that I was able to look at this code and have that assumption, even though I know better. I've even caught and resolved a couple XSS vulnerabilities at companies I've worked for. What does this say about me? Maybe another question to ask is, what does this say about the value of a web framework?


> I'm pretty terrified that I was able to look at this code and have that assumption, even though I know better. I've even caught and resolved a couple XSS vulnerabilities at companies I've worked for. What does this say about me? Maybe another question to ask is, what does this say about the value of a web framework?

I don't think it says anything about you, it's a very easy mistake to make. But it should say something to you, which is to stay vigilant and to try very hard to not dismiss security concerns without giving them some thought.

And of course always assume code will be misused if you let it, by others who don't know what you're going for and by yourself when you're trying to make a deadline. So always design your interfaces to be secure by default. Obviously easier said than done...


Often times you write code to defend against other developers who don't know any better.


Most of the time I write code to defend against future me, because I know that in a few days I'll have forgotten half of the code along with all the optimization hacks in it.


The problem is that the code $element("span", text) looks harmless, appears to work, and yet is dangerously wrong.

Dynamically setting text is very common. While dynamically setting innerHTML is a rare and dangerous operation which should be explicit in the code. The alternative syntax also supported by this function $element("span", { innerHTML: html }) is much better.


https://portswigger.net/web-security/cross-site-scripting/st...

You protect against someone else abusing it on other users.


Neat 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.


At 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)


This approach with destructuring is great but you will need to transpile the code if you're targeting IE11 which is still often the case in enterprise settings unfortunately.


Others 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/...


Yeah, 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.



Not 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-tool...


You used to be able to do that with React, too. I think I still got an app running that uses the JSX transpiler you loaded in a <script> tag...


I 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...


That 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.


I 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


Oh 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?


> I suppose I could rephrase my question to something like "What is the point of this library?"

It's just a lightweight, no-build-steps-required library that's similar to React. If you already like using React there's nothing much compelling to make you switch.

I like using it with HTM for a nice alternative to JSX which doesn't need any build steps.

I can't speak on its interaction with TypeScript, I've only used this for simple pages where I want to add some interactivity without involving the rest of the nightmarish JS ecosystem.

https://github.com/developit/htm


Because React tries to do so many things nowadays, I can no longer justify the bundle size cost when I can have Preact do what I need at a fraction of the size.


$E is pretty close to React.createElement, which JSX's tags compile to

https://reactjs.org/docs/react-without-jsx.html


I’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.)


Using tagged template literals the way lit-html does is the nicest JSX-substitute I’ve seen: https://github.com/Polymer/lit-html


It'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.


In emacs, with evil mode, I can do:

    <esc>vi`:edit-indirect-region<ret>:html-mode
And edit the string contents as HTML.


There are plugins for many editors that give syntax highlighting, code completion, type-checking, etc. The experience is great.


The 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.


I 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/Refe...


lit-html is not a "string only" template system. Because tagged literals can contain JavaScript expressions we process many other data types and handle them appropriately.

For event handlers, only the event name is in the string. The handler function is passed directly in and we add it to elements with addEventListener().

The only "magic" is that we wrap the user's handler call it with the host component as the `this` value so that you don't have to create closures like in Reach. You can just do:

    class MyElement extends LitElement {
      render() {
        return html`<button @click=${this._onClick}`>Click Ma</button>
      }
      _onClick(e) {
        console.log('this is', this);
      }
    }


And 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.


To 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.


You 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


I 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/


I 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


getAncestor is built-in: element.closest() :)


Slow. It uses a selector as an argument, which requires a parse step. It doesn’t take much effort to write something substantially better.


Tiny templating libraries like mustache are ideal for that. https://mustache.github.io/


Could 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.


I'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.


Maybe 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.


I 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?


Funny 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.


Would you mind explaining what this does?


Its 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.


Could you explain how it does this?


I 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.


> 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. :)




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: