GithubHelp home page GithubHelp logo

maragudk / gomponents Goto Github PK

View Code? Open in Web Editor NEW
701.0 13.0 20.0 381 KB

View components in pure Go, that render to HTML 5.

Home Page: https://www.gomponents.com

License: MIT License

Go 99.53% Makefile 0.47%
golang go dom html component declarative declarative-ui view ui ui-components

gomponents's People

Contributors

markuswustenberg avatar oderwat avatar teutonicjoe avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

gomponents's Issues

Support Template interface

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?

Make Group renderable

Maybe all the rendering logic can be moved from El into a shared function easily?

Handling internationalization

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.

Summary

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.

Current Challenge

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.

Proposed Solution

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:

Current Node Interface

type Node interface {
	Render(w io.Writer) error
}

Proposed Node Interface

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.

Example Usage

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.

Benefits

  • Encapsulation: Keeps the context usage internal, avoiding direct exposure in the function body.
  • Improved Readability: Simplifies component signatures, making them more readable.
  • Scalability: Enhances the capability of Gomponents in handling extensive i18n requirements.

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.

Proposal to Add String Rendering Method to Node Interface

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.

Feedback on the API design

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.

Potential XSS: Values passed to Attr(...) are not escaped by default

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.

Example

Div(g.Attr("foo", `bar" baz="`)).Render(os.Stdout)
<!-- Current behavior -->
<div foo="bar" baz=""></div>

<!-- Expected behavior -->
<div foo="bar&#34; baz=&#34;"></div>

Why is this important?

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.

Insecure Example

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">

Proposal

  1. Escape all values passed to Attr(name, value) by default. (This will and should also affect all other attribute helper functions)
  2. Introduce a second, insecure function e.g. AttrRaw(name, rawvalue)

cf.

The html/template package has a whole range of escaping functions, including one for html attributes.

component specific css and js

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.

Proposal: Re-export packages with shorter names (g, h, c)

The problem:

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.

Proposed solution:

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.

proposal: move attributes nodes to html/attribute

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

Else statement

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

Inline if with nil pointer

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
}

Support XML Elements

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!

performance benchmarks

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?

Use with TailwindCSS JIT

Hi! There is any way to connect gomponents with TailwindCSS JIT? It should be very lightweight and useful.

trigger an error on duplicate attributes

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.

Merge multiple occurrences of the `class` attribute

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.

Why can't a group be rendered?

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?

Add constants for common attribute values

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

CSS Modules in gomponents

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 ✌🏼

cssmodules

Using gomponents to create custom reactive elements

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?

picture & img element improvements

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:

  • Missing srcset attribute in picture element
  • Missing loading attribute in img element

With 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 πŸ‡³πŸ‡±

Generics g.Map performance

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:

  1. Adding a func MapRef func(ts []T, cb(*T) g.Node) function that doesn't copy the elements
  2. Extending g.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.

Fragments

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"),
	),
},

Label can be an attribute as well

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?

Is output escaped?

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?

Why g.Map doesn't also do g.Group?

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.

HTML -> gomponents converter tool

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?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.