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

proposal: support HTML style component authoring #663

Open
a-h opened this issue Apr 3, 2024 · 11 comments
Open

proposal: support HTML style component authoring #663

a-h opened this issue Apr 3, 2024 · 11 comments
Labels
NeedsDecision Issue needs some more discussion so a decision can be made proposal

Comments

@a-h
Copy link
Owner

a-h commented Apr 3, 2024

Would templ benefit from a similar concept to React of props where, if you have a single argument to a React component, you can use a HTML style to use it?

i.e. given the following TypeScript:

interface HelloProps {
  name: string
}

const Hello = (props: HelloProps) => (<div>Hello, { props.name }!</div>)

You can use this style in JSX:

<Hello name="john"/>

In templ, it might look like:

package main

type Hello struct {
  Name      string
  Children []templ.Component
}

templ (h Hello) Component() {
  <div>Hello, { c.Name }!</div>
  <div>
     { children... }
  </div>
}

You could then write this:

<main:Hello name="john">
  Hello
</main:Hello>

The struct fields would be populated with attribute values using reflection at generation time to match the attributes to field names. The LSP would warn about unknown fields.

@joerdav
Copy link
Collaborator

joerdav commented Apr 3, 2024

I think we did talk a bit about this syntax when we were designing block components. I think we opted for the current syntax because it is fewer mental jumps when thinking about it.

In this syntax would we support the existing syntax or would this replace it?
Apart from being familiar to react, what other advantages does this new syntax offer?

@gabrielvincent
Copy link
Contributor

gabrielvincent commented Apr 3, 2024

I think it could be a little confusing that the attributes in the struct are StartCased and we would use them lowercased in the proposed syntax. Maybe this would be less confusing?

<main:Hello Name="john">
  Hello
</main:Hello>

Also, how much work would it take to make the LSP correctly suggest <main:Hello when you start typing <main...? It would also be nice to have the LSP suggest available attributes when you have the cursor inside the tag definition (<main:Hello >) and invoke LSP autocomplete suggestions.

@joerdav
Copy link
Collaborator

joerdav commented Apr 3, 2024

Just had a look out of curiosity, and webcomponents do not support : in element names, so no issue of a clash there. Interestingly . aren't either, so maybe an option `<main.Hello Name="john">'

In terms of prop names, could be a use for field tags, but LSP might be difficult:

type Hello struct {
  Name      string `templ:"name"`
  Children []templ.Component
}

@sirgallifrey
Copy link
Contributor

+1 to this idea.
I was getting a bit annoyed of writing components like this:

@ui.Input(ui.InputArgs{
    Id:    "repeat-password",
    Type:  "password",
    Label: "Repeat your password",
})

So I changed to something like this to avoid the @ui.Input ui.InputArgs repetition, but I still do not super like it

@ui.Input{
    Id:    "repeat-password",
    Type:  "password",
    Label: "Repeat your password",
}.Comp()

Being able to write like below would feel much better

<ui.Input Id="repeat-password" Type="Password" Label="Repeat your password"/>

@bastianwegge
Copy link
Contributor

I would vote against this idea. I believe that the ViewModel abstraction and it's implementation in the current state are very well thought and give you exactly what you'd expect as a resulting component, coming from go. You get the exact behavior you want with little overhead.

Something like this...

@ui.Input(ui.InputArgs{
    Id:    "repeat-password",
    Type:  "password",
    Label: "Repeat your password",
})

... feels very natural to me.

In one of my projects we define a ViewModel for a FormPage(.templ) for example with multiple inputs and just iterate over the configured inputs. This way you'd only have to call your component once and you can define a primary entrance point to InputComponent behavior. This also makes working with templ a breeze, since you can create and test the ViewModel inside of a go file. This way the LoginPage can just have some styling and a FormPage inside.

Maybe I haven't understood completely what's the benefit here 😅

@bessey
Copy link

bessey commented Apr 9, 2024

FWIW here's another data point. I have no opinions on the View Model component declaration side of things, but would certainly advocate for bringing the component usage closer to HTML.

give you exactly what you'd expect as a resulting component, coming from go

IMO the competition of any HTML templating language is not Go, its HTML.

At my work we have an in-house (React) variant & constraint based design system, and just skimming through a random page in dev tools, I can see that 90%+ elements are individually wrapped in a React component (e.g. a => Link, td => Cell, etc etc). Not for fun, but to introduce typed design constraints, similar to what has been discussed over in #432 (reply in thread)

In a system like that, which I believe are increasingly common, you really need a JSX like system, that reduces the friction of switching from native DOM elements to a higher order constructs atop them to almost zero.

I think it would be fantastic if Templ could achieve something similar.

@ionrock
Copy link

ionrock commented Apr 13, 2024

Just to clarify the intent, would the idea be that you could create a struct like the one defined and use it like?

templ Foo() {
  <main:Hello Name="john">
    Hello
  </main:Hello>
}

If the goal is to help avoid the boilerplate of passing in the necessary state to the subsequent child templates, I don't see how this would solve for that. The caller would still likely need to pass in the data that would end up as arguments to the underly component.

templ Foo(c MyContext) {
  <main:Hello Name={ c.Name }>
    Hello
  </main:Hello>
}

I gather you could instantiate a component and pass that to the renderer, in which case it would be available as a web component.

type Comp interface {
  Component()
}
components := []Comp{
  &Hello{Name: "John"},
}
page.RenderWithComponents(context.Backgroun(), w, components...)

I can see this being beneficial because it might be easier for a library to return something in a more reusable format without forcing the caller to pass everything. For example, if I had a component defined that allowed adjusting classes used, I could import the pkg, adjust the css/classes and pass that in without having to change the arguments to my entrypoint component.

Offhand, I'm not sure about the HTML syntax, but I do see the benefit of adding instances of structs generated at runtime that can be used.

My comment here is to primarily to clarify some details for myself, and hopefully others.

@bastianwegge
Copy link
Contributor

I thought about this a lot the last couple of days. I researched a lot of component libraries (like shoelace.style which joined font-awesome, or microsofts FAST ui) and thus I might be biased to writing <hs-combobox></hs-combobox>. It appeared to me that we already have a namespaced way of writing components. Inventing one here doesn't seem necessary.

@a-h just as a stupid question from somebody that is not deep in parsing: Do you think it would be possible to allow for the <hs-combobox></hs-combobox> notation to be used for a package hs and a templ Combobox() {}?

Maybe this is something we shouldn't do? I'm just curious.

@jackielii
Copy link
Contributor

Maybe another point to consider:

How to set a default value if a field is not set.

In the current version, if I have a templ component like this:

templ textField(id, name, label, value, err string, span int, disabled bool) {
	<div class={ fmt.Sprintf("sm:col-span-%d", span) }>
		<label for={ id } class="block text-sm font-medium leading-6 text-gray-900">{ label }</label>
		<div class="mt-2">
			<input type="text" name={ name } id={ id } value={ value } autocomplete={ name } disabled?={ disabled } class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200"/>
		</div>
		@inputError(name, err)
	</div>
}

In the above, there is a lot of parameters. It's hard to see what's what when it's called.

@textField("name", "name", "Name", name, errs["name"], 3, false)

Also the last two parameters, I'd like to have them as default values. i.e. span=3 and disabled=false

With this and #713 , there can be a nice pattern for this?

@joerdav
Copy link
Collaborator

joerdav commented May 14, 2024

@jackielii for your case I would probably use #713 to assign defaults to a struct:

type textFieldProps struct {
    id, name, label, value, err string
    span int
    disabled bool
}

templ textField(props textFieldProps) {
        {{  props.span = cmp.Or(props.span, 3) }}
	<div class={ fmt.Sprintf("sm:col-span-%d", props.span) }>
		<label for={ id } class="block text-sm font-medium leading-6 text-gray-900">{ props.label }</label>
		<div class="mt-2">
			<input type="text" name={ props.name } id={ props.id } value={ props.value } autocomplete={ props.name } disabled?={ props.disabled } class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200"/>
		</div>
		@inputError(props.name, props.err)
	</div>
}
@textField(textField{ id: "name", name: "name", label: "Name", value: name, err: errs["name"] })

@a-h
Copy link
Owner Author

a-h commented May 14, 2024

Methods on the struct could also work.

type TextFieldProps struct {
	id, name, label, value, err string
	span                        int
	disabled                    bool
}

func (tfp TextFieldProps) ID() string {
	return tfp.id
}

func (tfp TextFieldProps) Name() string {
	return tfp.name
}

func (tfp TextFieldProps) Span() int {
	if tfp.span > 0 {
		return tfp.span
	}
	return 3
}

However, you have to remember to reference the method on the struct, and not access the internal variable.

@joerdav joerdav added NeedsDecision Issue needs some more discussion so a decision can be made proposal labels May 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
NeedsDecision Issue needs some more discussion so a decision can be made proposal
Projects
None yet
Development

No branches or pull requests

8 participants