Skip to content

Commit

Permalink
feat: add new way of declaring forms (#218)
Browse files Browse the repository at this point in the history
  • Loading branch information
EdieLemoine authored Nov 21, 2023
1 parent 49f83d5 commit 9546c82
Show file tree
Hide file tree
Showing 39 changed files with 816 additions and 208 deletions.
7 changes: 6 additions & 1 deletion apps/demo/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
to="/forms/no-prop">
Form without element props
</NLink>
<NLink
class="hover:text-pink-200 transition-colors"
to="/forms/new-form">
New form structure
</NLink>
</nav>
</div>
</header>
Expand All @@ -31,7 +36,7 @@
</NContainer>
</template>

<script setup lang="ts">
<script lang="ts" setup>
import {RouterView} from 'vue-router';
import NLink from './components/NLink.vue';
import NContainer from './components/NContainer.vue';
Expand Down
6 changes: 6 additions & 0 deletions apps/demo/src/assets/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
@tailwind components;
@tailwind utilities;

@layer base {
* {
@apply border-gray-400 dark:border-gray-700;
}
}

h1, h2, h3, h4, h5, h6 {
@apply font-bold pb-2 pt-4;
}
Expand Down
37 changes: 37 additions & 0 deletions apps/demo/src/components/FormDiagnostics.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<template>
<div class="bg-opacity-5 bg-white mt-4 px-8 py-4 rounded-lg">
<div class="auto-rows-auto gap-2 grid grid-cols-[1fr_3fr] grid-flow-row">
<b>Name</b>
<span v-text="form.name"></span>

<b>isDirty</b>
<span v-text="form.isDirty" />

<b>isValid</b>
<span v-text="form.isValid" />

<b>getValues()</b>
<pre v-text="values" />

<b>Event log</b>
<textarea
v-model="eventLog"
class="font-mono w-full"
rows="10"
readonly />
</div>
</div>
</template>

<script setup lang="ts">
import {computed, toRefs} from 'vue';
import {type MaybeUnwrapNestedRefs} from '@myparcel-vfb/core';
import {type FormInstance} from '@myparcel/vue-form-builder';
import {useFormEventLog} from '../composables/useFormEventLog';
const props = defineProps<{form: MaybeUnwrapNestedRefs<FormInstance>}>();
const propRefs = toRefs(props);
const eventLog = useFormEventLog(propRefs.form.value);
const values = computed(() => props.form.getValues());
</script>
27 changes: 27 additions & 0 deletions apps/demo/src/components/StandAloneSubmitButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<template>
<TSubmitButton
v-if="element"
:element="element"
:outline="outline"
@click="() => form?.submit()">
<template
v-for="name in Object.keys($slots)"
:key="name"
#[name]="scope">
<slot
:name="name"
v-bind="scope || {}" />
</template>
</TSubmitButton>
</template>

<script lang="ts" setup>
import {inject} from 'vue';
import {INJECT_ELEMENT, INJECT_FORM} from '@myparcel-vfb/core';
import TSubmitButton from './template/TSubmitButton.vue';
defineProps<{outline?: boolean}>();
const form = inject(INJECT_FORM);
const element = inject(INJECT_ELEMENT);
</script>
14 changes: 14 additions & 0 deletions apps/demo/src/components/template/ErrorBox.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<template>
<div class="bg-red-700 border border-red-800 col-span-2 dark:bg-red-900 mt-3 p-5 rounded-lg">
<ul>
<li
v-for="error in errors"
:key="error"
v-text="error" />
</ul>
</div>
</template>

<script lang="ts" setup>
defineProps<{errors: string[]}>();
</script>
13 changes: 4 additions & 9 deletions apps/demo/src/components/template/FormGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,20 @@
<slot name="info-after" />

<template v-if="element.errors?.length">
<div class="bg-red-700 border border-red-800 col-span-2 dark:bg-red-900 mt-3 p-5 rounded-lg">
<ul>
<li
v-for="warning in element.errors"
:key="warning"
v-text="warning" />
</ul>
</div>
<ErrorBox :errors="element.errors" />
</template>
</div>
</template>

<script lang="ts">
import {type PropType, type UnwrapNestedRefs, defineComponent} from 'vue';
import {defineComponent, type PropType, type UnwrapNestedRefs} from 'vue';
import {type InteractiveElementInstance} from '@myparcel/vue-form-builder';
import {translate} from '../../translate';
import ErrorBox from './ErrorBox.vue';
export default defineComponent({
name: 'FormGroup',
components: {ErrorBox},
props: {
element: {
type: Object as PropType<UnwrapNestedRefs<InteractiveElementInstance>>,
Expand Down
6 changes: 3 additions & 3 deletions apps/demo/src/components/template/TButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
</template>

<script lang="ts">
import {type PropType, defineComponent} from 'vue';
import {type InteractiveElementInstance} from '@myparcel/vue-form-builder';
import {defineComponent, type PropType} from 'vue';
import {type PlainElementInstance} from '@myparcel/vue-form-builder';
import LoadingOverlay from '../LoadingOverlay.vue';
import {translate} from '../../translate';
Expand All @@ -24,7 +24,7 @@ export default defineComponent({
components: {LoadingOverlay},
props: {
element: {
type: Object as PropType<InteractiveElementInstance>,
type: Object as PropType<PlainElementInstance>,
required: true,
},
},
Expand Down
6 changes: 3 additions & 3 deletions apps/demo/src/components/template/TResetButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
</template>

<script lang="ts">
import {type PropType, defineComponent} from 'vue';
import {type InteractiveElementInstance} from '@myparcel/vue-form-builder';
import {defineComponent, type PropType} from 'vue';
import {type PlainElementInstance} from '@myparcel/vue-form-builder';
import {translate} from '../../translate';
import TButton from './TButton.vue';
Expand All @@ -18,7 +18,7 @@ export default defineComponent({
components: {TButton},
props: {
element: {
type: Object as PropType<InteractiveElementInstance>,
type: Object as PropType<PlainElementInstance>,
required: true,
},
},
Expand Down
36 changes: 28 additions & 8 deletions apps/demo/src/components/template/TSubmitButton.vue
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
<template>
<button
type="submit"
class="bg-pink-600 inline-flex mt-2 px-5 py-3 rounded-full text-gray-900"
:disabled="element.isDisabled || element.isSuspended"
:class="{
'opacity-50 cursor-not-allowed': element.isDisabled,
'cursor-wait': element.isSuspended,
'hover:bg-pink-700': !element.isDisabled,
}">
'hover:bg-pink-700': !outline && !element.isDisabled,
'hover:bg-pink-600': outline && !element.isDisabled,
'bg-pink-600 text-gray-900': !outline,
'border border-pink-600 hover:bg-pink-600 hover:text-white text:pink-600': outline,
}"
:disabled="element.isDisabled || element.isSuspended"
class="inline-flex mt-2 px-5 py-3 rounded-full transition-colors"
type="submit">
<slot
:icon="icon"
:scope="element.name"
name="icon" />

<LoadingOverlay v-if="element.isSuspended" />
{{ translate('form_submit') }}

<slot>
{{ translate('form_submit') }}
</slot>
</button>
</template>

<script lang="ts">
import {defineComponent, type PropType} from 'vue';
import {type InteractiveElementInstance} from '@myparcel/vue-form-builder';
import {type PlainElementInstance} from '@myparcel/vue-form-builder';
import LoadingOverlay from '../LoadingOverlay.vue';
import {translate} from '../../translate';
Expand All @@ -24,9 +35,18 @@ export default defineComponent({
components: {LoadingOverlay},
props: {
element: {
type: Object as PropType<InteractiveElementInstance>,
type: Object as PropType<PlainElementInstance>,
required: true,
},
icon: {
type: String,
default: '🍀',
},
outline: {
type: Boolean,
},
},
setup: () => ({
Expand Down
24 changes: 24 additions & 0 deletions apps/demo/src/composables/useFormEventLog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {type Ref, ref} from 'vue';
import {FormHook, type FormInstance, type InteractiveElement, type MaybeUnwrapNestedRefs} from '@myparcel-vfb/core';
import {isOfType} from '@myparcel/ts-utils';

export const useFormEventLog = (form: MaybeUnwrapNestedRefs<FormInstance>): Ref<string> => {
const eventLog = ref<string>('');

Object.values(FormHook).forEach((hook) => {
form.off(hook);
form.on(hook, (_, ...args) => {
const additionalArgs = args as unknown as [unknown, unknown];

let log = `${new Date().toISOString()} | ${hook}`;

if (additionalArgs.length && isOfType<InteractiveElement>(additionalArgs[0], 'ref')) {
log += ` (${additionalArgs[0].name}, ${additionalArgs[1]})`;
}

eventLog.value = `${log}\n${eventLog.value}`;
});
});

return eventLog;
};
5 changes: 5 additions & 0 deletions apps/demo/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ const router = createRouter({
name: 'forms-no-prop',
component: () => import('../views/NoPropView.vue'),
},
{
path: '/forms/new-form',
name: 'forms-new-form',
component: () => import('../views/NewFormView.vue'),
},
],
});

Expand Down
9 changes: 5 additions & 4 deletions apps/demo/src/translate.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
const translations: Record<string, string> = Object.freeze({
bsn: 'BSN',
carrier: 'Vervoerder',
contact_details_title: 'Contactgegevens',
delivery_message: 'Bericht aan de bezorger',
first_name: 'Voornaam',
form_optional_suffix: ' (optioneel)',
form_reset: 'Reset',
form_submit: 'Verzenden',
label_amount: 'Aantal labels',
last_name: 'Achternaam',
name: 'Naam',
name_all_firstnames: 'Alle voornamen',
name_as_in_passport: 'Naam zoals in paspoort',
number: 'Huisnummer',
package_info_title: 'Zendinginformatie',
package_type: 'Pakkettype',
Expand All @@ -24,10 +29,6 @@ const translations: Record<string, string> = Object.freeze({
shipment_option_signature: 'Handtekening',
shipment_options_title: 'Verzendopties',
street: 'Straat',
delivery_message: 'Bericht aan de bezorger',
bsn: 'BSN',
name_as_in_passport: 'Naam zoals in paspoort',
name_all_firstnames: 'Alle voornamen',
});

export function translate(key: string): string {
Expand Down
6 changes: 6 additions & 0 deletions apps/demo/src/validation/emailValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {type ComponentOrHtmlElement, type ElementName, type Validator} from '@myparcel-vfb/core';

export const emailValidator = (): Validator<ComponentOrHtmlElement, ElementName, string> => ({
validate: (_, value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
errorMessage: `Does this look like a valid email address to you?`,
});
5 changes: 5 additions & 0 deletions apps/demo/src/validation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './emailValidator';
export * from './regexValidator';
export * from './stringContainsValidator';
export * from './stringLengthValidator';
export * from './stringNotContainsValidator';
6 changes: 6 additions & 0 deletions apps/demo/src/validation/regexValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {type ComponentOrHtmlElement, type ElementName, type Validator} from '@myparcel-vfb/core';

export const regexValidator = (regex: RegExp): Validator<ComponentOrHtmlElement, ElementName, string> => ({
validate: (_, value) => regex.test(value),
errorMessage: `Value must match ${regex.toString()}`,
});
13 changes: 13 additions & 0 deletions apps/demo/src/validation/stringContainsValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {type ComponentOrHtmlElement, type ElementName, type Validator} from '@myparcel/vue-form-builder';
import {type OneOrMore, toArray} from '@myparcel/ts-utils';

export const stringContainsValidator = (
search: OneOrMore<string>,
): Validator<ComponentOrHtmlElement, ElementName, string> => {
const array = toArray(search);

return {
validate: (_, value) => array.every((search) => value.includes(search)),
errorMessage: `Value must contain "${array.join('", "')}"`,
};
};
13 changes: 13 additions & 0 deletions apps/demo/src/validation/stringLengthValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {type ComponentOrHtmlElement, type ElementName, type Validator} from '@myparcel-vfb/core';

export const stringLengthValidator = (
minLength: number,
maxLength?: number,
): Validator<ComponentOrHtmlElement, ElementName, string> => {
return {
validate: (_, value) => value.length >= minLength && value.length <= (maxLength ?? Infinity),
errorMessage: maxLength
? `Value must be between ${minLength} and ${maxLength} characters long.`
: `Value must be at least ${minLength} characters long.`,
};
};
13 changes: 13 additions & 0 deletions apps/demo/src/validation/stringNotContainsValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {type ComponentOrHtmlElement, type ElementName, type Validator} from '@myparcel-vfb/core';
import {type OneOrMore, toArray} from '@myparcel/ts-utils';

export const stringNotContainsValidator = (
search: OneOrMore<string>,
): Validator<ComponentOrHtmlElement, ElementName, string> => {
const array = toArray(search);

return {
validate: (_, value) => array.every((search) => !value.includes(search)),
errorMessage: `Value must not contain "${array.join('", "')}"`,
};
};
Loading

0 comments on commit 9546c82

Please sign in to comment.