Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: chasefleming/elem-go
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.18.0
Choose a base ref
...
head repository: chasefleming/elem-go
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref
Loading
56 changes: 47 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# elem-go
![elem-go logo](./logo.png)

`elem` is a lightweight Go library for creating HTML elements programmatically. Utilizing the strong typing features of Go, `elem` ensures type safety in defining and manipulating HTML elements, minimizing potential runtime errors. It simplifies the generation of HTML views by providing a simple and intuitive way to create elements and set their attributes, properties, and content.

@@ -9,6 +9,7 @@
- Supports common HTML elements and attributes.
- Utilities for simplified element generation and manipulation.
- Advanced CSS styling capabilities with the [styles](styles/README.md) subpackage.
- Use the [`StyleManager`](styles/STYLEMANAGER.md) for advanced CSS features like pseudo-classes, animations, and media queries.

## Installation

@@ -57,7 +58,7 @@ When the above Go code is executed and the `.Render()` method is called, it prod

### Attributes and Styles

The `attrs` subpackage provides type-safe attribute functions that ensure you're setting valid attributes for your elements. This helps eliminate potential issues at runtime due to misspelled or unsupported attribute names.
The [`attrs`](attrs/README.md) subpackage provides type-safe attribute functions that ensure you're setting valid attributes for your elements. This helps eliminate potential issues at runtime due to misspelled or unsupported attribute names.

For boolean attributes like `checked` and `selected`, you can simply assign them the value `"true"` or `"false"`. When set to `"true"`, the library will correctly render these attributes without needing an explicit value. For instance:

@@ -69,7 +70,7 @@ checkbox := elem.Input(attrs.Props{
})
```

For setting styles, the `styles` subpackage enables you to create style objects and convert them to inline CSS strings:
For setting styles, the [`styles`](styles/README.md) subpackage enables you to create style objects and convert them to inline CSS strings:

```go
// Define a style
@@ -166,17 +167,17 @@ Additionally, `None` can be used to create an empty element, as in `elem.Div(nil

`elem` provides utility functions for creating HTML elements:

- **Document Structure**: `Html`, `Head`, `Body`, `Title`, `Link`, `Meta`, `Style`
- **Text Content**: `H1`, `H2`, `H3`, `P`, `Blockquote`, `Pre`, `Code`, `I`, `Br`, `Hr`
- **Sectioning & Semantic Layout**: `Article`, `Aside`, `FigCaption`, `Figure`, `Footer`, `Header`, `Main`, `Mark`, `Nav`, `Section`
- **Document Structure**: `Html`, `Head`, `Body`, `Title`, `Link`, `Meta`, `Style`, `Base`
- **Text Content**: `H1`, `H2`, `H3`, `H4`, `H5`, `H6`, `P`, `Blockquote`, `Pre`, `Code`, `I`, `Br`, `Hr`, `Small`, `Q`, `Cite`, `Abbr`, `Data`, `Time`, `Var`, `Samp`, `Kbd`
- **Sectioning & Semantic Layout**: `Article`, `Aside`, `FigCaption`, `Figure`, `Footer`, `Header`, `Hgroup`, `Main`, `Mark`, `Nav`, `Section`
- **Form Elements**: `Form`, `Input`, `Textarea`, `Button`, `Select`, `Optgroup`, `Option`, `Label`, `Fieldset`, `Legend`, `Datalist`, `Meter`, `Output`, `Progress`
- **Interactive Elements**: `Dialog`, `Menu`
- **Interactive Elements**: `Details`, `Dialog`, `Menu`, `Summary`
- **Grouping Content**: `Div`, `Span`, `Li`, `Ul`, `Ol`, `Dl`, `Dt`, `Dd`
- **Tables**: `Table`, `Tr`, `Td`, `Th`, `TBody`, `THead`, `TFoot`
- **Hyperlinks and Multimedia**: `Img`
- **Hyperlinks and Multimedia**: `Img`, `Map`, `Area`
- **Embedded Content**: `Audio`, `Iframe`, `Source`, `Video`
- **Script-supporting Elements**: `Script`, `Noscript`
- **Inline Semantic**: `A`, `Strong`, `Em`, `Code`, `I`
- **Inline Semantic**: `A`, `Strong`, `Em`, `Code`, `I`, `B`, `U`, `Sub`, `Sup`, `Ruby`, `Rt`, `Rp`

### Raw HTML Insertion

@@ -204,6 +205,43 @@ comment := elem.Comment("Section: Main Content Start")
// Generates: <!-- Section: Main Content Start -->
```

### Grouping Elements with Fragment

The `Fragment` function allows you to group multiple elements together without adding an extra wrapper element to the DOM. This is particularly useful when you want to merge multiple nodes into the same parent element without any additional structure.

```go
nodes := []elem.Node{
elem.P(nil, elem.Text("1")),
elem.P(nil, elem.Text("2")),
}

content := elem.Div(nil,
elem.P(nil, elem.Text("0")),
elem.Fragment(nodes...),
elem.P(nil, elem.Text("3")),
)
```

In this example, the Fragment function is used to insert the nodes into the parent div without introducing any additional wrapper elements. This keeps the HTML output clean and simple.

### Handling JSON Strings and Special Characters in Attributes

When using attributes that require JSON strings or special characters (like quotes), make sure to wrap these strings in single quotes. This prevents the library from adding extra quotes around your value. For example:

```go
content := elem.Div(attrs.Props{
attrs.ID: "my-div",
attrs.Class: "special 'class'",
attrs.Data: `'{"key": "value"}'`,
}, elem.Text("Content"))
```

## Advanced CSS Styling with `StyleManager`

For projects requiring advanced CSS styling capabilities, including support for animations, pseudo-classes, and responsive design via media queries, the `stylemanager` subpackage offers a powerful solution. Integrated seamlessly with `elem-go`, it allows developers to programmatically create and manage complex CSS styles within the type-safe environment of Go.

Explore the [`stylemanager` subpackage](stylemanager/README.md) to leverage advanced styling features in your web applications.

## HTMX Integration

We provide a subpackage for htmx integration. [Read more about htmx integration here](htmx/README.md).
47 changes: 46 additions & 1 deletion attrs/README.md
Original file line number Diff line number Diff line change
@@ -9,6 +9,9 @@ The `attrs` subpackage within `elem-go` offers a comprehensive set of constants
- [Available HTML Attributes](#available-html-attributes)
- [Using `Props` Type](#using-props-type)
- [Examples](#examples)
- [Utilities](#utilities)
- [`Merge`](#merge)
- [`DataAttr`](#dataattr)

## Introduction

@@ -54,4 +57,46 @@ buttonAttrs := attrs.Props{
button := elem.Button(buttonAttrs, elem.Text("Submit"))
```

In this example, attributes for the button element are defined using the attrs.Props map with attrs constants.
In this example, attributes for the button element are defined using the attrs.Props map with attrs constants.

## Utilities

The `attrs` subpackage also includes utility functions to enhance the attribute manipulation process.

### `Merge`

The `Merge` function allows you to merge multiple `attrs.Props` maps into a single map. This is useful when you want to combine attribute maps for an element. Note that if there are conflicting keys, the last map's value will override the previous ones.

#### Usage

```go
defaultButtonAttrs := attrs.Props{
attrs.Class: "btn",
attrs.Type: "button",
}

primaryButtonAttrs := attrs.Props{
attrs.Class: "btn btn-primary", // Overrides the Class attribute from defaultButtonAttrs
attrs.ID: "submitBtn",
}

mergedButtonAttrs := attrs.Merge(defaultButtonAttrs, primaryButtonAttrs)

button := elem.Button(mergedButtonAttrs, elem.Text("Submit"))
```

In this example, the `Merge` function is used to combine the default button attributes with the primary button attributes. The `Class` attribute from the `primaryButtonAttrs` map overrides the `Class` attribute from the `defaultButtonAttrs` map.

### `DataAttr`

The `DataAttr` function is a convenient way to define `data-*` attributes for HTML elements. It takes the attribute name and value as arguments and returns a map of `data-*` attributes.

#### Usage

```go
dataAttrs := attrs.DataAttr("foobar") // Outputs "data-foobar"
```

In this example, the `DataAttr` function is used to define a `data-foobar` attribute key for an HTML element.

By using the `attrs` subpackage, you can ensure type safety and correctness when working with HTML attributes in Go, making your development process smoother and more efficient.
3 changes: 3 additions & 0 deletions attrs/attrs.go
Original file line number Diff line number Diff line change
@@ -16,13 +16,15 @@ const (

// Link/Script Attributes

As = "as"
Async = "async"
// Deprecated: Use Crossorigin instead
CrossOrigin = "crossorigin"
Crossorigin = "crossorigin"
Defer = "defer"
Href = "href"
Integrity = "integrity"
Nomodule = "nomodule"
Rel = "rel"
Src = "src"
Target = "target"
@@ -44,6 +46,7 @@ const (

// Semantic Text Attributes

Cite = "cite"
// Deprecated: Use Datetime instead
DateTime = "datetime"
Datetime = "datetime"
13 changes: 12 additions & 1 deletion attrs/utils.go
Original file line number Diff line number Diff line change
@@ -2,10 +2,21 @@ package attrs

import "strings"

// DataAttr returns the name for a data attribute
// DataAttr returns the name for a data attribute.
func DataAttr(name string) string {
var builder strings.Builder
builder.WriteString(DataPrefix)
builder.WriteString(name)
return builder.String()
}

// Merge merges multiple attribute maps into a single map, with later maps overriding earlier ones.
func Merge(attrMaps ...Props) Props {
mergedAttrs := Props{}
for _, attrMap := range attrMaps {
for key, value := range attrMap {
mergedAttrs[key] = value
}
}
return mergedAttrs
}
22 changes: 22 additions & 0 deletions attrs/utils_test.go
Original file line number Diff line number Diff line change
@@ -11,3 +11,25 @@ func TestDataAttr(t *testing.T) {
expected := "data-foobar"
assert.Equal(t, expected, actual)
}

func TestMerge(t *testing.T) {
baseStyle := Props{
"Width": "100px",
"Color": "blue",
}

additionalStyle := Props{
"Color": "red", // This should override the blue color in baseStyle
"BackgroundColor": "yellow",
}

expectedMergedStyle := Props{
"Width": "100px",
"Color": "red",
"BackgroundColor": "yellow",
}

mergedStyle := Merge(baseStyle, additionalStyle)

assert.Equal(t, expectedMergedStyle, mergedStyle)
}
80 changes: 69 additions & 11 deletions elem.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package elem

import (
"fmt"
"sort"
"strings"

@@ -45,6 +46,7 @@ var booleanAttrs = map[string]struct{}{
attrs.Loop: {},
attrs.Multiple: {},
attrs.Muted: {},
attrs.Nomodule: {},
attrs.Novalidate: {},
attrs.Open: {},
attrs.Playsinline: {},
@@ -53,9 +55,14 @@ var booleanAttrs = map[string]struct{}{
attrs.Selected: {},
}

type CSSGenerator interface {
GenerateCSS() string // TODO: Change to CSS()
}

type RenderOptions struct {
// DisableHtmlPreamble disables the doctype preamble for the HTML tag if it exists in the rendering tree
DisableHtmlPreamble bool
StyleManager CSSGenerator
}

type Node interface {
@@ -142,9 +149,13 @@ func (e *Element) RenderTo(builder *strings.Builder, opts RenderOptions) {
builder.WriteString("<!DOCTYPE html>")
}

isFragment := e.Tag == "fragment"

// Start with opening tag
builder.WriteString("<")
builder.WriteString(e.Tag)
if !isFragment {
builder.WriteString("<")
builder.WriteString(e.Tag)
}

// Sort the keys for consistent order
keys := make([]string, 0, len(e.Attrs))
@@ -164,18 +175,22 @@ func (e *Element) RenderTo(builder *strings.Builder, opts RenderOptions) {
return
}

// Close opening tag
builder.WriteString(`>`)
if !isFragment {
// Close opening tag
builder.WriteString(`>`)
}

// Build the content
for _, child := range e.Children {
child.RenderTo(builder, opts)
}

// Append closing tag
builder.WriteString(`</`)
builder.WriteString(e.Tag)
builder.WriteString(`>`)
if !isFragment {
// Append closing tag
builder.WriteString(`</`)
builder.WriteString(e.Tag)
builder.WriteString(`>`)
}
}

// return string representation of given attribute with its value
@@ -188,11 +203,22 @@ func (e *Element) renderAttrTo(attrName string, builder *strings.Builder) {
}
} else {
// regular attribute has a name and a value
attrVal := e.Attrs[attrName]

// A necessary check to to avoid adding extra quotes around values that are already single-quoted
// An example is '{"quantity": 5}'
isSingleQuoted := strings.HasPrefix(attrVal, "'") && strings.HasSuffix(attrVal, "'")

builder.WriteString(` `)
builder.WriteString(attrName)
builder.WriteString(`="`)
builder.WriteString(e.Attrs[attrName])
builder.WriteString(`"`)
builder.WriteString(`=`)
if !isSingleQuoted {
builder.WriteString(`"`)
}
builder.WriteString(attrVal)
if !isSingleQuoted {
builder.WriteString(`"`)
}
}
}

@@ -203,6 +229,38 @@ func (e *Element) Render() string {
func (e *Element) RenderWithOptions(opts RenderOptions) string {
var builder strings.Builder
e.RenderTo(&builder, opts)

if opts.StyleManager != nil {
htmlContent := builder.String()
cssContent := opts.StyleManager.GenerateCSS()

// Define the <style> element with the generated CSS content
styleElement := fmt.Sprintf("<style>%s</style>", cssContent)

// Check if a <head> tag exists in the HTML content
headStartIndex := strings.Index(htmlContent, "<head>")
headEndIndex := strings.Index(htmlContent, "</head>")

if headStartIndex != -1 && headEndIndex != -1 {
// If <head> exists, inject the style content just before </head>
beforeHead := htmlContent[:headEndIndex]
afterHead := htmlContent[headEndIndex:]
modifiedHTML := beforeHead + styleElement + afterHead
return modifiedHTML
} else {
// If <head> does not exist, create it and inject the style content
// Assuming <html> tag exists and injecting <head> immediately after <html>
htmlTagEnd := strings.Index(htmlContent, ">") + 1
if htmlTagEnd > 0 {
beforeHTML := htmlContent[:htmlTagEnd]
afterHTML := htmlContent[htmlTagEnd:]
modifiedHTML := beforeHTML + "<head>" + styleElement + "</head>" + afterHTML
return modifiedHTML
}
}
}

// Return the original HTML content if no modifications were made
return builder.String()
}

Loading