When working with the library, i struggled a bit with the custom syntax for events and htmx attributes. I looked into the merits of this custom syntax and am wondering if it wouldn't be better to drop it and use default html/htmx syntax instead.
Below is a thorough analysis of the situation. Very curious to hear how you look at this.
Current Situation
This library currently uses a custom @ prefix syntax for event handlers and HTMX attributes:
<!-- Current syntax -->
<c-button @click="handleClick()">Click</c-button>
<c-button @hx-post="/api/data" @hx-target="#result">Submit</c-button>
<!-- Standard HTML/HTMX syntax -->
<button onclick="handleClick()">Click</button>
<button hx-post="/api/data" hx-target="#result">Submit</button>
What the Custom Syntax Currently Does
The @ prefix provides these benefits:
-
Validation Bypass - Event attributes with @ skip strict attribute validation, so you don't need to define every possible event in each component's attribute list.
-
Centralized Handling - All events go through the _event_mixin.j2 macro, providing consistent processing across components.
Important: The @ syntax does NOT add any runtime capabilities. It's purely a template preprocessing convenience that outputs standard HTML:
@click="..." → onclick="..."
@hx-post="..." → hx-post="..."
It was thought that Jinja variable resolution was a benefit of the @ syntax. However, code analysis reveals that Jinja variable resolution works for ANY attribute (with or without @). The preprocessor detects {{ in any attribute value and applies the same resolution logic:
# From html_parser.py lines 369-377
elif '{{' in value or '{%' in value:
# Regular attribute with Jinja expressions
# [Creates template variable and resolves it]
This means:
onclick="func('{{ var }}')" would get Jinja resolution if onclick was in passthrough list
hx-post="/api/{{ id }}" would get Jinja resolution if hx-post was in passthrough list
- The
@ prefix does not provide this feature - it's available to all attributes
Comparison with Other Server-Side Component Libraries
Click to expand: How other server-side component libraries handle event syntax
Django Components
django-components - Python/Django component library
Event handler syntax:
{% component "my_comp"
date=date
attrs:@click="(e) => onClick(e, 'from_parent')"
/ %}
Key features:
- Uses
attrs: prefix for HTML attributes including event handlers
- Supports
@click syntax for passing to Alpine.js/Vue.js components
- Uses
{% html_attrs %} tag to render passed attributes
- Standard HTML passthrough - accepts
onclick, data-*, any attribute
- Special characters like
@ . - _ are allowed in attribute names
- Designed for integration with Alpine.js, HTMX, jQuery
Their approach: Accept any attribute, pass through to HTML. Let client frameworks handle their own syntax:
- Alpine.js handles
@click on the client side
- HTMX uses standard
hx-* attributes (NOT @hx-*)
- Plain JavaScript uses standard
onclick
Phoenix LiveView
Phoenix LiveView - Elixir/Phoenix server-rendered components with WebSocket updates
Event handler syntax:
<button phx-click="increment">+</button>
<button phx-click={JS.push("clicked", target: @myself)}>Advanced</button>
Key features:
- Custom
phx-* prefix for LiveView events (phx-click, phx-change, etc.)
- Events sent to server over WebSocket, handled by
handle_event/3 callbacks
- Different from standard HTML -
phx-click is NOT the same as onclick
phx-click triggers server-side logic, onclick triggers client-side JavaScript
- Both can coexist:
<button phx-click="server_action" onclick="clientFunction()">
Why custom syntax makes sense here:
phx-click provides technical value - server-side event handling over WebSocket
- Different behavior than
onclick - not just passthrough
- Clear distinction between server events (
phx-*) and client events (on*)
Rails ViewComponent + Stimulus
ViewComponent + Stimulus - Ruby on Rails component library with JavaScript controllers
Event handler syntax:
<!-- ViewComponent renders standard HTML -->
<div data-controller="toggle">
<button data-action="click->toggle#toggle">Toggle</button>
<button data-action="toggle#toggle">Toggle (shorthand)</button>
</div>
Key features:
- ViewComponent outputs standard HTML - no custom event syntax
- Stimulus uses
data-action="click->controller#method" for event binding
- Shorthand: omit
click-> for button elements (implied)
- No custom syntax in templates - pure HTML with data attributes
- JavaScript controllers attach to HTML via data attributes
Their approach: Server-side components output standard HTML. Client-side framework (Stimulus) uses standard data-* attributes for progressive enhancement.
Laravel Blade + Alpine.js
Blade Components + Alpine.js - PHP/Laravel templating with Alpine.js
Event handler syntax:
<!-- Blade components use standard HTML + Alpine directives -->
<x-button x-on:click="handleClick()">Click</x-button>
<x-button @click="handleClick()">Click (Alpine shorthand)</x-button>
<!-- Pass through to component -->
<x-button ::@click="dynamicHandler">Click</x-button>
Key features:
- Blade outputs standard HTML
- Alpine.js
@click syntax is client-side only
- Blade's
:: prefix prevents PHP evaluation, passes to Alpine
{{ $attributes }} passes through all HTML attributes
- No custom server-side event syntax - it's all Alpine's client-side syntax
Their approach: Server renders HTML. Client framework (Alpine.js) handles the @click syntax at runtime.
Comparison with React and Vue
Click to expand: How React and Vue handle event syntax (client-side frameworks)
React's Approach
React uses camelCase event props (onClick, not onclick):
<button onClick={handleClick}>Click</button>
Why React does this:
- React controls the entire runtime - events go through a synthetic event system
- Cross-browser normalization (all events behave consistently)
- Event delegation (single listener at root for performance)
- Programmatic event handling (function references, not strings)
- Technical necessity - React's JSX isn't HTML, it's JavaScript
You cannot use onclick in React - it won't work.
Vue's Approach
Vue uses @ prefix (@click or v-on:click):
<button @click="handleClick">Click</button>
<button @click.prevent.stop="handleSubmit">Submit</button>
Why Vue does this:
- Event modifiers -
.prevent, .stop, .once, .capture, .passive
- Key modifiers -
@keyup.enter, @keyup.ctrl.s
- Mouse modifiers -
@click.left, @click.right, @click.middle
- Custom component events - Listen to child component events the same way
- Method references - Intelligently handles both expressions and method names
- Scoping - Methods are component-scoped, not global
- Technical power - Vue compiles these into sophisticated runtime event handling
Vue's @click adds substantial runtime features that onclick cannot provide.
You can technically use onclick in Vue, but you lose all these features and pollute global scope.
This Library's Approach
This library uses @ syntax but only for template-time conveniences, not runtime features:
<c-button @click="handleClick()">Click</button>
<!-- Outputs: <button onclick="handleClick()">Click</button> -->
Key difference:
- Vue's
@ = Runtime compiler magic (modifiers, scoping, custom events)
- This library's
@ = Template preprocessing (passthrough)
Pros and Cons
Pros of Custom @ Syntax
- Pattern Familiarity - Developers from Vue/Alpine.js recognize the pattern
- Mental Model - "@" means dynamic behavior happens
- Visual Distinction - Clear signal that you're using component syntax, not raw HTML (if this leads to something different)
- Validation Bypass - Don't need to define every event in component definitions
Cons of Custom @ Syntax
-
Progressive Adoption Friction
- Copy-pasted examples need changes:
- HTMX documentation (examples use
hx-post, not @hx-post)
- MDN/W3Schools (examples use
onclick, not @click)
- Updating existing code needs changes beyond c-* difference
-
Steeper learnings from html
- Need to learn custom syntax over normal html/htmx
onclick → @click
hx-post → @hx-post
-
No Actual Benefits Over Passthrough - @hx-post is just passthrough to hx-post
- No modifiers, no special handling
- It's purely a syntactic transformation
- Jinja variable resolution works already for all attributes (not just
@)
- Passthrough
hx-post and onclick would have identical capabilities
-
Looks like vue, but isnt - Looks like Vue but doesn't do what Vue does
-
Opionated on htmx
- it allows for use of htmx and explicitly maps the "@" syntax
- not possible to use this in combination with alpinejs ("@click" syntax)
Alternative: Passthrough Attributes
Alternative to the custom syntaxt, the library could support standard HTML/HTMX syntax through a passthrough mechanism:
PASSTHROUGH_PATTERNS = {
'hx-*', # All HTMX attributes
'on*', # All event handlers (onclick, oninput, etc.)
'data-*', # Data attributes
'aria-*', # ARIA attributes
}
This would enable:
<!-- Standard syntax - copy-paste from docs -->
<c-button hx-post="/api/data" onclick="track()">Submit</c-button>
<!-- Current syntax -->
<c-button @hx-post="/api/data" @click="track()">Submit</c-button>
<!-- Both work, same output -->
Benefits:
- Zero friction adoption - paste HTMX examples directly
- Standard HTML event handlers work
- Jinja variables work in both (same code path)
- Simpler implementation (no hardcoded list of 14 HTMX attributes)
- Future-proof (new HTMX attributes work automatically)
- could also add passthrough for all "@" attributes to enable alpinejs
Technical Deep Dive: What About Vue-Style Modifiers?
Could this library implement Vue's .prevent, .stop, key modifiers, etc.?
Technically: Partial yes, practically: No.
What's possible:
# Could generate:
@click.prevent → onclick="event.preventDefault(); handleClick()"
@keyup.enter → onkeyup="if(event.key==='Enter'){submit()}"
@click.left → onclick="if(event.button===0){leftClick()}"
What's not possible:
.capture, .passive (need addEventListener options, not inline handlers)
- Component scoping (everything is global functions)
- Custom component events (no runtime system)
Why not implement modifiers:
-
CSP Incompatibility - Modern security best practices ban inline onclick:
Content-Security-Policy: script-src 'self'
Generated inline handlers won't work.
-
Ugly Output - Generated code is verbose and hard to debug
-
Still Inferior to Vue - No real scoping, no component events, no true reactivity
-
Wrong Abstraction Layer - This is a server-side templating system, not a client-side framework
If users need Vue-style event handling, they should use Vue/Alpine.js/htmx for the client side. This library's strength is server-side component composition.
Questions for Discussion
- Given that
@ provides currently no technical advantages, should we prioritize Vue/React-style familiarity or HTML/HTMX standard compatibility?
- Are there other benefits to the
@ syntax I haven't considered?
When working with the library, i struggled a bit with the custom syntax for events and htmx attributes. I looked into the merits of this custom syntax and am wondering if it wouldn't be better to drop it and use default html/htmx syntax instead.
Below is a thorough analysis of the situation. Very curious to hear how you look at this.
Current Situation
This library currently uses a custom
@prefix syntax for event handlers and HTMX attributes:What the Custom Syntax Currently Does
The
@prefix provides these benefits:Validation Bypass - Event attributes with
@skip strict attribute validation, so you don't need to define every possible event in each component's attribute list.Centralized Handling - All events go through the
_event_mixin.j2macro, providing consistent processing across components.Important: The
@syntax does NOT add any runtime capabilities. It's purely a template preprocessing convenience that outputs standard HTML:@click="..."→onclick="..."@hx-post="..."→hx-post="..."It was thought that Jinja variable resolution was a benefit of the
@syntax. However, code analysis reveals that Jinja variable resolution works for ANY attribute (with or without@). The preprocessor detects{{in any attribute value and applies the same resolution logic:This means:
onclick="func('{{ var }}')"would get Jinja resolution ifonclickwas in passthrough listhx-post="/api/{{ id }}"would get Jinja resolution ifhx-postwas in passthrough list@prefix does not provide this feature - it's available to all attributesComparison with Other Server-Side Component Libraries
Click to expand: How other server-side component libraries handle event syntax
Django Components
django-components - Python/Django component library
Event handler syntax:
Key features:
attrs:prefix for HTML attributes including event handlers@clicksyntax for passing to Alpine.js/Vue.js components{% html_attrs %}tag to render passed attributesonclick,data-*, any attribute@ . - _are allowed in attribute namesTheir approach: Accept any attribute, pass through to HTML. Let client frameworks handle their own syntax:
@clickon the client sidehx-*attributes (NOT@hx-*)onclickPhoenix LiveView
Phoenix LiveView - Elixir/Phoenix server-rendered components with WebSocket updates
Event handler syntax:
Key features:
phx-*prefix for LiveView events (phx-click,phx-change, etc.)handle_event/3callbacksphx-clickis NOT the same asonclickphx-clicktriggers server-side logic,onclicktriggers client-side JavaScript<button phx-click="server_action" onclick="clientFunction()">Why custom syntax makes sense here:
phx-clickprovides technical value - server-side event handling over WebSocketonclick- not just passthroughphx-*) and client events (on*)Rails ViewComponent + Stimulus
ViewComponent + Stimulus - Ruby on Rails component library with JavaScript controllers
Event handler syntax:
Key features:
data-action="click->controller#method"for event bindingclick->for button elements (implied)Their approach: Server-side components output standard HTML. Client-side framework (Stimulus) uses standard
data-*attributes for progressive enhancement.Laravel Blade + Alpine.js
Blade Components + Alpine.js - PHP/Laravel templating with Alpine.js
Event handler syntax:
Key features:
@clicksyntax is client-side only::prefix prevents PHP evaluation, passes to Alpine{{ $attributes }}passes through all HTML attributesTheir approach: Server renders HTML. Client framework (Alpine.js) handles the
@clicksyntax at runtime.Comparison with React and Vue
Click to expand: How React and Vue handle event syntax (client-side frameworks)
React's Approach
React uses camelCase event props (
onClick, notonclick):Why React does this:
You cannot use
onclickin React - it won't work.Vue's Approach
Vue uses
@prefix (@clickorv-on:click):Why Vue does this:
.prevent,.stop,.once,.capture,.passive@keyup.enter,@keyup.ctrl.s@click.left,@click.right,@click.middleVue's
@clickadds substantial runtime features thatonclickcannot provide.You can technically use
onclickin Vue, but you lose all these features and pollute global scope.This Library's Approach
This library uses
@syntax but only for template-time conveniences, not runtime features:Key difference:
@= Runtime compiler magic (modifiers, scoping, custom events)@= Template preprocessing (passthrough)Pros and Cons
Pros of Custom
@SyntaxCons of Custom
@SyntaxProgressive Adoption Friction
hx-post, not@hx-post)onclick, not@click)Steeper learnings from html
onclick→@clickhx-post→@hx-postNo Actual Benefits Over Passthrough -
@hx-postis just passthrough tohx-post@)hx-postandonclickwould have identical capabilitiesLooks like vue, but isnt - Looks like Vue but doesn't do what Vue does
Opionated on htmx
Alternative: Passthrough Attributes
Alternative to the custom syntaxt, the library could support standard HTML/HTMX syntax through a passthrough mechanism:
This would enable:
Benefits:
Technical Deep Dive: What About Vue-Style Modifiers?
Could this library implement Vue's
.prevent,.stop, key modifiers, etc.?Technically: Partial yes, practically: No.
What's possible:
What's not possible:
.capture,.passive(needaddEventListeneroptions, not inline handlers)Why not implement modifiers:
CSP Incompatibility - Modern security best practices ban inline
onclick:Content-Security-Policy: script-src 'self'Generated inline handlers won't work.
Ugly Output - Generated code is verbose and hard to debug
Still Inferior to Vue - No real scoping, no component events, no true reactivity
Wrong Abstraction Layer - This is a server-side templating system, not a client-side framework
If users need Vue-style event handling, they should use Vue/Alpine.js/htmx for the client side. This library's strength is server-side component composition.
Questions for Discussion
@provides currently no technical advantages, should we prioritize Vue/React-style familiarity or HTML/HTMX standard compatibility?@syntax I haven't considered?