BuilderJS

Embed a Drag-and-Drop Builder in Laravel Livewire (the wire:ignore trick)

BuilderJS is a framework-agnostic, vanilla-JavaScript drag-and-drop editor for HTML email and web/landing pages that you self-host and embed inside your own app — including a Livewire component. It is the editor you own, not a SaaS: no monthly fee, no per-seat pricing, no hosted backend to depend on. Livewire embedding has exactly one gotcha worth internalizing up front, and it is the whole reason this page exists: Livewire re-renders (morphs) your component's DOM on every server round-trip, and BuilderJS is a DOM-mutating editor that builds its own canvas, palette, and overlays into that DOM. Left alone, Livewire will fight the editor — it diffs the live editor markup against its own stale render and tears your canvas apart. The fix is one attribute. Wrap the BuilderJS container in wire:ignore so Livewire leaves that subtree completely alone, mount window.Builder once inside an Alpine x-init (or a @script block), and bridge persistence by calling a Livewire action — $wire.save(builder.getData()) — or a plain fetch to your own route. That is the entire integration. This is distinct from the Blade-only /embed/laravel/ guide: here the challenge is not mounting, it is teaching Livewire's morphdom to keep its hands off the editor's DOM. Everything below matches the real BuilderJS API so you can copy, paste, and ship.

Mount it

{{-- resources/views/livewire/email-builder.blade.php --}}
  {{-- Livewire morphs the DOM on every round-trip; wire:ignore tells it to
       leave the editor's subtree alone so BuilderJS owns those nodes. --}}
  <div>
    <div wire:ignore x-data x-init="
          // Mount exactly once. builder.js (loaded globally) attaches window.Builder.
          const builder = new window.Builder({
            mainContainer: '#bjs-canvas',       // required CSS selector string
            widgetsContainer: '#bjs-widgets',   // optional
            settingsContainer: '#bjs-settings', // optional
          });
  
          // load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl) — 4 args
          builder.load(@js($design), {}, {}, '{{ asset('builder/media') }}/');
  
          // Bridge save to a Livewire action (validates + persists server-side)...
          window.saveDesign = () => $wire.save(builder.getData());
  
          // ...or stay framework-neutral with a plain fetch to your own route:
          // window.saveDesign = () => fetch('{{ route('builder.save') }}', {
          //   method: 'POST',
          //   headers: { 'Content-Type': 'application/json',
          //              'X-CSRF-TOKEN': '{{ csrf_token() }}' },
          //   body: JSON.stringify({ data: builder.getData(), html: builder.getHtml() }),
          // });
          // No destroy() exists — teardown discards the DOM and the editor with it.
        ">
      <div id="bjs-widgets"></div>
      <div id="bjs-canvas"></div>
      <div id="bjs-settings"></div>
    </div>
  
    <button type="button" onclick="window.saveDesign()">Save</button>
  </div>
  
  {{-- Public action on the component:
       public function save(array $data) { $this->design->update(['data' => $data]); } --}}

wire:ignore is the whole trick

Livewire keeps a DOM in sync with server state by morphing: after every action it renders the component on the server and diffs that HTML against what is in the browser, patching the differences. BuilderJS, the moment you construct it, mutates that same DOM heavily — it injects the canvas, the widgets palette, the settings panel, drag handles, and overlays. So on the next Livewire round-trip the morph compares the editor's live markup against the component's original (now stale) render and starts removing and rewriting nodes the editor created. The canvas flickers, overlays vanish, drag handles detach. The fix is a single attribute: put wire:ignore on the wrapping element (or wire:ignore.self if you only want to skip the element itself). Livewire then treats that subtree as opaque and never morphs it, so BuilderJS owns those nodes uncontested for the component's lifetime. This is the standard, documented way to host any DOM-owning JavaScript widget inside Livewire — it is not a hack, it is the intended escape hatch. Wrap the three container divs the editor mounts into (canvas, widgets, settings) in one wire:ignore parent and the DOM fight disappears entirely.

Mount window.Builder in Alpine x-init (or a @script block)

BuilderJS ships as a compiled browser bundle, not a Composer or npm package, and loading builder.js attaches a global named window.Builder. There is no module export, so never write import { Builder } from anything. You need two things: the global present on the page, and a client-side hook that fires once after the DOM exists. In Livewire the two clean options are Alpine and @script. With Alpine — which ships with Livewire 3 — give the wire:ignore wrapper an x-init that runs the mount: new window.Builder({ mainContainer: '#bjs-canvas', widgetsContainer: '#bjs-widgets', settingsContainer: '#bjs-settings' }). Those options are CSS selector strings passed to querySelector — not refs, not DOM nodes. Only mainContainer is required; the other two are optional sidebars. Alternatively, Livewire's @script directive runs JavaScript exactly once when the component is initialized and is not re-executed on subsequent updates, which is precisely the lifecycle a single-instance editor wants. Either way, construct the builder once and keep the reference (on the Alpine component, or a module-scoped variable) so you can call getData() and load() later. There is no destroy() method — on a real teardown Livewire/Alpine discards the container DOM and the editor goes with it; never call builder.destroy(), it does not exist.

Bridge save: $wire.save(builder.getData()) or a plain fetch

Persistence is plain JSON and stays entirely on your side — BuilderJS does not send, host, or publish anything. To capture the design call const data = builder.getData(), which returns a lossless snapshot of the canvas, and builder.getHtml() for the rendered markup. You have two natural bridges back to the server. The Livewire-native path: call $wire.save(builder.getData()) from your mount scope — $wire is the magic object Livewire exposes for invoking PHP actions from JavaScript — and a public save($data) method on your component validates and persists it to your model (a designs table, a JSON column, wherever). The framework-neutral path: POST the JSON to one of your own routes with a plain fetch, sending Laravel's CSRF token in the headers. Both are equivalent; choose $wire when you want the save to flow through your component's lifecycle and validation, and fetch when you want the editor's save to stay independent of Livewire's request cycle. Note that BuilderJS also offers a built-in builder.save() that POSTs html and data url-encoded to the saveUrl you pass in the constructor — handy for a non-Livewire route — but there is no save:click event, so wiring a save button to $wire or fetch is host-driven by design. To reopen a design, echo the stored JSON into the view and call builder.load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl) — four positional arguments, in that order. The data lives in your database; the editor imposes no storage backend.

One editor for both email and landing pages

The same Livewire-hosted editor produces two distinct outputs, so a single integration can power both your transactional-email template editor and your marketing landing-page editor. For web and landing pages it emits responsive, modern-CSS HTML. For email it runs an export pipeline that rewrites the document to survive the real world of inboxes: flexbox and grid layouts become nested presentation tables, Outlook MSO conditional comments and ghost tables are injected, @media (max-width:600px) rules collapse the layout to a mobile stack, images go fluid (max-width:100%; height:auto), SVG icons rasterize to PNG, and a CSS inliner finishes the job. The bundled 30+ templates and 200+ samples were Litmus-tested across 22 email clients, with RTL and 20+ locales available. BuilderJS builds the markup in each case; bringing an ESP to actually send the email, or a host to serve the page, is your application's responsibility — it is the editor, never the delivery platform.

Own the source with a one-time license

BuilderJS is sold once on CodeCanyon (item 27146783) as Regular $52 or Extended $99 — you get the source, self-host builder.js and builder.css from your own origin, and embed it in as many of your own Livewire screens as the license tier allows. There is no SaaS subscription, no monthly bill, no per-seat charge, no usage metering, and no phone-home telemetry; your only ongoing cost is the Laravel server you already run. Because the bundle has no external runtime dependency on a vendor service, your app stays fast and private and your customers' designs live in your database as portable JSON. You extend it without forking — register custom Elements, Widgets, and Controls, theme the editor chrome to match your product, and localize with built-in i18n. The honest boundary: author support and version updates through CodeCanyon are time-boxed (roughly 6–12 months), after which you keep and maintain the source you already own. Try the full getData()/load() round-trip on the live demo at builder.emotsy.com before you commit.

Frequently asked questions

Explore