goship.it

Combobox

Code

type ComboboxProps struct {
	Label    string
	Name     string
	URL      string
	Options  []string
	Selected []string
}

templ Combobox(props ComboboxProps) {
	<details class="dropdown w-full max-w-md min-h-8">
		<summary
			class={
				"cursor-pointer flex space-x-2 w-full rounded-box",
				"border border-base-content py-1 px-2",
			}
		>
			<span class="text-sm text-nowrap">{ props.Label }</span>
			<div class="w-full flex items-center space-x-1">
				<div
					id={ fmt.Sprintf("%s_selections", props.Name) }
					class="w-full grid-flow-col-dense"
				>
					for _, s := range props.Selected {
						@ComboBadge(props.Name, s)
					}
				</div>
			</div>
		</summary>
		<ul class="menu dropdown-content bg-base-200 rounded-box z-50 p-2 shadow-sm">
			for _, opt := range props.Options {
				<li>
					<label class="label cursor-pointer space-x-2">
						<span class="label-text">
							{ opt }
						</span>
						<input
							hx-post={ fmt.Sprintf(props.URL, props.Name, url.PathEscape(opt)) }
							hx-target={ fmt.Sprintf("#%s_selections", props.Name) }
							hx-swap="beforeend"
							type="checkbox"
							name={ props.Name }
							class={ "checkbox" }
						/>
					</label>
				</li>
			}
		</ul>
		<script data-checkbox-name={ props.Name } type="text/javascript">
			var name = document.currentScript.getAttribute("data-checkbox-name");
			((name) => {
				document.addEventListener("htmx:configRequest", (evt) => {
					if (evt.target.getAttribute("name") === name && !evt.target.checked) {
						// prevent htmx request when checkbox is unchecked
						evt.preventDefault()

						// remove from selected elements
						let label = evt.target.closest("label")
						if (label !== null && label !== undefined) {
							let span = label.querySelector("span.label-text")
							let value = span.innerText
							let input = document.querySelector(`input[value="${value}"]`)
							if (input.getAttribute("name") === name) {
								input.closest("div").remove()
							}
						}
					}
				})
			})(name);
		</script>
	</details>
}

templ ComboBadge(name, value string) {
	<div class="ml-2 badge badge-neutral p-1 text-nowrap select-none">
		<input type="hidden" name={ name } value={ value }/>
		<span>{ value }</span>
		<button
			onclick="uncheckAndRemoveBadge(event)"
			class="ml-1 btn btn-xs btn-circle btn-ghost"
		>
			@crossIcon()
		</button>
		<script>
			function uncheckAndRemoveBadge(evt) {
				var div = evt.target.parentElement
				while (div.nodeName !== "DIV") {
					div = div.parentElement
				}
				let input = div.querySelector("input[type=hidden]")
				let name = input.getAttribute("name")
				let labelText = input.value

				let details = div.closest("details")
				let ul = details.querySelector("ul")
				let checkboxes = ul.querySelectorAll(`input[name="${name}"]`)
				checkboxes.forEach((cb) => {
					if (cb.checked) {
						let label = cb.parentElement
						label.querySelectorAll("span.label-text").forEach((el) => {
							if (el.innerHTML === labelText) {
								cb.checked = false
							}
						})
					}
				})
				div.remove()
			}
		</script>
	</div>
}

templ crossIcon() {
	<svg
		class="h-3 w-3"
		viewBox="0 0 25 25"
		version="1.1"
		xmlns="http://www.w3.org/2000/svg"
		xmlns:xlink="http://www.w3.org/1999/xlink"
		xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"
	>
		<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
			<g class="fill-base-content" id="Icon-Set-Filled" sketch:type="MSLayerGroup" transform="translate(-469.000000, -1041.000000)">
				<path d="M487.148,1053.48 L492.813,1047.82 C494.376,1046.26 494.376,1043.72 492.813,1042.16 C491.248,1040.59 488.712,1040.59 487.148,1042.16 L481.484,1047.82 L475.82,1042.16 C474.257,1040.59 471.721,1040.59 470.156,1042.16 C468.593,1043.72 468.593,1046.26 470.156,1047.82 L475.82,1053.48 L470.156,1059.15 C468.593,1060.71 468.593,1063.25 470.156,1064.81 C471.721,1066.38 474.257,1066.38 475.82,1064.81 L481.484,1059.15 L487.148,1064.81 C488.712,1066.38 491.248,1066.38 492.813,1064.81 C494.376,1063.25 494.376,1060.71 492.813,1059.15 L487.148,1053.48" sketch:type="MSShapeGroup"></path>
			</g>
		</g>
	</svg>
}

Examples

templ BasicCombobox() {
	<div class="h-96">
		<form hx-post="/combobox-submit/example_combo" class="space-x-4">
			@components.Combobox(components.ComboboxProps{
				Name:  "example_combo",
				Label: "Example",
				URL:   "/combobox/%s/%s",
				Options: []string{
					"Thing 1", "Thing 2", "Thing 3", "Thing 4", "Thing 5", "Thing 6", "Thing 7",
				},
			})
			<button type="submit" class="btn btn-sm btn-primary">Submit</button>
			<script>
				// update the form to only include half of the input values
				// corresponding to the combobox's name since it will contain enabled checkbox values as well
				((form) => {
					form.addEventListener("htmx:configRequest", (evt) => {
						let values = evt.detail.parameters["example_combo"]
						if (values !== undefined) {
							evt.detail.parameters["example_combo"] = values.slice(0, Math.floor(values.length / 2))
						}
					})
				})(document.currentScript.parentElement)
			</script>
		</form>
	</div>
}
func PostCombobox(c echo.Context) error {
	name := c.Param("name")
	value := c.Param("value")

	return render(c, http.StatusOK, components.ComboBadge(name, value))
}

func PostComboboxSubmit(c echo.Context) error {
	urlValues, err := c.FormParams()
	if err != nil {
		return newErrorToast(http.StatusUnprocessableEntity, "unable to parse form data")
	}

	name := c.Param("name")
	comboboxPostData := urlValues[name]

	return renderInfoFade(c, http.StatusOK, comboboxPostData)
}