Add fields

This commit is contained in:
philipp lang 2023-12-26 00:44:49 +01:00
parent 288533efd3
commit bde8d48807
15 changed files with 366 additions and 60 deletions

View File

@ -0,0 +1,23 @@
<?php
namespace App\Form\Fields;
class DropdownField extends Field
{
public static function name(): string
{
return 'Dropdown';
}
public static function meta(): array
{
return [
'options' => [],
];
}
public static function default()
{
return null;
}
}

46
app/Form/Fields/Field.php Normal file
View File

@ -0,0 +1,46 @@
<?php
namespace App\Form\Fields;
use Illuminate\Support\Collection;
abstract class Field
{
abstract public static function name(): string;
abstract public static function meta(): array;
abstract public static function default();
public static function asMeta(): array
{
return self::classNames()->map(fn ($class) => $class::allMeta())->toArray();
}
/**
* @return Collection<int, class-string<self>>
*/
private static function classNames(): Collection
{
return collect(glob(base_path('app/Form/Fields/*.php')))
->filter(fn ($fieldClass) => preg_match('/[A-Za-z]Field\.php$/', $fieldClass) === 1)
->map(fn ($fieldClass) => str($fieldClass)->replace(base_path(''), '')->replace('/app', '/App')->replace('.php', '')->replace('/', '\\')->toString())
->values();
}
public static function allMeta(): array
{
return [
'id' => class_basename(static::class),
'name' => static::name(),
'default' => [
'name' => '',
'type' => class_basename(static::class),
'columns' => ['mobile' => 2, 'tablet' => 4, 'desktop' => 12],
'default' => static::default(),
'required' => false,
...static::meta(),
],
];
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Form\Fields;
class RadioField extends Field
{
public static function name(): string
{
return 'Radio';
}
public static function meta(): array
{
return [
'options' => [],
];
}
public static function default()
{
return null;
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Form\Fields;
class TextField extends Field
{
public static function name(): string
{
return 'Text';
}
public static function meta(): array
{
return [];
}
public static function default(): string
{
return '';
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Form\Fields;
class TextareaField extends Field
{
public static function name(): string
{
return 'Textarea';
}
public static function meta(): array
{
return [
'rows' => 5,
];
}
public static function default(): string
{
return '';
}
}

View File

@ -2,6 +2,7 @@
namespace App\Form\Resources;
use App\Form\Fields\Field;
use App\Lib\HasMeta;
use Illuminate\Http\Resources\Json\JsonResource;
@ -32,7 +33,8 @@ class FormtemplateResource extends JsonResource
public static function meta(): array
{
return [
'fields' => [
'fields' => Field::asMeta(),
[
[
'id' => 'TextField',
'name' => 'Text',

View File

@ -0,0 +1,28 @@
<?php
namespace Database\Factories\Form\Models;
use App\Form\Models\Formtemplate;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Formtemplate>
*/
class FormtemplateFactory extends Factory
{
public $model = Formtemplate::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
'name' => $this->faker->words(4, true),
'config' => [],
];
}
}

View File

@ -5,8 +5,20 @@
<span v-show="required" class="text-red-800">&nbsp;*</span>
</span>
<div class="real-field-wrap size-sm" :class="sizes[size].field">
<input :name="name" :type="type" :value="transformedValue" :disabled="disabled" :placeholder="placeholder"
@keypress="$emit('keypress', $event)" @input="onInput" @change="onChange" @focus="onFocus" @blur="onBlur" />
<input
:name="name"
:type="type"
:value="transformedValue"
:disabled="disabled"
:placeholder="placeholder"
:min="min"
:max="max"
@keypress="$emit('keypress', $event)"
@input="onInput"
@change="onChange"
@focus="onFocus"
@blur="onBlur"
/>
<div v-if="hint" class="info-wrap">
<div v-tooltip="hint">
<ui-sprite src="info-button" class="info-button"></ui-sprite>
@ -281,6 +293,12 @@ export default {
default: false,
type: Boolean,
},
min: {
default: () => '',
},
max: {
default: () => '',
},
name: {},
},
data: function () {
@ -324,7 +342,7 @@ export default {
},
created() {
if (typeof this.modelValue === 'undefined') {
this.$emit('input', this.default === undefined ? '' : this.default);
this.$emit('update:modelValue', this.default === undefined ? '' : this.default);
}
},
methods: {

View File

@ -19,8 +19,6 @@
>{{ label }}<span v-show="required" class="text-red-800">&nbsp;*</span></span
>
<textarea
v-text="value"
@input="trigger"
:placeholder="placeholder"
class="h-full outline-none bg-gray-700 border-gray-600 border-solid"
:rows="rows"
@ -28,6 +26,8 @@
'rounded-lg text-sm border-2 p-2 text-gray-300': size === null,
'rounded-lg py-2 px-2 text-xs border-2 text-gray-300': size == 'sm',
}"
@input="trigger"
v-text="modelValue"
></textarea>
<div v-if="hint" v-tooltip="hint" class="absolute right-0 top-0 mr-2 mt-2">
<ui-sprite src="info-button" class="w-5 h-5 text-indigo-200"></ui-sprite>
@ -35,66 +35,57 @@
</label>
</template>
<script>
export default {
data: function () {
return {
focus: false,
};
<script setup>
const emit = defineEmits(['update:modelValue']);
const props = defineProps({
required: {
type: Boolean,
default: false,
},
props: {
required: {
type: Boolean,
default: false,
},
inset: {
default: false,
type: Boolean,
},
size: {
default: null,
},
rows: {
default: function () {
return 4;
},
},
id: {
required: true,
},
hint: {
default: null,
},
value: {
default: undefined,
},
mask: {
default: undefined,
},
label: {
default: false,
},
type: {
required: false,
default: function () {
return 'text';
},
},
placeholder: {
default: '',
inset: {
default: false,
type: Boolean,
},
size: {
default: null,
},
rows: {
default: function () {
return 4;
},
},
methods: {
trigger(v) {
this.$emit('input', v.target.value);
id: {
required: true,
},
hint: {
default: null,
},
modelValue: {
default: undefined,
},
mask: {
default: undefined,
},
label: {
default: false,
},
type: {
required: false,
default: function () {
return 'text';
},
},
created() {
if (typeof this.value === 'undefined') {
this.$emit('input', '');
}
placeholder: {
default: '',
},
};
});
function trigger(v) {
emit('update:modelValue', v.target.value);
}
if (typeof props.modelValue === 'undefined') {
emit('update:modelValue', '');
}
</script>
<style scope>

View File

@ -0,0 +1,23 @@
<template>
<ui-box class="relative" :heading="heading" container-class="grid gap-3">
<slot></slot>
<template #in-title>
<div class="flex ml-3 space-x-3 absolute right-0 top-0 mr-4 mt-4">
<a href="#" @click.prevent="$emit('submit')">
<ui-sprite src="save" class="text-zinc-400 w-4 h-4"></ui-sprite>
</a>
<a href="#" @click.prevent="$emit('close')">
<ui-sprite src="close" class="text-zinc-400 w-4 h-4"></ui-sprite>
</a>
</div>
</template>
</ui-box>
</template>
<script setup>
const emit = defineEmits(['close', 'submit']);
const props = defineProps({
heading: {},
});
</script>

View File

@ -0,0 +1,23 @@
<template>
<div class="space-y-1">
<span class="text-xs font-semibold text-gray-400">Optionen</span>
<f-text
v-for="(option, index) in modelValue.options"
:id="`options-${index}`"
:key="index"
size="sm"
:name="`options-${index}`"
:model-value="option"
@update:modelValue="$emit('update:modelValue', {...props.modelValue, options: props.modelValue.options.toSpliced(index, 1, $event)})"
></f-text>
<ui-icon-button icon="plus" @click="$emit('update:modelValue', {...modelValue, options: [...modelValue.options, '']})">Option einfügen</ui-icon-button>
</div>
</template>
<script setup>
const props = defineProps({
modelValue: {},
});
const emit = defineEmits(['update:modelValue']);
</script>

View File

@ -27,6 +27,7 @@
>
<f-text id="fieldname" v-model="singleField.model.name" label="Name" size="sm" name="fieldname"></f-text>
<f-switch id="fieldrequired" v-model="singleField.model.required" label="Erforderlich" size="sm" name="fieldrequired" inline></f-switch>
<component :is="fields[singleField.model.type]" v-model="singleField.model"></component>
</asideform>
</div>
<ui-box heading="Vorschau" container-class="grid gap-3" class="w-[800px]">
@ -49,6 +50,9 @@
import {computed, ref} from 'vue';
import '!/eventform/dist/main.js';
import Asideform from './Asideform.vue';
import TextareaField from './TextareaField.vue';
import DropdownField from './DropdownField.vue';
import RadioField from './RadioField.vue';
const sectionVisible = ref(-1);
const singleSection = ref(null);
@ -60,6 +64,12 @@ const props = defineProps({
});
const emit = defineEmits(['submit', 'cancel']);
const fields = {
TextareaField: TextareaField,
DropdownField: DropdownField,
RadioField: RadioField,
};
function editSection(sectionIndex) {
singleSection.value = {
model: {...inner.value.config.sections[sectionIndex]},

View File

@ -0,0 +1,23 @@
<template>
<div class="space-y-1">
<span class="text-xs font-semibold text-gray-400">Optionen</span>
<f-text
v-for="(option, index) in modelValue.options"
:id="`options-${index}`"
:key="index"
size="sm"
:name="`options-${index}`"
:model-value="option"
@update:modelValue="$emit('update:modelValue', {...props.modelValue, options: props.modelValue.options.toSpliced(index, 1, $event)})"
></f-text>
<ui-icon-button icon="plus" @click="$emit('update:modelValue', {...modelValue, options: [...modelValue.options, '']})">Option einfügen</ui-icon-button>
</div>
</template>
<script setup>
const props = defineProps({
modelValue: {},
});
const emit = defineEmits(['update:modelValue']);
</script>

View File

@ -0,0 +1,22 @@
<template>
<div>
<f-text
id="rows"
label="Zeilen"
size="sm"
name="rows"
:model-value="modelValue.rows"
type="number"
min="1"
@update:modelValue="$emit('update:modelValue', {...modelValue, rows: $event})"
></f-text>
</div>
</template>
<script setup>
const props = defineProps({
modelValue: {},
});
const emit = defineEmits(['update:modelValue']);
</script>

View File

@ -2,6 +2,7 @@
namespace Tests\Feature\Form;
use App\Form\Models\Formtemplate;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
@ -12,10 +13,27 @@ class FormtemplateIndexActionTest extends TestCase
public function testItDisplaysIndexPage(): void
{
$formtemplate = Formtemplate::factory()->create();
$this->login()->loginNami()->withoutExceptionHandling();
$this->get(route('formtemplate.index'))
->assertInertiaPath('data.data.0.links', [
'update' => route('formtemplate.update', ['formtemplate' => $formtemplate]),
])
->assertInertiaPath('data.meta.fields.0', [
'id' => 'DropdownField',
'name' => 'Dropdown',
'default' => [
'name' => '',
'type' => 'DropdownField',
'columns' => ['mobile' => 2, 'tablet' => 4, 'desktop' => 12],
'default' => [],
'required' => false,
'options' => [],
]
])
->assertInertiaPath('data.meta.fields.1', [
'id' => 'TextField',
'name' => 'Text',
'default' => [
@ -26,6 +44,18 @@ class FormtemplateIndexActionTest extends TestCase
'required' => false,
]
])
->assertInertiaPath('data.meta.fields.2', [
'id' => 'TextareaField',
'name' => 'Textarea',
'default' => [
'name' => '',
'type' => 'TextareaField',
'columns' => ['mobile' => 2, 'tablet' => 4, 'desktop' => 12],
'default' => '',
'required' => false,
'rows' => 5,
]
])
->assertInertiaPath('data.meta.default', [
'name' => '',
'config' => [