Skip to content

Commit 070c6ce

Browse files
committed
wip html live refresh
1 parent 37c6014 commit 070c6ce

27 files changed

Lines changed: 337 additions & 132 deletions

demo/app/Sharp/Posts/PostForm.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Code16\Sharp\Form\Fields\SharpFormCheckField;
1717
use Code16\Sharp\Form\Fields\SharpFormDateField;
1818
use Code16\Sharp\Form\Fields\SharpFormEditorField;
19+
use Code16\Sharp\Form\Fields\SharpFormHtmlField;
1920
use Code16\Sharp\Form\Fields\SharpFormListField;
2021
use Code16\Sharp\Form\Fields\SharpFormTagsField;
2122
use Code16\Sharp\Form\Fields\SharpFormTextareaField;
@@ -27,6 +28,7 @@
2728
use Code16\Sharp\Form\Layout\FormLayoutTab;
2829
use Code16\Sharp\Form\SharpForm;
2930
use Code16\Sharp\Utils\Fields\FieldsContainer;
31+
use Illuminate\Support\Facades\Blade;
3032

3133
class PostForm extends SharpForm
3234
{
@@ -104,6 +106,25 @@ public function buildFormFields(FieldsContainer $formFields): void
104106
->setLabel('Publication date')
105107
->setHasTime(),
106108
)
109+
->addField(
110+
SharpFormHtmlField::make('publication_label')
111+
->setLiveRefresh(linkedFields: ['author_id', 'published_at'])
112+
->setTemplate(function (array $data) {
113+
if (! isset($data['published_at'])) {
114+
return '';
115+
}
116+
117+
return Blade::render(<<<'blade'
118+
This post will be published on {{ $published_at }}
119+
@if($author)
120+
by {{ $author->name }}.
121+
@endif
122+
blade, [
123+
'published_at' => \Carbon\Carbon::parse($data['published_at'])->isoFormat('LLLL'),
124+
'author' => \App\Models\User::find($data['author_id']),
125+
]);
126+
})
127+
)
107128
->addField(
108129
SharpFormListField::make('attachments')
109130
->setLabel('Attachments')
@@ -168,6 +189,7 @@ public function buildFormLayout(FormLayout $formLayout): void
168189
fn ($column) => $column->withField('author_id')
169190
)
170191
->withFields('published_at', 'categories')
192+
->withField('publication_label')
171193
->withListField('attachments', function (FormLayoutColumn $item) {
172194
$item->withFields(title: 8, is_link: 4)
173195
->withField('link_url')

resources/js/Pages/Form/Form.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import { onUnmounted, ref, useTemplateRef, watch } from "vue";
1111
import { __ } from "@/utils/i18n";
1212
import { Button } from '@/components/ui/button';
13-
import { useResizeObserver } from "@vueuse/core";
1413
import { slugify } from "@/utils";
1514
1615
const props = defineProps<{

resources/js/form/Form.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,14 @@ export class Form implements FormData, CommandFormData, EventTarget {
287287
}, 0);
288288
}
289289

290+
shouldRefresh(updatedFieldKey: string, fields = this.fields): boolean {
291+
return Object.values(fields).some(field =>
292+
field.type === 'html'
293+
&& field.liveRefresh
294+
&& (!field.liveRefreshLinkedFields || field.liveRefreshLinkedFields.includes(updatedFieldKey))
295+
);
296+
}
297+
290298
eventTarget: EventTarget = new EventTarget();
291299

292300
addEventListener(type: FormEvents, callback: EventListener, options?: AddEventListenerOptions | boolean): void {

resources/js/form/components/Form.vue

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,19 @@
1818
import { slugify } from "@/utils";
1919
import { Badge } from "@/components/ui/badge";
2020
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
21-
import { FieldMeta } from "@/form/types";
21+
import { FieldMeta, FormFieldEmitInputOptions } from "@/form/types";
2222
import StickyTop from "@/components/StickyTop.vue";
2323
import StickyBottom from "@/components/StickyBottom.vue";
2424
import { Menu } from 'lucide-vue-next';
2525
import { Label } from "@/components/ui/label";
2626
import RootCardHeader from "@/components/ui/RootCardHeader.vue";
2727
import { vScrollIntoView } from "@/directives/scroll-into-view";
2828
import { useResizeObserver } from "@vueuse/core";
29+
import debounce from "lodash/debounce";
30+
import { api } from "@/api/api";
31+
import { route } from "@/utils/url";
32+
import { useParentCommands } from "@/commands/useCommands";
33+
import merge from 'lodash/merge';
2934
3035
const props = defineProps<{
3136
form: Form
@@ -77,13 +82,31 @@
7782
props.form.setMeta(fieldKey, { uploading });
7883
}
7984
80-
function onFieldInput(fieldKey: string, value: FormFieldData['value'], { force = false } = {}) {
85+
const parentCommands = useParentCommands();
86+
const refresh = debounce((data) => {
87+
api.post(route('code16.sharp.api.form.refresh.update', {
88+
entityKey: props.form.entityKey,
89+
instance_id: props.form.instanceId,
90+
embed_key: props.form.embedKey,
91+
entity_list_command_key: parentCommands?.commandContainer === 'entityList' ? props.form.commandKey : null,
92+
show_command_key: parentCommands?.commandContainer === 'show' ? props.form.commandKey : null,
93+
}), data)
94+
.then(response => {
95+
props.form.data = merge({}, props.form.data, response.data.form.data);
96+
});
97+
}, 200);
98+
99+
function onFieldInput(fieldKey: string, value: FormFieldData['value'], inputOptions: FormFieldEmitInputOptions = {}) {
81100
const data = {
82101
...props.form.data,
83-
...(!force ? getDependantFieldsResetData(props.form.fields, fieldKey) : null),
102+
...(!inputOptions.force ? getDependantFieldsResetData(props.form.fields, fieldKey) : null),
84103
[fieldKey]: value,
85104
};
86105
106+
if((props.form.shouldRefresh(fieldKey) || inputOptions.shouldRefresh) && !inputOptions.skipRefresh) {
107+
refresh(data);
108+
}
109+
87110
props.form.data = data;
88111
props.form.serializedData = data;
89112
}

resources/js/form/components/fields/List.vue

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { computed, nextTick, ref, watch, watchEffect } from "vue";
77
import { Button, buttonVariants } from '@/components/ui/button';
88
import { showAlert } from "@/utils/dialogs";
9-
import { FieldMeta, FieldsMeta, FormFieldEmits, FormFieldProps } from "@/form/types";
9+
import { FieldMeta, FieldsMeta, FormFieldEmitInputOptions, FormFieldEmits, FormFieldProps } from "@/form/types";
1010
import FieldGridRow from "@/components/ui/FieldGridRow.vue";
1111
import FieldGridColumn from "@/components/ui/FieldGridColumn.vue";
1212
import { Toggle } from "@/components/ui/toggle";
@@ -81,7 +81,7 @@
8181
emit('input', props.value?.map(((item, index) => ({ ...item, [errorIndex]: index }))), { preserveError: true });
8282
});
8383
84-
emit('input', props.value?.map(item => ({ ...item, [itemKey]: itemKeyIndex++ })), { force: true });
84+
emit('input', props.value?.map(item => ({ ...item, [itemKey]: itemKeyIndex++ })), { force: true, skipRefresh: true });
8585
8686
watchArray(() => props.value ?? [], async (newList, oldList, added) => {
8787
if(!added.length) {
@@ -142,17 +142,26 @@
142142
e.target.value = '';
143143
}
144144
145-
function onFieldInput(itemIndex: number, itemFieldKey: string, itemFieldValue: FormFieldData['value'], { force = false } = {}) {
145+
function onFieldInput(
146+
itemIndex: number,
147+
itemFieldKey: string,
148+
itemFieldValue: FormFieldData['value'],
149+
inputOptions: FormFieldEmitInputOptions
150+
) {
146151
emit('input', props.value.map((item, i) => {
147152
if(i === itemIndex) {
148153
return {
149154
...item,
150-
...(!force ? getDependantFieldsResetData(props.field.itemFields, itemFieldKey) : null),
155+
...(!inputOptions.force ? getDependantFieldsResetData(props.field.itemFields, itemFieldKey) : null),
151156
[itemFieldKey]: itemFieldValue,
152157
}
153158
}
154159
return item;
155-
}));
160+
}), {
161+
force: inputOptions.force,
162+
skipRefresh: inputOptions.skipRefresh,
163+
shouldRefresh: form.shouldRefresh(itemFieldKey, props.field.itemFields)
164+
});
156165
}
157166
158167
function onFieldLocaleChange(fieldKey: string, locale: string) {

resources/js/form/components/fields/Tags.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
&& !props.field.options.find(o => o.label === searchTerm.value);
6161
});
6262
63-
emit('input', props.value?.map(option => withItemKey(option)));
63+
emit('input', props.value?.map(option => withItemKey(option)), { skipRefresh: true });
6464
</script>
6565

6666
<template>

resources/js/form/components/fields/editor/Editor.vue

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup lang="ts">
22
import { __ } from "@/utils/i18n";
33
import { FormEditorFieldData } from "@/types";
4-
import { computed, provide, ref, watch } from "vue";
4+
import { computed, nextTick, onBeforeUnmount, onMounted, provide, ref, watch } from "vue";
55
import { Editor, BubbleMenu, isActive } from "@tiptap/vue-3";
66
import debounce from 'lodash/debounce';
77
import { EditorContent } from '@tiptap/vue-3';
@@ -60,13 +60,17 @@
6060
});
6161
const embedModal = ref<InstanceType<typeof EditorEmbedModal>>();
6262
const linkDropdown = ref<InstanceType<typeof LinkDropdown>>();
63+
const isMounted = ref(false);
64+
const isUnmounting = ref(false);
6365
6466
provide<ParentEditor>('editor', {
6567
props,
6668
uploadManager,
6769
uploadModal,
6870
embedManager,
6971
embedModal,
72+
isMounted,
73+
isUnmounting,
7074
} satisfies ParentEditor);
7175
7276
const editor = useLocalizedEditor(
@@ -151,6 +155,16 @@
151155
}
152156
);
153157
158+
onMounted(() => {
159+
setTimeout(() => {
160+
isMounted.value = true;
161+
}, 10);
162+
});
163+
164+
onBeforeUnmount(() => {
165+
isUnmounting.value = true;
166+
});
167+
154168
const dropdownEmbeds = computed(() =>
155169
Object.values(props.field.embeds ?? {})
156170
.filter(embed => !props.field.toolbar?.includes(`embed:${embed.key}`))

resources/js/form/components/fields/editor/extensions/embed/EmbedNode.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
});
3232
}
3333
34-
useEditorNode({
34+
useEditorNode(props, {
3535
onAdded: () => {
3636
embedManager.restoreEmbed(props.node.attrs['data-key'], props.extension.options.embed)
3737
},

resources/js/form/components/fields/editor/extensions/upload/UploadNode.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
const uploadComponent = ref<InstanceType<typeof Upload>>();
2020
const upload = computed(() => uploadManager.getUpload(props.node.attrs['data-key']));
2121
22-
useEditorNode({
22+
useEditorNode(props, {
2323
onAdded: () => {
2424
uploadManager.restoreUpload(props.node.attrs['data-key']);
2525
},

resources/js/form/components/fields/editor/useEditorNode.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { onBeforeUnmount, onMounted } from "vue";
22
import { useParentEditor } from "@/form/components/fields/editor/useParentEditor";
3+
import { ExtensionNodeProps } from "@/form/components/fields/editor/types";
34

45

5-
export function useEditorNode({ onAdded, onRemoved }: { onAdded: () => void, onRemoved: () => void }) {
6+
export function useEditorNode(
7+
props: ExtensionNodeProps<any, any>,
8+
{ onAdded, onRemoved }: { onAdded: () => void, onRemoved: () => void }
9+
) {
610
const parentEditor = useParentEditor();
711
const locale = parentEditor.props.locale;
812

@@ -12,10 +16,12 @@ export function useEditorNode({ onAdded, onRemoved }: { onAdded: () => void, onR
1216
// }, { flush: 'sync' });
1317

1418
onMounted(() => {
15-
onAdded();
19+
if(parentEditor.isMounted.value) {
20+
onAdded();
21+
}
1622
});
1723
onBeforeUnmount(() => {
18-
if(!parentEditor.props.field.localized || locale === parentEditor.props.locale) {
24+
if(!parentEditor.isUnmounting.value && (!parentEditor.props.field.localized || locale === parentEditor.props.locale)) {
1925
onRemoved();
2026
}
2127
});

0 commit comments

Comments
 (0)