rpgf-application-form.svelte
1 <script lang="ts"> 2 import { run } from 'svelte/legacy'; 3 4 import type { 5 ApplicationAnswerDto, 6 ApplicationFormFields, 7 } from '$lib/utils/rpgf/types/application'; 8 import DividerField from './components/divider-field.svelte'; 9 import EmailField from './components/email-field.svelte'; 10 import ListField from './components/list-field.svelte'; 11 import MarkdownField from './components/markdown-field.svelte'; 12 import SelectField from './components/select-field.svelte'; 13 import TextAreaField from './components/text-area-field.svelte'; 14 import TextField from './components/text-field.svelte'; 15 import UrlField from './components/url-field.svelte'; 16 17 interface Props { 18 fields: ApplicationFormFields; 19 disabled?: boolean; 20 forceRevealErrors?: boolean; 21 answers?: ApplicationAnswerDto; 22 valid?: boolean; 23 } 24 25 let fieldsValidStates: Record<string, boolean> = $state({}); 26 27 let { 28 fields, 29 disabled = false, 30 forceRevealErrors = false, 31 answers = $bindable(), 32 valid = $bindable(false), 33 }: Props = $props(); 34 run(() => { 35 valid = Object.values(fieldsValidStates).every((v) => v); 36 }); 37 38 // eslint-disable-next-line @typescript-eslint/no-explicit-any 39 let internalAnswers: Record<string, any> = $state( 40 answers?.reduce( 41 (acc, answer) => { 42 acc[answer.fieldId] = answer; 43 return acc; 44 }, 45 // eslint-disable-next-line @typescript-eslint/no-explicit-any 46 {} as Record<string, any>, 47 ) ?? {}, 48 ); 49 50 run(() => { 51 answers = Object.values(internalAnswers).filter((a) => a !== undefined); 52 }); 53 </script> 54 55 <form class:disabled> 56 {#each fields as applicationField} 57 {@const type = applicationField.type} 58 59 {#if type === 'divider'} 60 <DividerField /> 61 {:else if type === 'markdown'} 62 <MarkdownField field={applicationField} /> 63 {:else if type === 'list'} 64 <ListField field={applicationField} bind:answer={internalAnswers[applicationField.id]} /> 65 {:else if type === 'select'} 66 <SelectField 67 field={applicationField} 68 bind:answer={internalAnswers[applicationField.id]} 69 bind:valid={fieldsValidStates[applicationField.id]} 70 /> 71 {:else if type === 'text'} 72 <TextField 73 forceRevealError={forceRevealErrors} 74 field={applicationField} 75 bind:answer={internalAnswers[applicationField.id]} 76 bind:valid={fieldsValidStates[applicationField.id]} 77 /> 78 {:else if type === 'textarea'} 79 <TextAreaField 80 forceRevealError={forceRevealErrors} 81 field={applicationField} 82 bind:answer={internalAnswers[applicationField.id]} 83 bind:valid={fieldsValidStates[applicationField.id]} 84 /> 85 {:else if type === 'url'} 86 <UrlField 87 forceRevealError={forceRevealErrors} 88 field={applicationField} 89 bind:answer={internalAnswers[applicationField.id]} 90 bind:valid={fieldsValidStates[applicationField.id]} 91 /> 92 {:else if type === 'email'} 93 <EmailField 94 forceRevealError={forceRevealErrors} 95 field={applicationField} 96 bind:answer={internalAnswers[applicationField.id]} 97 bind:valid={fieldsValidStates[applicationField.id]} 98 /> 99 {/if} 100 {/each} 101 </form> 102 103 <style> 104 form { 105 display: flex; 106 width: 100%; 107 flex-direction: column; 108 gap: 3rem; 109 transition: opacity 0.3s; 110 } 111 112 form.disabled { 113 opacity: 0.5; 114 pointer-events: none; 115 } 116 </style>