Skip to content

Commit b609652

Browse files
authored
Merge pull request #620 from code16/form-live-field
Form Html Field : live refresh
2 parents 53a49c7 + 2aa879c commit b609652

39 files changed

Lines changed: 850 additions & 107 deletions

demo/app/Sharp/Posts/PostForm.php

Lines changed: 28 additions & 3 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,29 @@ 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', 'attachments'])
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+
<br>
123+
{{ count($linkAttachments) }} link attachments, {{ count($fileAttachments) }} file attachments.
124+
blade, [
125+
'published_at' => \Carbon\Carbon::parse($data['published_at'])->isoFormat('LLLL'),
126+
'author' => \App\Models\User::find($data['author_id']),
127+
'linkAttachments' => collect($data['attachments'])->where('is_link', true)->values(),
128+
'fileAttachments' => collect($data['attachments'])->where('is_link', false)->values(),
129+
]);
130+
})
131+
)
107132
->addField(
108133
SharpFormListField::make('attachments')
109134
->setLabel('Attachments')
@@ -131,7 +156,7 @@ public function buildFormFields(FieldsContainer $formFields): void
131156
->setStorageDisk('local')
132157
->setStorageBasePath('data/posts/{id}')
133158
->addConditionalDisplay('!is_link'),
134-
),
159+
)
135160
)
136161
->when(sharp()->context()->isUpdate(), fn ($formFields) => $formFields->addField(
137162
SharpFormAutocompleteRemoteField::make('author_id')
@@ -168,10 +193,10 @@ public function buildFormLayout(FormLayout $formLayout): void
168193
fn ($column) => $column->withField('author_id')
169194
)
170195
->withFields('published_at', 'categories')
196+
->withField('publication_label')
171197
->withListField('attachments', function (FormLayoutColumn $item) {
172198
$item->withFields(title: 8, is_link: 4)
173-
->withField('link_url')
174-
->withField('document');
199+
->withField('link_url');
175200
});
176201
})
177202
->addColumn(6, function (FormLayoutColumn $column) {

demo/app/Sharp/TestForm/TestForm.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Code16\Sharp\Form\Layout\FormLayoutTab;
2626
use Code16\Sharp\Form\SharpSingleForm;
2727
use Code16\Sharp\Utils\Fields\FieldsContainer;
28+
use Illuminate\Database\Eloquent\Builder;
2829

2930
class TestForm extends SharpSingleForm
3031
{
@@ -161,7 +162,16 @@ public function buildFormFields(FieldsContainer $formFields): void
161162
->setListItemTemplate('{{ $name }}')
162163
->setResultItemTemplate('{{ $name }} ({{ $id }})')
163164
->setRemoteCallback(function ($search, $data) {
164-
dd($data);
165+
$users = User::orderBy('name');
166+
167+
foreach (explode(' ', trim($search)) as $word) {
168+
$users->where(function (Builder $query) use ($word) {
169+
$query->orWhere('name', 'like', "%$word%")
170+
->orWhere('email', 'like', "%$word%");
171+
});
172+
}
173+
174+
return $users->limit(10)->get();
165175
}, linkedFields: ['select']),
166176
)
167177
->addItemField(SharpFormEditorField::make('markdown2')
@@ -170,6 +180,18 @@ public function buildFormFields(FieldsContainer $formFields): void
170180
->setToolbar([
171181
SharpFormEditorField::B, SharpFormEditorField::I, SharpFormEditorField::A,
172182
]),
183+
)
184+
->addItemField(
185+
SharpFormHtmlField::make('document_infos')
186+
->setLiveRefresh(linkedFields: ['select'])
187+
->setTemplate(function (array $data) {
188+
return isset($data['select'])
189+
? sprintf(
190+
'You have selected : %s',
191+
$this->options()[$data['select']]
192+
)
193+
: '';
194+
})
173195
),
174196
)
175197
->addField(

docs/guide/form-fields/html.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This field is read-only, and is meant to display some dynamic information in the
66

77
## Configuration
88

9-
### `setTemplate(string|View $template)`
9+
### `setTemplate(string|View|Closure $template)`
1010

1111
Write the blade template as a string. Example:
1212

@@ -35,6 +35,33 @@ SharpFormHtmlField::make('panel')
3535
->setTemplate(view('sharp.form-htm-field'))
3636
```
3737

38+
Using a closure:
39+
40+
```php
41+
SharpFormHtmlField::make('panel')
42+
->setTemplate(function (array $data) {
43+
return 'You have chosen:'.$data['another_form_field'].'. Date: '.$data['date'];
44+
})
45+
```
46+
47+
#### Accessing to other field values in the form
48+
49+
In the template, all other field values of the form are available (alongside the Html field value). This is particularly useful when using `setLiveRefresh()` (described below).
50+
51+
### `setLiveRefresh(bool $liveRefresh = true, ?array $linkedFields = null)`
52+
53+
Use this method to dynamically update Html field when the user changes another field.
54+
The `$linkedFields` parameter allows filtering which field to watch (without it the internal refresh endpoint is called on any field update).
55+
56+
```php
57+
SharpFormHtmlField::make('total')
58+
->setLiveRefresh(linkedFields: ['products'])
59+
->setTemplate(function (array $data) {
60+
return 'Total:'.collect($data['products'])
61+
->sum(fn ($product) => $product['price']);
62+
})
63+
```
64+
3865
## Formatter
3966

4067
- `toFront`: sent as provided.

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
@@ -284,6 +284,14 @@ export class Form implements FormData, CommandFormData, EventTarget {
284284
}, 0);
285285
}
286286

287+
shouldRefresh(updatedFieldKey: string, fields = this.fields): boolean {
288+
return Object.values(fields).some(field =>
289+
field.type === 'html'
290+
&& field.liveRefresh
291+
&& (!field.liveRefreshLinkedFields || field.liveRefreshLinkedFields.includes(updatedFieldKey))
292+
);
293+
}
294+
287295
eventTarget: EventTarget = new EventTarget();
288296

289297
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,14 +82,32 @@
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+
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
87106
props.form.data = data;
107+
108+
if((props.form.shouldRefresh(fieldKey) || inputOptions.shouldRefresh) && !inputOptions.skipRefresh) {
109+
refresh(data);
110+
}
88111
}
89112
90113
const title = useTemplateRef<HTMLElement>('title');

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
},

0 commit comments

Comments
 (0)