Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to parameterize styles or css components #173

Closed
jonerer opened this issue Sep 28, 2023 · 6 comments
Closed

How to parameterize styles or css components #173

jonerer opened this issue Sep 28, 2023 · 6 comments

Comments

@jonerer
Copy link
Contributor

jonerer commented Sep 28, 2023

Hello!

Loving templ, having a blast with it.
I've been reading the docs, but I'm stumped on how to get a variable into a style or css.

I'm trying to render a grid, and would like to do something like:

css gridThing(columns int) {
	grid-template-columns: { fmt.Sprintf("grid-template-columns: repeat(%d, 100px);", columns) }
}

But it seems that css components don't take parameters: views/items.templ: views/items.templ parsing error: css expression: found unexpected parameters: line 30, col 25

I also tried to do:

<style type="text/css">
	.grid {
		display: grid;
		grid-template-columns: { fmt.Sprintf("repeat (%d, 100px)", tree.Grid.Columns()) };
	}
</style>

But it just rendered as text. And I was unable to use an expression in the "style" attribute, as noted on https://templ.guide/security/

So yeah, I guess my question is how should I work with CSS Grids in templ? I need to input some row and col data. Like for instance shown at https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout

And I don't know at design-time how the grid will look, since it's based on user input

@a-h
Copy link
Owner

a-h commented Sep 30, 2023

Thanks for bringing this up. Paramaters for CSS expressions is something we want to add. We started talking about this over at #88

One approach is to use utility classes to do it, e.g. https://tailwindcss.com/docs/grid-template-columns

package main

import "fmt"

func getColsClass(cols int) string {
	return fmt.Sprintf("grid-cols-%d", cols)
}

templ ColsTest(cols int) {
	<div class={ "grid", getColsClass(cols), fmt.Sprintf("gap-%d", cols) }>
		<div>01</div>
		<!-- ... -->
		<div>09</div>
	</div>
}

With the corresponding main.go of:

package main

import (
	"context"
	"fmt"
	"io"
	"os"

	"github.com/a-h/htmlformat"
)

func main() {
	r, w := io.Pipe()
	go func() {
		ColsTest(4).Render(context.Background(), w)
		w.Close()
	}()
	err := htmlformat.Fragment(os.Stdout, r)
	if err != nil {
		fmt.Println("failed to format")
	}
}

You get the HTML output of:

<div class="grid grid-cols-4 gap-4">
 <div>
  01
 </div>
 <!-- ... -->
 <div>
  09
 </div>
</div>

Another way is to use a custom class component.

Since templ generates Go code, there's usually a way to bypass templ completely and do whatever you want. For example, , you can use a custom CSS class component (ColumnsClassComponent) written in code that you generate anyway you like.

package main

import "fmt"

templ ColsTest(cols int) {
	<div class={ "grid", ColumnsClassComponent(cols), fmt.Sprintf("gap-%d", cols) }>
		<div>Grid 1</div>
	</div>
	<div class={ "grid", ColumnsClassComponent(cols), fmt.Sprintf("gap-%d", cols) }>
		<div>Grid 2</div>
	</div>
	<div class={ "grid", ColumnsClassComponent(3), fmt.Sprintf("gap-%d", cols) }>
		<div>Grid 3</div>
	</div>
}

So here, you can create a ColumnsClassComponent function that returns that templ.ComponentCSSClass. Obviously, you're on your own here with regards to safe CSS, content escaping etc. which isn't ideal.

package main

import (
	"context"
	"fmt"
	"io"
	"os"

	"github.com/a-h/htmlformat"
	"github.com/a-h/templ"
)

func ColumnsClassComponent(cols int) templ.ComponentCSSClass {
	id := fmt.Sprintf("grid-cols-%d", cols)
	return templ.ComponentCSSClass{
		ID: id,
		Class: templ.SafeCSS(fmt.Sprintf(`.%s {
			display: grid;
			grid-template-columns: repeat(%d, 1fr);
			grid-gap: 1rem;
		}`, id, cols)),
	}
}

func main() {
	r, w := io.Pipe()
	go func() {
		ColsTest(4).Render(context.Background(), w)
		w.Close()
	}()
	err := htmlformat.Fragment(os.Stdout, r)
	if err != nil {
		fmt.Println("failed to format")
	}
}

But, as you can see from the output. templ takes care of conditional rendering of the required CSS classes - there's no waste in rendering classes that aren't used, or multiple copies of the same

<style type="text/css">
 .grid-cols-4 {
			display: grid;
			grid-template-columns: repeat(4, 1fr);
			grid-gap: 1rem;
		}
</style>
<div class="grid grid-cols-4 gap-4">
 <div>
  Grid 1
 </div>
</div>
<div class="grid grid-cols-4 gap-4">
 <div>
  Grid 2
 </div>
</div>
<style type="text/css">
 .grid-cols-3 {
			display: grid;
			grid-template-columns: repeat(3, 1fr);
			grid-gap: 1rem;
		}
</style>
<div class="grid grid-cols-3 gap-4">
 <div>
  Grid 3
 </div>
</div>

This concept isn't in the docs at the moment. I think that it would be better to have CSS params though!

@jonerer
Copy link
Contributor Author

jonerer commented Oct 3, 2023

Thanks a lot for the detailed and thought through response!

I went with a little cheeky

func GridItemCss(column int, row int) templ.CSSClass {
	templCSSID := fmt.Sprintf("grid-colrow-%d-%d", column, row)

	cls := fmt.Sprintf(`.%s { 
		grid-column: %d;
		grid-row: %d;
		}`, templCSSID, column+1, row+1)

	return templ.ComponentCSSClass{
		ID:    templCSSID,
		Class: templ.SafeCSS(cls),
	}
}

And it works great. But I consider this using undocumented/unformalized behaviour and wouldn't be mad or surprised if it's stops working in a future release. It would be great to have this behaviour formalized in a future version at some point.

From my perspective it would be nice to allow parameters to css components. But I'm not sure how things like fmt.Sprintf (and sanitization?) would work in a smooth way.

@jonerer
Copy link
Contributor Author

jonerer commented Oct 5, 2023

One thing I was thinking about: CSS Components get formalized in a way that can generate classes on the fly with dynamic data, it would probably be nice to have the context.Context injected there too. Just like template components have.

The point being that different classes can be generated depending on what user is visiting. E.g. for dark vs light mode

@angaz
Copy link
Contributor

angaz commented Oct 13, 2023

I have found a way to get a dynamic style attribute...

In the templ file:

<image style="REPLACE_THIS_STYLE_POSITION" height="32px" src="..."></image>

Then in the HandlerFunc:

sb := new(strings.Builder)

index := public.Index(...)
index.Render(r.Context(), sb)

// This is the worst, but templating the style attribute is
// not allowed for security reasons.
out := strings.Replace(
	sb.String(),
	`style="REPLACE_THIS_STYLE_POSITION"`,
	fmt.Sprintf(
		`style="position: absolute; top: %dpx; left: %dpx; transform: translate(-50%%, -50%%)"`,
		y,
		x,
	),
	1,
)

w.Write([]byte(out))

@joerdav
Copy link
Collaborator

joerdav commented Jan 30, 2024

This has ran it's course I believe, css parameters will be tracked in #88

@joerdav joerdav closed this as completed Jan 30, 2024
@a-h
Copy link
Owner

a-h commented Feb 2, 2024

Also, #88 was just closed in #484, so the next version of templ will have params.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants