Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Html Element Bundle Laravel Package

setono/html-element-bundle

A Symfony bundle for building HTML elements as reusable PHP objects instead of loose markup. Compose attributes and child nodes, then render clean, testable HTML from services or Twig, keeping templates maintainable and markup consistent.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and confirm the bundle is wired up

composer require setono/html-element-bundle

Flex should register it in config/bundles.php. If it doesn't (the package is tiny, so the recipe may be thin or missing), add it manually:

Setono\HtmlElementBundle\SetonoHtmlElementBundle::class => ['all' => true],

2. Look at these three places before writing any code

  • README.md — for a 1-star package this is the documentation. Read it end to end; there is no blog-post ecosystem to fall back on.
  • src/DependencyInjection/Configuration.php — the full config tree. Dump it directly:
    bin/console config:dump-reference setono_html_element
    
  • The Twig extension in src/Twig/ — this tells you the actual public API (function, tag, or filter) and its argument order. Confirm what's registered:
    bin/console debug:twig | grep -i element
    

3. First use case: one small, self-contained element

Don't start with your design system. Pick a single leaf component that has behaviour and no dependencies — a copy-to-clipboard button, a character counter, a disclosure/toggle. That exercises the whole loop (class → template → registration → Twig usage → browser behaviour) in under an hour and tells you whether the package's abstractions fit your project.

The shape you're aiming for is roughly: a PHP class (or a template + registration entry) describing the element, a Twig template that renders its markup, a JS file that calls customElements.define(...), and a Twig-side call site. Confirm the exact class names and registration mechanism from the README before assuming — the package is small enough that the API surface is intentionally narrow, and guessing wastes more time than reading.

4. Sanity-check the render before touching JavaScript

Render the element in a scratch template and look at the HTML source (View Source, not the inspector — you want pre-JS markup). If the custom element tag isn't in the source, the problem is Twig-side, not browser-side. Splitting the debugging that way saves a lot of time on a package with no community answers.

Implementation Patterns

One element, one directory

Keep the PHP class/template/JS co-located so the element is movable and deletable as a unit:

templates/components/copy-button.html.twig
assets/components/copy-button.js
src/HtmlElement/CopyButton.php

This matters more here than in a mature package: because there are zero dependents, there's no convention to inherit. Establish your own and write it in the repo README.

Server-render first, then upgrade

The most robust pattern with this bundle is to let Twig render complete, meaningful HTML, and treat the custom element as a progressive enhancement. The element works without JS; the JS only adds behaviour.

<copy-button data-target="#install-command">
    <button type="button" class="btn">Copy</button>
</copy-button>
class CopyButton extends HTMLElement {
  connectedCallback() {
    if (this.dataset.initialized) return;   // guard against re-connect
    this.dataset.initialized = 'true';
    this.querySelector('button')?.addEventListener('click', () => {
      navigator.clipboard.writeText(this.dataset.target ? document.querySelector(this.dataset.target).textContent : '');
    });
  }
}
customElements.define('copy-button', CopyButton);

This keeps you safe if the package's asset-loading integration doesn't do what you expect, and it survives Turbo/htmx navigation (which replaces DOM nodes and re-fires connectedCallback).

Configuration over inline attributes

If the element takes structured data (a list of options, a config object), serialize it once via a data-* attribute with json_encode, and parse it in connectedCallback. Avoid scattering many attributes — it makes the Twig call site unreadable and makes the element hard to test in isolation.

Asset delivery: pick one path and commit

Two viable integrations:

  • AssetMapper — put the JS under assets/, import it from assets/app.js, and let AssetMapper handle versioning. Simplest, and pairs naturally with a small bundle like this.
  • Encore/Webpack — add the element JS as an entrypoint and include it from your layout.

Don't rely on the bundle to auto-inject assets unless the README explicitly promises it. Explicitly including your element JS from base.html.twig is one line and removes an entire class of failure.

Compose elements from elements

Once two elements exist, use them inside each other's templates. This is where a custom-element approach pays off compared to Twig includes: each element owns its behaviour, and nesting is just HTML. Keep parent/child communication through DOM events (this.dispatchEvent(new CustomEvent('copy-button:copied', { bubbles: true }))) rather than reaching across the DOM — it keeps elements independently testable.

Testing

  • Twig/PHP side: a functional test that requests a route and asserts the rendered HTML contains the custom element tag and expected attributes with assertSelectorExists('copy-button[data-target]').
  • JS side: test the element class directly in a browser test or a JSDOM harness — import the class, customElements.define in the test, and assert on connectedCallback behaviour. Keep the class exported so it's importable; don't put it inline in a <script> tag.

Where it fits vs. Symfony UX TwigComponent

Before adopting broadly, compare against native symfony/ux-twig-component (<twig:CopyButton />). The bundle's advantage is real custom elements — shadow DOM, slots, framework-agnostic reuse of the same markup outside Symfony. If you only need reusable Twig markup with props and no browser-side lifecycle, the native component is better supported and you should use it instead.

Gotchas and Tips

Custom element names must contain a hyphen. customElements.define('copybutton', …) throws; copy-button is required. If your element silently does nothing, this is the first thing to check in the console.

Registration order and FOUC. If your JS loads after the element is in the DOM, the browser upgrades it on define() — usually fine — but any styling keyed to :defined will flash. Load the element scripts in <head> with defer, or accept the upgrade flash deliberately. Do not assume the bundle enforces an order for you.

Guard connectedCallback against repeat invocation. DOM moves (Turbo, htmx, live-reload dev servers) detach and reattach nodes, firing connectedCallback again. Without a guard you get doubled event listeners and doubled network calls. Use disconnectedCallback to clean up anything you attach outside the element.

Don't |raw your way out of escaping. Attribute values passed from Twig are autoescaped. If you find yourself adding |raw to make an attribute render, the real problem is that you're injecting a data structure through a string attribute — encode it with json_encode(..., JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) inside a data- attribute instead. This is also your XSS boundary; treat element attributes as untrusted user input.

Shadow DOM cuts both ways. Styles don't leak in, and your global design tokens/CSS reset don't reach inside. If the element uses shadow DOM, copy CSS custom properties across the boundary (:host inherits custom properties, so a token-based design system works) and don't expect utility classes to function inside.

Stale Twig cache in prod. Template changes to elements are not hot-reloaded in production; bin/console cache:clear after deploys or after editing element templates. This bites people who test element edits on a production-like environment and conclude the package is broken.

Template lookup is a common mystery. If the bundle resolves element templates by convention from a configurable path, check the config for that path after moving files or changing the template namespace. A silent fallback to a default template is a classic symptom.

Low-adoption risk — plan for it. One star, zero dependents, one maintainer. The last release (2026-05-01) says it's alive, but don't expect Stack Overflow answers or third-party tutorials. Mitigations:

  • Pin the version ("setono/html-element-bundle": "^x.y") and commit your lockfile.
  • Keep your element classes plain and framework-independent so a future migration to raw customElements.define or Symfony UX is a mechanical change, not a rewrite.
  • It's MIT — you can fork and vendor it if the maintainer steps away. Keep your usage surface narrow enough that a fork is a day's work.

Debugging checklist, in order:

  1. bin/console debug:twig — is the function/tag actually registered? (Wrong bundle registration.)
  2. View source — is the tag in the server HTML? (Twig side.)
  3. Console — customElements.get('my-element') returns undefined? (JS never loaded, or name lacks a hyphen.)
  4. Console — :not(:defined) elements everywhere? (JS 404'd; check network tab and AssetMapper/Encore manifest.)
  5. If it renders but misbehaves, confirm attributes actually reached the DOM: document.querySelector('my-element').dataset in the console. Twig attribute-name casing differences (data-fooBar vs data-foo-bar) are a frequent silent mismatch with dataset.fooBar.

Extension points worth auditing. Look for service tags (debug:container --tag=<something>) and any AbstractHtmlElement-style base class in the source. Registering elements via a service tag is usually the cleanest integration — your elements get autowired dependencies (router, translator, security) rather than being pure templates. If the package offers that, use it; if it only offers template conventions, keep element logic out of the class and accept that it's markup plumbing.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource