Back

Third Party

Let me see the code 👀
routes/web.php
1<?php
2 
3use App\Livewire\ThirdParty;
4 
5Route::get('/third-party', ThirdParty::class);
app/Livewire/ThirdParty.php
1<?php
2 
3namespace App\Livewire;
4 
5use Livewire\Attributes\On;
6use Livewire\Component;
7 
8class ThirdParty extends Component
9{
10 public $options = [];
11 public $selectedOption = '1';
12 public $selectedOptions = ['3', '4'];
13 public $visible = true;
14 public $text;
15 public $sortableData;
16 public $sortableItem;
17 
18 public function mount()
19 {
20 $this->options = [
21 [
22 'id' => '1',
23 'name' => 'Jujutsu Kaisen'
24 ],
25 [
26 'id' => '2',
27 'name' => 'One Piece'
28 ],
29 [
30 'id' => '3',
31 'name' => 'Elusive Samurai'
32 ],
33 [
34 'id' => '4',
35 'name' => 'Black Clover'
36 ]
37 ];
38 }
39 
40 function clearSelectedOption()
41 {
42 $this->selectedOption = '';
43 }
44 
45 function clearSelectedOptions()
46 {
47 $this->selectedOptions = [];
48 }
49 
50 #[On('destroy')]
51 function destroy()
52 {
53 // Mock delete process in server with sleep function
54 sleep(3);
55 $this->dispatch('alert:ok', data: [
56 'message' => 'Item has been deleted!'
57 ]);
58 $this->visible = false;
59 }
60 
61 public function submitEditor()
62 {
63 // May be add some validation here!
64 }
65 
66 #[On('set-data')]
67 function setSortableData($data)
68 {
69 $this->sortableData = $data;
70 }
71 
72 public function submitSortable()
73 {
74 if (!empty($this->sortableItem)) {
75 $this->dispatch('nested-sortable-store-item', data: [
76 'item' => $this->sortableItem
77 ]);
78 $this->sortableItem = '';
79 }
80 }
81 
82 public function render()
83 {
84 return view('livewire.third-party')
85 ->title('Third Party');
86 }
87}
resources/views/livewire/third-party.blade.php
1@push('styles')
2<link href="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/css/tom-select.css" rel="stylesheet">
3<style>
4 [x-cloak] {
5 display: none !important;
6 }
7 
8 /* Nested sortable style */
9 ul {
10 list-style-type: none;
11 margin: 0;
12 }
13 
14 .list-group-item {
15 border: 1px solid rgba(0, 0, 0, .125);
16 padding: .5rem;
17 cursor: move;
18 }
19 
20 .list-group-item:last-child {
21 border-bottom-left-radius: .25rem;
22 border-bottom-right-radius: .25rem;
23 }
24 
25 .list-group-item:hover {
26 z-index: 0;
27 }
28 
29 .nested-sortable,
30 .nested-1,
31 .nested-2,
32 .nested-3 {
33 margin-top: 5px;
34 }
35 
36 .nested-1 {
37 background-color: #e6e6e6;
38 }
39 
40 .nested-2 {
41 background-color: #cccccc;
42 }
43 
44 .nested-3 {
45 background-color: #b3b3b3;
46 }
47</style>
48<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/trix/dist/trix.min.css" crossorigin="anonymous">
49@endpush
50@push('scripts')
51<script src="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/js/tom-select.complete.min.js"></script>
52<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js" crossorigin="anonymous"></script>
53<script>
54 function confirmDestroy(el) {
55 swal({
56 title: 'Warning!',
57 text: el.getAttribute('data-message'),
58 icon: 'warning',
59 buttons: true,
60 dangerMode: true,
61 })
62 .then((willDelete) => {
63 if (willDelete) {
64 swal('Data is being deleted. Please wait...', {
65 icon: 'info',
66 closeOnClickOutside: false,
67 button: false
68 });
69 window.Livewire.dispatch('destroy');
70 } else {
71 swal('Your data still saved!')
72 }
73 })
74 }
75 
76 window.addEventListener('alert:ok', function (e) {
77 swal({
78 icon: 'success',
79 text: e.detail.data.message
80 });
81 })
82</script>
83<script src="https://cdn.jsdelivr.net/npm/trix/dist/trix.umd.min.js" crossorigin="anonymous"></script>
84<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
85<script>
86 let nestedSortables = Array.from(document.querySelectorAll('.nested-sortable'));
87 for (let i = 0; i < nestedSortables.length; i++) {
88 new Sortable(nestedSortables[i], {
89 group: 'nested',
90 animation: 150,
91 fallbackOnBody: true,
92 swapThreshold: 0.65,
93 onEnd: function (evt) {
94 let newData = JSON.stringify(serialize(root));
95 console.log(newData);
96 Livewire.dispatch('set-data', { data: newData });
97 }
98 });
99 }
100 
101 const nestedQuery = '.nested-sortable';
102 const identifier = 'id';
103 const root = document.getElementById('wrapper-nested-sortable');
104 function serialize(sortable) {
105 const serialized = [];
106 const children = [].slice.call(sortable.children);
107 for (const i in children) {
108 const nested = children[i].querySelector(nestedQuery);
109 const parentId = children[i].closest(nestedQuery).getAttribute('data-parent-id');
110 serialized.push({
111 id: children[i].dataset[identifier],
112 parent_id: parentId,
113 children: nested ? serialize(nested) : [],
114 order: (parseInt(i) + 1).toString(),
115 });
116 }
117 return serialized;
118 }
119 
120 window.addEventListener('nested-sortable-store-item', (e) => {
121 document.getElementById('wrapper-nested-sortable').appendChild(
122 document
123 .createRange()
124 .createContextualFragment(`<li data-id="${e.detail.data.item}" data-parent="0"
125 class="list-group-item nested-1">${e.detail.data.item}
126 </li>`));
127 });
128</script>
129@endpush
130 
131<div>
132 <a href="/" class="underline text-blue-500">Back</a>
133 <h1 class="text-2xl mb-4">Third Party</h1>
134 
135 <h2 class="text-xl mb-4">Select with Tom Select</h2>
136 <h3 class="text-lg">Single option</h3>
137 <div wire:ignore>
138 <select x-data="{
139 tomSelectInstance: null,
140 options: {{ collect($options) }},
141 items: $wire.entangle('selectedOption').live
142 }"
143 x-init="tomSelectInstance = new TomSelect($refs.select, {
144 valueField: 'id',
145 labelField: 'name',
146 searchField: 'name',
147 options: options,
148 items: items,
149 onItemAdd: function () {
150 this.setTextboxValue('');
151 }
152 }); $watch('items', (value, oldValue) => {
153 const result = JSON.parse(JSON.stringify(value));
154 if (result.length === 0) {
155 tomSelectInstance.clear();
156 }
157 });"
158 x-ref="select"
159 x-cloak
160 id="selectedOption"
161 wire:model.live="selectedOption"
162 autocomplete="off">
163 
164 </select>
165 </div>
166 <p class="mb-4">Selected option: {{ @json_encode($selectedOption) }}</p>
167 
168 <h3 class="text-lg">Multiple options</h3>
169 <div wire:ignore>
170 <select x-data="{
171 tomSelectInstance: null,
172 options: {{ collect($options) }},
173 items: $wire.entangle('selectedOptions').live
174 }"
175 x-init="tomSelectInstance = new TomSelect($refs.select, {
176 valueField: 'id',
177 labelField: 'name',
178 searchField: 'name',
179 options: options,
180 items: items,
181 maxItems: 2,
182 onItemAdd: function () {
183 this.setTextboxValue('');
184 this.refreshOptions();
185 }
186 }); $watch('items', (value, oldValue) => {
187 const result = JSON.parse(JSON.stringify(value));
188 if (result.length === 0) {
189 tomSelectInstance.clear();
190 }
191 })"
192 x-ref="select"
193 x-cloak
194 id="selectedOptions"
195 wire:model.live="selectedOptions"
196 multiple
197 autocomplete="off">
198 
199 </select>
200 </div>
201 <p class="mb-4">Selected options: {{ @json_encode($selectedOptions) }}</p>
202 
203 <h2 class="text-xl mb-4">Alert with Sweet Alert</h2>
204 @if ($visible)
205 <div class="flex items-center gap-2 mb-4">
206 <p>Delete me and I will say good bye!</p>
207 <button onclick="confirmDestroy(this);" data-message="Are you sure to delete this item?" class="bg-red-500 p-1 rounded text-white">Delete</button>
208 </div>
209 @else
210 <p class="mb-4">Good bye! 👋🏼</p>
211 @endif
212 
213 <h2 class="text-xl mb-4">Text Area with Trix Editor</h2>
214 <form class="mb-4" wire:submit="submitEditor">
215 <input type="hidden" id="x">
216 <div wire:ignore>
217 <trix-editor x-data="{
218 setValue(event) {
219 $wire.text = event.target.value;
220 },
221 upload(event) {
222 const data = new FormData();
223 data.append('file', event.attachment.file);
224 
225 fetch('/upload-file', {
226 headers: {
227 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').getAttribute('content')
228 },
229 method: 'POST',
230 credentials: 'same-origin',
231 body: data
232 })
233 .then(response => response.json())
234 .then(data => {
235 event.attachment.setAttributes({
236 url: data.image_url
237 });
238 });
239 }
240 }"
241 x-on:trix-file-accept="event.preventDefault()"
242 x-on:trix-attachment-add="upload"
243 x-on:trix-change="setValue"
244 x-ref="trix"
245 input="x" class="trix-content"></trix-editor>
246 </div>
247 <button class="bg-blue-500 text-white p-2 rounded my-4">Submit</button>
248 {!! $text !!}
249 </form>
250 
251 <h2 class="text-xl mb-4">Nested Sortable</h2>
252 <p class="mb-4">Usually, I use nested sortable to display tree menu then drag and drop menu position. Imagine you build dynamic menus for your site.</p>
253 <form class="flex gap-2" wire:submit.prevent="submitSortable">
254 <input type="text" wire:model="sortableItem" placeholder="Insert an item" class="flex-1 rounded">
255 <button type="submit" class="bg-blue-500 p-2 rounded text-white">Submit</button>
256 </form>
257 <div wire:ignore>
258 {!! build_sortable_list(get_menus()) !!}
259 </div>
260 @if (!empty($data))
261 <pre style="overflow-x: scroll;">
262 <code>
263 {{ @json_encode($data) }}
264 </code>
265 </pre>
266 @else
267 <p>Please drag and drop the list above to see updated list.</p>
268 @endif
269</div>

Select with Tom Select

Single option

Selected option: "1"

Multiple options

Selected options: ["3","4"]

Alert with Sweet Alert

Delete me and I will say good bye!

Text Area with Trix Editor

Nested Sortable

Usually, I use nested sortable to display tree menu then drag and drop menu position. Imagine you build dynamic menus for your site.

  • Item 1.1
    • Item 2.1
    • Item 2.2
      • Item 3.1
      • Item 3.2
      • Item 3.3
      • Item 3.4
  • Item 1.2
  • Item 1.3
  • Item 1.4
    • Item 2.3
    • Item 2.4
    • Item 2.5
    • Item 2.6
  • Item 1.5

Please drag and drop the list above to see updated list.