├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── attrs ├── README.md ├── attrs.go ├── utils.go └── utils_test.go ├── elem.go ├── elem_test.go ├── elements.go ├── elements_test.go ├── examples ├── htmx-counter │ ├── README.md │ ├── go.mod │ └── main.go ├── htmx-fiber-counter │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── htmx-fiber-form │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── htmx-fiber-todo │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go └── stylemanager-demo │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── go.mod ├── go.sum ├── htmx ├── README.md └── htmx.go ├── logo.png ├── styles ├── README.md ├── STYLEMANAGER.md ├── constants.go ├── stylemanager.go ├── stylemanager_test.go ├── styles.go ├── styles_test.go ├── utils.go └── utils_test.go ├── utils.go └── utils_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version: '1.21.x' 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | - name: Test 28 | run: go test -v ./... 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Chase Fleming 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test tidy 2 | 3 | tidy: 4 | @echo "Tidying up module..." 5 | go mod tidy 6 | 7 | test: 8 | @echo "Running tests..." 9 | go test ./... -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![elem-go logo](./logo.png) 2 | 3 | `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. 4 | 5 | ## Features 6 | 7 | - Easily create HTML elements with Go code. 8 | - Type-safe definition and manipulation of elements, attributes, and properties. 9 | - Supports common HTML elements and attributes. 10 | - Utilities for simplified element generation and manipulation. 11 | - Advanced CSS styling capabilities with the [styles](styles/README.md) subpackage. 12 | - Use the [`StyleManager`](styles/STYLEMANAGER.md) for advanced CSS features like pseudo-classes, animations, and media queries. 13 | 14 | ## Installation 15 | 16 | To install `elem`, use `go get`: 17 | 18 | ```bash 19 | go get github.com/chasefleming/elem-go 20 | ``` 21 | 22 | ## Usage 23 | 24 | Import the `elem` package in your Go code: 25 | 26 | ```go 27 | import ( 28 | "github.com/chasefleming/elem-go" 29 | "github.com/chasefleming/elem-go/attrs" 30 | "github.com/chasefleming/elem-go/styles" 31 | ) 32 | ``` 33 | 34 | ### Creating Elements 35 | 36 | Here's an example of creating a `
` element with nested `

`, `

`, and `

` elements using elem: 37 | 38 | ```go 39 | content := elem.Div(attrs.Props{ 40 | attrs.ID: "container", 41 | attrs.Class: "my-class", 42 | }, 43 | elem.H1(nil, elem.Text("Hello, Elem!")), 44 | elem.H2(nil, elem.Text("Subheading")), 45 | elem.P(nil, elem.Text("This is a paragraph.")), 46 | ) 47 | ``` 48 | 49 | When the above Go code is executed and the `.Render()` method is called, it produces the following HTML: 50 | 51 | ```html 52 |

53 |

Hello, Elem!

54 |

Subheading

55 |

This is a paragraph.

56 |
57 | ``` 58 | 59 | ### Attributes and Styles 60 | 61 | 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. 62 | 63 | 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: 64 | 65 | ```go 66 | // Using boolean attributes 67 | checkbox := elem.Input(attrs.Props{ 68 | attrs.Type: "checkbox", 69 | attrs.Checked: "true", // This will render as 70 | }) 71 | ``` 72 | 73 | For setting styles, the [`styles`](styles/README.md) subpackage enables you to create style objects and convert them to inline CSS strings: 74 | 75 | ```go 76 | // Define a style 77 | buttonStyle := styles.Props{ 78 | styles.BackgroundColor: "blue", 79 | styles.Color: "white", 80 | } 81 | 82 | // Convert style to inline CSS and apply it 83 | button := elem.Button( 84 | attrs.Props{ 85 | attrs.Style: buttonStyle.ToInline(), 86 | }, 87 | elem.Text("Click Me"), 88 | ) 89 | ``` 90 | 91 | See the complete list of supported attributes in [the `attrs` package](./attrs/attrs.go), and for a full overview of style properties and information on using the `styles` subpackage, see the [styles README](styles/README.md). 92 | 93 | ### Rendering Elements 94 | 95 | The `.Render()` method is used to convert the structured Go elements into HTML strings. This method is essential for generating the final HTML output that can be served to a web browser or integrated into templates. 96 | 97 | ```go 98 | html := content.Render() 99 | ``` 100 | 101 | In this example, `content` refers to an `elem` element structure. When the `.Render()` method is called on content, it generates the HTML representation of the constructed elements. 102 | 103 | > NOTE: When using an element, this method automatically includes a preamble in the rendered HTML, ensuring compliance with modern web standards. 104 | 105 | #### Custom Rendering Options 106 | 107 | For more control over the rendering process, such as disabling the HTML preamble, use the `RenderWithOptions` method. This method accepts a `RenderOptions` struct, allowing you to specify various rendering preferences. 108 | 109 | ```go 110 | options := RenderOptions{DisableHtmlPreamble: true} 111 | htmlString := myHtmlElement.RenderWithOptions(options) 112 | ``` 113 | 114 | This flexibility is particularly useful in scenarios where default rendering behaviors need to be overridden or customized. 115 | 116 | ### Generating Lists of Elements with `TransformEach` 117 | 118 | With `elem`, you can easily generate lists of elements from slices of data using the `TransformEach` function. This function abstracts the repetitive task of iterating over a slice and transforming its items into elements. 119 | 120 | ```go 121 | items := []string{"Item 1", "Item 2", "Item 3"} 122 | 123 | liElements := elem.TransformEach(items, func(item string) elem.Node { 124 | return elem.Li(nil, elem.Text(item)) 125 | }) 126 | 127 | ulElement := elem.Ul(nil, liElements) 128 | ``` 129 | 130 | In this example, we transformed a slice of strings into a list of `li` elements and then wrapped them in a `ul` element. 131 | 132 | ### Conditional Rendering with `If` 133 | 134 | `elem` provides a utility function `If` for conditional rendering of elements. 135 | 136 | ```go 137 | isAdmin := true 138 | adminLink := elem.A(attrs.Props{attrs.Href: "/admin"}, elem.Text("Admin Panel")) 139 | guestLink := elem.A(attrs.Props{attrs.Href: "/login"}, elem.Text("Login")) 140 | 141 | content := elem.Div(nil, 142 | elem.H1(nil, elem.Text("Dashboard")), 143 | elem.If(isAdmin, adminLink, guestLink), 144 | ) 145 | ``` 146 | 147 | In this example, if `isAdmin` is `true`, the `Admin Panel` link is rendered. Otherwise, the `Login` link is rendered. 148 | 149 | #### `None` in Conditional Rendering 150 | 151 | `elem` provides a specialized node `None` that implements the `Node` interface but does not produce any visible output. It's particularly useful in scenarios where rendering nothing for a specific condition is required. 152 | 153 | ```go 154 | showWelcomeMessage := false 155 | welcomeMessage := elem.Div(nil, elem.Text("Welcome to our website!")) 156 | 157 | content := elem.Div(nil, 158 | elem.If[elem.Node](showWelcomeMessage, welcomeMessage, elem.None()), 159 | ) 160 | ``` 161 | 162 | In this example, `welcomeMessage` is rendered only if `showWelcomeMessage` is `true`. If it's `false`, `None` is rendered instead, which produces no visible output. 163 | 164 | Additionally, `None` can be used to create an empty element, as in `elem.Div(nil, elem.None())`, which results in `
`. This can be handy for creating placeholders or structuring your HTML document without adding additional content. 165 | 166 | ### Supported Elements 167 | 168 | `elem` provides utility functions for creating HTML elements: 169 | 170 | - **Document Structure**: `Html`, `Head`, `Body`, `Title`, `Link`, `Meta`, `Style`, `Base` 171 | - **Text Content**: `H1`, `H2`, `H3`, `H4`, `H5`, `H6`, `P`, `Blockquote`, `Pre`, `Code`, `I`, `Br`, `Hr`, `Small`, `Q`, `Cite`, `Abbr`, `Data`, `Time`, `Var`, `Samp`, `Kbd` 172 | - **Sectioning & Semantic Layout**: `Article`, `Aside`, `FigCaption`, `Figure`, `Footer`, `Header`, `Hgroup`, `Main`, `Mark`, `Nav`, `Section` 173 | - **Form Elements**: `Form`, `Input`, `Textarea`, `Button`, `Select`, `Optgroup`, `Option`, `Label`, `Fieldset`, `Legend`, `Datalist`, `Meter`, `Output`, `Progress` 174 | - **Interactive Elements**: `Details`, `Dialog`, `Menu`, `Summary` 175 | - **Grouping Content**: `Div`, `Span`, `Li`, `Ul`, `Ol`, `Dl`, `Dt`, `Dd` 176 | - **Tables**: `Table`, `Tr`, `Td`, `Th`, `TBody`, `THead`, `TFoot` 177 | - **Hyperlinks and Multimedia**: `Img`, `Map`, `Area` 178 | - **Embedded Content**: `Audio`, `Iframe`, `Source`, `Video` 179 | - **Script-supporting Elements**: `Script`, `Noscript` 180 | - **Inline Semantic**: `A`, `Strong`, `Em`, `Code`, `I`, `B`, `U`, `Sub`, `Sup`, `Ruby`, `Rt`, `Rp` 181 | 182 | ### Raw HTML Insertion 183 | 184 | The `Raw` function allows for the direct inclusion of raw HTML content within your document structure. This function can be used to insert HTML strings, which will be rendered as part of the final HTML output. 185 | 186 | ```go 187 | rawHTML := `

Custom HTML content

` 188 | content := elem.Div(nil, 189 | elem.H1(nil, elem.Text("Welcome to Elem-Go")), 190 | elem.Raw(rawHTML), // Inserting the raw HTML 191 | elem.P(nil, elem.Text("More content here...")), 192 | ) 193 | 194 | htmlOutput := content.Render() 195 | // Output:

Welcome to Elem-Go

Custom HTML content

More content here...

196 | ``` 197 | > **NOTE**: If you are passing HTML from an untrusted source, make sure to sanitize it to prevent potential security risks such as Cross-Site Scripting (XSS) attacks. 198 | 199 | ### HTML Comments 200 | 201 | Apart from standard elements, `elem-go` also allows you to insert HTML comments using the `Comment` function: 202 | 203 | ```go 204 | comment := elem.Comment("Section: Main Content Start") 205 | // Generates: 206 | ``` 207 | 208 | ### Grouping Elements with Fragment 209 | 210 | 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. 211 | 212 | ```go 213 | nodes := []elem.Node{ 214 | elem.P(nil, elem.Text("1")), 215 | elem.P(nil, elem.Text("2")), 216 | } 217 | 218 | content := elem.Div(nil, 219 | elem.P(nil, elem.Text("0")), 220 | elem.Fragment(nodes...), 221 | elem.P(nil, elem.Text("3")), 222 | ) 223 | ``` 224 | 225 | 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. 226 | 227 | ### Handling JSON Strings and Special Characters in Attributes 228 | 229 | 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: 230 | 231 | ```go 232 | content := elem.Div(attrs.Props{ 233 | attrs.ID: "my-div", 234 | attrs.Class: "special 'class'", 235 | attrs.Data: `'{"key": "value"}'`, 236 | }, elem.Text("Content")) 237 | ``` 238 | 239 | ## Advanced CSS Styling with `StyleManager` 240 | 241 | 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. 242 | 243 | Explore the [`stylemanager` subpackage](stylemanager/README.md) to leverage advanced styling features in your web applications. 244 | 245 | ## HTMX Integration 246 | 247 | We provide a subpackage for htmx integration. [Read more about htmx integration here](htmx/README.md). 248 | 249 | ## Examples 250 | 251 | For hands-on examples showcasing the usage of `elem`, you can find sample implementations in the `examples/` folder of the repository. Dive into the examples to get a deeper understanding of how to leverage the library in various scenarios. 252 | 253 | [Check out the examples here.](./examples) 254 | 255 | ## Tutorials & Guides 256 | 257 | Dive deeper into the capabilities of `elem` and learn best practices through our collection of tutorials and guides: 258 | 259 | - [Building a Counter App with htmx, Go Fiber, and elem-go](https://dev.to/chasefleming/building-a-counter-app-with-htmx-go-fiber-and-elem-go-9jd/) 260 | - [Building a Go Static Site Generator Using elem-go](https://dev.to/chasefleming/building-a-go-static-site-generator-using-elem-go-3fhh) 261 | 262 | Stay tuned for more tutorials and guides in the future! 263 | 264 | ## Contributing 265 | 266 | Contributions are welcome! If you have ideas for improvements or new features, please open an issue or submit a pull request. 267 | 268 | ## License 269 | 270 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 271 | -------------------------------------------------------------------------------- /attrs/README.md: -------------------------------------------------------------------------------- 1 | # `attrs` Subpackage in `elem-go` 2 | 3 | The `attrs` subpackage within `elem-go` offers a comprehensive set of constants representing HTML attributes, enhancing the process of setting attributes for HTML elements in a type-safe manner. This document outlines the usage and features of the `attrs` subpackage. 4 | 5 | ## Table of Contents 6 | 7 | - [Introduction](#introduction) 8 | - [Usage](#usage) 9 | - [Available HTML Attributes](#available-html-attributes) 10 | - [Using `Props` Type](#using-props-type) 11 | - [Examples](#examples) 12 | - [Utilities](#utilities) 13 | - [`Merge`](#merge) 14 | - [`DataAttr`](#dataattr) 15 | 16 | ## Introduction 17 | 18 | The `attrs` subpackage is designed to simplify the process of defining HTML attributes in Go. By providing constants for common attributes, it helps avoid errors due to typos and enhances code readability. 19 | 20 | ## Usage 21 | 22 | To use the `attrs` subpackage, import it alongside the main `elem` package: 23 | 24 | ```go 25 | import ( 26 | "github.com/chasefleming/elem-go/attrs" 27 | ) 28 | ``` 29 | 30 | ## Available HTML Attributes 31 | 32 | The subpackage includes a wide range of constants representing universal attributes, link/script attributes, meta attributes, image/embed attributes, semantic text attributes, form/input attributes, interactive attributes, miscellaneous attributes, table attributes, iframe attributes, audio/video attributes, and video-specific attributes. 33 | 34 | For instance, some of the constants are: 35 | 36 | - Universal Attributes like `Class`, `ID`, `Style` 37 | - Link/Script Attributes such as `Href`, `Src` 38 | - Form/Input Attributes including `Type`, `Value`, `Placeholder` 39 | 40 | For a full list of available constants, see the [attrs.go file](attrs.go). 41 | 42 | ## Using `Props` Type 43 | 44 | The `Props` type is a map of strings that can be used to pass attribute values to HTML elements in a structured way. This type-safe approach ensures the correct assignment of attributes to elements. 45 | 46 | ## Examples 47 | 48 | Here's an example of using `attrs` constants to set attributes for a button: 49 | 50 | ```go 51 | buttonAttrs := attrs.Props{ 52 | attrs.Type: "button", 53 | attrs.Class: "btn btn-primary", 54 | attrs.ID: "submitBtn", 55 | } 56 | 57 | button := elem.Button(buttonAttrs, elem.Text("Submit")) 58 | ``` 59 | 60 | In this example, attributes for the button element are defined using the attrs.Props map with attrs constants. 61 | 62 | ## Utilities 63 | 64 | The `attrs` subpackage also includes utility functions to enhance the attribute manipulation process. 65 | 66 | ### `Merge` 67 | 68 | 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. 69 | 70 | #### Usage 71 | 72 | ```go 73 | defaultButtonAttrs := attrs.Props{ 74 | attrs.Class: "btn", 75 | attrs.Type: "button", 76 | } 77 | 78 | primaryButtonAttrs := attrs.Props{ 79 | attrs.Class: "btn btn-primary", // Overrides the Class attribute from defaultButtonAttrs 80 | attrs.ID: "submitBtn", 81 | } 82 | 83 | mergedButtonAttrs := attrs.Merge(defaultButtonAttrs, primaryButtonAttrs) 84 | 85 | button := elem.Button(mergedButtonAttrs, elem.Text("Submit")) 86 | ``` 87 | 88 | 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. 89 | 90 | ### `DataAttr` 91 | 92 | 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. 93 | 94 | #### Usage 95 | 96 | ```go 97 | dataAttrs := attrs.DataAttr("foobar") // Outputs "data-foobar" 98 | ``` 99 | 100 | In this example, the `DataAttr` function is used to define a `data-foobar` attribute key for an HTML element. 101 | 102 | 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. -------------------------------------------------------------------------------- /attrs/attrs.go: -------------------------------------------------------------------------------- 1 | package attrs 2 | 3 | const ( 4 | // Universal Attributes 5 | 6 | Alt = "alt" 7 | Class = "class" 8 | Contenteditable = "contenteditable" 9 | Dir = "dir" // Direction, e.g., "ltr" or "rtl" 10 | ID = "id" 11 | Lang = "lang" 12 | Style = "style" 13 | Tabindex = "tabindex" 14 | Title = "title" 15 | Loading = "loading" 16 | 17 | // Link/Script Attributes 18 | 19 | As = "as" 20 | Async = "async" 21 | // Deprecated: Use Crossorigin instead 22 | CrossOrigin = "crossorigin" 23 | Crossorigin = "crossorigin" 24 | Defer = "defer" 25 | Href = "href" 26 | Integrity = "integrity" 27 | Nomodule = "nomodule" 28 | Rel = "rel" 29 | Src = "src" 30 | Target = "target" 31 | 32 | // Meta Attributes 33 | 34 | Charset = "charset" 35 | Content = "content" 36 | HTTPequiv = "http-equiv" // e.g., for refresh or setting content type 37 | 38 | // Image/Embed Attributes 39 | 40 | Height = "height" 41 | Width = "width" 42 | // Deprecated: Use Ismap instead 43 | IsMap = "ismap" 44 | Ismap = "ismap" 45 | Usemap = "usemap" 46 | 47 | // Semantic Text Attributes 48 | 49 | Cite = "cite" 50 | // Deprecated: Use Datetime instead 51 | DateTime = "datetime" 52 | Datetime = "datetime" 53 | 54 | // Form/Input Attributes 55 | 56 | Accept = "accept" 57 | Action = "action" 58 | Autocapitalize = "autocapitalize" 59 | Autocomplete = "autocomplete" 60 | Autofocus = "autofocus" 61 | Cols = "cols" 62 | Checked = "checked" 63 | Disabled = "disabled" 64 | For = "for" 65 | Form = "form" 66 | Label = "label" 67 | List = "list" 68 | Low = "low" 69 | High = "high" 70 | Max = "max" 71 | // Deprecated: Use Maxlength instead 72 | MaxLength = "maxlength" 73 | Maxlength = "maxlength" 74 | Method = "method" // e.g., "GET", "POST" 75 | Min = "min" 76 | Minlength = "minlength" 77 | Multiple = "multiple" 78 | Name = "name" 79 | // Deprecated: Use Novalidate instead 80 | NoValidate = "novalidate" 81 | Novalidate = "novalidate" 82 | Optimum = "optimum" 83 | Placeholder = "placeholder" 84 | Readonly = "readonly" 85 | Required = "required" 86 | Rows = "rows" 87 | Selected = "selected" 88 | Size = "size" 89 | Step = "step" 90 | Type = "type" 91 | Value = "value" 92 | 93 | // Interactive Attributes 94 | 95 | Open = "open" 96 | 97 | // Area-Specific Attributes 98 | Shape = "shape" 99 | Coords = "coords" 100 | 101 | // Miscellaneous Attributes 102 | 103 | DataPrefix = "data-" // Used for custom data attributes e.g., "data-custom" 104 | Download = "download" 105 | Draggable = "draggable" 106 | Role = "role" // Used for ARIA roles 107 | Spellcheck = "spellcheck" 108 | 109 | // Table Attributes 110 | 111 | RowSpan = "rowspan" 112 | ColSpan = "colspan" 113 | Scope = "scope" 114 | Headers = "headers" 115 | 116 | // IFrame Attributes 117 | 118 | Allow = "allow" 119 | // Deprecated: Use AllowFullscreen instead 120 | AllowFullScreen = "allowfullscreen" 121 | AllowFullscreen = "allowfullscreen" 122 | CSP = "csp" 123 | // Deprecated: Use Referrerpolicy instead 124 | ReferrerPolicy = "referrerpolicy" 125 | Referrerpolicy = "referrerpolicy" 126 | Sandbox = "sandbox" 127 | // Deprecated: Use Srcdoc instead 128 | SrcDoc = "srcdoc" 129 | Srcdoc = "srcdoc" 130 | 131 | // Audio/Video Attributes 132 | 133 | Controls = "controls" 134 | Loop = "loop" 135 | Muted = "muted" 136 | Preload = "preload" 137 | Autoplay = "autoplay" 138 | 139 | // Video-Specific Attributes 140 | 141 | Poster = "poster" 142 | Playsinline = "playsinline" 143 | 144 | // Source Element-Specific Attributes 145 | 146 | Media = "media" 147 | Sizes = "sizes" 148 | 149 | // ARIA Attributes 150 | 151 | AriaActivedescendant = "aria-activedescendant" 152 | AriaAtomic = "aria-atomic" 153 | AriaAutocomplete = "aria-autocomplete" 154 | AriaBusy = "aria-busy" 155 | AriaChecked = "aria-checked" 156 | AriaControls = "aria-controls" 157 | AriaDescribedby = "aria-describedby" 158 | AriaDisabled = "aria-disabled" 159 | AriaExpanded = "aria-expanded" 160 | AriaFlowto = "aria-flowto" 161 | AriaHaspopup = "aria-haspopup" 162 | AriaHidden = "aria-hidden" 163 | AriaInvalid = "aria-invalid" 164 | AriaLabel = "aria-label" 165 | AriaLabelledby = "aria-labelledby" 166 | AriaLevel = "aria-level" 167 | AriaLive = "aria-live" 168 | AriaModal = "aria-modal" 169 | AriaMultiline = "aria-multiline" 170 | AriaMultiselectable = "aria-multiselectable" 171 | AriaOrientation = "aria-orientation" 172 | AriaOwns = "aria-owns" 173 | AriaPlaceholder = "aria-placeholder" 174 | AriaPressed = "aria-pressed" 175 | AriaReadonly = "aria-readonly" 176 | AriaRequired = "aria-required" 177 | AriaRoledescription = "aria-roledescription" 178 | AriaSelected = "aria-selected" 179 | AriaSort = "aria-sort" 180 | AriaValuemax = "aria-valuemax" 181 | AriaValuemin = "aria-valuemin" 182 | AriaValuenow = "aria-valuenow" 183 | AriaValuetext = "aria-valuetext" 184 | ) 185 | 186 | type Props map[string]string 187 | -------------------------------------------------------------------------------- /attrs/utils.go: -------------------------------------------------------------------------------- 1 | package attrs 2 | 3 | import "strings" 4 | 5 | // DataAttr returns the name for a data attribute. 6 | func DataAttr(name string) string { 7 | var builder strings.Builder 8 | builder.WriteString(DataPrefix) 9 | builder.WriteString(name) 10 | return builder.String() 11 | } 12 | 13 | // Merge merges multiple attribute maps into a single map, with later maps overriding earlier ones. 14 | func Merge(attrMaps ...Props) Props { 15 | mergedAttrs := Props{} 16 | for _, attrMap := range attrMaps { 17 | for key, value := range attrMap { 18 | mergedAttrs[key] = value 19 | } 20 | } 21 | return mergedAttrs 22 | } 23 | -------------------------------------------------------------------------------- /attrs/utils_test.go: -------------------------------------------------------------------------------- 1 | package attrs 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestDataAttr(t *testing.T) { 10 | actual := DataAttr("foobar") 11 | expected := "data-foobar" 12 | assert.Equal(t, expected, actual) 13 | } 14 | 15 | func TestMerge(t *testing.T) { 16 | baseStyle := Props{ 17 | "Width": "100px", 18 | "Color": "blue", 19 | } 20 | 21 | additionalStyle := Props{ 22 | "Color": "red", // This should override the blue color in baseStyle 23 | "BackgroundColor": "yellow", 24 | } 25 | 26 | expectedMergedStyle := Props{ 27 | "Width": "100px", 28 | "Color": "red", 29 | "BackgroundColor": "yellow", 30 | } 31 | 32 | mergedStyle := Merge(baseStyle, additionalStyle) 33 | 34 | assert.Equal(t, expectedMergedStyle, mergedStyle) 35 | } 36 | -------------------------------------------------------------------------------- /elem.go: -------------------------------------------------------------------------------- 1 | package elem 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/chasefleming/elem-go/attrs" 9 | ) 10 | 11 | // List of HTML5 void elements. Void elements, also known as self-closing or empty elements, 12 | // are elements that don't have a closing tag because they can't contain any content. 13 | // For example, the tag cannot wrap text or other tags, it stands alone, so it doesn't have a closing tag. 14 | var voidElements = map[string]struct{}{ 15 | "area": {}, 16 | "base": {}, 17 | "br": {}, 18 | "col": {}, 19 | "command": {}, 20 | "embed": {}, 21 | "hr": {}, 22 | "img": {}, 23 | "input": {}, 24 | "keygen": {}, 25 | "link": {}, 26 | "meta": {}, 27 | "param": {}, 28 | "source": {}, 29 | "track": {}, 30 | "wbr": {}, 31 | } 32 | 33 | // List of boolean attributes. Boolean attributes can't have literal values. The presence of an boolean 34 | // attribute represents the "true" value. To represent the "false" value, the attribute has to be omitted. 35 | // See https://html.spec.whatwg.org/multipage/indices.html#attributes-3 for reference 36 | var booleanAttrs = map[string]struct{}{ 37 | attrs.AllowFullscreen: {}, 38 | attrs.Async: {}, 39 | attrs.Autofocus: {}, 40 | attrs.Autoplay: {}, 41 | attrs.Checked: {}, 42 | attrs.Controls: {}, 43 | attrs.Defer: {}, 44 | attrs.Disabled: {}, 45 | attrs.Ismap: {}, 46 | attrs.Loop: {}, 47 | attrs.Multiple: {}, 48 | attrs.Muted: {}, 49 | attrs.Nomodule: {}, 50 | attrs.Novalidate: {}, 51 | attrs.Open: {}, 52 | attrs.Playsinline: {}, 53 | attrs.Readonly: {}, 54 | attrs.Required: {}, 55 | attrs.Selected: {}, 56 | } 57 | 58 | type CSSGenerator interface { 59 | GenerateCSS() string // TODO: Change to CSS() 60 | } 61 | 62 | type RenderOptions struct { 63 | // DisableHtmlPreamble disables the doctype preamble for the HTML tag if it exists in the rendering tree 64 | DisableHtmlPreamble bool 65 | StyleManager CSSGenerator 66 | } 67 | 68 | type Node interface { 69 | RenderTo(builder *strings.Builder, opts RenderOptions) 70 | Render() string 71 | RenderWithOptions(opts RenderOptions) string 72 | } 73 | 74 | // NoneNode represents a node that renders nothing. 75 | type NoneNode struct{} 76 | 77 | // RenderTo for NoneNode does nothing. 78 | func (n NoneNode) RenderTo(builder *strings.Builder, opts RenderOptions) { 79 | // Intentionally left blank to render nothing 80 | } 81 | 82 | // Render for NoneNode returns an empty string. 83 | func (n NoneNode) Render() string { 84 | return "" 85 | } 86 | 87 | // RenderWithOptions for NoneNode returns an empty string. 88 | func (n NoneNode) RenderWithOptions(opts RenderOptions) string { 89 | return "" 90 | } 91 | 92 | type TextNode string 93 | 94 | func (t TextNode) RenderTo(builder *strings.Builder, opts RenderOptions) { 95 | builder.WriteString(string(t)) 96 | } 97 | 98 | func (t TextNode) Render() string { 99 | return string(t) 100 | } 101 | 102 | func (t TextNode) RenderWithOptions(opts RenderOptions) string { 103 | return string(t) 104 | } 105 | 106 | type RawNode string 107 | 108 | func (r RawNode) RenderTo(builder *strings.Builder, opts RenderOptions) { 109 | builder.WriteString(string(r)) 110 | } 111 | 112 | func (r RawNode) Render() string { 113 | return string(r) 114 | } 115 | 116 | func (t RawNode) RenderWithOptions(opts RenderOptions) string { 117 | return string(t) 118 | } 119 | 120 | type CommentNode string 121 | 122 | func (c CommentNode) RenderTo(builder *strings.Builder, opts RenderOptions) { 123 | builder.WriteString("") 126 | } 127 | 128 | func (c CommentNode) Render() string { 129 | return c.RenderWithOptions(RenderOptions{}) 130 | } 131 | 132 | func (c CommentNode) RenderWithOptions(opts RenderOptions) string { 133 | var builder strings.Builder 134 | c.RenderTo(&builder, opts) 135 | return builder.String() 136 | } 137 | 138 | type Element struct { 139 | Tag string 140 | Attrs attrs.Props 141 | Children []Node 142 | } 143 | 144 | func (e *Element) RenderTo(builder *strings.Builder, opts RenderOptions) { 145 | // The HTML tag needs a doctype preamble in order to ensure 146 | // browsers don't render in legacy/quirks mode 147 | // https://developer.mozilla.org/en-US/docs/Glossary/Doctype 148 | if !opts.DisableHtmlPreamble && e.Tag == "html" { 149 | builder.WriteString("") 150 | } 151 | 152 | isFragment := e.Tag == "fragment" 153 | 154 | // Start with opening tag 155 | if !isFragment { 156 | builder.WriteString("<") 157 | builder.WriteString(e.Tag) 158 | } 159 | 160 | // Sort the keys for consistent order 161 | keys := make([]string, 0, len(e.Attrs)) 162 | for k := range e.Attrs { 163 | keys = append(keys, k) 164 | } 165 | sort.Strings(keys) 166 | 167 | // Append the attributes to the builder 168 | for _, k := range keys { 169 | e.renderAttrTo(k, builder) 170 | } 171 | 172 | // If it's a void element, close it and return 173 | if _, exists := voidElements[e.Tag]; exists { 174 | builder.WriteString(`>`) 175 | return 176 | } 177 | 178 | if !isFragment { 179 | // Close opening tag 180 | builder.WriteString(`>`) 181 | } 182 | 183 | // Build the content 184 | for _, child := range e.Children { 185 | child.RenderTo(builder, opts) 186 | } 187 | 188 | if !isFragment { 189 | // Append closing tag 190 | builder.WriteString(``) 193 | } 194 | } 195 | 196 | // return string representation of given attribute with its value 197 | func (e *Element) renderAttrTo(attrName string, builder *strings.Builder) { 198 | if _, exists := booleanAttrs[attrName]; exists { 199 | // boolean attribute presents its name only if the value is "true" 200 | if e.Attrs[attrName] == "true" { 201 | builder.WriteString(` `) 202 | builder.WriteString(attrName) 203 | } 204 | } else { 205 | // regular attribute has a name and a value 206 | attrVal := e.Attrs[attrName] 207 | 208 | // A necessary check to to avoid adding extra quotes around values that are already single-quoted 209 | // An example is '{"quantity": 5}' 210 | isSingleQuoted := strings.HasPrefix(attrVal, "'") && strings.HasSuffix(attrVal, "'") 211 | 212 | builder.WriteString(` `) 213 | builder.WriteString(attrName) 214 | builder.WriteString(`=`) 215 | if !isSingleQuoted { 216 | builder.WriteString(`"`) 217 | } 218 | builder.WriteString(attrVal) 219 | if !isSingleQuoted { 220 | builder.WriteString(`"`) 221 | } 222 | } 223 | } 224 | 225 | func (e *Element) Render() string { 226 | return e.RenderWithOptions(RenderOptions{}) 227 | } 228 | 229 | func (e *Element) RenderWithOptions(opts RenderOptions) string { 230 | var builder strings.Builder 231 | e.RenderTo(&builder, opts) 232 | 233 | if opts.StyleManager != nil { 234 | htmlContent := builder.String() 235 | cssContent := opts.StyleManager.GenerateCSS() 236 | 237 | // Define the ", cssContent) 239 | 240 | // Check if a tag exists in the HTML content 241 | headStartIndex := strings.Index(htmlContent, "") 242 | headEndIndex := strings.Index(htmlContent, "") 243 | 244 | if headStartIndex != -1 && headEndIndex != -1 { 245 | // If exists, inject the style content just before 246 | beforeHead := htmlContent[:headEndIndex] 247 | afterHead := htmlContent[headEndIndex:] 248 | modifiedHTML := beforeHead + styleElement + afterHead 249 | return modifiedHTML 250 | } else { 251 | // If does not exist, create it and inject the style content 252 | // Assuming tag exists and injecting immediately after 253 | htmlTagEnd := strings.Index(htmlContent, ">") + 1 254 | if htmlTagEnd > 0 { 255 | beforeHTML := htmlContent[:htmlTagEnd] 256 | afterHTML := htmlContent[htmlTagEnd:] 257 | modifiedHTML := beforeHTML + "" + styleElement + "" + afterHTML 258 | return modifiedHTML 259 | } 260 | } 261 | } 262 | 263 | // Return the original HTML content if no modifications were made 264 | return builder.String() 265 | } 266 | 267 | func newElement(tag string, attrs attrs.Props, children ...Node) *Element { 268 | return &Element{ 269 | Tag: tag, 270 | Attrs: attrs, 271 | Children: children, 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /elem_test.go: -------------------------------------------------------------------------------- 1 | package elem 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | // MockStyleManager simulates the StyleManager for testing purposes. 9 | type MockStyleManager struct{} 10 | 11 | // GenerateCSS returns a fixed CSS string for testing. 12 | func (m *MockStyleManager) GenerateCSS() string { 13 | return "body { background-color: #fff; }" 14 | } 15 | 16 | func TestRenderWithOptionsInjectsCSSIntoHead(t *testing.T) { 17 | // Setup a simple element that represents an HTML document structure 18 | e := &Element{ 19 | Tag: "html", 20 | Children: []Node{ 21 | &Element{Tag: "head"}, 22 | &Element{Tag: "body"}, 23 | }, 24 | } 25 | 26 | // Use the MockStyleManager 27 | mockStyleManager := &MockStyleManager{} 28 | 29 | // Assuming RenderOptions expects a StyleManager interface, pass the mock 30 | opts := RenderOptions{ 31 | StyleManager: mockStyleManager, // This should be adjusted to how your options are structured 32 | } 33 | htmlOutput := e.RenderWithOptions(opts) 34 | 35 | // Construct the expected HTML string with the CSS injected 36 | expectedHTML := "" 37 | 38 | // Use testify's assert.Equal to check if the HTML output matches the expected HTML 39 | assert.Equal(t, expectedHTML, htmlOutput, "The generated HTML should include the CSS in the section") 40 | } 41 | -------------------------------------------------------------------------------- /elements.go: -------------------------------------------------------------------------------- 1 | package elem 2 | 3 | import ( 4 | "github.com/chasefleming/elem-go/attrs" 5 | ) 6 | 7 | // ========== Document Structure ========== 8 | 9 | // Body creates a element. 10 | func Body(attrs attrs.Props, children ...Node) *Element { 11 | return newElement("body", attrs, children...) 12 | } 13 | 14 | // Head creates a element. 15 | func Head(attrs attrs.Props, children ...Node) *Element { 16 | return newElement("head", attrs, children...) 17 | } 18 | 19 | // Html creates an element. 20 | func Html(attrs attrs.Props, children ...Node) *Element { 21 | return newElement("html", attrs, children...) 22 | } 23 | 24 | // Title creates a element. 25 | func Title(attrs attrs.Props, children ...Node) *Element { 26 | return newElement("title", attrs, children...) 27 | } 28 | 29 | // ========== Text Formatting and Structure ========== 30 | 31 | // A creates an <a> element. 32 | func A(attrs attrs.Props, children ...Node) *Element { 33 | return newElement("a", attrs, children...) 34 | } 35 | 36 | // Br creates a <br> element. 37 | func Br(attrs attrs.Props) *Element { 38 | return newElement("br", attrs) 39 | } 40 | 41 | // Blockquote creates a <blockquote> element. 42 | func Blockquote(attrs attrs.Props, children ...Node) *Element { 43 | return newElement("blockquote", attrs, children...) 44 | } 45 | 46 | // Code creates a <code> element. 47 | func Code(attrs attrs.Props, children ...Node) *Element { 48 | return newElement("code", attrs, children...) 49 | } 50 | 51 | // Div creates a <div> element. 52 | func Div(attrs attrs.Props, children ...Node) *Element { 53 | return newElement("div", attrs, children...) 54 | } 55 | 56 | // Em creates an <em> element. 57 | func Em(attrs attrs.Props, children ...Node) *Element { 58 | return newElement("em", attrs, children...) 59 | } 60 | 61 | // H1 creates an <h1> element. 62 | func H1(attrs attrs.Props, children ...Node) *Element { 63 | return newElement("h1", attrs, children...) 64 | } 65 | 66 | // H2 creates an <h2> element. 67 | func H2(attrs attrs.Props, children ...Node) *Element { 68 | return newElement("h2", attrs, children...) 69 | } 70 | 71 | // H3 creates an <h3> element. 72 | func H3(attrs attrs.Props, children ...Node) *Element { 73 | return newElement("h3", attrs, children...) 74 | } 75 | 76 | // H4 creates an <h4> element. 77 | func H4(attrs attrs.Props, children ...Node) *Element { 78 | return newElement("h4", attrs, children...) 79 | } 80 | 81 | // H5 creates an <h5> element. 82 | func H5(attrs attrs.Props, children ...Node) *Element { 83 | return newElement("h5", attrs, children...) 84 | } 85 | 86 | // H6 creates an <h6> element. 87 | func H6(attrs attrs.Props, children ...Node) *Element { 88 | return newElement("h6", attrs, children...) 89 | } 90 | 91 | // Hgroup creates an <hgroup> element. 92 | func Hgroup(attrs attrs.Props, children ...Node) *Element { 93 | return newElement("hgroup", attrs, children...) 94 | } 95 | 96 | // Hr creates an <hr> element. 97 | func Hr(attrs attrs.Props) *Element { 98 | return newElement("hr", attrs) 99 | } 100 | 101 | // I creates an <i> element. 102 | func I(attrs attrs.Props, children ...Node) *Element { 103 | return newElement("i", attrs, children...) 104 | } 105 | 106 | // P creates a <p> element. 107 | func P(attrs attrs.Props, children ...Node) *Element { 108 | return newElement("p", attrs, children...) 109 | } 110 | 111 | // Pre creates a <pre> element. 112 | func Pre(attrs attrs.Props, children ...Node) *Element { 113 | return newElement("pre", attrs, children...) 114 | } 115 | 116 | // Span creates a <span> element. 117 | func Span(attrs attrs.Props, children ...Node) *Element { 118 | return newElement("span", attrs, children...) 119 | } 120 | 121 | // Strong creates a <strong> element. 122 | func Strong(attrs attrs.Props, children ...Node) *Element { 123 | return newElement("strong", attrs, children...) 124 | } 125 | 126 | // Sub creates a <sub> element. 127 | func Sub(attrs attrs.Props, children ...Node) *Element { 128 | return newElement("sub", attrs, children...) 129 | } 130 | 131 | // Sup creates a <sub> element. 132 | func Sup(attrs attrs.Props, children ...Node) *Element { 133 | return newElement("sup", attrs, children...) 134 | } 135 | 136 | // B creates a <b> element. 137 | func B(attrs attrs.Props, children ...Node) *Element { 138 | return newElement("b", attrs, children...) 139 | } 140 | 141 | // U creates a <u> element. 142 | func U(attrs attrs.Props, children ...Node) *Element { 143 | return newElement("u", attrs, children...) 144 | } 145 | 146 | // Text creates a TextNode. 147 | func Text(content string) TextNode { 148 | return TextNode(content) 149 | } 150 | 151 | // Comment creates a CommentNode. 152 | func Comment(comment string) CommentNode { 153 | return CommentNode(comment) 154 | } 155 | 156 | // ========== Lists ========== 157 | 158 | // Li creates an <li> element. 159 | func Li(attrs attrs.Props, children ...Node) *Element { 160 | return newElement("li", attrs, children...) 161 | } 162 | 163 | // Ul creates a <ul> element. 164 | func Ul(attrs attrs.Props, children ...Node) *Element { 165 | return newElement("ul", attrs, children...) 166 | } 167 | 168 | // Ol creates an <ol> element. 169 | func Ol(attrs attrs.Props, children ...Node) *Element { 170 | return newElement("ol", attrs, children...) 171 | } 172 | 173 | // Dl creates a <dl> element. 174 | func Dl(attrs attrs.Props, children ...Node) *Element { 175 | return newElement("dl", attrs, children...) 176 | } 177 | 178 | // Dt creates a <dt> element. 179 | func Dt(attrs attrs.Props, children ...Node) *Element { 180 | return newElement("dt", attrs, children...) 181 | } 182 | 183 | // Dd creates a <dd> element. 184 | func Dd(attrs attrs.Props, children ...Node) *Element { 185 | return newElement("dd", attrs, children...) 186 | } 187 | 188 | // ========== Forms ========== 189 | 190 | // Button creates a <button> element. 191 | func Button(attrs attrs.Props, children ...Node) *Element { 192 | return newElement("button", attrs, children...) 193 | } 194 | 195 | // Form creates a <form> element. 196 | func Form(attrs attrs.Props, children ...Node) *Element { 197 | return newElement("form", attrs, children...) 198 | } 199 | 200 | // Input creates an <input> element. 201 | func Input(attrs attrs.Props) *Element { 202 | return newElement("input", attrs) 203 | } 204 | 205 | // Label creates a <label> element. 206 | func Label(attrs attrs.Props, children ...Node) *Element { 207 | return newElement("label", attrs, children...) 208 | } 209 | 210 | // Optgroup creates an <optgroup> element to group <option>s within a <select> element. 211 | func Optgroup(attrs attrs.Props, children ...Node) *Element { 212 | return newElement("optgroup", attrs, children...) 213 | } 214 | 215 | // Option creates an <option> element. 216 | func Option(attrs attrs.Props, content TextNode) *Element { 217 | return newElement("option", attrs, content) 218 | } 219 | 220 | // Select creates a <select> element. 221 | func Select(attrs attrs.Props, children ...Node) *Element { 222 | return newElement("select", attrs, children...) 223 | } 224 | 225 | // Textarea creates a <textarea> element. 226 | func Textarea(attrs attrs.Props, content TextNode) *Element { 227 | return newElement("textarea", attrs, content) 228 | } 229 | 230 | // ========== Hyperlinks and Multimedia ========== 231 | 232 | // Img creates an <img> element. 233 | func Img(attrs attrs.Props) *Element { 234 | return newElement("img", attrs) 235 | } 236 | 237 | // ========== Head Elements ========== 238 | 239 | // Base creates a <base> element. 240 | func Base(attrs attrs.Props) *Element { 241 | return newElement("base", attrs) 242 | } 243 | 244 | // Link creates a <link> element. 245 | func Link(attrs attrs.Props) *Element { 246 | return newElement("link", attrs) 247 | } 248 | 249 | // Meta creates a <meta> element. 250 | func Meta(attrs attrs.Props) *Element { 251 | return newElement("meta", attrs) 252 | } 253 | 254 | // Script creates a <script> element. 255 | func Script(attrs attrs.Props, children ...Node) *Element { 256 | return newElement("script", attrs, children...) 257 | } 258 | 259 | // Style creates a <style> element. 260 | func Style(attrs attrs.Props, children ...Node) *Element { 261 | return newElement("style", attrs, children...) 262 | } 263 | 264 | // ========== Semantic Elements ========== 265 | 266 | // --- Semantic Sectioning Elements --- 267 | 268 | // Article creates an <article> element. 269 | func Article(attrs attrs.Props, children ...Node) *Element { 270 | return newElement("article", attrs, children...) 271 | } 272 | 273 | // Aside creates an <aside> element. 274 | func Aside(attrs attrs.Props, children ...Node) *Element { 275 | return newElement("aside", attrs, children...) 276 | } 277 | 278 | // Footer creates a <footer> element. 279 | func Footer(attrs attrs.Props, children ...Node) *Element { 280 | return newElement("footer", attrs, children...) 281 | } 282 | 283 | // Header creates a <header> element. 284 | func Header(attrs attrs.Props, children ...Node) *Element { 285 | return newElement("header", attrs, children...) 286 | } 287 | 288 | // Main creates a <main> element. 289 | func Main(attrs attrs.Props, children ...Node) *Element { 290 | return newElement("main", attrs, children...) 291 | } 292 | 293 | // Nav creates a <nav> element. 294 | func Nav(attrs attrs.Props, children ...Node) *Element { 295 | return newElement("nav", attrs, children...) 296 | } 297 | 298 | // Section creates a <section> element. 299 | func Section(attrs attrs.Props, children ...Node) *Element { 300 | return newElement("section", attrs, children...) 301 | } 302 | 303 | // Details creates a <details> element. 304 | func Details(attrs attrs.Props, children ...Node) *Element { 305 | return newElement("details", attrs, children...) 306 | } 307 | 308 | // Summary creates a <summary> element. 309 | func Summary(attrs attrs.Props, children ...Node) *Element { 310 | return newElement("summary", attrs, children...) 311 | } 312 | 313 | // ========== Semantic Form Elements ========== 314 | 315 | // Fieldset creates a <fieldset> element. 316 | func Fieldset(attrs attrs.Props, children ...Node) *Element { 317 | return newElement("fieldset", attrs, children...) 318 | } 319 | 320 | // Legend creates a <legend> element. 321 | func Legend(attrs attrs.Props, children ...Node) *Element { 322 | return newElement("legend", attrs, children...) 323 | } 324 | 325 | // Datalist creates a <datalist> element. 326 | func Datalist(attrs attrs.Props, children ...Node) *Element { 327 | return newElement("datalist", attrs, children...) 328 | } 329 | 330 | // Meter creates a <meter> element. 331 | func Meter(attrs attrs.Props, children ...Node) *Element { 332 | return newElement("meter", attrs, children...) 333 | } 334 | 335 | // Output creates an <output> element. 336 | func Output(attrs attrs.Props, children ...Node) *Element { 337 | return newElement("output", attrs, children...) 338 | } 339 | 340 | // Progress creates a <progress> element. 341 | func Progress(attrs attrs.Props, children ...Node) *Element { 342 | return newElement("progress", attrs, children...) 343 | } 344 | 345 | // --- Semantic Interactive Elements --- 346 | 347 | // Dialog creates a <dialog> element. 348 | func Dialog(attrs attrs.Props, children ...Node) *Element { 349 | return newElement("dialog", attrs, children...) 350 | } 351 | 352 | // Menu creates a <menu> element. 353 | func Menu(attrs attrs.Props, children ...Node) *Element { 354 | return newElement("menu", attrs, children...) 355 | } 356 | 357 | // --- Semantic Script Supporting Elements --- 358 | 359 | // NoScript creates a <noscript> element. 360 | func NoScript(attrs attrs.Props, children ...Node) *Element { 361 | return newElement("noscript", attrs, children...) 362 | } 363 | 364 | // --- Semantic Text Content Elements --- 365 | 366 | // Abbr creates an <abbr> element. 367 | func Abbr(attrs attrs.Props, children ...Node) *Element { 368 | return newElement("abbr", attrs, children...) 369 | } 370 | 371 | // Address creates an <address> element. 372 | func Address(attrs attrs.Props, children ...Node) *Element { 373 | return newElement("address", attrs, children...) 374 | } 375 | 376 | // Cite creates a <cite> element. 377 | func Cite(attrs attrs.Props, children ...Node) *Element { 378 | return newElement("cite", attrs, children...) 379 | } 380 | 381 | // Data creates a <data> element. 382 | func Data(attrs attrs.Props, children ...Node) *Element { 383 | return newElement("data", attrs, children...) 384 | } 385 | 386 | // FigCaption creates a <figcaption> element. 387 | func FigCaption(attrs attrs.Props, children ...Node) *Element { 388 | return newElement("figcaption", attrs, children...) 389 | } 390 | 391 | // Figure creates a <figure> element. 392 | func Figure(attrs attrs.Props, children ...Node) *Element { 393 | return newElement("figure", attrs, children...) 394 | } 395 | 396 | // Kbd creates a <kbd> element. 397 | func Kbd(attrs attrs.Props, children ...Node) *Element { 398 | return newElement("kbd", attrs, children...) 399 | } 400 | 401 | // Mark creates a <mark> element. 402 | func Mark(attrs attrs.Props, children ...Node) *Element { 403 | return newElement("mark", attrs, children...) 404 | } 405 | 406 | // Q creates a <q> element. 407 | func Q(attrs attrs.Props, children ...Node) *Element { 408 | return newElement("q", attrs, children...) 409 | } 410 | 411 | // Samp creates a <samp> element. 412 | func Samp(attrs attrs.Props, children ...Node) *Element { 413 | return newElement("samp", attrs, children...) 414 | } 415 | 416 | // Small creates a <small> element. 417 | func Small(attrs attrs.Props, children ...Node) *Element { 418 | return newElement("small", attrs, children...) 419 | } 420 | 421 | // Time creates a <time> element. 422 | func Time(attrs attrs.Props, children ...Node) *Element { 423 | return newElement("time", attrs, children...) 424 | } 425 | 426 | // Var creates a <var> element. 427 | func Var(attrs attrs.Props, children ...Node) *Element { 428 | return newElement("var", attrs, children...) 429 | } 430 | 431 | // Ruby creates a <ruby> element. 432 | func Ruby(attrs attrs.Props, children ...Node) *Element { 433 | return newElement("ruby", attrs, children...) 434 | } 435 | 436 | // Rt creates a <rt> element. 437 | func Rt(attrs attrs.Props, children ...Node) *Element { 438 | return newElement("rt", attrs, children...) 439 | } 440 | 441 | // Rp creates a <rp> element. 442 | func Rp(attrs attrs.Props, children ...Node) *Element { 443 | return newElement("rp", attrs, children...) 444 | } 445 | 446 | // ========== Tables ========== 447 | 448 | // Table creates a <table> element. 449 | func Table(attrs attrs.Props, children ...Node) *Element { 450 | return newElement("table", attrs, children...) 451 | } 452 | 453 | // THead creates a <thead> element. 454 | func THead(attrs attrs.Props, children ...Node) *Element { 455 | return newElement("thead", attrs, children...) 456 | } 457 | 458 | // TBody creates a <tbody> element. 459 | func TBody(attrs attrs.Props, children ...Node) *Element { 460 | return newElement("tbody", attrs, children...) 461 | } 462 | 463 | // TFoot creates a <tfoot> element. 464 | func TFoot(attrs attrs.Props, children ...Node) *Element { 465 | return newElement("tfoot", attrs, children...) 466 | } 467 | 468 | // Tr creates a <tr> element. 469 | func Tr(attrs attrs.Props, children ...Node) *Element { 470 | return newElement("tr", attrs, children...) 471 | } 472 | 473 | // Th creates a <th> element. 474 | func Th(attrs attrs.Props, children ...Node) *Element { 475 | return newElement("th", attrs, children...) 476 | } 477 | 478 | // Td creates a <td> element. 479 | func Td(attrs attrs.Props, children ...Node) *Element { 480 | return newElement("td", attrs, children...) 481 | } 482 | 483 | // ========== Embedded Content ========== 484 | 485 | // IFrames creates an <iframe> element. 486 | func IFrame(attrs attrs.Props, children ...Node) *Element { 487 | return newElement("iframe", attrs, children...) 488 | } 489 | 490 | // Audio creates an <audio> element. 491 | func Audio(attrs attrs.Props, children ...Node) *Element { 492 | return newElement("audio", attrs, children...) 493 | } 494 | 495 | // Video creates a <video> element. 496 | func Video(attrs attrs.Props, children ...Node) *Element { 497 | return newElement("video", attrs, children...) 498 | } 499 | 500 | // Source creates a <source> element. 501 | func Source(attrs attrs.Props, children ...Node) *Element { 502 | return newElement("source", attrs, children...) 503 | } 504 | 505 | // ========== Image Map Elements ========== 506 | 507 | // Map creates a <map> element. 508 | func Map(attrs attrs.Props, children ...Node) *Element { 509 | return newElement("map", attrs, children...) 510 | } 511 | 512 | // Area creates an <area> element. 513 | func Area(attrs attrs.Props) *Element { 514 | return newElement("area", attrs) 515 | } 516 | 517 | // ========== Other ========== 518 | 519 | // None creates a NoneNode, representing a no-operation in rendering. 520 | func None() NoneNode { 521 | return NoneNode{} 522 | } 523 | 524 | // Raw takes html content and returns a RawNode. 525 | func Raw(html string) RawNode { 526 | return RawNode(html) 527 | } 528 | 529 | // CSS takes css content and returns a TextNode. 530 | func CSS(content string) TextNode { 531 | return TextNode(content) 532 | } 533 | 534 | // Fragments are a way to group multiple elements together without adding an extra node to the DOM. 535 | func Fragment(children ...Node) *Element { 536 | return newElement("fragment", attrs.Props{}, children...) 537 | } 538 | -------------------------------------------------------------------------------- /elements_test.go: -------------------------------------------------------------------------------- 1 | package elem 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/chasefleming/elem-go/attrs" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | // ========== Document Structure ========== 11 | 12 | func TestBody(t *testing.T) { 13 | expected := `<body class="page-body"><p>Welcome to Elem!</p></body>` 14 | el := Body(attrs.Props{attrs.Class: "page-body"}, P(nil, Text("Welcome to Elem!"))) 15 | assert.Equal(t, expected, el.Render()) 16 | } 17 | 18 | func TestHtml(t *testing.T) { 19 | expected := `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Elem Page

Welcome to Elem!

` 20 | el := Html(attrs.Props{attrs.Lang: "en"}, 21 | Head(nil, 22 | Meta(attrs.Props{attrs.Charset: "UTF-8"}), 23 | Title(nil, Text("Elem Page")), 24 | ), 25 | Body(nil, P(nil, Text("Welcome to Elem!"))), 26 | ) 27 | assert.Equal(t, expected, el.Render()) 28 | } 29 | 30 | func TestHtmlWithOptions(t *testing.T) { 31 | expected := `Elem Page

Welcome to Elem!

` 32 | el := Html(attrs.Props{attrs.Lang: "en"}, 33 | Head(nil, 34 | Meta(attrs.Props{attrs.Charset: "UTF-8"}), 35 | Title(nil, Text("Elem Page")), 36 | ), 37 | Body(nil, P(nil, Text("Welcome to Elem!"))), 38 | ) 39 | assert.Equal(t, expected, el.RenderWithOptions(RenderOptions{DisableHtmlPreamble: true})) 40 | } 41 | 42 | // ========== Text Formatting and Structure ========== 43 | 44 | func TestA(t *testing.T) { 45 | expected := `Visit Example` 46 | el := A(attrs.Props{attrs.Href: "https://example.com"}, Text("Visit Example")) 47 | assert.Equal(t, expected, el.Render()) 48 | } 49 | 50 | func TestBlockquote(t *testing.T) { 51 | expected := `
Quote text
` 52 | el := Blockquote(nil, Text("Quote text")) 53 | assert.Equal(t, expected, el.Render()) 54 | } 55 | 56 | func TestBr(t *testing.T) { 57 | expected := `
` 58 | el := Br(nil) 59 | assert.Equal(t, expected, el.Render()) 60 | } 61 | 62 | func TestCode(t *testing.T) { 63 | expected := `Code snippet` 64 | el := Code(nil, Text("Code snippet")) 65 | assert.Equal(t, expected, el.Render()) 66 | } 67 | 68 | func TestDiv(t *testing.T) { 69 | expected := `
Hello, Elem!
` 70 | el := Div(attrs.Props{attrs.Class: "container"}, Text("Hello, Elem!")) 71 | assert.Equal(t, expected, el.Render()) 72 | } 73 | 74 | func TestEm(t *testing.T) { 75 | expected := `Italic text` 76 | el := Em(nil, Text("Italic text")) 77 | assert.Equal(t, expected, el.Render()) 78 | } 79 | 80 | func TestH1(t *testing.T) { 81 | expected := `

Hello, Elem!

` 82 | el := H1(attrs.Props{attrs.Class: "title"}, Text("Hello, Elem!")) 83 | assert.Equal(t, expected, el.Render()) 84 | } 85 | 86 | func TestH2(t *testing.T) { 87 | expected := `

Hello, Elem!

` 88 | el := H2(attrs.Props{attrs.Class: "subtitle"}, Text("Hello, Elem!")) 89 | assert.Equal(t, expected, el.Render()) 90 | } 91 | 92 | func TestH3(t *testing.T) { 93 | expected := `

Hello, Elem!

` 94 | el := H3(nil, Text("Hello, Elem!")) 95 | assert.Equal(t, expected, el.Render()) 96 | } 97 | 98 | func TestH4(t *testing.T) { 99 | expected := `

Hello, Elem!

` 100 | el := H4(nil, Text("Hello, Elem!")) 101 | assert.Equal(t, expected, el.Render()) 102 | } 103 | 104 | func TestH5(t *testing.T) { 105 | expected := `
Hello, Elem!
` 106 | el := H5(nil, Text("Hello, Elem!")) 107 | assert.Equal(t, expected, el.Render()) 108 | } 109 | 110 | func TestH6(t *testing.T) { 111 | expected := `
Hello, Elem!
` 112 | el := H6(nil, Text("Hello, Elem!")) 113 | assert.Equal(t, expected, el.Render()) 114 | } 115 | 116 | func TestHr(t *testing.T) { 117 | expected := `
` 118 | el := Hr(nil) 119 | assert.Equal(t, expected, el.Render()) 120 | } 121 | 122 | func TestI(t *testing.T) { 123 | expected1 := `Idiomatic Text` 124 | expected2 := `` 125 | el := I(nil, Text("Idiomatic Text")) 126 | assert.Equal(t, expected1, el.Render()) 127 | el = I(attrs.Props{attrs.Class: "fa-regular fa-face-smile"}) 128 | assert.Equal(t, expected2, el.Render()) 129 | } 130 | 131 | func TestP(t *testing.T) { 132 | expected := `

Hello, Elem!

` 133 | el := P(nil, Text("Hello, Elem!")) 134 | assert.Equal(t, expected, el.Render()) 135 | } 136 | 137 | func TestPre(t *testing.T) { 138 | expected := `
Preformatted text
` 139 | el := Pre(nil, Text("Preformatted text")) 140 | assert.Equal(t, expected, el.Render()) 141 | } 142 | 143 | func TestSpan(t *testing.T) { 144 | expected := `Hello, Elem!` 145 | el := Span(attrs.Props{attrs.Class: "highlight"}, Text("Hello, Elem!")) 146 | assert.Equal(t, expected, el.Render()) 147 | } 148 | 149 | func TestStrong(t *testing.T) { 150 | expected := `Bold text` 151 | el := Strong(nil, Text("Bold text")) 152 | assert.Equal(t, expected, el.Render()) 153 | } 154 | 155 | func TestSub(t *testing.T) { 156 | expected := `2` 157 | el := Sub(nil, Text("2")) 158 | assert.Equal(t, expected, el.Render()) 159 | } 160 | 161 | func TestSup(t *testing.T) { 162 | expected := `2` 163 | el := Sup(nil, Text("2")) 164 | assert.Equal(t, expected, el.Render()) 165 | } 166 | 167 | func TestB(t *testing.T) { 168 | expected := `Important text` 169 | el := B(nil, Text("Important text")) 170 | assert.Equal(t, expected, el.Render()) 171 | } 172 | 173 | func TestU(t *testing.T) { 174 | expected := `Unarticulated text` 175 | el := U(nil, Text("Unarticulated text")) 176 | assert.Equal(t, expected, el.Render()) 177 | } 178 | 179 | // ========== Comments ========== 180 | func TestComment(t *testing.T) { 181 | expected := `` 182 | actual := Comment("this is a comment").Render() 183 | assert.Equal(t, expected, actual) 184 | } 185 | 186 | func TestCommentInElement(t *testing.T) { 187 | expected := `
not a comment
` 188 | actual := Div(nil, Text("not a comment"), Comment("this is a comment")).Render() 189 | assert.Equal(t, expected, actual) 190 | } 191 | 192 | // ========== Lists ========== 193 | 194 | func TestLi(t *testing.T) { 195 | expected := `
  • Item 1
  • ` 196 | el := Li(nil, Text("Item 1")) 197 | assert.Equal(t, expected, el.Render()) 198 | } 199 | 200 | func TestUl(t *testing.T) { 201 | expected := `` 202 | el := Ul(nil, Li(nil, Text("Item 1")), Li(nil, Text("Item 2"))) 203 | assert.Equal(t, expected, el.Render()) 204 | } 205 | 206 | func TestOl(t *testing.T) { 207 | expected := `
    1. Item 1
    2. Item 2
    ` 208 | el := Ol(nil, Li(nil, Text("Item 1")), Li(nil, Text("Item 2"))) 209 | assert.Equal(t, expected, el.Render()) 210 | } 211 | 212 | func TestDl(t *testing.T) { 213 | expected := `
    Term 1
    Description 1
    Term 2
    Description 2
    ` 214 | el := Dl(nil, Dt(nil, Text("Term 1")), Dd(nil, Text("Description 1")), Dt(nil, Text("Term 2")), Dd(nil, Text("Description 2"))) 215 | assert.Equal(t, expected, el.Render()) 216 | } 217 | 218 | func TestDt(t *testing.T) { 219 | expected := `
    Term 1
    ` 220 | el := Dt(nil, Text("Term 1")) 221 | assert.Equal(t, expected, el.Render()) 222 | } 223 | 224 | func TestDd(t *testing.T) { 225 | expected := `
    Description 1
    ` 226 | el := Dd(nil, Text("Description 1")) 227 | assert.Equal(t, expected, el.Render()) 228 | } 229 | 230 | // ========== Forms ========== 231 | 232 | func TestButton(t *testing.T) { 233 | expected := `` 234 | el := Button(attrs.Props{attrs.Class: "btn"}, Text("Click Me")) 235 | assert.Equal(t, expected, el.Render()) 236 | } 237 | 238 | func TestForm(t *testing.T) { 239 | expected := `
    ` 240 | el := Form(attrs.Props{attrs.Action: "/submit", attrs.Method: "post"}, Input(attrs.Props{attrs.Type: "text", attrs.Name: "username"})) 241 | assert.Equal(t, expected, el.Render()) 242 | } 243 | 244 | func TestInput(t *testing.T) { 245 | expected := `` 246 | el := Input(attrs.Props{attrs.Type: "text", attrs.Name: "username", attrs.Placeholder: "Enter your username"}) 247 | assert.Equal(t, expected, el.Render()) 248 | } 249 | 250 | func TestLabel(t *testing.T) { 251 | expected := `` 252 | el := Label(attrs.Props{attrs.For: "username"}, Text("Username")) 253 | assert.Equal(t, expected, el.Render()) 254 | } 255 | 256 | func TestSelectAndOption(t *testing.T) { 257 | expected := `` 258 | el := Select(attrs.Props{attrs.Name: "color"}, Option(attrs.Props{attrs.Value: "red"}, Text("Red")), Option(attrs.Props{attrs.Value: "blue"}, Text("Blue"))) 259 | assert.Equal(t, expected, el.Render()) 260 | } 261 | 262 | func TestSelectAndOptgroup(t *testing.T) { 263 | expected := `` 264 | el := Select(attrs.Props{attrs.Name: "cars"}, Optgroup(attrs.Props{attrs.Label: "Swedish Cars"}, Option(attrs.Props{attrs.Value: "volvo"}, Text("Volvo")), Option(attrs.Props{attrs.Value: "saab"}, Text("Saab"))), Optgroup(attrs.Props{attrs.Label: "German Cars"}, Option(attrs.Props{attrs.Value: "mercedes"}, Text("Mercedes")), Option(attrs.Props{attrs.Value: "audi"}, Text("Audi")))) 265 | assert.Equal(t, expected, el.Render()) 266 | } 267 | 268 | func TestTextarea(t *testing.T) { 269 | expected := `` 270 | el := Textarea(attrs.Props{attrs.Name: "comment", attrs.Rows: "5"}, Text("Leave a comment...")) 271 | assert.Equal(t, expected, el.Render()) 272 | } 273 | 274 | // ========== Boolean attributes ========== 275 | func TestCheckedTrue(t *testing.T) { 276 | expected := `` 277 | el := Input(attrs.Props{attrs.Type: "checkbox", attrs.Name: "allow", attrs.Checked: "true"}) 278 | assert.Equal(t, expected, el.Render()) 279 | } 280 | 281 | func TestCheckedFalse(t *testing.T) { 282 | expected := `` 283 | el := Input(attrs.Props{attrs.Type: "checkbox", attrs.Name: "allow", attrs.Checked: "false"}) 284 | assert.Equal(t, expected, el.Render()) 285 | } 286 | 287 | func TestCheckedEmpty(t *testing.T) { 288 | expected := `` 289 | el := Input(attrs.Props{attrs.Type: "checkbox", attrs.Name: "allow", attrs.Checked: ""}) 290 | assert.Equal(t, expected, el.Render()) 291 | } 292 | 293 | // ========== Hyperlinks and Multimedia ========== 294 | 295 | func TestImg(t *testing.T) { 296 | expected := `An image` 297 | el := Img(attrs.Props{attrs.Src: "image.jpg", attrs.Alt: "An image"}) 298 | assert.Equal(t, expected, el.Render()) 299 | } 300 | 301 | // ========== Head Elements ========== 302 | 303 | func TestBase(t *testing.T) { 304 | expected := `` 305 | el := Base(attrs.Props{attrs.Href: "https://example.com"}) 306 | assert.Equal(t, expected, el.Render()) 307 | } 308 | 309 | func TestLink(t *testing.T) { 310 | expected := `` 311 | el := Link(attrs.Props{attrs.Rel: "stylesheet", attrs.Href: "https://example.com/styles.css"}) 312 | assert.Equal(t, expected, el.Render()) 313 | } 314 | 315 | func TestMeta(t *testing.T) { 316 | expected := `` 317 | el := Meta(attrs.Props{attrs.Charset: "UTF-8"}) 318 | assert.Equal(t, expected, el.Render()) 319 | } 320 | 321 | func TestScript(t *testing.T) { 322 | expected := `` 323 | el := Script(attrs.Props{attrs.Src: "https://example.com/script.js"}) 324 | assert.Equal(t, expected, el.Render()) 325 | } 326 | 327 | func TestStyle(t *testing.T) { 328 | expected := `` 329 | cssContent := `.test-class {color: #333;}` 330 | el := Style(attrs.Props{attrs.Type: "text/css"}, CSS(cssContent)) 331 | assert.Equal(t, expected, el.Render()) 332 | } 333 | 334 | // ========== Semantic Elements ========== 335 | 336 | // --- Semantic Sectioning Elements --- 337 | 338 | func TestArticle(t *testing.T) { 339 | expected := `

    Article Title

    Article content.

    ` 340 | el := Article(nil, H2(nil, Text("Article Title")), P(nil, Text("Article content."))) 341 | assert.Equal(t, expected, el.Render()) 342 | } 343 | 344 | func TestAside(t *testing.T) { 345 | expected := `` 346 | el := Aside(nil, P(nil, Text("Sidebar content."))) 347 | assert.Equal(t, expected, el.Render()) 348 | } 349 | 350 | func TestFooter(t *testing.T) { 351 | expected := `` 352 | el := Footer(nil, P(nil, Text("Footer content."))) 353 | assert.Equal(t, expected, el.Render()) 354 | } 355 | 356 | func TestHeader(t *testing.T) { 357 | expected := `` 358 | el := Header(attrs.Props{attrs.Class: "site-header"}, H1(nil, Text("Welcome to Elem!"))) 359 | assert.Equal(t, expected, el.Render()) 360 | } 361 | 362 | func TestMainElem(t *testing.T) { 363 | expected := `

    Main content goes here.

    ` 364 | el := Main(nil, P(nil, Text("Main content goes here."))) 365 | assert.Equal(t, expected, el.Render()) 366 | } 367 | 368 | func TestNav(t *testing.T) { 369 | expected := `` 370 | el := Nav(nil, A(attrs.Props{attrs.Href: "/home"}, Text("Home")), A(attrs.Props{attrs.Href: "/about"}, Text("About"))) 371 | assert.Equal(t, expected, el.Render()) 372 | } 373 | 374 | func TestSection(t *testing.T) { 375 | expected := `

    Section Title

    Section content.

    ` 376 | el := Section(nil, H3(nil, Text("Section Title")), P(nil, Text("Section content."))) 377 | assert.Equal(t, expected, el.Render()) 378 | } 379 | 380 | func TestHgroup(t *testing.T) { 381 | expected := `

    Frankenstein

    Or: The Modern Prometheus

    ` 382 | el := Hgroup(nil, H1(nil, Text("Frankenstein")), P(nil, Text("Or: The Modern Prometheus"))) 383 | assert.Equal(t, expected, el.Render()) 384 | } 385 | 386 | // --- Semantic Form Elements --- 387 | 388 | func TestFieldset(t *testing.T) { 389 | expected := `
    Personal Information
    ` 390 | el := Fieldset(attrs.Props{attrs.Class: "custom-fieldset"}, Legend(nil, Text("Personal Information")), Input(attrs.Props{attrs.Type: "text", attrs.Name: "name"})) 391 | assert.Equal(t, expected, el.Render()) 392 | } 393 | 394 | func TestLegend(t *testing.T) { 395 | expected := `Legend Title` 396 | el := Legend(attrs.Props{attrs.Class: "custom-legend"}, Text("Legend Title")) 397 | assert.Equal(t, expected, el.Render()) 398 | } 399 | 400 | func TestDatalist(t *testing.T) { 401 | expected := `` 402 | el := Datalist(attrs.Props{attrs.ID: "exampleList"}, Option(attrs.Props{attrs.Value: "Option1"}, Text("Option 1")), Option(attrs.Props{attrs.Value: "Option2"}, Text("Option 2"))) 403 | assert.Equal(t, expected, el.Render()) 404 | } 405 | 406 | func TestMeter(t *testing.T) { 407 | expected := `50%` 408 | el := Meter(attrs.Props{attrs.Min: "0", attrs.Max: "100", attrs.Value: "50"}, Text("50%")) 409 | assert.Equal(t, expected, el.Render()) 410 | } 411 | 412 | func TestOutput(t *testing.T) { 413 | expected := `Output` 414 | el := Output(attrs.Props{attrs.For: "inputId", attrs.Name: "result"}, Text("Output")) 415 | assert.Equal(t, expected, el.Render()) 416 | } 417 | 418 | func TestProgress(t *testing.T) { 419 | expected := `` 420 | el := Progress(attrs.Props{attrs.Max: "100", attrs.Value: "60"}) 421 | assert.Equal(t, expected, el.Render()) 422 | } 423 | 424 | // --- Semantic Interactive Elements --- 425 | 426 | func TestDialog(t *testing.T) { 427 | expected := `

    This is an open dialog window

    ` 428 | el := Dialog(attrs.Props{attrs.Open: "true"}, P(nil, Text("This is an open dialog window"))) 429 | assert.Equal(t, expected, el.Render()) 430 | } 431 | 432 | func TestMenu(t *testing.T) { 433 | expected := `
  • Item One
  • Item Two
  • ` 434 | el := Menu(nil, Li(nil, Text("Item One")), Li(nil, Text("Item Two"))) 435 | assert.Equal(t, expected, el.Render()) 436 | } 437 | 438 | // --- Semantic Script Supporting Elements --- 439 | 440 | func TestNoScript(t *testing.T) { 441 | expected := `` 442 | el := NoScript(nil, P(nil, Text("JavaScript is required for this application."))) 443 | assert.Equal(t, expected, el.Render()) 444 | } 445 | 446 | // --- Semantic Text Content Elements --- 447 | 448 | func TestAbbr(t *testing.T) { 449 | expected := `WHATWG` 450 | el := Abbr(attrs.Props{attrs.Title: "Web Hypertext Application Technology Working Group"}, Text("WHATWG")) 451 | assert.Equal(t, expected, el.Render()) 452 | } 453 | 454 | func TestAddress(t *testing.T) { 455 | expected := `
    123 Example St.
    ` 456 | el := Address(nil, Text("123 Example St.")) 457 | assert.Equal(t, expected, el.Render()) 458 | } 459 | 460 | func TestCite(t *testing.T) { 461 | expected := `

    My favorite book is The Reality Dysfunction by Peter F. Hamilton.

    ` 462 | el := P(nil, Text("My favorite book is "), Cite(nil, Text("The Reality Dysfunction")), Text(" by Peter F. Hamilton.")) 463 | assert.Equal(t, expected, el.Render()) 464 | } 465 | 466 | func TestDetails(t *testing.T) { 467 | expected := `
    More Info

    Details content here.

    ` 468 | el := Details(nil, Summary(nil, Text("More Info")), P(nil, Text("Details content here."))) 469 | assert.Equal(t, expected, el.Render()) 470 | } 471 | 472 | func TestDetailsWithOpenFalse(t *testing.T) { 473 | expected := `
    More Info

    Details content here.

    ` 474 | el := Details(attrs.Props{attrs.Open: "false"}, Summary(nil, Text("More Info")), P(nil, Text("Details content here."))) 475 | assert.Equal(t, expected, el.Render()) 476 | } 477 | 478 | func TestDetailsWithOpenTrue(t *testing.T) { 479 | expected := `
    More Info

    Details content here.

    ` 480 | el := Details(attrs.Props{attrs.Open: "true"}, Summary(nil, Text("More Info")), P(nil, Text("Details content here."))) 481 | assert.Equal(t, expected, el.Render()) 482 | } 483 | 484 | func TestData(t *testing.T) { 485 | expected := `Eight` 486 | el := Data(attrs.Props{attrs.Value: "8"}, Text("Eight")) 487 | assert.Equal(t, expected, el.Render()) 488 | } 489 | 490 | func TestFigCaption(t *testing.T) { 491 | expected := `
    Description of the figure.
    ` 492 | el := FigCaption(nil, Text("Description of the figure.")) 493 | assert.Equal(t, expected, el.Render()) 494 | } 495 | 496 | func TestFigure(t *testing.T) { 497 | expected := `
    An image
    An image
    ` 498 | el := Figure(nil, Img(attrs.Props{attrs.Src: "image.jpg", attrs.Alt: "An image"}), FigCaption(nil, Text("An image"))) 499 | assert.Equal(t, expected, el.Render()) 500 | } 501 | 502 | func TestKbd(t *testing.T) { 503 | expected := `

    To make George eat an apple, select File | Eat Apple...

    ` 504 | el := P(nil, Text("To make George eat an apple, select "), Kbd(nil, Text("File | Eat Apple..."))) 505 | assert.Equal(t, expected, el.Render()) 506 | } 507 | 508 | func TestMark(t *testing.T) { 509 | expected := `

    You must highlight this word.

    ` 510 | el := P(nil, Text("You must "), Mark(nil, Text("highlight")), Text(" this word.")) 511 | assert.Equal(t, expected, el.Render()) 512 | } 513 | 514 | func TestQ(t *testing.T) { 515 | expected := `

    The W3C's mission is To lead the World Wide Web to its full potential.

    ` 516 | el := P(nil, Text("The W3C's mission is "), Q(attrs.Props{attrs.Cite: "https://www.w3.org/Consortium/"}, Text("To lead the World Wide Web to its full potential")), Text(".")) 517 | assert.Equal(t, expected, el.Render()) 518 | } 519 | 520 | func TestSamp(t *testing.T) { 521 | expected := `

    The computer said Too much cheese in tray two but I didn't know what that meant.

    ` 522 | el := P(nil, Text("The computer said "), Samp(nil, Text("Too much cheese in tray two")), Text(" but I didn't know what that meant.")) 523 | assert.Equal(t, expected, el.Render()) 524 | } 525 | 526 | func TestSmall(t *testing.T) { 527 | expected := `

    Single room breakfast included, VAT not included

    ` 528 | el := P(nil, Text("Single room "), Small(nil, Text("breakfast included, VAT not included"))) 529 | assert.Equal(t, expected, el.Render()) 530 | } 531 | 532 | func TestSummary(t *testing.T) { 533 | expected := `
    Summary Title
    ` 534 | el := Details(nil, Summary(nil, Text("Summary Title"))) 535 | assert.Equal(t, expected, el.Render()) 536 | } 537 | 538 | func TestTime(t *testing.T) { 539 | expected := `` 540 | el := Time(attrs.Props{attrs.Datetime: "2023-01-01T00:00:00Z"}, Text("New Year's Day")) 541 | assert.Equal(t, expected, el.Render()) 542 | } 543 | 544 | func TestVar(t *testing.T) { 545 | expected := `

    After a few moment's thought, she wrote E.

    ` 546 | el := P(nil, Text("After a few moment's thought, she wrote "), Var(nil, Text("E")), Text(".")) 547 | assert.Equal(t, expected, el.Render()) 548 | } 549 | 550 | func TestRuby(t *testing.T) { 551 | expected := `` 552 | el := Ruby(nil, Text("漢")) 553 | assert.Equal(t, expected, el.Render()) 554 | } 555 | 556 | func TestRt(t *testing.T) { 557 | expected := `(kan)(ji) ` 558 | el := Ruby(nil, Text(" 漢 "), Rp(nil, Text("(")), Rt(nil, Text("kan")), Rp(nil, Text(")")), Text(" 字 "), Rp(nil, Text("(")), Rt(nil, Text("ji")), Rp(nil, Text(")")), Text(" ")) 559 | assert.Equal(t, expected, el.Render()) 560 | } 561 | 562 | func TestRp(t *testing.T) { 563 | expected := `() ` 564 | el := Ruby(nil, Text(" 漢 "), Rp(nil, Text("(")), Text(" 字 "), Rp(nil, Text(")")), Text(" ")) 565 | assert.Equal(t, expected, el.Render()) 566 | } 567 | 568 | // ========== Tables ========== 569 | 570 | func TestTr(t *testing.T) { 571 | expected := `Row content.` 572 | el := Tr(nil, Text("Row content.")) 573 | assert.Equal(t, expected, el.Render()) 574 | } 575 | 576 | func TestTd(t *testing.T) { 577 | expected := `

    Cell one.

    Cell two.` 578 | el := Tr(nil, Td(nil, H1(nil, Text("Cell one."))), Td(nil, Text("Cell two."))) 579 | assert.Equal(t, expected, el.Render()) 580 | } 581 | 582 | func TestTh(t *testing.T) { 583 | expected := `First nameLast nameAge` 584 | el := Tr(nil, Th(nil, Text("First name")), Th(nil, Text("Last name")), Th(nil, Text("Age"))) 585 | assert.Equal(t, expected, el.Render()) 586 | } 587 | 588 | func TestTHead(t *testing.T) { 589 | expected := `TextLink` 590 | el := THead(nil, Tr(nil, Td(nil, Text("Text")), Td(nil, A(attrs.Props{attrs.Href: "/link"}, Text("Link"))))) 591 | assert.Equal(t, expected, el.Render()) 592 | } 593 | 594 | func TestTBody(t *testing.T) { 595 | expected := `Table body` 596 | el := TBody(nil, Tr(nil, Td(nil, Text("Table body")))) 597 | assert.Equal(t, expected, el.Render()) 598 | } 599 | 600 | func TestTFoot(t *testing.T) { 601 | expected := `Table footer` 602 | el := TFoot(nil, Tr(nil, Td(nil, A(attrs.Props{attrs.Href: "/footer"}, Text("Table footer"))))) 603 | assert.Equal(t, expected, el.Render()) 604 | } 605 | 606 | func TestTable(t *testing.T) { 607 | expected := `
    Table header
    Table content
    ` 608 | el := Table(nil, Tr(nil, Th(nil, Text("Table header"))), Tr(nil, Td(nil, Text("Table content")))) 609 | assert.Equal(t, expected, el.Render()) 610 | } 611 | 612 | // ========== Embedded Content ========== 613 | 614 | func TestEmbedLink(t *testing.T) { 615 | expected := `` 616 | el := IFrame(attrs.Props{attrs.Src: "https://www.youtube.com/embed/446E-r0rXHI"}) 617 | assert.Equal(t, expected, el.Render()) 618 | } 619 | 620 | func TestAllowFullScreen(t *testing.T) { 621 | expected := `` 622 | el := IFrame(attrs.Props{attrs.Src: "https://www.youtube.com/embed/446E-r0rXHI", attrs.AllowFullScreen: "true"}) 623 | assert.Equal(t, expected, el.Render()) 624 | } 625 | 626 | func TestAudioWithSourceElementsAndFallbackText(t *testing.T) { 627 | expected := `` 628 | el := Audio(attrs.Props{attrs.Controls: "true"}, 629 | Source(attrs.Props{attrs.Src: "horse.ogg", attrs.Type: "audio/ogg"}), 630 | Source(attrs.Props{attrs.Src: "horse.mp3", attrs.Type: "audio/mpeg"}), 631 | Text("Your browser does not support the audio tag."), 632 | ) 633 | assert.Equal(t, expected, el.Render()) 634 | } 635 | 636 | func TestVideoWithSourceElementsAndFallbackText(t *testing.T) { 637 | expected := `` 638 | el := Video(attrs.Props{attrs.Width: "320", attrs.Height: "240", attrs.Controls: "true"}, 639 | Source(attrs.Props{attrs.Src: "movie.mp4", attrs.Type: "video/mp4"}), 640 | Source(attrs.Props{attrs.Src: "movie.ogg", attrs.Type: "video/ogg"}), 641 | Text("Your browser does not support the video tag."), 642 | ) 643 | assert.Equal(t, expected, el.Render()) 644 | } 645 | 646 | // ========== Image Map Elements ========== 647 | 648 | func TestMapAndArea(t *testing.T) { 649 | expectedMap := `Area 1` 650 | mapEl := Map(attrs.Props{attrs.Name: "map-name"}, 651 | Area(attrs.Props{ 652 | attrs.Href: "#area1", 653 | attrs.Alt: "Area 1", 654 | attrs.Shape: "rect", 655 | attrs.Coords: "34,44,270,350", 656 | }), 657 | ) 658 | assert.Equal(t, expectedMap, mapEl.Render()) 659 | } 660 | 661 | // ========== Other ========== 662 | 663 | func TestNone(t *testing.T) { 664 | el := None() 665 | expected := "" 666 | 667 | assert.Equal(t, expected, el.Render(), "None should render an empty string") 668 | } 669 | 670 | func TestNoneInDiv(t *testing.T) { 671 | expected := `
    ` 672 | actual := Div(nil, None()).Render() 673 | assert.Equal(t, expected, actual) 674 | } 675 | 676 | func TestRaw(t *testing.T) { 677 | rawHTML := `

    Test paragraph

    ` 678 | el := Raw(rawHTML) 679 | expected := rawHTML 680 | 681 | assert.Equal(t, expected, el.Render()) 682 | } 683 | 684 | func TestCSS(t *testing.T) { 685 | cssContent := `.test-class {color: #333;}` 686 | expected := `.test-class {color: #333;}` 687 | el := CSS(cssContent) 688 | assert.Equal(t, expected, el.Render()) 689 | } 690 | 691 | func TestSingleQuote(t *testing.T) { 692 | expected := `
    ` 693 | el := Div(attrs.Props{ 694 | "data-values": `'{"quantity": 5}'`, 695 | }) 696 | actual := el.Render() 697 | assert.Equal(t, expected, actual) 698 | } 699 | 700 | func TestFragment(t *testing.T) { 701 | expected := `

    0

    1

    2

    3

    4

    ` 702 | nodes1 := []Node{ 703 | P(nil, 704 | Text("1"), 705 | ), 706 | P(nil, 707 | Text("2"), 708 | ), 709 | } 710 | nodes2 := []Node{ 711 | P(nil, 712 | Text("3"), 713 | ), 714 | P(nil, 715 | Text("4"), 716 | ), 717 | } 718 | el := Div(nil, 719 | P(nil, 720 | Text("0"), 721 | ), 722 | Fragment(nodes1...), 723 | Fragment(nodes2...), 724 | ) 725 | actual := el.Render() 726 | assert.Equal(t, expected, actual) 727 | } 728 | -------------------------------------------------------------------------------- /examples/htmx-counter/README.md: -------------------------------------------------------------------------------- 1 | # Simple Counter Application with `elem-go` and `htmx` 2 | 3 | A basic web application demonstrating a simple counter that can be incremented without reloading the page. It uses `elem-go` to construct HTML elements programmatically and `htmx` for handling the asynchronous increment of the counter. 4 | 5 | ## Prerequisites 6 | 7 | Ensure that Go is installed on your system. 8 | 9 | ## Installation 10 | 11 | Clone or download the repository, then run the following commands to download the necessary packages: 12 | 13 | ```bash 14 | go mod tidy 15 | ``` 16 | 17 | This will install `elem-go` and the `htmx` subpackage required to run the application. 18 | 19 | ## Running the Application 20 | 21 | To run the application, execute the following command: 22 | 23 | ```bash 24 | go run main.go 25 | ``` 26 | 27 | The server will start on `localhost` at port `8080`. You can view the application by navigating to `http://localhost:8080` in your web browser. 28 | 29 | ## Features 30 | 31 | **Increment Counter**: Click the "Increment" button to increase the counter value. This action will send a POST request to `/increment` and update the counter display asynchronously. 32 | 33 | ## Structure 34 | 35 | - `GET /`: Renders the home page with the current value of the counter and an "Increment" button. 36 | - `POST /increment`: Increases the counter by one and returns the updated value. 37 | 38 | ## Asynchronous Updates with htmx 39 | 40 | The counter is updated asynchronously using `htmx`. When the "Increment" button is clicked, `htmx` sends a POST request to `/increment`, receives the new counter value, and updates the relevant part of the page without reloading. 41 | 42 | ## HTML Generation 43 | 44 | The HTML for the page is generated on the server using the `elem-go` library, with elements such as `button` and `div` created programmatically. `htmx` attributes are applied directly to elements to enable the asynchronous behavior. 45 | 46 | ## Stopping the Server 47 | 48 | To stop the application, press `Ctrl + C` in the terminal where the server is running. -------------------------------------------------------------------------------- /examples/htmx-counter/go.mod: -------------------------------------------------------------------------------- 1 | module htmx-counter 2 | 3 | go 1.21.1 4 | 5 | require github.com/chasefleming/elem-go v0.0.0 6 | 7 | replace github.com/chasefleming/elem-go => ../../../elem-go -------------------------------------------------------------------------------- /examples/htmx-counter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/chasefleming/elem-go" 6 | "github.com/chasefleming/elem-go/attrs" 7 | "github.com/chasefleming/elem-go/htmx" 8 | "github.com/chasefleming/elem-go/styles" 9 | "net/http" 10 | "strconv" 11 | ) 12 | 13 | var counter int 14 | 15 | func main() { 16 | http.HandleFunc("/", counterHomeHandler) 17 | http.HandleFunc("/increment", incrementCounterHandler) 18 | 19 | err := http.ListenAndServe(":8080", nil) 20 | if err != nil { 21 | panic(err) 22 | } 23 | } 24 | 25 | func counterHomeHandler(w http.ResponseWriter, r *http.Request) { 26 | content := generateCounterContent() 27 | w.Header().Set("Content-Type", "text/html") 28 | w.WriteHeader(http.StatusOK) 29 | w.Write([]byte(content)) 30 | } 31 | 32 | func incrementCounterHandler(w http.ResponseWriter, r *http.Request) { 33 | counter++ 34 | w.Header().Set("Content-Type", "text/plain") 35 | w.Write([]byte(strconv.Itoa(counter))) 36 | } 37 | 38 | func generateCounterContent() string { 39 | head := elem.Head(nil, 40 | elem.Script(attrs.Props{attrs.Src: "https://unpkg.com/htmx.org@1.6.1"}), 41 | ) 42 | 43 | buttonStyle := styles.Props{ 44 | styles.BackgroundColor: "blue", 45 | styles.Color: "white", 46 | } 47 | 48 | body := elem.Body(nil, 49 | elem.Button(attrs.Props{ 50 | htmx.HXPost: "/increment", 51 | htmx.HXTarget: "#counter-div", 52 | htmx.HXSwap: "innerText", 53 | attrs.Style: buttonStyle.ToInline(), 54 | }, elem.Text("Increment")), 55 | elem.Div(attrs.Props{attrs.ID: "counter-div"}, elem.Text(fmt.Sprintf("%d", counter))), 56 | ) 57 | 58 | htmlDoc := elem.Html(nil, head, body) 59 | 60 | return htmlDoc.Render() 61 | } 62 | -------------------------------------------------------------------------------- /examples/htmx-fiber-counter/README.md: -------------------------------------------------------------------------------- 1 | # Counter App Example with `elem-go`, `htmx`, `Go Fiber` 2 | 3 | This is a simple counter application demonstrating the integration of a `Go Fiber` backend with `elem-go` for server-side HTML element creation and `htmx` for dynamic front-end interactions. 4 | 5 | ## Prerequisites 6 | 7 | Before you begin, ensure you have Go installed on your system. 8 | 9 | ## Installation 10 | 11 | Clone or download the repository, then run the following commands to download the necessary packages: 12 | 13 | ```bash 14 | go mod tidy 15 | ``` 16 | 17 | This will install `elem-go` along with `Go Fiber` and the `htmx` subpackage required to run the application. 18 | 19 | ## Running the App 20 | 21 | Navigate to the directory containing the application's source code and execute the following command: 22 | 23 | ```bash 24 | go run main.go 25 | ``` 26 | 27 | This command compiles and runs the Go program, starting the Go Fiber server on port `3000`. 28 | 29 | ## Using the Application 30 | 31 | Open your web browser and go to `http://localhost:3000` to view and interact with the counter application. 32 | 33 | - Click on "+" to increment the counter. 34 | - Click on "-" to decrement the counter. 35 | 36 | These actions will trigger `htmx` requests to the server at the `/increment` and `/decrement` endpoints, respectively, which will update the counter value displayed on the page without needing a full page refresh. 37 | 38 | ## Code Explanation 39 | 40 | The Go application defines three routes: 41 | 42 | - `POST /increment`: Increases the counter and returns the new value as a string. 43 | - `POST /decrement`: Decreases the counter and returns the new value as a string. 44 | - `GET /`: Serves the main HTML page which includes: 45 | - The `htmx` script for handling attributes like `hx-post` and `hx-target`. 46 | - A styled button to increment and decrement the counter. 47 | - A `div` element to display the current value of the counter, which is updated by `htmx` when either button is clicked. 48 | 49 | The `elem-go` library is used to programmatically create the HTML content served on the root path. The HTML content includes a head element with the required `htmx` script and a body element with inline styling for the counter and buttons. 50 | 51 | ## Stopping the Server 52 | 53 | To stop the application, switch to the terminal window where it's running and press `Ctrl + C`. -------------------------------------------------------------------------------- /examples/htmx-fiber-counter/go.mod: -------------------------------------------------------------------------------- 1 | module htmx-fiber-counter 2 | 3 | go 1.21.1 4 | 5 | require ( 6 | github.com/chasefleming/elem-go v0.1.0 7 | github.com/gofiber/fiber/v2 v2.52.2 8 | ) 9 | 10 | require ( 11 | github.com/andybalholm/brotli v1.0.5 // indirect 12 | github.com/google/uuid v1.5.0 // indirect 13 | github.com/klauspost/compress v1.17.0 // indirect 14 | github.com/mattn/go-colorable v0.1.13 // indirect 15 | github.com/mattn/go-isatty v0.0.20 // indirect 16 | github.com/mattn/go-runewidth v0.0.15 // indirect 17 | github.com/rivo/uniseg v0.2.0 // indirect 18 | github.com/valyala/bytebufferpool v1.0.0 // indirect 19 | github.com/valyala/fasthttp v1.51.0 // indirect 20 | github.com/valyala/tcplisten v1.0.0 // indirect 21 | golang.org/x/sys v0.15.0 // indirect 22 | ) 23 | 24 | replace github.com/chasefleming/elem-go => ../../../elem-go 25 | -------------------------------------------------------------------------------- /examples/htmx-fiber-counter/go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= 2 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/gofiber/fiber/v2 v2.52.2 h1:b0rYH6b06Df+4NyrbdptQL8ifuxw/Tf2DgfkZkDaxEo= 6 | github.com/gofiber/fiber/v2 v2.52.2/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= 7 | github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= 8 | github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 9 | github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= 10 | github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 11 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 12 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 13 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 14 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 15 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 16 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 17 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 21 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 22 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 23 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 24 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 25 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 26 | github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= 27 | github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= 28 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 29 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 30 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 31 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 33 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 34 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 35 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 36 | -------------------------------------------------------------------------------- /examples/htmx-fiber-counter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/chasefleming/elem-go" 6 | "github.com/chasefleming/elem-go/attrs" 7 | "github.com/chasefleming/elem-go/htmx" 8 | "github.com/chasefleming/elem-go/styles" 9 | "github.com/gofiber/fiber/v2" 10 | ) 11 | 12 | func main() { 13 | app := fiber.New() 14 | 15 | count := 0 16 | 17 | app.Post("/increment", func(c *fiber.Ctx) error { 18 | count++ 19 | return c.SendString(fmt.Sprintf("%d", count)) 20 | }) 21 | 22 | app.Post("/decrement", func(c *fiber.Ctx) error { 23 | count-- 24 | return c.SendString(fmt.Sprintf("%d", count)) 25 | }) 26 | 27 | app.Get("/", func(c *fiber.Ctx) error { 28 | 29 | // Define the head with the htmx script and styling 30 | head := elem.Head(nil, 31 | elem.Script(attrs.Props{attrs.Src: "https://unpkg.com/htmx.org@1.9.6"}), 32 | ) 33 | 34 | // Define styling 35 | bodyStyle := styles.Props{ 36 | styles.BackgroundColor: "#f4f4f4", 37 | styles.FontFamily: "Arial, sans-serif", 38 | styles.Height: "100vh", 39 | styles.Display: "flex", 40 | styles.FlexDirection: "column", 41 | styles.AlignItems: "center", 42 | styles.JustifyContent: "center", 43 | } 44 | 45 | buttonStyle := styles.Props{ 46 | styles.Padding: "10px 20px", 47 | styles.BackgroundColor: "#007BFF", 48 | styles.Color: "#fff", 49 | styles.BorderColor: "#007BFF", 50 | styles.BorderRadius: "5px", 51 | styles.Margin: "10px", 52 | styles.Cursor: "pointer", 53 | } 54 | 55 | // Define the body content for our counter page 56 | body := elem.Body(attrs.Props{ 57 | attrs.Style: bodyStyle.ToInline(), 58 | }, 59 | elem.H1(nil, elem.Text("Counter App")), 60 | elem.Div(attrs.Props{attrs.ID: "count"}, elem.Text(fmt.Sprintf("%d", count))), 61 | elem.Button(attrs.Props{ 62 | htmx.HXPost: "/increment", 63 | htmx.HXTarget: "#count", 64 | attrs.Style: buttonStyle.ToInline(), 65 | }, elem.Text("+")), 66 | elem.Button(attrs.Props{ 67 | htmx.HXPost: "/decrement", 68 | htmx.HXTarget: "#count", 69 | attrs.Style: buttonStyle.ToInline(), 70 | }, elem.Text("-")), 71 | ) 72 | 73 | // Wrap the head and body content within an HTML tag 74 | pageContent := elem.Html(nil, head, body) 75 | 76 | html := pageContent.Render() 77 | 78 | // Specify that the response content type is HTML before sending the response 79 | c.Type("html") 80 | return c.SendString(html) 81 | }) 82 | 83 | app.Listen(":3000") 84 | } 85 | -------------------------------------------------------------------------------- /examples/htmx-fiber-form/README.md: -------------------------------------------------------------------------------- 1 | # Simple Form App with `elem-go`, `htmx`, and `Go Fiber` 2 | 3 | This simple form application demonstrates the power of `elem-go` for HTML generation, `htmx` for dynamic UIs, and the `Go Fiber` web framework for handling server-side logic in Go. 4 | 5 | ## Overview 6 | 7 | The application presents a basic form to the user for inputting their name and email. Upon submission, it uses `htmx` to send the data to the server without a full page reload and displays the captured data back to the user. 8 | 9 | ## Installation 10 | 11 | Clone or download the repository, then run the following commands to download the necessary packages: 12 | 13 | ```bash 14 | go mod tidy 15 | ``` 16 | 17 | This will install `elem-go` along with `Go Fiber` and the `htmx` subpackage required to run the application. 18 | 19 | ## Running the App 20 | 21 | Navigate to the directory containing the application's source code and execute the following command: 22 | 23 | ```bash 24 | go run main.go 25 | ``` 26 | 27 | This command compiles and runs the Go program, starting the `Go Fiber` server on port `3000`. 28 | 29 | ## Usage 30 | 31 | Open your web browser and navigate to `http://localhost:3000`. You will see a form requesting your name and email. Fill in the details and click submit to see the captured data displayed without reloading the page, thanks to `htmx`. 32 | 33 | ## Code Explanation 34 | 35 | - The Go Fiber app is initialized and set to listen on port 3000. 36 | - Two routes are defined: `/` for serving the form and `/submit-form` for processing the form submission. 37 | - When the form is submitted, the `/submit-form` route captures the input from the `name` and `email` fields and sends a response back. 38 | - `htmx` is used to handle the form submission asynchronously, allowing the server response to update the content on the page without a full refresh. 39 | - `elem-go` library is used to programmatically build the HTML content served when the root route is accessed. 40 | 41 | ## Stopping the Server 42 | 43 | To stop the application, use `Ctrl + C` in the terminal. -------------------------------------------------------------------------------- /examples/htmx-fiber-form/go.mod: -------------------------------------------------------------------------------- 1 | module htmx-fiber-counter 2 | 3 | go 1.21.1 4 | 5 | require ( 6 | github.com/chasefleming/elem-go v0.2.0 7 | github.com/gofiber/fiber/v2 v2.52.2 8 | ) 9 | 10 | require ( 11 | github.com/andybalholm/brotli v1.0.5 // indirect 12 | github.com/google/uuid v1.5.0 // indirect 13 | github.com/klauspost/compress v1.17.0 // indirect 14 | github.com/mattn/go-colorable v0.1.13 // indirect 15 | github.com/mattn/go-isatty v0.0.20 // indirect 16 | github.com/mattn/go-runewidth v0.0.15 // indirect 17 | github.com/rivo/uniseg v0.2.0 // indirect 18 | github.com/valyala/bytebufferpool v1.0.0 // indirect 19 | github.com/valyala/fasthttp v1.51.0 // indirect 20 | github.com/valyala/tcplisten v1.0.0 // indirect 21 | golang.org/x/sys v0.15.0 // indirect 22 | ) 23 | 24 | replace github.com/chasefleming/elem-go => ../../../elem-go 25 | -------------------------------------------------------------------------------- /examples/htmx-fiber-form/go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= 2 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/gofiber/fiber/v2 v2.52.2 h1:b0rYH6b06Df+4NyrbdptQL8ifuxw/Tf2DgfkZkDaxEo= 6 | github.com/gofiber/fiber/v2 v2.52.2/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= 7 | github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= 8 | github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 9 | github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= 10 | github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 11 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 12 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 13 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 14 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 15 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 16 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 17 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 21 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 22 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 23 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 24 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 25 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 26 | github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= 27 | github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= 28 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 29 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 30 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 31 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 33 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 34 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 35 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 36 | -------------------------------------------------------------------------------- /examples/htmx-fiber-form/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/chasefleming/elem-go" 6 | "github.com/chasefleming/elem-go/attrs" 7 | "github.com/chasefleming/elem-go/htmx" 8 | "github.com/gofiber/fiber/v2" 9 | ) 10 | 11 | func main() { 12 | app := fiber.New() 13 | 14 | // Define a simple structure to hold our form data 15 | var formData struct { 16 | Name string 17 | Email string 18 | } 19 | 20 | app.Post("/submit-form", func(c *fiber.Ctx) error { 21 | // Capture the form data 22 | formData.Name = c.FormValue("name") 23 | formData.Email = c.FormValue("email") 24 | 25 | // Send a response with the captured data 26 | return c.SendString(fmt.Sprintf("Name: %s, Email: %s", formData.Name, formData.Email)) 27 | }) 28 | 29 | app.Get("/", func(c *fiber.Ctx) error { 30 | 31 | // Define the head with the htmx script and styling 32 | head := elem.Head(nil, 33 | elem.Script(attrs.Props{attrs.Src: "https://unpkg.com/htmx.org@1.9.6"}), 34 | ) 35 | 36 | // Define the body content for our form page 37 | body := elem.Body(nil, 38 | elem.H1(nil, elem.Text("Simple Form App")), 39 | elem.Form(attrs.Props{ 40 | attrs.Action: "/submit-form", 41 | attrs.Method: "POST", 42 | htmx.HXPost: "/submit-form", 43 | htmx.HXSwap: "outerHTML", 44 | }, 45 | elem.Label(attrs.Props{attrs.For: "name"}, elem.Text("Name: ")), 46 | elem.Input(attrs.Props{ 47 | attrs.Type: "text", 48 | attrs.Name: "name", 49 | attrs.ID: "name", 50 | }), 51 | elem.Br(nil), 52 | elem.Label(attrs.Props{attrs.For: "email"}, elem.Text("Email: ")), 53 | elem.Input(attrs.Props{ 54 | attrs.Type: "email", 55 | attrs.Name: "email", 56 | attrs.ID: "email", 57 | }), 58 | elem.Br(nil), 59 | elem.Input(attrs.Props{ 60 | attrs.Type: "submit", 61 | attrs.Value: "Submit", 62 | }), 63 | ), 64 | elem.Div(attrs.Props{attrs.ID: "response"}, elem.Text("")), 65 | ) 66 | 67 | // Wrap the head and body content within an HTML tag 68 | pageContent := elem.Html(nil, head, body) 69 | 70 | html := pageContent.Render() 71 | 72 | // Specify that the response content type is HTML before sending the response 73 | c.Type("html") 74 | return c.SendString(html) 75 | }) 76 | 77 | app.Listen(":3000") 78 | } 79 | -------------------------------------------------------------------------------- /examples/htmx-fiber-todo/README.md: -------------------------------------------------------------------------------- 1 | # Todo List Application with `elem-go`, `htmx`, and `Go Fiber` 2 | 3 | This project is a Todo List web application built with Go using the `Go Fiber` framework. It features server-side rendering of HTML with `elem-go` and uses `htmx` to handle asynchronous actions without needing to reload the page. 4 | 5 | ## Prerequisites 6 | 7 | Before you begin, ensure you have Go installed on your system. 8 | 9 | ## Installation 10 | 11 | Clone or download the repository, then run the following commands to download the necessary packages: 12 | 13 | ```bash 14 | go mod tidy 15 | ``` 16 | 17 | This will install elem-go along with Go Fiber and the `htmx` subpackage required to run the application. 18 | 19 | ## Running the App 20 | 21 | Navigate to the directory containing the application's source code and execute the following command: 22 | 23 | ```bash 24 | go run main.go 25 | ``` 26 | 27 | This command compiles and runs the Go program, starting the `Go Fiber` server on port `3000`. 28 | 29 | ## Features 30 | 31 | - **Add Todos**: Enter a new task in the text box and click "Add" or press enter to add a new Todo to the list. 32 | - **Toggle Todos**: Click on a Todo item to toggle its Done status. Items marked as done will have a line-through style. 33 | 34 | ## Structure 35 | 36 | - `GET /`: Displays the list of Todo items. 37 | - `POST /toggle/:id`: Toggles the Done status of a Todo based on its ID. 38 | - `POST /add`: Adds a new Todo to the list. 39 | 40 | ## HTML Generation 41 | 42 | HTML content is programmatically generated using the `elem-go` library. This includes: 43 | 44 | - A form for adding new Todos. 45 | - A dynamic list that renders each Todo as an `li` element with an `input` checkbox to toggle completion. 46 | - Styling for elements is handled inline using the `styles` subpackage. 47 | 48 | ## Asynchronous Behavior 49 | 50 | Using `htmx`, the application performs actions like adding a new Todo or toggling the completion status without a full page reload. This provides a smoother user experience. 51 | 52 | ## Stopping the Server 53 | 54 | To stop the application, use `Ctrl + C` in the terminal. -------------------------------------------------------------------------------- /examples/htmx-fiber-todo/go.mod: -------------------------------------------------------------------------------- 1 | module htmx-fiber-todo 2 | 3 | go 1.21.1 4 | 5 | require ( 6 | github.com/chasefleming/elem-go v0.4.0 7 | github.com/gofiber/fiber/v2 v2.52.2 8 | ) 9 | 10 | require ( 11 | github.com/andybalholm/brotli v1.0.5 // indirect 12 | github.com/google/uuid v1.5.0 // indirect 13 | github.com/klauspost/compress v1.17.0 // indirect 14 | github.com/mattn/go-colorable v0.1.13 // indirect 15 | github.com/mattn/go-isatty v0.0.20 // indirect 16 | github.com/mattn/go-runewidth v0.0.15 // indirect 17 | github.com/rivo/uniseg v0.2.0 // indirect 18 | github.com/valyala/bytebufferpool v1.0.0 // indirect 19 | github.com/valyala/fasthttp v1.51.0 // indirect 20 | github.com/valyala/tcplisten v1.0.0 // indirect 21 | golang.org/x/sys v0.15.0 // indirect 22 | ) 23 | 24 | replace github.com/chasefleming/elem-go => ../../../elem-go 25 | -------------------------------------------------------------------------------- /examples/htmx-fiber-todo/go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= 2 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/gofiber/fiber/v2 v2.52.2 h1:b0rYH6b06Df+4NyrbdptQL8ifuxw/Tf2DgfkZkDaxEo= 6 | github.com/gofiber/fiber/v2 v2.52.2/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= 7 | github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= 8 | github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 9 | github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= 10 | github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 11 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 12 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 13 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 14 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 15 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 16 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 17 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 21 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 22 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 23 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 24 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 25 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 26 | github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= 27 | github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= 28 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 29 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 30 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 31 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 33 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 34 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 35 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 36 | -------------------------------------------------------------------------------- /examples/htmx-fiber-todo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/chasefleming/elem-go/htmx" 5 | "strconv" 6 | 7 | "github.com/chasefleming/elem-go" 8 | "github.com/chasefleming/elem-go/attrs" 9 | "github.com/chasefleming/elem-go/styles" 10 | "github.com/gofiber/fiber/v2" 11 | "github.com/gofiber/fiber/v2/utils" 12 | ) 13 | 14 | // Todo model 15 | type Todo struct { 16 | ID int 17 | Title string 18 | Done bool 19 | } 20 | 21 | // Global todos slice (for simplicity) 22 | var todos = []Todo{ 23 | {ID: 1, Title: "First task", Done: false}, 24 | {ID: 2, Title: "Second task", Done: true}, 25 | } 26 | 27 | func main() { 28 | app := fiber.New() 29 | 30 | // Routes 31 | app.Get("/", renderTodosRoute) 32 | app.Post("/toggle/:id", toggleTodoRoute) 33 | app.Post("/add", addTodoRoute) 34 | 35 | app.Listen(":3000") 36 | } 37 | 38 | func renderTodosRoute(c *fiber.Ctx) error { 39 | c.Type("html") 40 | return c.SendString(renderTodos(todos)) 41 | } 42 | 43 | func toggleTodoRoute(c *fiber.Ctx) error { 44 | id, _ := strconv.Atoi(c.Params("id")) 45 | var updatedTodo Todo 46 | for i, todo := range todos { 47 | if todo.ID == id { 48 | todos[i].Done = !todo.Done 49 | updatedTodo = todos[i] 50 | break 51 | } 52 | } 53 | c.Type("html") 54 | return c.SendString(createTodoNode(updatedTodo).Render()) 55 | } 56 | 57 | func addTodoRoute(c *fiber.Ctx) error { 58 | newTitle := utils.CopyString(c.FormValue("newTodo")) 59 | if newTitle != "" { 60 | todos = append(todos, Todo{ID: len(todos) + 1, Title: newTitle, Done: false}) 61 | } 62 | return c.Redirect("/") 63 | } 64 | 65 | func createTodoNode(todo Todo) elem.Node { 66 | checkbox := elem.Input(attrs.Props{ 67 | attrs.Type: "checkbox", 68 | attrs.Checked: strconv.FormatBool(todo.Done), 69 | htmx.HXPost: "/toggle/" + strconv.Itoa(todo.ID), 70 | htmx.HXTarget: "#todo-" + strconv.Itoa(todo.ID), 71 | }) 72 | 73 | return elem.Li(attrs.Props{ 74 | attrs.ID: "todo-" + strconv.Itoa(todo.ID), 75 | }, checkbox, elem.Span(attrs.Props{ 76 | attrs.Style: styles.Props{ 77 | styles.TextDecoration: elem.If(todo.Done, "line-through", "none"), 78 | }.ToInline(), 79 | }, elem.Text(todo.Title))) 80 | } 81 | 82 | func renderTodos(todos []Todo) string { 83 | inputButtonStyle := styles.Props{ 84 | styles.Width: "100%", 85 | styles.Padding: "10px", 86 | styles.MarginBottom: "10px", 87 | styles.Border: "1px solid #ccc", 88 | styles.BorderRadius: "4px", 89 | styles.BackgroundColor: "#f9f9f9", 90 | } 91 | 92 | buttonStyle := styles.Props{ 93 | styles.BackgroundColor: "#007BFF", 94 | styles.Color: "white", 95 | styles.BorderStyle: "none", 96 | styles.BorderRadius: "4px", 97 | styles.Cursor: "pointer", 98 | styles.Width: "100%", 99 | styles.Padding: "8px 12px", 100 | styles.FontSize: "14px", 101 | styles.Height: "36px", 102 | styles.MarginRight: "10px", 103 | } 104 | 105 | listContainerStyle := styles.Props{ 106 | styles.ListStyleType: "none", 107 | styles.Padding: "0", 108 | styles.Width: "100%", 109 | } 110 | 111 | centerContainerStyle := styles.Props{ 112 | styles.MaxWidth: "300px", 113 | styles.Margin: "40px auto", 114 | styles.Padding: "20px", 115 | styles.Border: "1px solid #ccc", 116 | styles.BoxShadow: "0px 0px 10px rgba(0,0,0,0.1)", 117 | styles.BackgroundColor: "#f9f9f9", 118 | } 119 | 120 | headContent := elem.Head(nil, 121 | elem.Script(attrs.Props{attrs.Src: "https://unpkg.com/htmx.org"}), 122 | ) 123 | 124 | bodyContent := elem.Div( 125 | attrs.Props{attrs.Style: centerContainerStyle.ToInline()}, 126 | elem.H1(nil, elem.Text("Todo List")), 127 | elem.Form( 128 | attrs.Props{attrs.Method: "post", attrs.Action: "/add"}, 129 | elem.Input( 130 | attrs.Props{ 131 | attrs.Type: "text", 132 | attrs.Name: "newTodo", 133 | attrs.Placeholder: "Add new task...", 134 | attrs.Style: inputButtonStyle.ToInline(), 135 | }, 136 | ), 137 | elem.Button( 138 | attrs.Props{ 139 | attrs.Type: "submit", 140 | attrs.Style: buttonStyle.ToInline(), 141 | }, 142 | elem.Text("Add"), 143 | ), 144 | ), 145 | elem.Ul( 146 | attrs.Props{attrs.Style: listContainerStyle.ToInline()}, 147 | elem.TransformEach(todos, createTodoNode)..., 148 | ), 149 | ) 150 | 151 | htmlContent := elem.Html(nil, headContent, bodyContent) 152 | 153 | return htmlContent.Render() 154 | } 155 | -------------------------------------------------------------------------------- /examples/stylemanager-demo/README.md: -------------------------------------------------------------------------------- 1 | # Advanced Styling App with elem-go and StyleManager 2 | 3 | This web application demonstrates the power of advanced CSS styling within a Go server environment, using the `elem-go` library and its `StyleManager` for dynamic styling. It features a button with hover effects, an animated element, and a responsive design element that adjusts styles based on the viewport width. 4 | 5 | ## Prerequisites 6 | 7 | Ensure that Go is installed on your system. 8 | 9 | ## Installation 10 | 11 | Clone or download the repository, then run the following commands to download the necessary packages: 12 | 13 | ```bash 14 | go mod tidy 15 | ``` 16 | 17 | This will install `elem-go` and the `styles` subpackage required to run the application. 18 | 19 | ## Running the Application 20 | 21 | To run the application, execute the following command: 22 | 23 | ```bash 24 | go run main.go 25 | ``` 26 | 27 | The server will start on `localhost` at port `8080`. You can view the application by navigating to `http://localhost:8080` in your web browser. 28 | 29 | ## Features 30 | 31 | **Button Hover Effect**: The button changes color and scale when hovered over, providing visual feedback to the user. 32 | **Animated Element**: The animated element moves across the screen in a loop, demonstrating the use of animations with `StyleManager`. 33 | **Responsive Design**: The application adjusts the background color based on the viewport width, showcasing the use of media queries for responsive styling. 34 | 35 | ## Advanced CSS Styling with `StyleManager` 36 | 37 | The `StyleManager` within the `styles` package provides a powerful solution for managing CSS styles in Go-based web applications. It enables the creation of dynamic and responsive styles, including pseudo-classes, animations, and media queries, directly in Go. 38 | 39 | To learn more about `StyleManager` and its advanced features, refer to the [StyleManager documentation](../../styles/STYLEMANAGER.md). 40 | 41 | ## Stopping the Server 42 | 43 | To stop the application, press `Ctrl + C` in the terminal where the server is running. -------------------------------------------------------------------------------- /examples/stylemanager-demo/go.mod: -------------------------------------------------------------------------------- 1 | module stylemanager_demo 2 | 3 | go 1.21.1 4 | 5 | require github.com/chasefleming/elem-go v0.0.0 6 | 7 | replace github.com/chasefleming/elem-go => ../../../elem-go 8 | -------------------------------------------------------------------------------- /examples/stylemanager-demo/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 6 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 7 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 8 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 9 | -------------------------------------------------------------------------------- /examples/stylemanager-demo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/chasefleming/elem-go" 5 | "github.com/chasefleming/elem-go/attrs" 6 | "github.com/chasefleming/elem-go/styles" 7 | "net/http" 8 | ) 9 | 10 | func generateWebContent() string { 11 | // Initialize StyleManager 12 | styleMgr := styles.NewStyleManager() 13 | 14 | // Button with hover effect 15 | buttonClass := styleMgr.AddCompositeStyle(styles.CompositeStyle{ 16 | Default: styles.Props{ 17 | styles.Padding: "10px 20px", 18 | styles.BackgroundColor: "blue", 19 | styles.Color: "white", 20 | styles.Border: "none", 21 | styles.Cursor: "pointer", 22 | styles.Margin: "10px", 23 | }, 24 | PseudoClasses: map[string]styles.Props{ 25 | "hover": { 26 | styles.BackgroundColor: "darkblue", 27 | }, 28 | }, 29 | }) 30 | 31 | // Animated element 32 | animationName := styleMgr.AddAnimation(styles.Keyframes{ 33 | "0%": {styles.Transform: "translateY(0px)"}, 34 | "50%": {styles.Transform: "translateY(-20px)"}, 35 | "100%": {styles.Transform: "translateY(0px)"}, 36 | }) 37 | animatedClass := styleMgr.AddStyle(styles.Props{ 38 | styles.Width: "100px", 39 | styles.Height: "100px", 40 | styles.BackgroundColor: "green", 41 | styles.AnimationName: animationName, 42 | styles.AnimationDuration: "2s", 43 | styles.AnimationIterationCount: "infinite", 44 | }) 45 | 46 | // Responsive design 47 | responsiveClass := styleMgr.AddCompositeStyle(styles.CompositeStyle{ 48 | Default: styles.Props{ 49 | styles.Padding: "20px", 50 | styles.BackgroundColor: "lightgray", 51 | styles.Margin: "10px", 52 | }, 53 | MediaQueries: map[string]styles.Props{ 54 | "@media (max-width: 600px)": { 55 | styles.Padding: "10px", 56 | styles.BackgroundColor: "lightblue", 57 | }, 58 | }, 59 | }) 60 | 61 | pseudoElementClass := styleMgr.AddCompositeStyle(styles.CompositeStyle{ 62 | Default: styles.Props{ 63 | styles.Color: "black", 64 | styles.BackgroundColor: "white", 65 | styles.Padding: "10px", 66 | styles.Margin: "10px", 67 | styles.Border: "1px solid gray", 68 | styles.Position: "relative", 69 | }, 70 | PseudoElements: map[string]styles.Props{ 71 | styles.PseudoBefore: { 72 | styles.Content: `"Before "`, 73 | styles.Color: "red", 74 | styles.FontWeight: "bold", 75 | styles.Position: "absolute", 76 | styles.Left: "10px", 77 | styles.Top: "-20px", 78 | }, 79 | styles.PseudoAfter: { 80 | styles.Content: `" After"`, 81 | styles.Color: "blue", 82 | styles.FontWeight: "bold", 83 | styles.Position: "absolute", 84 | styles.Right: "10px", 85 | styles.Bottom: "-20px", 86 | }, 87 | }, 88 | }) 89 | 90 | // Composing the page 91 | pageContent := elem.Div(nil, 92 | elem.Button(attrs.Props{attrs.Class: buttonClass}, elem.Text("Hover Over Me")), 93 | elem.Div(attrs.Props{attrs.Class: animatedClass}, elem.Text("I animate!")), 94 | elem.Div(attrs.Props{attrs.Class: responsiveClass}, elem.Text("Resize the window")), 95 | elem.Div(attrs.Props{attrs.Class: pseudoElementClass}, elem.Text("I have pseudo-elements")), 96 | ) 97 | 98 | // Render with StyleManager 99 | return pageContent.RenderWithOptions(elem.RenderOptions{StyleManager: styleMgr}) 100 | } 101 | 102 | func main() { 103 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 104 | htmlContent := generateWebContent() // Assume this returns the HTML string 105 | w.Header().Set("Content-Type", "text/html") 106 | w.Write([]byte(htmlContent)) 107 | }) 108 | 109 | http.ListenAndServe(":8080", nil) 110 | } 111 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/chasefleming/elem-go 2 | 3 | go 1.21.1 4 | 5 | require github.com/stretchr/testify v1.8.4 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 6 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /htmx/README.md: -------------------------------------------------------------------------------- 1 | # `htmx` Integration with `elem-go` 2 | 3 | The `htmx` subpackage within `elem` provides a seamless way to integrate the [htmx](https://htmx.org/) library, allowing for easy creation of dynamic web applications. This document outlines how to use the `htmx` subpackage and its features. 4 | 5 | ## Table of Contents 6 | 7 | - [Introduction](#introduction) 8 | - [Usage](#usage) 9 | - [Creating Elements with htmx Attributes](#creating-elements-with-htmx-attributes) 10 | - [Supported htmx Attributes](#supported-htmx-attributes) 11 | - [Examples](#examples) 12 | 13 | ## Introduction 14 | 15 | The `htmx` subpackage offers constants and utility functions tailored for htmx-specific attributes. This makes it easier to add dynamic behaviors to your web elements without writing verbose attribute strings. 16 | 17 | ## Usage 18 | 19 | To utilize the `htmx` subpackage, import it alongside the main `elem` package: 20 | 21 | ```go 22 | import ( 23 | "github.com/chasefleming/elem-go" 24 | "github.com/chasefleming/elem-go/attrs" 25 | "github.com/chasefleming/elem-go/htmx" 26 | ) 27 | ``` 28 | 29 | ## Creating Elements with htmx Attributes 30 | 31 | With the `htmx` subpackage, you can effortlessly add htmx-specific attributes to your elements: 32 | 33 | ```go 34 | content := elem.Div(attrs.Props{ 35 | attrs.ID: "container", 36 | attrs.Class: "my-class", 37 | htmx.HXGet: "/path-to-new-content", 38 | htmx.HXTarget: "#content-div", 39 | }, 40 | elem.H1(nil, elem.Text("Hello, Elem!")), 41 | elem.Div(attrs.Props{attrs.ID: "content-div"}, elem.Text("Initial content")), 42 | ) 43 | ``` 44 | 45 | In this example, the main `div` has htmx attributes set to fetch content from `/path-to-new-content` and place it inside a `div` with the ID `content-div`. 46 | 47 | ## Supported `htmx` Attributes 48 | 49 | The `htmx` subpackage provides constants for commonly used `htmx` attributes: 50 | 51 | - **Request Modifiers** 52 | - `HXGet`: URL for GET requests. 53 | - `HXPost`: URL for POST requests. 54 | - `HXPut`: URL for PUT requests. 55 | - `HXDelete`: URL for DELETE requests. 56 | - `HXPatch`: URL for PATCH requests. 57 | 58 | - **Request Headers and Content-Type** 59 | - `HXHeaders`: Specifies request headers. 60 | - `HXContent`: Specifies the content type of the request. 61 | 62 | - **Request Parameters** 63 | - `HXParams`: Parameters to include with the request. 64 | - `HXValues`: Values to include with the request. 65 | 66 | - **Request Timeout and Retries** 67 | - `HXTimeout`: Timeout for the request. 68 | - `HXRetry`: Number of times to retry the request. 69 | - `HXRetryTimeout`: Timeout before retrying the request. 70 | 71 | - **Response Processing** 72 | - `HXSwap`: How to swap the content. 73 | - `HXTarget`: Where to place the content in the DOM. 74 | - `HXSwapOOB`: Out-of-band swapping. 75 | - `HXSelect`: CSS selector for element in returned HTML. 76 | - `HXExt`: htmx extensions to use. 77 | - `HXVals`: Values to process in the response. 78 | 79 | - **Events** 80 | - `HXTrigger`: Event that triggers the request. 81 | - `HXConfirm`: Confirmation message before sending the request. 82 | - `HXOn`: Event listener on the element. 83 | - `HXTriggeringElement`: Element that triggered the request. 84 | - `HXTriggeringEvent`: Event that triggered the request. 85 | 86 | - **Indicators** 87 | - `HXIndicator`: Element displayed as an indicator while processing. 88 | 89 | - **History** 90 | - `HXPushURL`: Pushes a new URL to the browser history. 91 | - `HXHistoryElt`: Element for history purposes. 92 | - `HXHistoryAttr`: Attribute for history purposes. 93 | 94 | - **Error Handling** 95 | - `HXBoost`: Enhances links and forms with AJAX. 96 | - `HXError`: Element for displaying error messages. 97 | 98 | - **Caching** 99 | - `HXCache`: Specifies caching behavior. 100 | 101 | ## Examples 102 | 103 | ### Loading Content on Button Click 104 | 105 | To create a button that loads content into a specific div when clicked: 106 | 107 | ```go 108 | button := elem.Button(attrs.Props{ 109 | htmx.HXGet: "/fetch-content", 110 | htmx.HXTarget: "#result-div", 111 | }, elem.Text("Load Content")) 112 | 113 | contentDiv := elem.Div(attrs.Props{attrs.ID: "result-div"}, elem.Text("Initial content")) 114 | 115 | pageContent := elem.Div(nil, button, contentDiv) 116 | ``` 117 | 118 | When the button is clicked, `htmx` will fetch content from the `/fetch-content` endpoint and replace the content inside the `#result-div`. 119 | 120 | ## Handling JSON Strings in Attributes 121 | 122 | When using attributes like `hx-vals` that require JSON strings, ensure the JSON is wrapped in single quotes. This is necessary for correct rendering by `htmx`. For example: 123 | 124 | ```go 125 | content := elem.Div(attrs.Props{ 126 | htmx.HXGet: "/example", 127 | htmx.HXVals: `'{"myVal": "My Value"}'`, 128 | }, elem.Text("Get Some HTML, Including A Value in the Request")) 129 | ``` -------------------------------------------------------------------------------- /htmx/htmx.go: -------------------------------------------------------------------------------- 1 | package htmx 2 | 3 | const ( 4 | // HTMX Reference: https://htmx.org/reference/ 5 | // Request Modifiers 6 | HXGet = "hx-get" 7 | HXPost = "hx-post" 8 | HXPut = "hx-put" 9 | HXDelete = "hx-delete" 10 | HXPatch = "hx-patch" 11 | 12 | // Request Headers, Content-Type, additional data and request control 13 | HXHeaders = "hx-headers" 14 | HXContent = "hx-content" 15 | HXInclude = "hx-include" 16 | HXRequest = "hx-request" 17 | HXSync = "hx-sync" 18 | HXValidate = "hx-validate" 19 | 20 | // Request Parameters 21 | HXParams = "hx-params" 22 | HXValues = "hx-values" 23 | 24 | // Request Timeout and Retries 25 | HXTimeout = "hx-timeout" 26 | HXRetry = "hx-retry" 27 | HXRetryTimeout = "hx-retry-timeout" 28 | 29 | // Response Processing 30 | HXSwap = "hx-swap" 31 | HXTarget = "hx-target" 32 | HXSwapOOB = "hx-swap-oob" 33 | HXSelect = "hx-select" 34 | HXSelectOOB = "hx-select-oob" 35 | HXExt = "hx-ext" 36 | HXVals = "hx-vals" 37 | 38 | // Events 39 | HXTrigger = "hx-trigger" 40 | HXConfirm = "hx-confirm" 41 | HXTriggeringElement = "hx-triggering-element" 42 | HXTriggeringEvent = "hx-triggering-event" 43 | 44 | // Indicators 45 | HXIndicator = "hx-indicator" 46 | 47 | // History 48 | HXPushURL = "hx-push-url" 49 | HXReplaceURL = "hx-replace-url" 50 | HXHistory = "hx-history" 51 | HXHistoryElt = "hx-history-elt" 52 | HXHistoryAttr = "hx-history-attr" 53 | 54 | // Error Handling 55 | HXBoost = "hx-boost" 56 | HXError = "hx-error" 57 | 58 | // Caching 59 | HXCache = "hx-cache" 60 | 61 | // HTMX Configuration 62 | HXDisable = "hx-disable" 63 | HXDisabledElt = "hx-disabled-elt" 64 | HXDisinherit = "hx-disinherit" 65 | HXEncoding = "hx-encoding" 66 | HXPreserve = "hx-preserve" 67 | HXPrompt = "hx-prompt" 68 | 69 | // Server side events 70 | HXSSE = "hx-sse" 71 | SSEConnect = "sse-connect" 72 | SSESwap = "sse-swap" 73 | 74 | // WebSockets 75 | HXWS = "hx-ws" 76 | WSConnect = "ws-connect" 77 | 78 | // HTMX Events, Reference: https://htmx.org/reference/#events 79 | // Reference for hx-on attribute: https://htmx.org/attributes/hx-on/ 80 | HXOn = "hx-on" 81 | HXOnAbort = "hx-on--abort" 82 | HXOnAfterOnLoad = "hx-on--after-on-load" 83 | HXOnAfterProcessNode = "hx-on--after-process-node" 84 | HXOnAfterRequest = "hx-on--after-request" 85 | HXOnAfterSettle = "hx-on--after-settle" 86 | HXOnAfterSwap = "hx-on--after-swap" 87 | HXOnBeforeCleanupElement = "hx-on--before-cleanup-element" 88 | HXOnBeforeOnLoad = "hx-on--before-on-load" 89 | HXOnBeforeProcessNode = "hx-on--before-process-node" 90 | HXOnBeforeRequest = "hx-on--before-request" 91 | HXOnBeforeSwap = "hx-on--before-swap" 92 | HXOnBeforeSend = "hx-on--before-send" 93 | HXOnConfigRequest = "hx-on--config-request" 94 | HXOnConfirm = "hx-on--confirm" 95 | HXOnHistoryCacheError = "hx-on--history-cache-error" 96 | HXOnHistoryCacheMiss = "hx-on--history-cache-miss" 97 | HXOnHistoryCacheMissError = "hx-on--history-cache-miss-error" 98 | HXOnHistoryCacheMissLoad = "hx-on--history-cache-miss-load" 99 | HXOnHistoryRestore = "hx-on--history-restore" 100 | HXOnBeforeHistorySave = "hx-on--before-history-save" 101 | HXOnLoad = "hx-on--load" 102 | HXOnNoSSESourceError = "hx-on--no-sse-source-error" 103 | HXOnOnLoadError = "hx-on--on-load-error" 104 | HXOnOOBAfterSwap = "hx-on--oob-after-swap" 105 | HXOnOOBErrorNoTarget = "hx-on--oob-error-no-target" 106 | HXOnPrompt = "hx-on--prompt" 107 | HXOnPushedIntoHistory = "hx-on--pushed-into-history" 108 | HXOnResponseError = "hx-on--response-error" 109 | HXOnSendError = "hx-on--send-error" 110 | HXOnSSEError = "hx-on--sse-error" 111 | HXOnSSEOpen = "hx-on--sse-open" 112 | HXOnSwapError = "hx-on--swap-error" 113 | HXOnTargetError = "hx-on--target-error" 114 | HXOnTimeout = "hx-on--timeout" 115 | HXOnValidationValidate = "hx-on--validation-validate" 116 | HXOnValidationFailed = "hx-on--validation-failed" 117 | HXOnValidationHalted = "hx-on--validation-halted" 118 | HXOnXHRAbort = "hx-on--xhr-abort" 119 | HXOnXHRLoadend = "hx-on--xhr-loadend" 120 | HXOnXHRLoadstart = "hx-on--xhr-loadstart" 121 | HXOnXHRProgress = "hx-on--xhr-progress" 122 | ) 123 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chasefleming/elem-go/778cb362f796ae5104a77ae37f5e87419e66b1e6/logo.png -------------------------------------------------------------------------------- /styles/README.md: -------------------------------------------------------------------------------- 1 | # `styles` Subpackage in `elem-go` 2 | 3 | The `styles` subpackage within `elem-go` offers enhanced functionality for CSS styling in Go-based web applications. This document provides a detailed overview of how to use the `styles` subpackage and its features. 4 | 5 | ## Table of Contents 6 | 7 | - [Introduction](#introduction) 8 | - [Usage](#usage) 9 | - [Styling Elements with `styles.Props`](#styling-elements-with-stylesprops) 10 | - [Features](#features) 11 | - [`Style` and `CSS` Functions](#style-and-css-functions) 12 | - [`Merge` Function](#merge-function) 13 | - [Type-Safe CSS Values](#type-safe-css-values) 14 | - [Advanced Styling with `StyleManager`](#advanced-styling-with-stylemanager) 15 | - [Key Features of `StyleManager`](#key-features-of-stylemanager) 16 | - [Example: Implementing a Hover Effect](#example-implementing-a-hover-effect) 17 | - [Detailed Usage](stylemanager/README.md) 18 | 19 | ## Introduction 20 | 21 | The `styles` subpackage provides a convenient way to define and manipulate CSS styles in Go. With utility functions and constants tailored for styling, it simplifies the process of applying styles to HTML elements. 22 | 23 | ## Usage 24 | 25 | To use the `styles` subpackage, import it alongside the main `elem` package: 26 | 27 | ```go 28 | import ( 29 | "github.com/chasefleming/elem-go" 30 | "github.com/chasefleming/elem-go/styles" 31 | ) 32 | ``` 33 | 34 | ## Styling Elements with `styles.Props` 35 | 36 | The `styles.Props` type allows for defining CSS properties in a structured, type-safe manner. It ensures that your style definitions are well-organized and easy to maintain. 37 | 38 | ### CSS Property Keys as Constants 39 | 40 | To further enhance type safety and reduce errors, the styles subpackage provides constants for CSS property keys. This means you don't have to rely on writing raw string literals for CSS properties, which are prone to typos and errors. Instead, you can use predefined constants that the package offers, ensuring correctness and saving time on debugging. 41 | 42 | ```go 43 | // Example of using constants for CSS properties 44 | buttonStyle := styles.Props{ 45 | styles.BackgroundColor: "#4CAF50", // Using constant instead of raw string 46 | styles.Border: "none", 47 | // ... other properties 48 | } 49 | ``` 50 | 51 | By using these constants, you can write more reliable and error-resistant style code in Go, making your development process smoother and more efficient. 52 | 53 | For a full list of available constants, see the [styles.go file](styles.go). 54 | 55 | ### Applying Styles to Elements 56 | 57 | Once you have defined your styles using `styles.Props`, you can convert them to an inline CSS string using the `ToInline` method. This inline CSS can then be applied directly to HTML elements. 58 | 59 | ```go 60 | // Define styles using styles.Props 61 | buttonStyle := styles.Props{ 62 | styles.BackgroundColor: "#4CAF50", 63 | styles.Border: "none", 64 | // ... other properties 65 | } 66 | 67 | // Convert styles to inline CSS 68 | inlineStyle := buttonStyle.ToInline() 69 | 70 | // Apply inline CSS to a button element 71 | button := elem.Button(attrs.Props{attrs.Style: inlineStyle}, elem.Text("Click Me")) 72 | ``` 73 | 74 | In this example, `buttonStyle` is first defined using `styles.Props` and then converted into an inline CSS string using `ToInline`. This string is used to set the style attribute of a button element. 75 | 76 | ## Features 77 | 78 | ### `Merge` Function 79 | 80 | The `Merge` method combines multiple style prop objects into one. It's useful for applying conditional styles or layering style sets. 81 | 82 | ```go 83 | // Example style definitions 84 | baseButtonStyle := styles.Props{ 85 | Padding: "10px 15px", 86 | Border: "none", 87 | FontWeight: "bold", 88 | } 89 | 90 | primaryStyles := styles.Props{ 91 | BackgroundColor: "blue", 92 | Color: "white", 93 | } 94 | 95 | secondaryStyles := styles.Props{ 96 | BackgroundColor: "red", 97 | Color: "white", 98 | } 99 | 100 | // Merging styles with the new Merge function 101 | primaryButtonStyles := styles.Merge(baseButtonStyle, primaryStyles) 102 | secondaryButtonStyles := styles.Merge(baseButtonStyle, secondaryStyles) 103 | ``` 104 | 105 | In the `Merge` function, later style objects take precedence over earlier ones for properties defined in multiple style objects. 106 | 107 | ### `Style` and `CSS` Functions 108 | 109 | These functions facilitate the embedding of CSS into HTML documents, particularly useful for creating