Replies: 3 comments 6 replies
-
The other night I was thinking it would be neat if this syntax worked: <^wrapper>
<^mycomponent size="large" ^value="x" />
</^wrapper> Basically just Angular/Vue/Alpine syntax-ish except that caret is the magic character that signals value interpolation instead of colon. Since it uses caret, you could include JS components that use colon and @ without conflicting. You could reuse the x/net/html parser, at least as a bootstrap. |
Beta Was this translation helpful? Give feedback.
-
I've been sketching out an alternative compilation strategy and project layout structure so that we can have Pushup components. I think in the course of doing this, Pushup also gets simpler and easier to use and even more Go-like. The main idea is to get closer to Go by just having .up files live alongside regular .go files. Directories as Go packages. Compiled .up files would just be importable via a package like any other Go code. This eliminates the awkward
For these examples let's assume the Go module is The next change is how .up files get compiled. First, Pushup pages (that is, .up files that get routed to per file-based routing) must live in a package named
The same file-based routing strategy as current Pushup works here, nothing about that part of Pushup changes. So Now we can introduce components. Components are .up files that are reusable, either in pages or in other components. Pages and components differ because pages are routable from the web app, and components are not. They are all .up files, but if a .up file is in a Components, like pages, are .up files that are compiled to .go, and live in Go packages. This means they can be in other modules that you To use a component in another .up file, use HTML-like tag syntax, with the element name being a construction of the package name and a derivative of the component's filesystem name. For example, let's say we have a
For now let's say our card component is entirely static, but we'll soon add a way to make it dynamic and be able to use data passed into it.
Again, .up files start in HTML parsing mode, so this is a valid, complete component. We would like to use the card component in one of our pages. Our
Notice that the element name of the component is the concatenation of the package name, a dot, and the uppercase name of the .up file minus the extension. (See generatedTypename() for the basic logic used here.) It needs to be uppercase in order to be exported from Go package per naming rules. The The generated code for the component might look like this: package components
import (
"io"
"net/http"
"github.com/adhocteam/pushup"
)
func Card(w http.ResponseWriter, req *http.Request) error {
io.WriteString(w, `<div class="card">\n`)
io.WriteString(w, `\t<h2>`)
io.WriteString(w, `Card title`)
io.WriteString(w, `</h2>\n`)
io.WriteString(w, `\t<p>`)
io.WriteString(w, `Card body`)
io.WriteString(w, `</p>\n`)
io.WriteString(w, `</div>\n`)
return nil
} Very straightforward and consistent with the The codegen for the index page looks like: package pages
import (
"fmt"
"io"
"net/http"
"example/myapp/components"
)
func Index(w http.ResponseWriter, req *http.Request) error {
io.WriteString(w, "<h1>")
io.WriteString(w, "Hello, Pushup")
io.WriteString(w, "</h1>\n")
if err := components.Card(w, req); err != nil {
return err
}
return nil
} So using a component in a .up file translates into calling a function in Go code. Very simple. (Note that the codegen for pages in this revised compilation strategy would include information about URL paths for the later "linking" step where all the routes of the Pushup app are wired up together, but I'm eliding that for now.) But static components wouldn't be very useful, so let's allow them to be passed in data from their callers. Since we're reusing HTML-ish syntax for Pushup components let's just use element attributes and properties for this. Let's say a card takes in a title and body from it's caller. The revised
The new To access this data in a component, the new I'm on the fence about whether In any case, from the caller side, passing data to a component would look like this (revised
This is just to demonstrate two different ways to pass data, either a Pushup expression or as a regular attribute value string. To accommodate the passed-in data, we need to change the generated Go code, both the signature of the functions that correspond to components, and the callsite. Our card component would now look like: package components
import (
"io"
"net/http"
"github.com/adhocteam/pushup"
)
func Card(w http.ResponseWriter, req *http.Request, props map[string]any) error {
io.WriteString(w, `<div class="card">\n`)
io.WriteString(w, `\t<h2>`)
pushup.EscapeHTML(w, pushup.Prop(props, "title"))
io.WriteString(w, `</h2>\n`)
io.WriteString(w, `\t<p>`)
pushup.EscapeHTML(w, pushup.Prop(props, "body"))
io.WriteString(w, `</p>\n`)
io.WriteString(w, `</div>\n`)
return nil
} Note the addition of the The updated codegen for our index page would be: package pages
import (
"fmt"
"io"
"net/http"
"example/myapp/components"
)
func Index(w http.ResponseWriter, req *http.Request) error {
title := "My card title"
io.WriteString(w, "<h1>")
io.WriteString(w, "Hello, Pushup")
io.WriteString(w, "</h1>\n")
if err := components.Card(w, req, map[string]any {
"title": title,
"body": "My card body",
}); err != nil {
return err
}
return nil
} Now the map that represents the props is constructed and used as an argument to the component function when called. There's one final major piece of the component puzzle, which is that HTML elements can have children. My proposed solution is a simple special With child elements and allowing non-page .up files to live in any package, we can dispense with the special handling of layouts in the current version of Pushup, and simply let layouts be components.
We add a Our
And we use it in our page as so:
Note the import of the To the codegen. The layout base component would look like: package layouts
import (
"io"
"net/http"
)
func Base(w http.ResponseWriter, req *http.Request, props map[string]any, children func()) {
io.WriteString(w, `<!DOCTYPE html>\n`)
io.WriteString(w, `<title>`)
io.WriteString(w, pushup.Escape(pushup.Prop(props, "title")))
io.WriteString(w, `</title>\n`)
io.WriteString(w, `<main>\n`)
children()
io.WriteString(w, `</main>\n`)
} Note the addition of the Let's see what that looks like in the caller. The updated page codegen: package pages
import (
"fmt"
"io"
"net/http"
"example/myapp/components"
"example/myapp/layouts"
)
func Index(w http.ResponseWriter, req *http.Request) error {
title := "My card title"
if err := layouts.Base(w, req, map[string]any {
"title": "Pushup components example",
}, func() {
io.WriteString(w, "<h1>")
io.WriteString(w, "Hello, Pushup")
io.WriteString(w, "</h1>\n")
if err := components.Card(w, req, map[string]any {
"title": title,
"body": "My card body",
}, nil); err != nil {
return err
}
}); err != nil {
return err
}
return nil
} Our previous content is now encapsulated in a function closure handed to the component to be executed at the appropriate time. Note that we also update the callsite for The function signature of codegen pages should not change to accommodate components, it's still This does draw a hard line in Pushup that pages cannot be components. If there is something in a page that wants to be reused across pages, extract it into a component. There are additional design questions, such as passing any additional unused attributes through to the component and the component having a way to access them (this is commonly done to provide wrappers around base HTML elements like This was a long-winded post but I treated it like a think-aloud, because I wanted to make sure we get the composability of components right, and that only works if the syntax and the codegen go hand in hand. |
Beta Was this translation helpful? Give feedback.
-
Another project in the space: https://templ.guide |
Beta Was this translation helpful? Give feedback.
-
It's always been my intention that Pushup pages be components, in the sense that they have come to mean in the modern web dev era: self-contained classes, or units of encapsulation of markup that can be included in other pages or components themselves. They should take "arguments" somehow from the inclusion site.
Having everything be a component simplifies Pushup and its implementation. Currently, layouts are a separate concept from pages, but they are both .up files and parsed identically (they differ in code generation mainly). If we had true components, layouts could just be regular components and sit alongside pages. We could also then remove the "^layout blah" syntax from the language.
There are a couple of design challenges to work out before we can claim that Pushup pages are components.
The first is what does the syntax look like? This may seem relatively minor but there is substantial practical implications. Because we are intermingling Go and HTML in the Pushup template language, the syntax of how components are referenced and how arguments are passed to them (and how, on the component implementation side, parameters are defined) is important to get right.
The possibly most obvious thing to do would be to adopt a JSX-like approach here, which is just a syntactical transformation from angle brackets-like markup to a function call. This appeals because much of the front-end dev world is very familiar with JSX at this point. The challenge is that it probably means inverting the current sense of Pushup pages, from one as fundamentally HTML with additional syntax to switch to Go code mode, to one that is fundamentally Go code source files with embedded JSX that is transformed to function calls (including what would be ultimately treated as plain HTML).
Another idea would be to adopt a web components-like approach, as most recently seen by the WebC plugin of the static-site generator 11ty, and also from Svelte. If we treat Pushup pages as essentially web components (which has implications especially with respect to scoped CSS and JS, which we can punt on for now), then referencing them and composing them becomes a matter of mapping the page name to the element name. The design challenge here is how to refer to pages that are down in some directory - some kind of import statement or clause might be necessary.
There are two kinds of values that can be passed to a component: attributes or "props", like
<my-comp bar="baz">
, and child nodes, like<my-comp><p>This</p></p>That</p></my-comp>
. Any approach will need to at least provide an API for Pushup authors to access these values in their pages, and at best type-check them (ultimately they will fall-through to the Go compiler but that's maybe not the best developer experience). It also needs to allow for dynamic values, which should be straightforward since we already have implicit and explicit Go expressions in Pushup syntax.Another design consideration: file-based routing. Currently, pages are automatically routable by virtue of being placed in the
app/pages
directory. If everything is just a component, how do we distinguish between components that want to be pages (and hence exposed to be routed to by the app) and components that want to play other non-route support roles?In the spirit of rough-consensus-and-working-code, I don't expect for this to be a lengthy discussion before making a decision decision and starting an implementation, but I did want to solicit some feedback before getting too far down the road.
Beta Was this translation helpful? Give feedback.
All reactions