Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions demo/app/Sharp/Posts/PostForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Code16\Sharp\Form\Fields\SharpFormCheckField;
use Code16\Sharp\Form\Fields\SharpFormDateField;
use Code16\Sharp\Form\Fields\SharpFormEditorField;
use Code16\Sharp\Form\Fields\SharpFormHtmlField;
use Code16\Sharp\Form\Fields\SharpFormListField;
use Code16\Sharp\Form\Fields\SharpFormTagsField;
use Code16\Sharp\Form\Fields\SharpFormTextareaField;
Expand All @@ -27,6 +28,7 @@
use Code16\Sharp\Form\Layout\FormLayoutTab;
use Code16\Sharp\Form\SharpForm;
use Code16\Sharp\Utils\Fields\FieldsContainer;
use Illuminate\Support\Facades\Blade;

class PostForm extends SharpForm
{
Expand Down Expand Up @@ -104,6 +106,29 @@ public function buildFormFields(FieldsContainer $formFields): void
->setLabel('Publication date')
->setHasTime(),
)
->addField(
SharpFormHtmlField::make('publication_label')
->setLiveRefresh(linkedFields: ['author_id', 'published_at', 'attachments'])
->setTemplate(function (array $data) {
if (! isset($data['published_at'])) {
return '';
}

return Blade::render(<<<'blade'
This post will be published on {{ $published_at }}
@if($author)
by {{ $author->name }}.
@endif
<br>
{{ count($linkAttachments) }} link attachments, {{ count($fileAttachments) }} file attachments.
blade, [
'published_at' => \Carbon\Carbon::parse($data['published_at'])->isoFormat('LLLL'),
'author' => \App\Models\User::find($data['author_id']),
'linkAttachments' => collect($data['attachments'])->where('is_link', true)->values(),
'fileAttachments' => collect($data['attachments'])->where('is_link', false)->values(),
]);
})
)
->addField(
SharpFormListField::make('attachments')
->setLabel('Attachments')
Expand Down Expand Up @@ -131,7 +156,7 @@ public function buildFormFields(FieldsContainer $formFields): void
->setStorageDisk('local')
->setStorageBasePath('data/posts/{id}')
->addConditionalDisplay('!is_link'),
),
)
)
->when(sharp()->context()->isUpdate(), fn ($formFields) => $formFields->addField(
SharpFormAutocompleteRemoteField::make('author_id')
Expand Down Expand Up @@ -168,10 +193,10 @@ public function buildFormLayout(FormLayout $formLayout): void
fn ($column) => $column->withField('author_id')
)
->withFields('published_at', 'categories')
->withField('publication_label')
->withListField('attachments', function (FormLayoutColumn $item) {
$item->withFields(title: 8, is_link: 4)
->withField('link_url')
->withField('document');
->withField('link_url');
});
})
->addColumn(6, function (FormLayoutColumn $column) {
Expand Down
24 changes: 23 additions & 1 deletion demo/app/Sharp/TestForm/TestForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use Code16\Sharp\Form\Layout\FormLayoutTab;
use Code16\Sharp\Form\SharpSingleForm;
use Code16\Sharp\Utils\Fields\FieldsContainer;
use Illuminate\Database\Eloquent\Builder;

class TestForm extends SharpSingleForm
{
Expand Down Expand Up @@ -161,7 +162,16 @@ public function buildFormFields(FieldsContainer $formFields): void
->setListItemTemplate('{{ $name }}')
->setResultItemTemplate('{{ $name }} ({{ $id }})')
->setRemoteCallback(function ($search, $data) {
dd($data);
$users = User::orderBy('name');

foreach (explode(' ', trim($search)) as $word) {
$users->where(function (Builder $query) use ($word) {
$query->orWhere('name', 'like', "%$word%")
->orWhere('email', 'like', "%$word%");
});
}

return $users->limit(10)->get();
}, linkedFields: ['select']),
)
->addItemField(SharpFormEditorField::make('markdown2')
Expand All @@ -170,6 +180,18 @@ public function buildFormFields(FieldsContainer $formFields): void
->setToolbar([
SharpFormEditorField::B, SharpFormEditorField::I, SharpFormEditorField::A,
]),
)
->addItemField(
SharpFormHtmlField::make('document_infos')
->setLiveRefresh(linkedFields: ['select'])
->setTemplate(function (array $data) {
return isset($data['select'])
? sprintf(
'You have selected : %s',
$this->options()[$data['select']]
)
: '';
})
),
)
->addField(
Expand Down
29 changes: 28 additions & 1 deletion docs/guide/form-fields/html.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This field is read-only, and is meant to display some dynamic information in the

## Configuration

### `setTemplate(string|View $template)`
### `setTemplate(string|View|Closure $template)`

Write the blade template as a string. Example:

Expand Down Expand Up @@ -35,6 +35,33 @@ SharpFormHtmlField::make('panel')
->setTemplate(view('sharp.form-htm-field'))
```

Using a closure:

```php
SharpFormHtmlField::make('panel')
->setTemplate(function (array $data) {
return 'You have chosen:'.$data['another_form_field'].'. Date: '.$data['date'];
})
```

#### Accessing to other field values in the form

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).

### `setLiveRefresh(bool $liveRefresh = true, ?array $linkedFields = null)`

Use this method to dynamically update Html field when the user changes another field.
The `$linkedFields` parameter allows filtering which field to watch (without it the internal refresh endpoint is called on any field update).

```php
SharpFormHtmlField::make('total')
->setLiveRefresh(linkedFields: ['products'])
->setTemplate(function (array $data) {
return 'Total:'.collect($data['products'])
->sum(fn ($product) => $product['price']);
})
```

## Formatter

- `toFront`: sent as provided.
Expand Down
1 change: 0 additions & 1 deletion resources/js/Pages/Form/Form.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import { onUnmounted, ref, useTemplateRef, watch } from "vue";
import { __ } from "@/utils/i18n";
import { Button } from '@/components/ui/button';
import { useResizeObserver } from "@vueuse/core";
import { slugify } from "@/utils";

const props = defineProps<{
Expand Down
8 changes: 8 additions & 0 deletions resources/js/form/Form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,14 @@ export class Form implements FormData, CommandFormData, EventTarget {
}, 0);
}

shouldRefresh(updatedFieldKey: string, fields = this.fields): boolean {
return Object.values(fields).some(field =>
field.type === 'html'
&& field.liveRefresh
&& (!field.liveRefreshLinkedFields || field.liveRefreshLinkedFields.includes(updatedFieldKey))
);
}

eventTarget: EventTarget = new EventTarget();

addEventListener(type: FormEvents, callback: EventListener, options?: AddEventListenerOptions | boolean): void {
Expand Down
29 changes: 26 additions & 3 deletions resources/js/form/components/Form.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,19 @@
import { slugify } from "@/utils";
import { Badge } from "@/components/ui/badge";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { FieldMeta } from "@/form/types";
import { FieldMeta, FormFieldEmitInputOptions } from "@/form/types";
import StickyTop from "@/components/StickyTop.vue";
import StickyBottom from "@/components/StickyBottom.vue";
import { Menu } from 'lucide-vue-next';
import { Label } from "@/components/ui/label";
import RootCardHeader from "@/components/ui/RootCardHeader.vue";
import { vScrollIntoView } from "@/directives/scroll-into-view";
import { useResizeObserver } from "@vueuse/core";
import debounce from "lodash/debounce";
import { api } from "@/api/api";
import { route } from "@/utils/url";
import { useParentCommands } from "@/commands/useCommands";
import merge from 'lodash/merge';

const props = defineProps<{
form: Form
Expand Down Expand Up @@ -77,14 +82,32 @@
props.form.setMeta(fieldKey, { uploading });
}

function onFieldInput(fieldKey: string, value: FormFieldData['value'], { force = false } = {}) {
const parentCommands = useParentCommands();
const refresh = debounce((data) => {
api.post(route('code16.sharp.api.form.refresh.update', {
entityKey: props.form.entityKey,
instance_id: props.form.instanceId,
embed_key: props.form.embedKey,
entity_list_command_key: parentCommands?.commandContainer === 'entityList' ? props.form.commandKey : null,
show_command_key: parentCommands?.commandContainer === 'show' ? props.form.commandKey : null,
}), data)
.then(response => {
merge(props.form.data, response.data.form.data);
});
}, 200);

function onFieldInput(fieldKey: string, value: FormFieldData['value'], inputOptions: FormFieldEmitInputOptions = {}) {
const data = {
...props.form.data,
...(!force ? getDependantFieldsResetData(props.form.fields, fieldKey) : null),
...(!inputOptions.force ? getDependantFieldsResetData(props.form.fields, fieldKey) : null),
[fieldKey]: value,
};

props.form.data = data;

if((props.form.shouldRefresh(fieldKey) || inputOptions.shouldRefresh) && !inputOptions.skipRefresh) {
refresh(data);
}
}

const title = useTemplateRef<HTMLElement>('title');
Expand Down
19 changes: 14 additions & 5 deletions resources/js/form/components/fields/List.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { computed, nextTick, ref, watch, watchEffect } from "vue";
import { Button, buttonVariants } from '@/components/ui/button';
import { showAlert } from "@/utils/dialogs";
import { FieldMeta, FieldsMeta, FormFieldEmits, FormFieldProps } from "@/form/types";
import { FieldMeta, FieldsMeta, FormFieldEmitInputOptions, FormFieldEmits, FormFieldProps } from "@/form/types";
import FieldGridRow from "@/components/ui/FieldGridRow.vue";
import FieldGridColumn from "@/components/ui/FieldGridColumn.vue";
import { Toggle } from "@/components/ui/toggle";
Expand Down Expand Up @@ -81,7 +81,7 @@
emit('input', props.value?.map(((item, index) => ({ ...item, [errorIndex]: index }))), { preserveError: true });
});

emit('input', props.value?.map(item => ({ ...item, [itemKey]: itemKeyIndex++ })), { force: true });
emit('input', props.value?.map(item => ({ ...item, [itemKey]: itemKeyIndex++ })), { force: true, skipRefresh: true });

watchArray(() => props.value ?? [], async (newList, oldList, added) => {
if(!added.length) {
Expand Down Expand Up @@ -142,17 +142,26 @@
e.target.value = '';
}

function onFieldInput(itemIndex: number, itemFieldKey: string, itemFieldValue: FormFieldData['value'], { force = false } = {}) {
function onFieldInput(
itemIndex: number,
itemFieldKey: string,
itemFieldValue: FormFieldData['value'],
inputOptions: FormFieldEmitInputOptions
) {
emit('input', props.value.map((item, i) => {
if(i === itemIndex) {
return {
...item,
...(!force ? getDependantFieldsResetData(props.field.itemFields, itemFieldKey) : null),
...(!inputOptions?.force ? getDependantFieldsResetData(props.field.itemFields, itemFieldKey) : null),
[itemFieldKey]: itemFieldValue,
}
}
return item;
}));
}), {
force: inputOptions?.force,
skipRefresh: inputOptions?.skipRefresh,
shouldRefresh: form.shouldRefresh(itemFieldKey, props.field.itemFields)
});
}

function onFieldLocaleChange(fieldKey: string, locale: string) {
Expand Down
2 changes: 1 addition & 1 deletion resources/js/form/components/fields/Tags.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
&& !props.field.options.find(o => o.label === searchTerm.value);
});

emit('input', props.value?.map(option => withItemKey(option)));
emit('input', props.value?.map(option => withItemKey(option)), { skipRefresh: true });
</script>

<template>
Expand Down
16 changes: 15 additions & 1 deletion resources/js/form/components/fields/editor/Editor.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { __ } from "@/utils/i18n";
import { FormEditorFieldData } from "@/types";
import { computed, provide, ref, watch } from "vue";
import { computed, nextTick, onBeforeUnmount, onMounted, provide, ref, watch } from "vue";
import { Editor, BubbleMenu, isActive } from "@tiptap/vue-3";
import debounce from 'lodash/debounce';
import { EditorContent } from '@tiptap/vue-3';
Expand Down Expand Up @@ -60,13 +60,17 @@
});
const embedModal = ref<InstanceType<typeof EditorEmbedModal>>();
const linkDropdown = ref<InstanceType<typeof LinkDropdown>>();
const isMounted = ref(false);
const isUnmounting = ref(false);

provide<ParentEditor>('editor', {
props,
uploadManager,
uploadModal,
embedManager,
embedModal,
isMounted,
isUnmounting,
} satisfies ParentEditor);

const editor = useLocalizedEditor(
Expand Down Expand Up @@ -151,6 +155,16 @@
}
);

onMounted(() => {
setTimeout(() => {
isMounted.value = true;
}, 10);
});

onBeforeUnmount(() => {
isUnmounting.value = true;
});

const dropdownEmbeds = computed(() =>
Object.values(props.field.embeds ?? {})
.filter(embed => !props.field.toolbar?.includes(`embed:${embed.key}`))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
});
}

useEditorNode({
useEditorNode(props, {
onAdded: () => {
embedManager.restoreEmbed(props.node.attrs['data-key'], props.extension.options.embed)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
const uploadComponent = ref<InstanceType<typeof Upload>>();
const upload = computed(() => uploadManager.getUpload(props.node.attrs['data-key']));

useEditorNode({
useEditorNode(props, {
onAdded: () => {
uploadManager.restoreUpload(props.node.attrs['data-key']);
},
Expand Down
12 changes: 9 additions & 3 deletions resources/js/form/components/fields/editor/useEditorNode.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { onBeforeUnmount, onMounted } from "vue";
import { useParentEditor } from "@/form/components/fields/editor/useParentEditor";
import { ExtensionNodeProps } from "@/form/components/fields/editor/types";


export function useEditorNode({ onAdded, onRemoved }: { onAdded: () => void, onRemoved: () => void }) {
export function useEditorNode(
props: ExtensionNodeProps<any, any>,
{ onAdded, onRemoved }: { onAdded: () => void, onRemoved: () => void }
) {
const parentEditor = useParentEditor();
const locale = parentEditor.props.locale;

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

onMounted(() => {
onAdded();
if(parentEditor.isMounted.value) {
onAdded();
}
});
onBeforeUnmount(() => {
if(!parentEditor.props.field.localized || locale === parentEditor.props.locale) {
if(!parentEditor.isUnmounting.value && (!parentEditor.props.field.localized || locale === parentEditor.props.locale)) {
onRemoved();
}
});
Expand Down
2 changes: 2 additions & 0 deletions resources/js/form/components/fields/editor/useParentEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export type ParentEditor = {
embedModal: Ref<InstanceType<typeof EditorEmbedModal>>
uploadManager: ContentUploadManager<Form>,
uploadModal: Ref<InstanceType<typeof EditorUploadModal>>,
isMounted: Ref<boolean>,
isUnmounting: Ref<boolean>,
};

export function useParentEditor() {
Expand Down
Loading
Loading