a-h / templ

A language for writing HTML user interfaces in Go.
https://templ.guide/
MIT License
7.72k stars 249 forks source link

css: Support for selectors of pseudo-classes, pseudo-elements. And nested css #262

Open otaxhu opened 9 months ago

otaxhu commented 9 months ago

I was experimenting with the library and found that there is no way to access the :hover pseudo-class nor any other pseudo-class or pseudo-element.

First I tried nested css, something like this:

package main

css myCss() {
    background-color: red;
    &:hover {
        background-color: green;
    }
}

Error message after trying generation of the code above:

failed to process path: /path/to/template.templ parsing error: css property expression: missing closing brace: line 4, col 4

I know that the css expression is like a class in a css file so I think that it could be the only one way to do it. Other way to do it could be with SCSS support or some other crazy thing

willoma commented 7 months ago

No need for SCSS: regular CSS supports nesting, I think templ may simply put it in the result without any transformation...

otaxhu commented 7 months ago

That is not 100% accurate if your target browser is an older one.

willoma commented 7 months ago

Sure! I did not say that all browsers support CSS nesting, but that nesting is now supported by regular CSS, therefore I think templ should allow us to do CSS nesting :-)

However, I think including SCSS into templ for nesting would be overkill...

willoma commented 7 months ago

After thinking again about this, I agree it does not fix the pseudo-class problem with non-nested CSS...

joerdav commented 6 months ago

I believe this one still requires some decisions to be made, so have labelled as such so noone jumps to an implementation.

a-h commented 6 months ago

I'm thinking that we may be able to implement CSS variables directly in <style> tags and provide an alternative to the existing style components that provides everything we might need.

The tdewolff parser is suitably open to extension such that I was able to put together a rough PoC of how JS variables could be introduced directly into script tags, without needing to have script components.

package main

import (
    "fmt"
    "io"
    "log"
    "strings"
    "unicode"

    "github.com/a-h/templ/parser/v2/goexpression"
    "github.com/tdewolff/parse/v2"
    "github.com/tdewolff/parse/v2/js"
)

func NewJSExpressionParser(content string) *JSExpressionParser {
    input := parse.NewInputString(content)
    return &JSExpressionParser{
        Content:       content,
        Input:         input,
        Lexer:         js.NewLexer(input),
        BraceCount:    0,
        GoExpressions: nil,
    }
}

type JSExpressionParser struct {
    Content string
    Input   *parse.Input
    Lexer   *js.Lexer
    // BraceCount is the number of sequential braces we've just seen.
    // If we see `{{` then we should start parsing Go code.
    BraceCount    int
    GoExpressions []Range
}

type Range struct {
    Index   int
    Content string
}

func (p *JSExpressionParser) Parse() (err error) {
    for {
        tt, _ := p.Lexer.Next()
        switch tt {
        case js.ErrorToken:
            if p.Lexer.Err() != io.EOF {
                return p.Lexer.Err()
            }
            return
        case js.OpenBraceToken:
            p.BraceCount++
            if p.BraceCount == 2 {
                expr := Range{
                    Index: p.Input.Offset(),
                }
                _, e, err := goexpression.Expression(p.Content[p.Input.Offset():])
                if err != nil {
                    return fmt.Errorf("failed to parse Go: %w", err)
                }
                expr.Content = p.Content[expr.Index : expr.Index+e]
                p.GoExpressions = append(p.GoExpressions, expr)

                // Seek past the end of the Go expression we just parsed.
                p.Input.Move(e + 1)

                // Get rid of whitespace.
                for {
                    r, l := p.Input.PeekRune(1)
                    if unicode.IsSpace(r) {
                        p.Input.Move(l)
                        continue
                    }
                    break
                }

                // Clear braces.
                if err = take(p.Input, "}}"); err != nil {
                    return fmt.Errorf("unclosed Go expression: %v", err)
                }
            }
        default:
            p.BraceCount = 0
        }
    }
}

func take(input *parse.Input, expected string) error {
    var actual strings.Builder
    for i := 0; i < len(expected); i++ {
        a, _ := input.PeekRune(i)
        actual.WriteRune(a)
    }
    if expected != actual.String() {
        return fmt.Errorf("expected %q, got %q", expected, actual.String())
    }
    input.Move(len(expected))
    return nil
}

func main() {
    content := `const x = {{ y }};`
    p := NewJSExpressionParser(content)
    err := p.Parse()
    if err != nil {
        log.Fatalf("failed to parse: %v", err)
    }
    fmt.Printf("%#+v", p.GoExpressions)
}

I think the same logic could be applied to CSS components, allowing the use of this sort of thing:

templ Page(bgColor string) {
  <style type="text/css">
  @media print {
    body {
      background-color: {{ bgColor }};
    }
  }
  </style>
}

Some work would be required to think about if/how class names get scoped to their components, but the parser part, at least, looks like it's not hard to achieve.

zapling commented 6 months ago

Some work would be required to think about if/how class names get scoped to their components, but the parser part, at least, looks like it's not hard to achieve.

On the topic of how class name would be scoped, Vue does this by looking for a special scoped property on the style tag.

<style scoped>
.myClass { }
</style>

https://vuejs.org/api/sfc-css-features.html#scoped-css

Another use cases that might be worth considering is when a project have seperate css files. It would be nice to be able to use embed on those files and also then get access to the class name scoping feature.

viirak commented 4 months ago

what's the solution for this now?

zapling commented 4 months ago

what's the solution for this now?

I used a plain CSS file and added conditional logic to add specific class names.

<head>
  <link rel="stylesheet" href="/assets/style.css"/>
</head>
hecs commented 3 months ago

First if all. Thank you for you work with templ. I love everything about it! (except for googling the name "templ") :)

I think CSS nesting and pseudo selectors would be fantastic to have in templ. And even other CSS features like media queries.

Here's my thoughts: When using templ + HTMX. Templ is giving a very nice way of "Componentify" each view. CSS scoping is usually handled by a big javascript framework + bundler + css preprocessor (like angular). In templ its just there out of the box. Right now it is limited by a number if CSS-features it doesn't support. It feels like its almost there. I have huge respect for if this is difficult to implement. But it would be truly awesome to have, I think.

jimafisk commented 3 months ago

we may be able to implement CSS variables directly in