Loading...

Button

Displays a button or a component that looks like a button.

Variants

Sizes

States

With Icon

package showcase

import (
	"github.com/axzilla/goilerplate/pkg/components"
	"github.com/axzilla/goilerplate/pkg/icons"
)

templ ButtonShowcase() {
	<div class="flex justify-center items-center border rounded-md py-16 px-4">
		<div>
			<div class="mb-8">
				<h2 class="font-semibold mb-2">Variants</h2>
				<div class="flex flex-wrap gap-2">
					@components.Button(components.ButtonProps{Text: "Default"})
					@components.Button(components.ButtonProps{Text: "Secondary", Variant: components.Secondary})
					@components.Button(components.ButtonProps{Text: "Destructive", Variant: components.Destructive})
					@components.Button(components.ButtonProps{Text: "Outline", Variant: components.Outline})
					@components.Button(components.ButtonProps{Text: "Ghost", Variant: components.Ghost})
					@components.Button(components.ButtonProps{Text: "Link", Variant: components.Link})
				</div>
			</div>
			<div class="mb-8">
				<h2 class="font-semibold mb-2">Sizes</h2>
				<div class="flex flex-wrap items-center gap-2">
					@components.Button(components.ButtonProps{Text: "Default"})
					@components.Button(components.ButtonProps{Text: "Small", Size: components.Sm})
					@components.Button(components.ButtonProps{Text: "Large", Size: components.Lg})
					@components.Button(components.ButtonProps{Size: components.ButtonIcon, IconLeft: icons.Rocket(icons.IconProps{Size: "16"})})
				</div>
			</div>
			<div class="mb-8">
				<h2 class="font-semibold mb-22">States</h2>
				<div class="flex flex-wrap gap-2">
					@components.Button(components.ButtonProps{Text: "Default"})
					// Alpine.js example
					@components.Button(components.ButtonProps{Text: "With Click", Attributes: templ.Attributes{"@click": "alert('Hey Dude!')"}})
					// Vanilla JS example
					// @components.Button(components.ButtonProps{Text: "With Click", Attributes: templ.Attributes{"onclick": "alert('Hey Dude!')"}})
					@components.Button(components.ButtonProps{Text: "Disabled", Disabled: true})
					// @components.Button(components.ButtonProps{Text: "Disabled", Disabled: "true"})
					@components.Button(components.ButtonProps{Text: "Full Width", Class: "w-full"})
				</div>
			</div>
			<div class="mb-8">
				<h2 class="font-semibold mb-2">With Icon</h2>
				<div class="flex flex-wrap gap-2">
					@components.Button(components.ButtonProps{
						Text:     "Icon Left",
						IconLeft: icons.Rocket(icons.IconProps{Size: "16"}),
					})
					@components.Button(components.ButtonProps{
						Text:      "Icon Right",
						IconRight: icons.Rocket(icons.IconProps{Size: "16"}),
					})
				</div>
			</div>
		</div>
	</div>
}
package components

import (
	"github.com/axzilla/goilerplate/pkg/utils"
	"strings"
)

// ButtonVariant represents the visual style of the button.
type ButtonVariant string

// ButtonSize represents the size of the button.
type ButtonSize string

// Constants for button variants and sizes.
const (
	Default     ButtonVariant = "default"
	Destructive ButtonVariant = "destructive"
	Outline     ButtonVariant = "outline"
	Secondary   ButtonVariant = "secondary"
	Ghost       ButtonVariant = "ghost"
	Link        ButtonVariant = "link"

	Md         ButtonSize = "md"
	Sm         ButtonSize = "sm"
	Lg         ButtonSize = "lg"
	ButtonIcon ButtonSize = "icon"
)

// Button defines the properties for the Button component.
type ButtonProps struct {
	// Class specifies additional CSS classes to apply to the button.
	// Default: "" (empty string)
	Class string

	// Text is the content of the button.
	// Default: "" (empty string)
	Text string

	// Variant determines the visual style of the button.
	// Default: Default
	Variant ButtonVariant

	// Size sets the size of the button.
	// Default: Md
	Size ButtonSize

	// FullWidth determines whether the button should take up the full width of its container.
	// Default: false
	FullWidth bool

	// Href, if provided, renders the button as an anchor tag with this URL.
	// Default: "" (empty string)
	Href string

	// Target specifies the target attribute for the anchor tag (only used when Href is provided).
	// Default: "" (empty string)
	Target string

	// Disabled can be either a bool or a string.
	// If bool (Go), it directly controls the disabled state.
	// If string, it's treated as a JavaScript expression for dynamic disabling.
	Disabled any

	// Type specifies the type of the button. Default: "button"
	// Default: "" (empty string)
	Type string

	// Attributes allows passing additional HTML attributes to the button or anchor element.
	// Default: nil
	Attributes templ.Attributes

	// IconLeft specifies an icon component to be displayed on the left side of the button text.
	// Default: nil
	IconLeft templ.Component

	// IconRight specifies an icon component to be displayed on the right side of the button text.
	// Default: nil
	IconRight templ.Component
}

// Variant als Methode
func (b ButtonProps) variantClasses() string {
	switch b.Variant {
	case Destructive:
		return "bg-destructive text-destructive-foreground hover:bg-destructive/90"
	case Outline:
		return "border border-input bg-background hover:bg-accent hover:text-accent-foreground"
	case Secondary:
		return "bg-secondary text-secondary-foreground hover:bg-secondary/80"
	case Ghost:
		return "hover:bg-accent hover:text-accent-foreground"
	case Link:
		return "text-primary underline-offset-4 hover:underline"
	default:
		return "bg-primary text-primary-foreground hover:bg-primary/90"
	}
}

// Size als Methode
func (b ButtonProps) sizeClasses() string {
	switch b.Size {
	case Sm:
		return "h-9 px-3 rounded-md"
	case Lg:
		return "h-11 px-8 rounded-md"
	case ButtonIcon:
		return "h-10 w-10"
	default:
		return "h-10 px-4 py-2 rounded-md"
	}
}

func (b ButtonProps) modifierClasses() string {
	classes := []string{}
	if b.FullWidth {
		classes = append(classes, "w-full")
	}
	return strings.Join(classes, " ")
}

// Button renders a button or anchor component based on the provided props.
// It can be customized with various visual styles, sizes, and behaviors.
//
// Usage:
//
//	@components.Button(components.ButtonProps{
//	  Text: "Click me",
//	  Variant: components.Primary,
//	  Size: components.Md,
//	  FullWidth: false,
//	  IconLeft: components.Icon(components.IconProps{Name: "user"}),
//	  IconRight: components.Icon(components.IconProps{Name: "arrow-right"}),
//	  Attributes: templ.Attributes{
//	    "aria-label": "Click this button",
//	    "data-testid": "main-button",
//	  },
//	})
//
// Props:
//   - Class: Additional CSS classes to apply to the button. Default: "" (empty string)
//   - Text: The text content of the button. Default: "" (empty string)
//   - Variant: The visual style of the button (e.g., Default, Destructive, Outline). Default: Default
//   - Size: The size of the button (Md, Sm, Lg, Icon). Default: Md
//   - FullWidth: Whether the button should take up the full width of its container. Default: false
//   - Href: If provided, renders the button as an anchor tag with this URL. Default: "" (empty string)
//   - Target: The target attribute for the anchor tag (only used when Href is provided). Default: "" (empty string)
//   - Disabled: Can be either a bool or a string. If bool (Go), it directly controls the disabled state. If string, it's treated as a JavaScript expression for dynamic disabling. Default: nil
//   - Type: The type of the button. Default: "button"
//   - Attributes: Additional HTML attributes to apply to the button or anchor element. Default: nil
//   - IconLeft: An icon component to be displayed on the left side of the button text. Default: nil
//   - IconRight: An icon component to be displayed on the right side of the button text. Default: nil
templ Button(props ButtonProps) {
	if props.Href != "" {
		<a
			href={ templ.SafeURL(props.Href) }
			target={ props.Target }
			class={
				utils.TwMerge(
					"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors",
					"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
					props.variantClasses(),
					props.sizeClasses(),
					props.modifierClasses(),
					props.Class,
				),
			}
			if props.Disabled != nil {
				if disabledBool, ok := props.Disabled.(bool); ok && disabledBool {
					aria-disabled="true"
				}
				if disabledStr, ok := props.Disabled.(string); ok {
					:aria-disabled={ disabledStr }
				}
			}
			{ props.Attributes... }
		>
			{ children... }
			@renderButtonContent(props)
		</a>
	} else {
		<button
			class={
				utils.TwMerge(
					"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors",
					"focus-visible:outline-none focus-visible:ring-2 focus-ring-ring focus-visible:ring-offset-2",
					"disabled:pointer-events-none disabled:opacity-50",
					props.variantClasses(),
					props.sizeClasses(),
					props.modifierClasses(),
					props.Class,
				),
			}
			if props.Type != "" {
				type={ props.Type }
			}
			if props.Disabled != nil {
				if disabledBool, ok := props.Disabled.(bool); ok {
					disabled?={ disabledBool }
				}
				if disabledStr, ok := props.Disabled.(string); ok {
					:disabled={ disabledStr }
				}
			}
			{ props.Attributes... }
		>
			{ children... }
			@renderButtonContent(props)
		</button>
	}
}

// renderButtonContent renders the content of the button, including icons and text
templ renderButtonContent(props ButtonProps) {
	<span class="flex gap-2 items-center">
		if props.IconLeft != nil {
			@props.IconLeft
		}
		{ props.Text }
		if props.IconRight != nil {
			@props.IconRight
		}
	</span>
}