templ icon indicating copy to clipboard operation
templ copied to clipboard

How to parameterize styles or css components

Open jonerer opened this issue 2 years ago • 4 comments

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

jonerer avatar Sep 28 '23 20:09 jonerer

Thanks for bringing this up. Paramaters for CSS expressions is something we want to add. We started talking about this over at https://github.com/a-h/templ/issues/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!

a-h avatar Sep 30 '23 10:09 a-h

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 avatar Oct 03 '23 18:10 jonerer

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

jonerer avatar Oct 05 '23 18:10 jonerer

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

angaz avatar Oct 13 '23 20:10 angaz

This has ran it's course I believe, css parameters will be tracked in https://github.com/a-h/templ/issues/88

joerdav avatar Jan 30 '24 17:01 joerdav

Also, #88 was just closed in https://github.com/a-h/templ/pull/484, so the next version of templ will have params.

a-h avatar Feb 02 '24 13:02 a-h