A workaround for caret issues in Livewire Flux editor component when using JSON
In October, I posted about how you can use the Flux editor component in your Livewire apps and output JSON – making it great for compatibility with other platforms like Statamic.
But one issue, which I didn’t notice until Mario Hoyos highlighted that the caret keeps jumping to the end of the editor instance. How I didn’t notice this is beyond me… I guess when building an app it was more about “yep, that can be saved and loaded and looks right” but didn’t use it for actual editing. So that’s on me.
In the Github discussion on a request to add JSON support enablable as a prop on the editor component, Mario posted a workaround which works… but also, in their words, feels a bit hacky.
What I didn’t like about their implementation was the amount of hard coding of attribute names – it expects content. But I have multiple places to use this editor, and it is not always called content.
For example, I use Livewire Forms, meaning it sometimes looks like form.content. And if I have an editor as an introduction field, it would be form.introduction.
I know this post is delayed from the original posting, but have finally had this move back to the top of the list.
And I wanted to find a way to use Mario’s idea but make it re-usable for any editor instance.
Mario’s approach removes the wire:model from the editor instance, and uses a timeout on update to set the Livewire attribute content to be the JSON of the editor. But $wire.set becomes very chatty to the backend – for large documents, that can be a lot of data going back to the server unnecessarily.
This got me thinking: is there a way to toggle between being live (or not) but still having Livewire (on the client) know about changes to the content made by some Alpine on the client side?
I started with Mario’s shadow approach: but from previous component development learned that if Alpine is updating a field’s value, Livewire doesn’t actually know about it.
Off to the docs I go, and there’s a really simple example about mutating Livewire properties. This example was about clearing a title property on a button click – same concept, but different application.
1<button type="button" x-on:click="$wire.title = ''">Clear</button>
Well that’s exactly what I want to do: I want to have some JS on the client-side do something to the model of the editor’s JSON.
Mario’s approach for initialisation and updating was a great start – but I removed a shadow element, and changed from $wire.set to $wire.form.content and it worked exactly as expected.
1// $wire.set('form.content') = editor.getJSON();2$wire.form.content = editor.getJSON();
OK so this is exciting… so I added a prop to my Blade component for the model – a simple string like you’d define for wire:model normally – and use that to get the initial content, and also to use for the $wire call.
1$wire.{{ $model }} = editor.getJSON();
This is getting somewhere! What if I added a second prop, live, to the component? This way I could make an editor live if I wanted to – maybe for an editor where I know the content will always be short.
The component evolved into something simple yet flexible.
1@props([ 2 'live' => false, 3 'model' 4]) 5<div x-data="{ 6 live: {{ $live ? 'true' : 'false' }}, 7 8 init() { 9 const editor = $el.querySelector('.tiptap').editor;10 11 // Hydrate once from Livewire value (JSON or HTML)12 const initial = $wire.get('{{ $model }}');13 if (initial) {14 try {15 // if it's JSON already16 editor.commands.setContent(initial, false);17 } catch (e) {18 // fallback if it were HTML19 editor.commands.setContent(initial, false);20 }21 }22 23 let t;24 editor.on('update', () => {25 clearTimeout(t);26 t = setTimeout(() => this.update(editor), 200);27 });28 29 editor.on('blur', () => this.update(editor));30 },31 32 update(editor) {33 let json = editor.getJSON()34 $wire.{{ $model }} = json;35 if (this.live) {36 $wire.set('{{ $model }}', json);37 }38 }39 }">40 41 <flux:editor {{ $attributes->whereDoesntStartWith('model') }}/>42</div>
And I can re-use it in multiple ways:
1{{-- For smaller editors, a live-updating component --}}2<x-fields.editor model="form.introduction" :live="true"/>3 4{{-- For larger editors, a non-live component --}}5<x-fields.editor model="form.content"/>
While I don’t like that all of my other fields are wire:model and this is just model, it does work.
And of course, native getJSON support for Flux’s editor component is still the goal… but until then, this is just what my project needs.
Comments
Reply on Bluesky to join the conversation.