maragudk / gomponents Goto Github PK
View Code? Open in Web Editor NEWView components in pure Go, that render to HTML 5.
Home Page: https://www.gomponents.com
License: MIT License
View components in pure Go, that render to HTML 5.
Home Page: https://www.gomponents.com
License: MIT License
You have Heroicons, HTMX, etc but not shown on main site.
Also I made a set of basically every icon available https://github.com/delaneyj/gomponents-iconify which is a super set of your Heroicons.
Hey there!
As a creator of go-ssc library, I'm really excited to see alternative opinion and approach. This library does not solve the problems I described in go-ssc, but it's very interesting in terms of templating. That's why I wanna to try to combine our approaches. I'm thinking about providing support of different template engines. Is there any chance that gomponents will implement Template interface in the future?
Maybe all the rendering logic can be moved from El
into a shared function easily?
One that takes a boolean and returns the attribute, or nil.
Is there an existing, recommended approach for internationalization with Gomponents? I'd like to share my thoughts on a potential enhancement and am eager to hear if similar solutions have already been considered or implemented.
This proposal aims to enhance Gomponents with a context-based approach for i18n support, focusing on improving usability without directly exposing the context in the function body, unlike the typical React's Context API pattern.
Presently, passing data and functions, such as translation helpers, is primarily done through prop drilling in Gomponents. This method becomes less manageable with smaller components, leading to complex and cluttered component trees.
I propose modifying the Node
interface to accept a context parameter. This parameter would be used internally for i18n and potentially other purposes. In this approach, the context would not be directly exposed within the function body, but only to the Node's Render function, ensuring encapsulation and simplicity. The proposed change to the Node
interface is as follows:
type Node interface {
Render(w io.Writer) error
}
type Node interface {
Render(w io.Writer, c interface{}) error
}
By adding a context parameter (c interface{}
) to the Render
method, we can pass necessary properties (like i18n settings) to components without the extensive prop drilling currently required.
func CheckoutButton() g.Node {
return Div(T("checkout.action.order")),
}
In this revised structure, T("checkout.action.order")
could efficiently utilize the context for accessing translation data, streamlining the process.
This enhancement not only aligns Gomponents with familiar design patterns from other frameworks but also adheres to Go's core value of simplicity by avoiding the introduction of complex or hard-to-understand 'magic' in the code. By not exposing the context directly within function bodies, we maintain straightforward and maintainable code structures. I am eager to receive feedback and engage in discussions about this proposed feature.
Hello,
I've been working with gomponents and I really appreciate the flexibility and power it brings to Go-based web development. Recently, I noticed a change in the rendering process that shifted from returning a rendered string directly:
renderedString := page.Render()
to utilizing an io.Writer for rendering:
w := bytes.Buffer{}
err := page.Render(&w)
This change, introduced in PR #39, optimizes the rendering process for different use cases. However, I find myself missing the convenience of directly obtaining the rendered output as a string for certain scenarios where working with a string is more suitable than dealing with io.Writer.
To address this, I propose adding a String() method to the Node interface, allowing users to easily obtain the rendered output as a string. This addition will improve usability by offering an alternative way to access rendered content. The proposed interface change would look like this:
type Node interface {
Render(w io.Writer) error
String() string
}
The String() method could internally utilize the existing Render method with a bytes.Buffer, then return the buffer's content as a string, offering a straightforward and efficient implementation.
I am more than willing to take on the implementation of this feature if you agree with this proposal. I believe this enhancement will make gomponents even more versatile and user-friendly.
Thank you for considering my suggestion. I look forward to hearing your thoughts and am ready to contribute to making this improvement a reality.
For returning Nodes and errors directly from a handler, and rendering them automatically.
browser console:
Resource interpreted as Stylesheet but transferred with MIME type text/plain: "https://unpkg.com/@tailwindcss/[email protected]/dist/typography.min.css".
screenshot:
i suggest you use this for each call:
https://github.com/gabriel-vasile/mimetype
If you're interested in using gomponents, please try it out and give constructive feedback! π Feedback can be delivered in this issue as a comment, or privately to [email protected] if you prefer.
Currently values passed to the Attr(name, value)
function are not escaped during rendering by default and impose a potential security risk. These values should be treated as unsafe data, like any other data rendered with the Text(...)
function.
Div(g.Attr("foo", `bar" baz="`)).Render(os.Stdout)
<!-- Current behavior -->
<div foo="bar" baz=""></div>
<!-- Expected behavior -->
<div foo="bar" baz=""></div>
In most cases html attribute like class="navbar"
, id="cta-button"
, name="email"
are statically defined by the developers and do not impose much risk.
However when rendering user supplied data in e.g. a change profile page a user could escape the html attributes by choosing a name like: Little Bobby"> Tables
which then leads to a potential XSS.
func InputField(name, defaultValue string) g.Node {
return Input(Name(name), Value(defaultValue)) // calling Attr(...)
}
InputField("firstname", `Little Bobby"> Tables`).Render(os.Stdout)
<input name="firstname" value="Little Booby"> Tables">
Attr(name, value)
by default. (This will and should also affect all other attribute helper functions)AttrRaw(name, rawvalue)
The html/template
package has a whole range of escaping functions, including one for html attributes.
Hi @markuswustenberg
thank you very much for creating this library. It's an intuitive, easy and fast way to write html in go. I come from js-world and i really love the declarative syntax to describe user interfaces. Has a little bit of SwiftUI when I think about it π
Right now I'm thinking about making a switch to go + gomponents for my project. I want to create a dynamic website with HTMX. Since gomponents are ultimately functions, I see no problems here. I have done exactly the same approach in JS so far. Instead of using bloated frameworks, I used ES6 template strings.
The only problem I have now is handling css and js. Basically I love the component approach. You have one component that consists of HTML, CSS and JS. When I use multiple components on a page, all the css and js code is merged into the head element. This is so handy and performant at the same time, because the css and js code is not always that big and therefore can be better inserted directly into the html document, instead of just referring to external files, so that a new request has to be made.
Hence my consideration: Would it be possible to extend the render function so that all components <style />
and <script />
tags are automatically merged and inserted into the head element, maybe like this?
func Page(title string) g.Node {
return Doctype(
HTML(
Lang("en"),
Head(
TitleEl(g.Text(title))
),
Body(
ComponentA(),
ComponentB(),
),
),
),
}
func ComponentA() g.Node {
return Group(
H2(g.Text("Component A")),
StyleEl(g.Raw("h2 { color: green; }")),
Script(g.Raw("alert("Component A")")),
),
}
func ComponentB() g.Node {
return Group(
H3(g.Text("Component B")),
StyleEl(g.Raw("h3 { color: red; }")),
Script(g.Raw("console.log("Component B")")),
),
}
this would be rendered to:
<!DOCTYPE html>
<html lang="end">
<head>
<title>Hello World</title>
<style>h2 { color: green; } h3 { color: red; }</style>
<script>alert("Component A"); console.log("Component B");</script>
</head>
<body>
<h2>Component A</h2>
<h3>Component B</h3>
</bod>
</html>
Perhaps some of this can be implemented. I am curious about your feedback.
Right now in the docs it is recommended to import library packages with aliases:
package main
import (
g "github.com/maragudk/gomponents"
c "github.com/maragudk/gomponents/components"
. "github.com/maragudk/gomponents/html"
)
func MyPage() g.Node {
return h.Main(
// ...
)
}
The problem comes when the file is saved, if an import is not being used then go fmt deletes it.
This is a problem because to import packages with aliases there is usually no help from the editor, so you must memorize the import, copy and paste it or create snippets to import the packages.
As you can well imagine, this kills productivity and completely takes you out of the flow state.
My proposal to solve this problem is to simply re-export the functions of the 3 main packages so that our import looks like this:
package main
import (
"github.com/maragudk/gomponents/shortexport/g" // For main package
"github.com/maragudk/gomponents/shortexport/c" // For components package
"github.com/maragudk/gomponents/shortexport/h" // For html package
)
func MyPage() g.Node {
return h.Main( // auto imported when file is saved
// ...
)
}
This would attack the problem directly by allowing our code editor tools to auto-import the package without having to write aliases or do it manually, which would be of great help to improve our productivity.
Tbh the Attr and El suffixes bother me alot. What do you think about moving attributes to /html/attribute package and removing the suffixes, You can then just use a regular import and use a.MyAttribute
I did not look into this much as I only just found this library but it's something which stood out and annoyed me...
β¦and if it's worth breaking backwards compatibility for it.
I've never liked that the error is ignored.
Hi,
In html/template
, you can use if
/else
instructions. In gomponents, only .If
is supported.
Here is an example.
{{if .IsAuthenticated}}
Hello {{.Username}}!
{{else}}
Please log in.
{{end}}
How would to handle such a situation with gomponents? Could a .If(...).Else(...)
be useful?
Same but different, how would you handle a loop on a range?
Thanks
Hello! Ran into the following issue today while trying gomponents. When using the inline if, g.If
it seems like the node is evaluated even though the statement is false. Made a small example app below, where session s
is nil
. There is an if statement checking if the session is not nil, and then proceeding to show the username. The code however panics with a nil reference error. Looking at the source code it would seem like the node wouldn't be evaluated, but maybe something else is going on?
package main
import (
"net/http"
g "github.com/maragudk/gomponents"
h "github.com/maragudk/gomponents/html"
)
type session struct {
Username string
}
func main() {
s := getSession()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
g.Node(
h.Div(g.Text("Hello!"),
g.If(s != nil,
h.Div(g.Text(s.Username),
g.Text("You are logged in!"),
),
),
),
).Render(w)
})
http.ListenAndServe(":8080", nil)
}
func getSession() *session {
return nil
}
Hello, thank you for this excellent library.
I'm trying to use gomponents to build an RSS XML along side my web site. The XML requires a <link></link>
element, however gomponents is not rendering this correctly because it's considered a voidElement
in HTML.
So far the best option I've thought of for fixing this is duplicating the El
function to create XMLEl
which does not use the isVoidElement
check. But I would love to hear if you have another suggestion.
Thanks!
Are there any performance benchmarks: speed / memory usage etc in comparison to libraries like Valyala's QuickTemplate (which I consider one of the fastest template libraries for go out there which I know of, representing the "top" performing template engine) and Go standard template/html?
There may be a bug because the element is expected to not be auto-closing.
Element functions are part of a separate package but the core has knowledge over tags and what tag is a void element which is strange to me. How about creating a new function VoidEl
or adding an extra param to existing function before children? This way the element function itself can decide if it is a void element or not.
Hi! There is any way to connect gomponents with TailwindCSS JIT? It should be very lightweight and useful.
Checkout the project and examples under https://github.com/hexops/vecty/
I spent a while trying to track down a bug I had which was caused by the fact that I defined the same attribute twice. The second instance was ignored by the browser. It would be better if Gomponents would error early when this happens rather than let the browser misinterpret my intent.
Currently, html.Class
and components.Classes
each render to an own class=[...]
attribute.
While this should be fine for most cases, there scenarios where this is an issue.
For a simple example, here is a BulmaCSS based button component:
func BulmaButton(children ...g.Node) g.Node {
return Button(Class("button"), g.Group(children))
}
Using this component works as expected with most attributes, but if one would like to declare this button as primary, the class attribute occours multiple times, and this will result in invalid HTML5
BulmaButton(Class("is-primary",g.Text("My Button"))
Would render to
<button class="button" class="is-primary">My Button</button>
To fix this, all direct children of an element must be inspected, to check for all attributes, that would render to child, and merge them into a single attribute. Sounds easy, but it look rather complicated, as we have (nested) groups: https://github.com/maragudk/gomponents/blob/main/gomponents.go#L76
I'm willing to provide this feature, but i would like to hear your opinion first. My idea would be to flatten those attribute groups first, then iterate over them to find all that render to class
and merge them. After that, the normal rendering process could resume. This sounds a little hacky to me, and will likely result in some impact to the rendering time, but other solutions would require more, deep, changes in the whole way how attributes work.
Hello,
I use gocomponents with HTMX for my hobby sites and I'm currently working on a POC for a project at work.
I noticed today that the Group
type satisfies the Node interface, but it cannot itself be directory rendered.
type group struct {
children []Node
}
// String satisfies fmt.Stringer.
func (g group) String() string {
panic("cannot render group directly")
}
// Render satisfies Node.
func (g group) Render(io.Writer) error {
panic("cannot render group directly")
}
// Group multiple Nodes into one Node. Useful for concatenation of Nodes in variadic functions.
// The resulting Node cannot Render directly, trying it will panic.
// Render must happen through a parent element created with El or a helper.
func Group(children []Node) Node {
return group{children: children}
}
I'd like to be able to use this to send multiple groups elements in a response for HTMX to perform an out of band swaps and the group method would allow this by looping over the nodes and writing them to the provided io.Writer
I'm more writing to ask what prompted this functionality to be designed to panic instead of rendering when the lib was written and is the advised work around to just write a function that loops and calls the Render func?
In order to write cleaner code when working with gomponents, a suggestion I have is that we create some exported constants that can be used for everyone using this project. This avoids typos, repetition and ensures a consistent usage of attribute values.
I would suggest creating a new file in gomponents/html/attributevalues.go
where we can add exported constants.
Of course it might make more sense to expose these constants in a different package. Also, take these constant names with a pinch of salt, we should strive to have some obvious and consistent naming.
What do you think of this idea @markuswustenberg ? I think is really worth doing.
For example (pseudocode):
Source(
Type("image/webp"),
)
Form(
AutoComplete("off"),
)
Input(
Type("text"),
)
Input(
Type("email"),
)
could be turned into:
Source(
Type(ImageWEBP),
)
Form(
AutoComplete(Off),
)
Input(
Type(InputTypeText),
)
Input(
Type(InputTypeEmail),
)
Hey @markuswustenberg!
I was just tinkering around with your library again and thought it would be awsome to have something similar to CSS Modules in gomponents. This would be very benefitial for encapsulation and the pain of writing CSS selectors by hand, other options like Tailwind CSS integrate well but can get out of hand pretty quickly.
I created a proof of concept which integrates nicely with your library and I think it would be an awesome addition.
Attached is a screenshot which should demonstrate the look and feel, not necessarily the implementation details.
Please let me know what you think, especially if you could see this being added to gomponents or if it should rather live in its own package.
Cheers βπΌ
First of all, very neat project!
I was thinking of a use case that I couldn't find any information on.
Let's say I wanted to create custom elements with event handlers, like I'd be able to in JavaScript using Web Components. In client-side code, I'd create a class that extends HTMLElement, register it and hook up some event handlers in the constructor. This is fairly handy, but it does mean I'd still have to write controllers for REST or WebSocket connections.
Using this project, I'm fantasising about being able to declare a custom "gomponent", write event handlers server side and trigger them via either WebSocket or REST from the client. This way you'd really have your entire behaviour in one class and not segmented across markup, client-side code and server-side code.
Are there any plans to implement something like this?
xmlns="http://www.w3.org/2000/svg"
Hi @markuswustenberg , I am really happy that I discovered your project here and I am excited to put it to good use and bring it to production for several of my clients.
I love Go, compiled language, type safety and the great re-usability that we get by using Go components.
I can only see one spot where some love is missing, and that is in the image elements.
I particularly make heavy use of the picture
element and the img
element in some sites. Some points I have noticed are:
srcset
attribute in picture
elementloading
attribute in img
elementWith those 2 additions I can really bring this to all my current and future projects. If you don't have the time to implement this, I would also not mind tips on how to proceed adding this to the codebase, although I would be a bit lost and it would take some time.
Thanks in advance man! And thanks for this wonderful creation!
Greetings from NL π³π±
Hi, thank you for the library!
I have been using gomponents to render views of large custom kubernetes resources and noticed that g.Map can be quite inefficient in these scenarios. The issue is that the generics version of g.Map copies every element upon calling the user's callback, which can be quite costly.
I wrote a benchmark to evaluate the performance impact of g.Map against a similar implementation that does not copy elements:
goos: linux
goarch: amd64
pkg: github.com/cezarguimaraes/bla
cpu: 12th Gen Intel(R) Core(TM) i7-12700KF
Benchmark_MapRef_BigStruct10x-20 46530 25108 ns/op 100135 B/op 1012 allocs/op
Benchmark_MapRef_BigStruct-20 47163 24908 ns/op 98586 B/op 1012 allocs/op
Benchmark_gMap_BigStruct10x-20 237 4806597 ns/op 435981 B/op 1012 allocs/op
Benchmark_gMap_BigStruct-20 6391 210963 ns/op 99668 B/op 1012 allocs/op
PASS
ok github.com/cezarguimaraes/bla 13.882s
Notice how g.Map
performance gets worse when mapping over large elements (8KB and 80KB per element). In particular, a no-copy version of g.Map
performed ~200 times better for lists of 80KB elements and ~8 times better for 8KB elements.
Benchmarking code (which also compares performance when preallocating the []g.Node result) available here https://github.com/cezarguimaraes/gomponents_map_bench
Pre allocating the resulting []g.Node also leads to ~25% better performance for ptr Map versions.
It's also worth noting that the previous non-generics version does not have this issue.
Another issue with the copying version of g.Map is that it is not suitable for structs that should not be copied, i.e any struct containing a sync object such as a Mutex, or user-defined values marked noCopy. Attempting to g.Map
over these values can lead to subtle bugs, and will cause go vet
to fail:
./main.go:59:42: func passes lock by value: github.com/cezarguimaraes/bla.NoCopyElement contains sync.Mutex
To conclude, I don't think the library can choose the best performing version for the user while maintaining type safety. However we could support both by either:
func MapRef func(ts []T, cb(*T) g.Node)
function that doesn't copy the elementsg.Map
to support both pointer and value callbacks:// type appoximations (~) are deliberately ignored
// so Map can be implemented without reflection
type NodeMapper[T any] interface {
func(T) g.Node | func(*T) g.Node
}
func Map2[Y NodeMapper[T], T any](ts []T, cb Y) []g.Node {
nodes := make([]g.Node, 0, len(ts))
switch f := interface{}(cb).(type) {
case func(T) g.Node:
for _, t := range ts {
nodes = append(nodes, f(t))
}
return nodes
case func(*T) g.Node:
for i := range ts {
// &ts[i] is also deliberate as calling cb(&t)
// destroys performance, possibly due to `t` escaping
// to the heap
nodes = append(nodes, f(&ts[i]))
}
return nodes
}
panic("unreachable")
}
In either case, I think there should also be a warning on Map
docs regarding poor performance when copying large objects.
I think it would be helpful to have a Fragment
function that outputs the children with no actual parent element (similar to React.Fragment https://legacy.reactjs.org/docs/fragments.html)
This would be useful in scenarios where you need to spread a slice into an element like this - but don't actually want to wrap it in a <div />
tag
Body: []g.Node{
content,
html.Div(modals...),
html.Script(
html.Src("https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"),
g.Attr("integrity", "sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"),
g.Attr("crossorigin", "anonymous"),
),
},
In addition to the label
element, there is also the label
attribute (e.g. on the option
element).
Gomponents currently only supports the element, not the attribute. I think this should be changed.
It could be handled the same way data
, form
, style
, and title
are, by splitting it into LabelEl
and LabelAttr
. The only problem I see is that this would break existing code.
What do you think, Markus?
Hello, interested in using this library for my next project - I had a question that I didnβt see an answer to in the documentation (unless I missed it somewhere): is the output escaped/sanitized to prevent XSS when outputting untrusted user input? Or is that something the user of this library would need to do manually?
I mean, I get it that the common map operation should return a slice.
Could we also have maybe a g.ForEach that does both operations at once?
By the way, really nice idea! This plays really really well with htmx.
The title is escaped, but the language and descriptions are not. Figure out if this needs to be done.
I really think this is a cool project and I am giving it a try.
Any advice for converting my current HTML files to gomponents? Either a tool to convert html files to gomponents, or allowing html to mix with gomponents so I can render html then some gomponents within my web page
IE
<html>
<body>
<h1>Outer shell<>
<div> <-- HERE INSERT GOCOMPONENTS-->
I just have alot of html and would love to prototype designs in html then convert them to gomponents afterwards.
What do you think of this idea?
https://developer.mozilla.org/en-US/docs/Web/HTML/Element#Content_sectioning
When they're in their own package, it might make sense?
Or perhaps it's better to move gomponents-heroicons into a more generic gomponents-icons and allow to include additional icons?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.