hexops / vecty

Vecty lets you build responsive and dynamic web frontends in Go using WebAssembly, competing with modern web frameworks like React & VueJS.
BSD 3-Clause "New" or "Revised" License
2.82k stars 144 forks source link

[feature request] static HTML/CSS components #287

Open austinjherman opened 3 years ago

austinjherman commented 3 years ago

There are some new static site generators out there (exhausting, I know) that support a concept of using the JSX syntax and component ideology, but they compile down to just HTML and CSS (unless you explicitly state that a component needs runtime JS). See Astro.

I could see this feature fitting nicely into this framework. I love the JSX and component-based architecture because you can keep track of your dependencies this way. HTML, scoped CSS, and JS are all in in the same file. Not to mention components literally accept dependencies.

I love go, and it seems like this library has accomplished a lot of this so far. But not everything is an SPA. Would it be possible to allow components to compile down to just HTML with no JavaScript if it wasn't required?

PS - I think it'd be advantageous to support such a feature because it'd allow consumers to ship less JS.

slimsag commented 3 years ago

Yeah interesting though, I think this would require a full rewrite. Maybe one day.

BTW, when you reference "components" do you mean "Web Components" https://developer.mozilla.org/en-US/docs/Web/Web_Components or just components in the general term?

austinjherman commented 2 years ago

Yeah interesting though, I think this would require a full rewrite. Maybe one day.

BTW, when you reference "components" do you mean "Web Components" https://developer.mozilla.org/en-US/docs/Web/Web_Components or just components in the general term?

Hey sorry, just saw this. Thanks for the response. I was referring to "components" in the more general sense, as in a function that takes props and returns HTML.

I can give you a more high-level explanation of what I was thinking for some more context. We've been looking for a static site generator for one of our web properties. We looked at Hugo but we weren't a huge fan of the rigidity of the templating system. I like how in modern frontend frameworks, pages can be composed of components, i.e.

import Header from './Header'
import Body from './Body'
import Footer from './Footer'

const Page = () => (
  <Header />
  <Body />
  <Footer />
)

That's React-esque, but the problem we've had with React-based static site generators (e.g. Gatsby), is that React was literally created to manage "Reactive" UIs. So there's a lot of unnecessary run-time javascript for what could be mostly determined at build time. This can have a pretty major impact on performance depending on the size of your site.

So there's another project I saw called https://github.com/8byt/gox (through which I found vecty). From gox's readme: "gox is an extension of Go's syntax that let's you write HTML-style tags directly in your source code." The simple example they provide is

package main

import "github.com/gopherjs/vecty"

type Page struct{
    vecty.Core
}

func (w *Page) Render() vecty.ComponentOrHTML {
    return <body>
        <div class="amazing">
            <h1>gox</h1>
            <span class={"you could put dynamic content here"}/>
            yeah you can do bare words too
        </div>
    </body>
}

func main() {
    vecty.RenderBody(new(Page))
}

So my initial thought was this: it would be nice if there was a way for purely static components to be compiled down to static HTML at build time so that less JS can ultimately be shipped to the browser. So maybe instead of:

func (w *Page) Render() vecty.ComponentOrHTML { ... }

There could be another variation that just results in Static HTML. Something like:

func (w *Page) Render() vecty.StaticComponentOrHTML { ... }

Does that makes sense? It's just a high-level thought right now, but I think it could potentially result in more finely-tuned control over the JS that gets shipped to the browser and potentially more performant websites and apps.

soypat commented 2 years ago

So I'm running into an issue using vecty which I have been hacking my way around: components which belong to the javascript world: These creatures can modify the DOM without vecty knowing and break things- So one has a few options here:

Explicitly tell vecty to skip rendering these components

Implementing a

Issues with method

This works in simple cases. An issue arises when creating relationships between elements where at least one implements the RenderSkip method and has a handler required for performing js calls/gets/sets.

Since Every call of vecty.Rerender(component) is being skipped, whatever tries to call javascript code from the component which ended up not being rendered will get a undefined handler. This can be remedied by storing the first component generated as parent state as I am doing here. This ain't pretty, and I believe will get quite hairy and difficult to read when adding more.

It also brings cognitive load on the user side, sadly, as I wish there was a way I as the API author or vecty could deal with it, but there seems to be none unless I can somehow dynamically seek the js object attached to a DOM node (just maybe the library has this functionality?).

Solutions?

Also, this got me thinking... I could just store global state since this component is likely just going to be used once. Still, this is still an issue for commodity components which need a js handler, like sliders and checkboxes. These may be present in large quantities exacerbating the problem. I'm kind of at a loss for actual sustainable solutions to this problem.

soypat commented 2 years ago

Great solution found shortly thereafter

So global state isn't so bad on UI's, I guess? It has let me fix this issue more or less elegantly. See my recent commit on mdc. By having a map[string]js.Value which has all active handlers I can consistently acquire handlers, no problem.

The show goes on.

slimsag commented 2 years ago

@soypat I don't really understand your message above, the code is quite complex and it's not clear to me what handler you are talking about.

An issue arises when creating relationships between elements where at least one implements the RenderSkip method and has a handler required for performing js calls/gets/sets.

What are the "relationships" being talked about here?

whatever tries to call javascript code from the component which ended up not being rendered will get a undefined handler.

This to me sounds like you are somehow relying on Vecty to render more elements after.. asking it not to render elements (by having a RenderSkipper implementation that always says "do not rerender")?

I should make it super clear: either you or Vecty should own the element, mixing will certainly lead to bad results because Vecty has no idea about any changes you have made to the elements and expects them to be unchanged.

unless I can somehow dynamically seek the js object attached to a DOM node (just maybe the library has this functionality?).

I don't know if it's what you want, but inside of Mount() callback you can of course call any html.Node().(js.Value) method to get a handle to the underlying JS object. All this requires is that you appropriately store a reference to the html.Whatever that your Render function is returning, e.g. as a state field in your component struct, so that you can invoke myfield.Node() from inside your Mount.

Hope this helps

soypat commented 2 years ago

Yeah, I was not clear at all, sorry about that.

either you or Vecty should own the element, mixing will certainly lead to bad results

So Ideally I should be using setUnsafeHTML to not cause problems with vecty- however, it's painful to work with strings for html rendering (I have to bring in more libraries, more cognitive load). I'd much rather write the HTML using vecty and then convert it to string somehow. This functionality would be a godsend for working with vecty's dom construction while not using it's renderer.

call any html.Node().(js.Value) method to get a handle to the underlying JS object.

The problem with this approach is that since I use the RenderSkip method for all nodes with attached javascript, all user-side JS syscalls to an element which is not rerendered will fail since Mount() has not been called to set the JS handler. I was thinking this morning: Does SkipRender(c Component) receive c as in the rendered, in-DOM component? If so I could just assign the c's Node() to the receiver's Node field and avoid all this global handler storage.