From dd51ba1089cde65d22ddd12439cf2503e6db2a45 Mon Sep 17 00:00:00 2001 From: Sebastiano Bellinzis Date: Fri, 22 Nov 2024 18:03:03 +0100 Subject: [PATCH] add examples and some qol changes throughout --- examples/tabs-htmx/content.gohtml | 44 +++++ examples/tabs-htmx/footer.gohtml | 1 + examples/tabs-htmx/index.gohtml | 17 ++ examples/tabs-htmx/main.go | 67 ++++++++ examples/tabs-htmx/tab1.gohtml | 1 + examples/tabs-htmx/tab2.gohtml | 1 + examples/tabs-htmx/tab3.gohtml | 1 + examples/tabs/content.gohtml | 18 ++ examples/tabs/footer.gohtml | 1 + examples/tabs/index.gohtml | 54 ++++++ examples/tabs/main.go | 66 ++++++++ examples/tabs/tab1.gohtml | 1 + examples/tabs/tab2.gohtml | 1 + examples/tabs/tab3.gohtml | 1 + js/htmx.partial.js | 9 +- js/standalone.js | 267 ++++++++++++++++++++++++++++++ partial.go | 84 +++++++--- template_functions.go | 7 +- 18 files changed, 614 insertions(+), 27 deletions(-) create mode 100644 examples/tabs-htmx/content.gohtml create mode 100644 examples/tabs-htmx/footer.gohtml create mode 100644 examples/tabs-htmx/index.gohtml create mode 100644 examples/tabs-htmx/main.go create mode 100644 examples/tabs-htmx/tab1.gohtml create mode 100644 examples/tabs-htmx/tab2.gohtml create mode 100644 examples/tabs-htmx/tab3.gohtml create mode 100644 examples/tabs/content.gohtml create mode 100644 examples/tabs/footer.gohtml create mode 100644 examples/tabs/index.gohtml create mode 100644 examples/tabs/main.go create mode 100644 examples/tabs/tab1.gohtml create mode 100644 examples/tabs/tab2.gohtml create mode 100644 examples/tabs/tab3.gohtml create mode 100644 js/standalone.js diff --git a/examples/tabs-htmx/content.gohtml b/examples/tabs-htmx/content.gohtml new file mode 100644 index 0000000..15eddcb --- /dev/null +++ b/examples/tabs-htmx/content.gohtml @@ -0,0 +1,44 @@ +
+ + + +
+ {{ selection }} +
+ +
The handler:
+ +
func (a *App) home(w http.ResponseWriter, r *http.Request) {
+	// the tabs for this page.
+	selectMap := map[string]*partial.Partial{
+		"tab1": partial.New("tab1.gohtml"),
+		"tab2": partial.New("tab2.gohtml"),
+		"tab3": partial.New("tab3.gohtml"),
+	}
+
+	// layout, footer, index could be abstracted away and shared over multiple handlers within the same module, for instance.
+	layout := a.PartialService.NewLayout()
+	footer := partial.NewID("footer", "footer.gohtml")
+	index := partial.NewID("index", "index.gohtml").WithOOB(footer)
+
+	content := partial.NewID("content", "content.gohtml").WithSelectMap("tab1", selectMap)
+
+	// set the layout content and wrap it with the main template
+	layout.Set(content).Wrap(index)
+
+	err := layout.WriteWithRequest(r.Context(), w, r)
+	if err != nil {
+		http.Error(w, fmt.Errorf("error rendering layout: %w", err).Error(), http.StatusInternalServerError)
+	}
+}
+
\ No newline at end of file diff --git a/examples/tabs-htmx/footer.gohtml b/examples/tabs-htmx/footer.gohtml new file mode 100644 index 0000000..4d52c23 --- /dev/null +++ b/examples/tabs-htmx/footer.gohtml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/tabs-htmx/index.gohtml b/examples/tabs-htmx/index.gohtml new file mode 100644 index 0000000..576ae1a --- /dev/null +++ b/examples/tabs-htmx/index.gohtml @@ -0,0 +1,17 @@ + + + + Tab Example + + + + + + +
+ {{ child "content" }} +
+ + {{ child "footer" }} + + \ No newline at end of file diff --git a/examples/tabs-htmx/main.go b/examples/tabs-htmx/main.go new file mode 100644 index 0000000..59e9dc4 --- /dev/null +++ b/examples/tabs-htmx/main.go @@ -0,0 +1,67 @@ +package main + +import ( + "fmt" + "github.com/donseba/go-partial" + "log/slog" + "net/http" +) + +type ( + App struct { + PartialService *partial.Service + } +) + +func main() { + logger := slog.Default() + + app := &App{ + PartialService: partial.NewService(&partial.Config{ + PartialHeader: "HX-Target", + Logger: logger, + }), + } + + mux := http.NewServeMux() + + mux.Handle("GET /files/", http.StripPrefix("/files/", http.FileServer(http.Dir("./files")))) + + mux.HandleFunc("GET /", app.home) + + server := &http.Server{ + Addr: ":8080", + Handler: mux, + } + + logger.Info("starting server on :8080") + err := server.ListenAndServe() + if err != nil { + logger.Error("error starting server", "error", err) + } +} + +// super simple example of how to use the partial service to render a layout with a content partial +func (a *App) home(w http.ResponseWriter, r *http.Request) { + // the tabs for this page. + selectMap := map[string]*partial.Partial{ + "tab1": partial.New("tab1.gohtml"), + "tab2": partial.New("tab2.gohtml"), + "tab3": partial.New("tab3.gohtml"), + } + + // layout, footer, index could be abstracted away and shared over multiple handlers within the same module, for instance. + layout := a.PartialService.NewLayout() + footer := partial.NewID("footer", "footer.gohtml") + index := partial.NewID("index", "index.gohtml").WithOOB(footer) + + content := partial.NewID("content", "content.gohtml").WithSelectMap("tab1", selectMap) + + // set the layout content and wrap it with the main template + layout.Set(content).Wrap(index) + + err := layout.WriteWithRequest(r.Context(), w, r) + if err != nil { + http.Error(w, fmt.Errorf("error rendering layout: %w", err).Error(), http.StatusInternalServerError) + } +} diff --git a/examples/tabs-htmx/tab1.gohtml b/examples/tabs-htmx/tab1.gohtml new file mode 100644 index 0000000..6805204 --- /dev/null +++ b/examples/tabs-htmx/tab1.gohtml @@ -0,0 +1 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque pulvinar massa pulvinar eros molestie pellentesque. Mauris facilisis libero leo, non cursus ex facilisis a. Donec tempor metus non ex efficitur, in vestibulum nunc dapibus. Etiam pretium tortor magna, eget tempus lacus varius et. Sed vestibulum velit sed odio facilisis dignissim. Fusce in dolor ac enim consequat cursus et id lorem. Donec convallis lorem dignissim tristique pellentesque. Etiam ultricies sed mauris vitae hendrerit. Maecenas accumsan ligula vel libero faucibus, in lacinia justo ullamcorper. Etiam pulvinar ex ac odio posuere bibendum. Pellentesque ipsum justo, finibus in egestas ac, dignissim varius neque. Fusce laoreet consequat diam, ut imperdiet libero laoreet quis. Aenean tincidunt a tellus vel posuere. Aenean vel elementum mauris. Pellentesque erat tortor, lobortis ac ullamcorper vitae, sagittis vel arcu. Morbi malesuada, justo ut dignissim mollis, diam nunc consequat enim, nec facilisis ex felis ac dui. \ No newline at end of file diff --git a/examples/tabs-htmx/tab2.gohtml b/examples/tabs-htmx/tab2.gohtml new file mode 100644 index 0000000..2e9f0dd --- /dev/null +++ b/examples/tabs-htmx/tab2.gohtml @@ -0,0 +1 @@ +Suspendisse blandit, nisl ac porta auctor, mauris nisi elementum enim, non laoreet mauris justo eu sem. Cras eget urna id libero posuere luctus vitae ac lacus. Nunc varius iaculis leo, eu ultrices ligula aliquam non. Suspendisse lacinia magna enim, a ornare leo placerat in. Sed accumsan sapien ligula, sed maximus enim rutrum ut. Fusce leo purus, vestibulum nec dui accumsan, ullamcorper viverra risus. Duis fermentum orci augue, non sagittis orci tempor at. Praesent quis ipsum fermentum, consequat massa at, finibus eros. Praesent nec massa nisi. Proin sed feugiat eros. \ No newline at end of file diff --git a/examples/tabs-htmx/tab3.gohtml b/examples/tabs-htmx/tab3.gohtml new file mode 100644 index 0000000..e44ce55 --- /dev/null +++ b/examples/tabs-htmx/tab3.gohtml @@ -0,0 +1 @@ +Morbi elementum varius suscipit. Phasellus congue feugiat sem, vel sodales odio varius eu. Fusce non ex nisi. Aenean nisi dui, tincidunt nec est quis, mollis tempus libero. Fusce placerat pharetra diam, ac mollis turpis bibendum ac. Nullam pulvinar venenatis lacinia. Nam vel quam non ante dignissim bibendum id et ex. Suspendisse potenti. \ No newline at end of file diff --git a/examples/tabs/content.gohtml b/examples/tabs/content.gohtml new file mode 100644 index 0000000..56d298a --- /dev/null +++ b/examples/tabs/content.gohtml @@ -0,0 +1,18 @@ +
+ + + +
+ {{ selection }} +
+
\ No newline at end of file diff --git a/examples/tabs/footer.gohtml b/examples/tabs/footer.gohtml new file mode 100644 index 0000000..2fdbef2 --- /dev/null +++ b/examples/tabs/footer.gohtml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/tabs/index.gohtml b/examples/tabs/index.gohtml new file mode 100644 index 0000000..526d399 --- /dev/null +++ b/examples/tabs/index.gohtml @@ -0,0 +1,54 @@ + + + + Tab Example + + + + + +
+ {{ child "content" }} +
+ +
+
(rendered on load at : {{ formatDate now "15:04:05" }})
+
What the handler looks like:
+ +
func (a *App) home(w http.ResponseWriter, r *http.Request) {
+	// the tabs for this page.
+	selectMap := map[string]*partial.Partial{
+		"tab1": partial.New("tab1.gohtml"),
+		"tab2": partial.New("tab2.gohtml"),
+		"tab3": partial.New("tab3.gohtml"),
+	}
+
+	// layout, footer, index could be abstracted away and shared over multiple handlers within the same module, for instance.
+	layout := a.PartialService.NewLayout()
+	footer := partial.NewID("footer", "footer.gohtml")
+	index := partial.NewID("index", "index.gohtml").WithOOB(footer)
+
+	content := partial.NewID("content", "content.gohtml").WithSelectMap("tab1", selectMap)
+
+	// set the layout content and wrap it with the main template
+	layout.Set(content).Wrap(index)
+
+	err := layout.WriteWithRequest(r.Context(), w, r)
+	if err != nil {
+		http.Error(w, fmt.Errorf("error rendering layout: %w", err).Error(), http.StatusInternalServerError)
+	}
+}
+
+ + {{ child "footer" }} + + + + \ No newline at end of file diff --git a/examples/tabs/main.go b/examples/tabs/main.go new file mode 100644 index 0000000..8ff19a3 --- /dev/null +++ b/examples/tabs/main.go @@ -0,0 +1,66 @@ +package main + +import ( + "fmt" + "github.com/donseba/go-partial" + "log/slog" + "net/http" +) + +type ( + App struct { + PartialService *partial.Service + } +) + +func main() { + logger := slog.Default() + + app := &App{ + PartialService: partial.NewService(&partial.Config{ + Logger: logger, + }), + } + + mux := http.NewServeMux() + + mux.Handle("GET /js/", http.StripPrefix("/js/", http.FileServer(http.Dir("../../js")))) + + mux.HandleFunc("GET /", app.home) + + server := &http.Server{ + Addr: ":8080", + Handler: mux, + } + + logger.Info("starting server on :8080") + err := server.ListenAndServe() + if err != nil { + logger.Error("error starting server", "error", err) + } +} + +// super simple example of how to use the partial service to render a layout with a content partial +func (a *App) home(w http.ResponseWriter, r *http.Request) { + // the tabs for this page. + selectMap := map[string]*partial.Partial{ + "tab1": partial.New("tab1.gohtml"), + "tab2": partial.New("tab2.gohtml"), + "tab3": partial.New("tab3.gohtml"), + } + + // layout, footer, index could be abstracted away and shared over multiple handlers within the same module, for instance. + layout := a.PartialService.NewLayout() + footer := partial.NewID("footer", "footer.gohtml") + index := partial.NewID("index", "index.gohtml").WithOOB(footer) + + content := partial.NewID("content", "content.gohtml").WithSelectMap("tab1", selectMap) + + // set the layout content and wrap it with the main template + layout.Set(content).Wrap(index) + + err := layout.WriteWithRequest(r.Context(), w, r) + if err != nil { + http.Error(w, fmt.Errorf("error rendering layout: %w", err).Error(), http.StatusInternalServerError) + } +} diff --git a/examples/tabs/tab1.gohtml b/examples/tabs/tab1.gohtml new file mode 100644 index 0000000..6805204 --- /dev/null +++ b/examples/tabs/tab1.gohtml @@ -0,0 +1 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque pulvinar massa pulvinar eros molestie pellentesque. Mauris facilisis libero leo, non cursus ex facilisis a. Donec tempor metus non ex efficitur, in vestibulum nunc dapibus. Etiam pretium tortor magna, eget tempus lacus varius et. Sed vestibulum velit sed odio facilisis dignissim. Fusce in dolor ac enim consequat cursus et id lorem. Donec convallis lorem dignissim tristique pellentesque. Etiam ultricies sed mauris vitae hendrerit. Maecenas accumsan ligula vel libero faucibus, in lacinia justo ullamcorper. Etiam pulvinar ex ac odio posuere bibendum. Pellentesque ipsum justo, finibus in egestas ac, dignissim varius neque. Fusce laoreet consequat diam, ut imperdiet libero laoreet quis. Aenean tincidunt a tellus vel posuere. Aenean vel elementum mauris. Pellentesque erat tortor, lobortis ac ullamcorper vitae, sagittis vel arcu. Morbi malesuada, justo ut dignissim mollis, diam nunc consequat enim, nec facilisis ex felis ac dui. \ No newline at end of file diff --git a/examples/tabs/tab2.gohtml b/examples/tabs/tab2.gohtml new file mode 100644 index 0000000..2e9f0dd --- /dev/null +++ b/examples/tabs/tab2.gohtml @@ -0,0 +1 @@ +Suspendisse blandit, nisl ac porta auctor, mauris nisi elementum enim, non laoreet mauris justo eu sem. Cras eget urna id libero posuere luctus vitae ac lacus. Nunc varius iaculis leo, eu ultrices ligula aliquam non. Suspendisse lacinia magna enim, a ornare leo placerat in. Sed accumsan sapien ligula, sed maximus enim rutrum ut. Fusce leo purus, vestibulum nec dui accumsan, ullamcorper viverra risus. Duis fermentum orci augue, non sagittis orci tempor at. Praesent quis ipsum fermentum, consequat massa at, finibus eros. Praesent nec massa nisi. Proin sed feugiat eros. \ No newline at end of file diff --git a/examples/tabs/tab3.gohtml b/examples/tabs/tab3.gohtml new file mode 100644 index 0000000..e44ce55 --- /dev/null +++ b/examples/tabs/tab3.gohtml @@ -0,0 +1 @@ +Morbi elementum varius suscipit. Phasellus congue feugiat sem, vel sodales odio varius eu. Fusce non ex nisi. Aenean nisi dui, tincidunt nec est quis, mollis tempus libero. Fusce placerat pharetra diam, ac mollis turpis bibendum ac. Nullam pulvinar venenatis lacinia. Nam vel quam non ante dignissim bibendum id et ex. Suspendisse potenti. \ No newline at end of file diff --git a/js/htmx.partial.js b/js/htmx.partial.js index 2abd5ad..2815d38 100644 --- a/js/htmx.partial.js +++ b/js/htmx.partial.js @@ -1,17 +1,12 @@ (function() { htmx.on('htmx:configRequest', function(event) { let element = event.detail.elt; - let partialValue = element.getAttribute('hx-partial'); - if (partialValue !== null) { - event.detail.headers['X-Partial'] = partialValue; - } - - let selectValue = element.getAttribute('hx-select'); + let selectValue = element.getAttribute('x-select'); if (selectValue !== null) { event.detail.headers['X-Select'] = selectValue; } - let actionValue = element.getAttribute('hx-action'); + let actionValue = element.getAttribute('x-action'); if (actionValue !== null) { event.detail.headers['X-Action'] = actionValue; } diff --git a/js/standalone.js b/js/standalone.js new file mode 100644 index 0000000..797e8e7 --- /dev/null +++ b/js/standalone.js @@ -0,0 +1,267 @@ +class XPartial { + constructor(options = {}) { + // Define the custom action attributes + this.actionAttributes = ['x-get', 'x-post', 'x-put', 'x-delete']; + // Optionally, allow extending action attributes via options + if (options.additionalAttributes) { + this.actionAttributes.push(...options.additionalAttributes); + } + + // Default swap option: 'outerHTML' or 'innerHTML' + this.defaultSwapOption = options.defaultSwapOption || 'outerHTML'; // Can be overridden per element + + // Bind methods to ensure correct 'this' context + this.scanForElements = this.scanForElements.bind(this); + this.setupElement = this.setupElement.bind(this); + this.handleAction = this.handleAction.bind(this); + this.handleOobSwapping = this.handleOobSwapping.bind(this); + + // Initialize the handler on DOMContentLoaded using an arrow function to avoid passing the event object + document.addEventListener('DOMContentLoaded', () => this.scanForElements()); + } + + /** + * Scans the entire document or a specific container for elements with defined action attributes. + * @param {HTMLElement | Document} [container=document] + */ + scanForElements(container = document) { + const selector = this.actionAttributes.map(attr => `[${attr}]`).join(','); + const elements = container.querySelectorAll(selector); + elements.forEach(this.setupElement); + } + + /** + * Sets up an individual element by attaching the appropriate event listener. + * @param {HTMLElement} element + */ + setupElement(element) { + const trigger = element.getAttribute('x-trigger') || 'click'; + // Avoid attaching multiple listeners + if (!element.__xRequestHandlerInitialized) { + element.addEventListener(trigger, (event) => { + event.preventDefault(); + this.handleAction(event, element); + }); + // Mark the element as initialized + element.__xRequestHandlerInitialized = true; + } + } + + /** + * Handles the action when an element is triggered. + * @param {Event} event + * @param {HTMLElement} element + */ + async handleAction(event, element) { + const method = this.getMethod(element); + const url = element.getAttribute(`x-${method.toLowerCase()}`); + + if (!url) { + console.error(`No URL specified for method ${method} on element:`, element); + return; + } + + const headers = this.getHeaders(element); // Includes X-Partial + const partialId = element.getAttribute('x-partial'); // Replaces x-target + const select = element.getAttribute('x-select'); // For UI state management + + if (!partialId) { + console.error(`Element does not have 'x-partial' attribute:`, element); + return; + } + + const partialSelector = `#${partialId}`; + const targetElement = document.querySelector(partialSelector); + + if (!targetElement) { + console.error(`No element found with ID '${partialId}' for 'x-partial' targeting.`); + return; + } + + try { + const responseText = await this.performRequest(method, url, headers, element); + + // Parse the response HTML + const parser = new DOMParser(); + const doc = parser.parseFromString(responseText, 'text/html'); + + // Extract OOB elements + const oobElements = Array.from(doc.querySelectorAll('[x-swap-oob]')); + // Remove OOB elements from the main content to prevent duplication + oobElements.forEach(el => el.parentNode.removeChild(el)); + + // Get the remaining HTML as the main content + const mainContent = doc.body.innerHTML; + + // Replace the target's innerHTML with main content + targetElement.innerHTML = mainContent; + // Re-scan the newly added content for XRequestHandler elements + this.scanForElements(targetElement); + + // Handle OOB swapping with the extracted OOB elements + this.handleOobSwapping(oobElements); + + // Handle 'x-select' attribute if present + if (select) { + this.handleSelect(select, element); + } + } catch (error) { + console.error('Request failed:', error); + // Optionally, handle error display in the UI + targetElement.innerHTML = `
An error occurred: ${error.message}
`; + } + } + + /** + * Determines the HTTP method based on the element's attributes. + * @param {HTMLElement} element + * @returns {string} HTTP method + */ + getMethod(element) { + for (const attr of this.actionAttributes) { + if (element.hasAttribute(attr)) { + return attr.replace('x-', '').toUpperCase(); + } + } + return 'GET'; // Default method + } + + /** + * Constructs headers from the element's attributes. + * @param {HTMLElement} element + * @returns {Object} Headers object + */ + getHeaders(element) { + const headers = {}; + const action = element.getAttribute('x-action'); + const select = element.getAttribute('x-select'); + const partial = element.getAttribute('x-partial'); // Used for frontend targeting + + if (action) headers['X-Action'] = action; + if (select) headers['X-Select'] = select; + if (partial) headers['X-Partial'] = partial; // Send to backend without '#' + + // Add any additional custom headers if needed + const customHeaders = element.getAttribute('x-headers'); + if (customHeaders) { + try { + const parsedHeaders = JSON.parse(customHeaders); + Object.assign(headers, parsedHeaders); + } catch (e) { + console.error('Invalid JSON in x-headers:', e); + } + } + + return headers; + } + + /** + * Performs the HTTP request using Fetch API. + * @param {string} method + * @param {string} url + * @param {Object} headers + * @param {HTMLElement} element + * @returns {Promise} Response text + */ + performRequest(method, url, headers, element) { + const options = { + method, + headers, + credentials: 'same-origin', + }; + + if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) { + let body = null; + + if (element.tagName === 'FORM') { + body = new FormData(element); + } else { + const closestForm = element.closest('form'); + if (closestForm) { + body = new FormData(closestForm); + } + } + + if (body) { + options.body = body; + } + } + + return fetch(url, options).then(response => { + if (!response.ok) { + return response.text().then(text => { + throw new Error(`HTTP error ${response.status}: ${text}`); + }); + } + return response.text(); + }); + } + + /** + * Handles the 'x-select' attribute if used for additional logic. + * This method can be customized based on what 'x-select' is intended to do. + * @param {string} select + * @param {HTMLElement} element + */ + handleSelect(select, element) { + // Example implementation: Add a 'selected' class to the element + const selectedElements = document.querySelectorAll(`[x-select="${select}"]`); + selectedElements.forEach(el => { + el.classList.remove('selected'); + }); + // Add 'selected' class to the current element + element.classList.add('selected'); + } + + /** + * Handles Out-of-Band (OOB) swapping by processing an array of OOB elements. + * Replaces existing elements in the document with the new content based on matching IDs. + * @param {HTMLElement[]} oobElements + */ + handleOobSwapping(oobElements) { + oobElements.forEach(oobElement => { + const targetId = oobElement.getAttribute('id'); + if (!targetId) { + console.error('OOB element does not have an ID:', oobElement); + return; + } + + const swapOption = oobElement.getAttribute('x-swap-oob') || this.defaultSwapOption; + const existingElement = document.getElementById(targetId); + + if (!existingElement) { + console.error(`No existing element found with ID '${targetId}' for OOB swapping.`); + return; + } + + if (swapOption === 'outerHTML') { + existingElement.outerHTML = oobElement.outerHTML; + } else if (swapOption === 'innerHTML') { + existingElement.innerHTML = oobElement.innerHTML; + } else { + console.error(`Invalid x-swap-oob option '${swapOption}' on element with ID '${targetId}'. Use 'outerHTML' or 'innerHTML'.`); + return; + } + + // After swapping, initialize any new elements within the replaced content + const newElement = document.getElementById(targetId); + if (newElement) { + this.scanForElements(newElement); + } + }); + } + + /** + * Allows manually re-scanning a specific container for XRequestHandler elements. + * Useful when dynamically adding content to the DOM. + * @param {HTMLElement} container + */ + refresh(container = document) { + this.scanForElements(container); + } +} + +// Initialize the handler with optional configuration +const xRequestHandler = new XRequestHandler({ + defaultSwapOption: 'outerHTML' // Default swap option: 'outerHTML' or 'innerHTML' +}); diff --git a/partial.go b/partial.go index f23a659..309e02d 100644 --- a/partial.go +++ b/partial.go @@ -23,18 +23,22 @@ var ( mutexCache = sync.Map{} // protectedFunctionNames is a set of function names that are protected from being overridden protectedFunctionNames = map[string]struct{}{ - "action": {}, - "actionHeader": {}, - "child": {}, - "context": {}, - "partialHeader": {}, - "requestedPartial": {}, - "requestedAction": {}, - "requestedSelect": {}, - "selectHeader": {}, - "selection": {}, - "swapOOB": {}, - "url": {}, + "action": {}, + "actionHeader": {}, + "child": {}, + "context": {}, + "ifRequestedAction": {}, + "ifRequestedPartial": {}, + "ifRequestedSelect": {}, + "ifSwapOOB": {}, + "partialHeader": {}, + "requestedPartial": {}, + "requestedAction": {}, + "requestedSelect": {}, + "selectHeader": {}, + "selection": {}, + "swapOOB": {}, + "url": {}, } ) @@ -78,6 +82,8 @@ type ( Ctx context.Context // URL is the URL of the request URL *url.URL + // Request contains the http.Request + Request *http.Request // Data contains the data specific to this partial Data map[string]any // Service contains global data available to all partials @@ -348,10 +354,6 @@ func (p *Partial) getFuncMap() template.FuncMap { func (p *Partial) getFuncs(data *Data) template.FuncMap { funcs := p.getFuncMap() - funcs["swapOOB"] = func() bool { - return p.swapOOB - } - funcs["child"] = childFunc(p, data) funcs["selection"] = selectionFunc(p, data) funcs["action"] = actionFunc(p, data) @@ -372,20 +374,62 @@ func (p *Partial) getFuncs(data *Data) template.FuncMap { return p.getRequestedPartial() } + funcs["ifRequestedPartial"] = func(out any, in ...string) any { + for _, v := range in { + if v == p.getRequestedPartial() { + return out + } + } + return nil + } + funcs["selectHeader"] = func() string { return p.getSelectHeader() } funcs["requestedSelect"] = func() string { + if p.getRequestedSelect() == "" { + return p.selection.Default + } return p.getRequestedSelect() } + funcs["ifRequestedSelect"] = func(out any, in ...string) any { + for _, v := range in { + if v == p.getRequestedSelect() { + return out + } + } + return nil + } + funcs["actionHeader"] = func() string { return p.getActionHeader() } funcs["requestedAction"] = func() string { - return p.getRequestedAction() + return p.GetRequestedAction() + } + + funcs["ifRequestedAction"] = func(out any, in ...string) any { + for _, v := range in { + if v == p.GetRequestedAction() { + return out + } + } + return nil + } + + funcs["swapOOB"] = func() bool { + return p.swapOOB + } + + funcs["ifSwapOOB"] = func(v string) template.HTML { + if p.swapOOB { + return template.HTML("x-swap-oob=\" + v + \"") + } + // Return an empty trusted HTML instead of a plain empty string + return template.HTML("") } return funcs @@ -499,12 +543,12 @@ func (p *Partial) getRequestedPartial() string { return "" } -func (p *Partial) getRequestedAction() string { +func (p *Partial) GetRequestedAction() string { if p.requestedAction != "" { return p.requestedAction } if p.parent != nil { - return p.parent.getRequestedAction() + return p.parent.GetRequestedAction() } return "" } @@ -525,6 +569,7 @@ func (p *Partial) renderWithTarget(ctx context.Context, r *http.Request) (templa if err != nil { return "", err } + // Render OOB children of parent if necessary if p.parent != nil { oobOut, oobErr := p.parent.renderOOBChildren(ctx, r, true) @@ -606,6 +651,7 @@ func (p *Partial) renderSelf(ctx context.Context, r *http.Request) (template.HTM data := &Data{ URL: currentURL, + Request: r, Ctx: ctx, Data: p.data, Service: p.getGlobalData(), diff --git a/template_functions.go b/template_functions.go index 7e651ef..e3c87c1 100644 --- a/template_functions.go +++ b/template_functions.go @@ -26,6 +26,7 @@ var DefaultTemplateFuncMap = template.FuncMap{ "replace": strings.Replace, "split": strings.Split, "join": strings.Join, + "stringSlice": stringSlice, "title": title, "substr": substr, "ucfirst": ucfirst, @@ -65,6 +66,10 @@ func ucfirst(s string) string { return string(runes) } +func stringSlice(values ...string) []string { + return values +} + // title capitalizes the first character of each word in the string. func title(s string) string { if s == "" { @@ -168,7 +173,7 @@ func selectionFunc(p *Partial, data *Data) func() template.HTML { } selectedPartial.fs = p.fs - selectedPartial.parent = p + //selectedPartial.parent = p html, err := selectedPartial.renderSelf(data.Ctx, p.getRequest()) if err != nil {