33 comments

[ 3.4 ms ] story [ 81.7 ms ] thread
I read the entire README and have absolutely zero idea what Hypen does (and I am a relatively experienced javascript/react programmer).

Perhaps a 1-2 sentence explanation, plus some code examples, would help.

I didn't understand it until I looked at the index.html source code example (code > words sometimes) and I think it's a thin wrapper around Web components https://developer.mozilla.org/en-US/docs/Web/API/Web_compone...

But don't quote me on that, I just use Vue for a living

This is correct. It's a wrapper around web components, kind of like Lit (which they credit in the acknowledgements).

Web components are cool, but the spec is bare. This means, a lot of rote work gets custom implemented. There are a lot of frameworks popping up, trying to "standardize" this.

Hyphen appears to one of these.

Thank you. That would be a great leading paragraph on their web page if anyone from the project is reading this.
Me neither. "An elegant custom element base class" could relate to any programming language. I skim the page and see angle brackets - so it's browser-related.

Great - but for what? Is this client-side, server-side? Does it relate to a specific framework or approach?

I don't blame the project. They know their audience. It's the HN policy where extra context in the post title is forbidden that hurts stuff like this.

Point taken, good idea on the summary and code to all who provided that feedback. Indeed code > words sometimes!

That was silly of me! I’m sorry, I’ll correct that. Thank y’all for pointing that out! :)

>Hyphen simplifies the creation of custom Web Components, which are a standard for reusable user interface elements for web pages.

Was that part added later?

(comment deleted)
It's really clear to me. (And I did account for the subsequent README commits since your comment). But that's probably because I already know Web Components well. I'm in the market.

In the following line...

> Hyphen - A custom element base class for great developer ergonomics.

...I would recommend adding a link on "custom element" that points to a definition somewhere. This might make it easier for skimmers to parse the meaning of this opening line.

Thank you, sir! I'll consider adding that link.

What do you think of this? I'd love to hear your thoughts on how it could be improved in the issues or discussions, or just here! :)

I’d suggest not naming the your main interface of your library “$”, for various obvious reasons.
Obvious if you've been around long enough. I wouldn't be surprised if the majority of people reading this have never actually worked with jquery, only heard the bedtime horror stories.

(I mean, leaving aside the fact that it's utterly nondescriptive...)

I still rather miss it. Sniff...
I used jQuery in a few personal projects and it was very useful. It was 10 - 15 years ago, so I am sure there are better tools now, but for a hobbyist it was great.
The syntax still seems much more pleasant for DOM manipulation than vanillaJS. It had a nice pseudo-functional interface and a ton of elegant helpers. Whenever I do DOM manipulation in vanilla I wonder to myself if the API couldn't be a bit more user-friendly and readable.
I think I'd definitely use some kind of library if I still had to touch the DOM heavily. I liked JQuery, but I like reactive frameworks a lot more.
For low-level-ish work (like DOM manipulation) there are still no better tools than jQuery
Also browser dev tools still implement $ as a document.querySelector sort of alias.
Thanks for your concern around possible naming conflicts!

Actually the name is deliberate. A homage to jquery, at first I even considered calling this library JCustom. I think the logic is cool, as Hyphen aspires to be a kind of layer atop custom elements, as JQuery was a layer atop the DOM (and MUCH more!).

Yet, the age of JQuery has passed. It's light has gone out of the world, and John Resig has retreated to the West with the Elves.

I don't think it's really an issue.

Also, it's time to consider that passing the torch of '$' is ok. Moreover, perhaps it was never not ok. As in, the existence of a library that binds to a tiny symbol should not restrict others from using the same symbol. Otherwise it would be kind of like 'variable name squatting'--not good! Dollars for all, I say! Who's with me???

So while I get your concern about conflicts there's always been ways to avoid namespace conflicts, such that these avoidance functions have been built into the libraries themselves, in the form of: noConflict().

I think that's sufficient.

And, not that it's the best guide of best practice, but for a long time Chrome, Safar and Firefox have included $, $$ and so on functions in their consoles, as part of the Web Console API.

I get you might be concerned about this, so you can easily modify this tiny base class of Hyphen to call the base whatever you like. Reasonably, there's plenty of names that could be good for a base class, given whatever it is that you might want! :)

For me, tho, I think $ is a beautiful symbol to name it after so I'm sticking with that. JQuery can bind to other symbols, but it's also not as common these days. I'm not worried about it.

Do you think I should be worried about this? If so why?

Fair warning about the use of eval in the readme.

Anyone caring about CSP headers can't use this project and that anyone should be everyone even though the CSP header is PITA to setup.

That's a good point re CSP, while I don't want to restrict it, I also don't want to restrict developers. For example, unsafe-inline may even be required as we use inline event handler syntax. But complying with a requirement to remove unsafe-inline that some may have, would produce a worse developer experience, as you could no longer write inline handlers.

I could implement turning those inlines into addEventListener calls, as I've done in good.html, but I just want to keep everything simple here, at least for now. Trade offs, ha! :)

Will `new Function` also be covered by the same unsafe-eval CSP restriction? I know I could just search, but I suspect more discussion with you will be fruitful.

Imho same problem as eval and on top of that it creates the function only in the global scope.

Honestly it's a construct I've never seen out in the wild so I am not an expert.

Odd that they chose to use the unsafe "eval", where there's a drop-in, safe(r) replacement with

``` const properties = Object.keys(injections);

const values = Object.values(injections);

htmlString = new Function(...properties, 'return `' + templateString + '`')(...values);

```

Other than that weirdness, this is a pretty sweet little library! I've already built my own version of it, so it's nice to see a lot of the same techniques I used re-applied here. Definitely feels like doing something right if other people are thinking along the same lines!

I hope this more project sees more utility than mine! I backpedaled on the idea of 'wrapping' custom web components after I figured that learning my abstractions wasn't any different than learning native web component abstractions (which I couldn't justify). I definitely think there are good uses for this type of thing, but it was very hard to scale it to app level and not have to build data-oriented abstractions which started mildly coupling components (on the interface level, rather than as a requirement for rendering). So once I started breaking down the components abstract enough pieces that they were useful as standalone components, I wasn't using any of my custom functionality, so I just migrated it all back to native, not-extended-from-my-library components.

I'm very eager to see mature projects using these kinds of wrappers, though, because I do think there are ways to push the native web-component experience further, and I think these are the kinds of projects that help with that.

The custom element spec definitely only deals with the mechanics of when are where to run your component's lifecycle code - it says nothing about data. So your choices are basically property accessors, which are interoperable, but require prop-drilling for global-ish data, or something proprietary like a state management library.

The Web Components Community Group (WCCG) is offering something of a third way with the community protocols: https://github.com/webcomponents-cg/community-protocols

The Context protocol provides tree-scoped ambient data in an interoperable way. It's implemented by Lit and FAST (I believe). It doesn't replace a data store, but it's often used to provide data stores to components, and at least reduce some coupling.

Ah, good to know! Hadn't really looked too far into component render batching, but I'm glad there are efforts underway!

I just meant that once I was rendering out each component without having to have it's parent know about its sub-components, I didn't have much use for global contexts or passing state to a component, itself. Auto-re-rendering isn't as compelling as just setting whatever data on the attribute change, or via a child element insertion/removal. Child element maintenance requires a lookup, instead of just re-rendering with the new state, but the DOM is really good at querying, so you can just look them up.

And so, once you remove auto-re-rendering (first render is still arcane, but it's still a pretty simple code snippet to accommodate templating), and state-based re-render triggering, there's not a lot that is added to the web component that isn't just a "different" way to do it, instead of demonstrably "better".

Of course, everyone's mileage may vary! Just my two cents after working with them for a few years. And my work has pushed me toward a "simpler is better" approach, which isn't universal. For people trying to make very complex web components that can benefit from batching tree renders, no amount of breakup will help (and my assumption is that it will actually hurt a lot? Web Components don't feel "cheap" as far as instantiation goes). So it's nice to have people working on these kinds of things so that we can one day get a drop in DAW web-component, or a performant "spread-sheet" component.

Thank you for your kind words about Hyphen, catapart! And that is a very sweet little thing, the code. I like your bloody clever little code hack, that is pretty cool.

I modified it a little to fit:

    return new Function(
      ...Object.keys(this.state), `
        return (function ${
          this.template.toString()
            .replace(/^\s*function\s+/,'')
        }())
    `)(...Object.values(this.state));
I might consider including it. Would you like to make the PR? If you don't want to get involved more than that, I get it! No worries. But I mean this you do care about this, and this is your work. It's very clever!

My concern is it will fail in some cases that with(state)/eval works in, and require annoying edge-case patching to support cases we already support. For dubious security gains (I don't see the risk of eval here--if anyone can point so specifics I'd value learning!)

As to why I didn't choose it, I didn't see it (!!), but I also didn't think I should bother trying to get all properties and values because there are some that can slip through (say state contained some prototype chain and had properties up the chain, these will not be returned by keys). So I just went with with(state), because I know that's safe regarding that. with(state)/eval is simple, neat and elegant. Also I just like the aesthetics of the simpler shorter code. And I guess I like to live dangerously, hahahaha! So those reasons are why I chose it.

But your way is very clever! And it looks very clean the way you present it above.

I love to see these new, like, tight, idiomatic ways of using JavaScript to push the language. I love what you've done, it's really cool! :)

Also, it's good to hear about your experience with that. I encourage you to keep working on your library. I mean, a diversity of approaches is valuable. Even if it's just we're researching and learning and progressing toward something, through our collective effort, even if none of it is actually the destination. Although with multiple demands on your time, I'm sure it makes sense to pull back if you don't see it as worth it.

If you want to contribute more of your really clever ideas and experience to Hyphen that would be cool. Maybe not everything is a fit but we could certainly learn from you. Up to you!

Thanks for your cool comment on this. High value! Have a good day :)

Oh! Ha ha! Nice! I'm glad you like the code! I'd love to tell you it was the product of a journey of discovery that embedded deep magics in a (possibly) one-liner... but it's actually recommended (for the most part) on MDN's documentation about eval (and why not to use it).

On that topic, though, my concerns about eval are this: Developer A is using my web components along with advertising that uses library X. Library X is actually malware that tries to harvest user data, but it does provide analytics, so it is silent malware that the advertisers nor Dev A can account for. Since Library X is loaded in an iFrame, it can't execute any code that will be local to Dev A's domain; everything will originate from the ad's domain. But since Dev A is using my web components, which use eval, he can query the dom of the iFrame's parent, get a reference to my component, and then inject the attack in a place that will trigger a re-render. The re-render runs the eval which still creates the html, but it also executes code to harvest user data both from localstorage/browser storage and using any tokens that Dev A's app provides for server access. It then stores a string of malware in the browsers local storage to execute every time the page refreshes.

Now, to be clear, I haven't tested this scenario in over a decade. There may be protections, now, that prevent this kind of thing. But that's what my functional testing would be based around: whether or not a third-party can start executing code from my domain. If you can prove that doesn't happen, then I would have much less concerns about security.

That aside, I understand your concerns about edge-cases. I can tell you that I haven't done any rigorous testing, in that regard. I used that method for my web components, but I was specifically trying to "not do anything fancy". Where that took me was trying to write loop executions and extracting object properties (like you mention; it's an operation in order to get the [correct] property values out of an object, instead of just the member variables). So even while getting somewhat robust, I didn't have any issues, but there are definitely some annoying things and some stuff to work around.

Something that jumps out at me is a weird issue with getting raw DOM text. What I would like to do is query my components for a template element, read the contents of that template element as raw text, and then provide injections for any of the string template variables that are present. The problem, though, is that there is no way to get the RAW text from a DOM element (at least that I could find), without something crazy like a direct text import (heavy cost; latency/bandwidth). If you try to read the text (innerHTML/textcontent/etc) of an element whose content is "My variable name is ${var1}, today.", you will get the string "My variable name is , today."

So my work-around was to leave off the $, and just regex for "handlebar" notation (ex: "{var1}"), which I would then prefix just before the template rendering. Not exactly a cheap solution, but fast enough for my work, anyway.

As far as contribution, I'm not saying "no", but I'm sure you can imagine I'm working on my own projects. To that end, I'll ping you when the results of my tortured process are public, just in case it's helpful to you (but, again, this project doesn't have a web-component wrapper lib; it's just pure native components). But that's just to say that I don't have a ton of bandwidth, at the moment, to actually contribute code in any standard worth receiving it. That said, it's obviously an area which I'm interested in, so I'm certainly open to being productive! As long as you're good with a slow pace, I'm happy to make some pull requests.

In the meantime, though, I did go ahead and upload some of my attempts as a gist:

Your answer is fantastic! You're a really cool person. I like how you handle each point comprehensively and informatively and maintain a friendly tone throughout.

And very funny about the:

Oh! Ha ha! Nice! I'm glad you like the code! I'd love to tell you it was the product of a journey of discovery that embedded deep magics in a (possibly) one-liner... but it's actually recommended (for the most part) on MDN's documentation about eval (and why not to use it).

Haha! You totally got my meaning. Thanks for telling me that. Yeah the MDN docs contain hidden gems, I'll check it out.

Regarding the eval situation you describe, JavaScript running inside an iframe from a different origin (like BIGCLICK.NET) cannot access the DOM of the parent page (like NYTIMES.COM) due to the Same-Origin Policy enforced by web browsers. They could communicate over postMessage, but you can check the origin of the message and discard it if it's from an untrusted origin. The main risk with eval I think is that is can turn any string into code, so user input can be misused by an attacker. I'm not sure it's an issue here, because:

  eval(`(function ${this.template.toString().replace(/^\s*function\s+/,'')}())`);
Simply evals the template function's string, which creates your template. The options for manipulating user input are potentially every template replacement variable, but, none of these will be treated as code by eval, as they are located in the template slots. Also, as the cooked template is injected into the DOM, this vulnerability exists independently of eval here. For example

  .template() { return `<h1>${myHeader}</h1>` }
If an attack tries to set

  myHeader = `;alert('evil');<script>alert('big evil');</script>`
It will not work because: 1) the first alert will not be executed as code, just as a string to insert into the template slot; and, 2) the second alert will not run because scripts injected via .innerHTML are not run.

However, if an attacker sets

  myHeader = `<a href=# onpointermove=alert('evil')>Oh No</a>`
It will work when you pointer over it. I suppose the real risk then is from allowing unsafe-inline. Crucially tho "myHeader" can never be executed as code, its variable is simply replaced.

This is just a quick analysis, it could be patchy.

As long as you're good with a slow pace, I'm happy to make some pull requests.

Yes! I am totally fine with a slow pace. I completely understand and agree with the idea of going to your own limit, knowing how much bandwidth you have, and managing your workload for productivity and self care. Super important, it's the main thing--stay comfortable, no stress!

Also, if you're just too overloaded or don't feel an interest at the time, don't worry about it. No pressure, merely an invitation, you're free to take that up if it's something you feel like, or if not, for any reason, there's no disappointment or judgement at all. It's ok, we just value your contribution so far!

And I appreciate you linking some code, and clarifying any license on it, let me take a look at it! :)

How does something like this work:

        template() {
          return `
            <span>${greeting}, ${host.name}!</span>
            <button onclick="changeGreeting">Change Greeting</button>
          `;
        }
I don't see `greeting` or `host` defined anywhere in scope, so this should be an error. Did you forget to prefix them with `this.`?
Here’s the function that evaluates the template:

  with (this.state) {
    return eval(`(function ${this.template.toString().replace(/^\s*function\s+/,'')}())`);
  }
So it’s effectively turning everything in `this.state` into a local var and evaluating the code in the template function in that context.
That's so difficult for humans and tools to reason about. It would completely break intellisense and lots of lint rules. What's the reason for this?