srcdev-nuxt-forms 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/assets/styles/forms/index.css +1 -0
  3. package/assets/styles/forms/themes/_primary.css +2 -0
  4. package/assets/styles/forms/themes/_secondary.css +2 -0
  5. package/assets/styles/forms/utils/_a11y.css +5 -0
  6. package/assets/styles/forms/utils/index.css +1 -0
  7. package/assets/styles/forms/variables/_theme.css +11 -17
  8. package/assets/styles/variables/colors/_gray.css +1 -0
  9. package/components/forms/c12/prop-validators/index.ts +8 -20
  10. package/components/forms/c12/utils.ts +14 -0
  11. package/components/forms/c12/validation-patterns/en.json +12 -0
  12. package/components/forms/form-errors/InputError.vue +132 -0
  13. package/components/forms/input-button/InputButtonCore.vue +11 -8
  14. package/components/forms/input-button/variants/InputButtonConfirm.vue +1 -1
  15. package/components/forms/input-button/variants/InputButtonSubmit.vue +1 -1
  16. package/components/forms/input-checkbox/InputCheckboxCore.vue +407 -0
  17. package/components/forms/input-checkbox/InputCheckboxWithLabel.vue +125 -0
  18. package/components/forms/input-checkbox/variants/MultipleCheckboxes.vue +194 -0
  19. package/components/forms/input-checkbox/variants/SingleCheckbox.vue +157 -0
  20. package/components/forms/input-radio/InputRadioCore.vue +226 -0
  21. package/components/forms/input-radio/InputRadioWithLabel.vue +118 -0
  22. package/components/forms/input-radio/variants/MultipleRadio.vue +183 -0
  23. package/components/forms/input-radio/variants/SingleRadio.vue +131 -0
  24. package/components/forms/input-range/InputRangeCore.vue +171 -0
  25. package/components/forms/input-range/variants/InputRangeDefault.vue +131 -0
  26. package/components/forms/input-text/InputTextCore.vue +61 -31
  27. package/components/forms/input-text/variants/material/InputPasswordMaterial.vue +27 -1
  28. package/components/forms/input-text/variants/material/InputTextMaterial.vue +1 -8
  29. package/components/forms/input-text/variants/material/InputTextMaterialCore.vue +83 -28
  30. package/components/forms/input-textarea/InputTextareaCore.vue +170 -0
  31. package/components/forms/input-textarea/variants/material/InputTextareaMaterial.vue +75 -0
  32. package/components/forms/input-textarea/variants/material/InputTextareaMaterialCore.vue +290 -0
  33. package/components/ui/content-grid/ContentGrid.vue +85 -0
  34. package/composables/useErrorMessages.ts +17 -5
  35. package/composables/useFormControl.ts +147 -37
  36. package/layouts/default.vue +7 -13
  37. package/nuxt.config.ts +22 -0
  38. package/package.json +9 -8
  39. package/pages/forms/examples/buttons/index.vue +14 -13
  40. package/pages/forms/examples/material/text-fields.vue +320 -84
  41. package/pages/limit-text.vue +43 -0
  42. package/server/api/places/list.get.ts +23 -0
  43. package/server/api/textFields.post.ts +37 -0
  44. package/server/api/utils/index.get.ts +20 -0
  45. package/server/data/places/cities.json +37 -0
  46. package/server/data/places/countries.json +55 -0
  47. package/server/data/utils/title.json +49 -0
  48. package/types/types.forms.ts +33 -3
  49. package/types/types.places.ts +8 -0
  50. package/pages/forms/examples/material/text-fields-compact.vue +0 -136
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="input-text-wrapper" :class="[{ 'has-left-content': hasLeftContent }]">
2
+ <div class="input-text-wrapper" :class="[{ 'has-left-content': hasLeftContent }, { 'has-right-content': hasRightContent }]">
3
3
  <template v-if="hasLeftContent">
4
4
  <span class="left-content">
5
5
  <slot name="left"></slot>
@@ -14,10 +14,11 @@
14
14
  :pattern="componentValidation.pattern"
15
15
  :maxlength="componentValidation.maxlength"
16
16
  :required
17
- :class="['input-text', 'text-normal', styleClassPassthrough, { active: isFocused }, { dirty: isDirty }, { error: fieldHasError() }]"
17
+ :class="['input-text-core', 'text-normal', styleClassPassthrough, { active: isFocused }, { dirty: fieldIsDirty }, { error: fieldHasError }]"
18
18
  v-model="modelValue.data[name]"
19
19
  ref="inputField"
20
- :readonly="isPending"
20
+ :aria-invalid="fieldHasError"
21
+ :aria-describedby="`${id}-error-message`"
21
22
  @focusin="updateFocus(name, true)"
22
23
  @focusout="updateFocus(name, false)"
23
24
  />
@@ -31,19 +32,18 @@
31
32
  </template>
32
33
 
33
34
  <script setup lang="ts">
34
- import type { InpuTextC12, IFormData } from '@/types/types.forms';
35
+ import type { InpuTextC12, IFormFieldC12, IFormData } from '@/types/types.forms';
35
36
  import { validationConfig } from '@/components/forms/c12/validation-patterns';
37
+ import propValidators from '../c12/prop-validators';
36
38
 
37
39
  const props = defineProps({
38
40
  type: {
39
- // type: String as PropType<"text" | "password" | "tel" | "number" | "email" | "url">, // This breaks props setup in unit tests
40
41
  type: String,
41
42
  validator(value: string) {
42
- return ['text', 'password', 'tel', 'number', 'email', 'url'].includes(value);
43
+ return propValidators.inputTypesText.includes(value);
43
44
  },
44
45
  },
45
46
  id: {
46
- // type: String as PropType<string>,
47
47
  type: String,
48
48
  required: true,
49
49
  },
@@ -79,18 +79,10 @@ const updateFocus = (name: string, isFocused: boolean) => {
79
79
  modelValue.value.focusedField = isFocused ? name : '';
80
80
  };
81
81
 
82
- const isPending = computed(() => {
83
- return modelValue.value.isPending;
84
- });
85
-
86
82
  const isFocused = computed(() => {
87
83
  return modelValue.value.focusedField == name.value;
88
84
  });
89
85
 
90
- const isDirty = computed(() => {
91
- return modelValue.value.dirtyFields[name.value];
92
- });
93
-
94
86
  const name = computed(() => {
95
87
  return props.name !== null ? props.name : props.id;
96
88
  });
@@ -100,32 +92,56 @@ const validatorLocale = toRef(useRuntimeConfig().public.validatorLocale);
100
92
  const componentValidation = validationConfig[validatorLocale.value][props.validation];
101
93
  const inputField = ref<HTMLInputElement | null>(null);
102
94
 
103
- const { hasCustomError, removeCustomError } = useErrorMessage(name.value, modelValue);
104
-
105
- const fieldHasError = () => {
106
- const hasApiErrorMessage = hasCustomError();
107
- const inputBad = !inputField.value?.validity.valid;
95
+ const fieldIsDirty = computed(() => {
96
+ return modelValue.value!.formFieldsC12[name.value].isDirty;
97
+ });
98
+ const fieldHasError = computed(() => {
99
+ return modelValue.value!.submitAttempted && !modelValue.value!.formFieldsC12[name.value].isValid;
100
+ });
108
101
 
109
- if (modelValue.value.isPending) {
110
- modelValue.value!.validityState[name.value] = inputField.value?.validity.valid ?? false;
111
- return hasApiErrorMessage ? hasApiErrorMessage : inputBad;
112
- }
113
- return false;
102
+ // const { fieldHasError } = useFormControl(name.value);
103
+
104
+ const formFieldC12 = <IFormFieldC12>{
105
+ label: props.c12.label,
106
+ placeholder: props.c12.placeholder,
107
+ errorMessage: props.c12.errorMessage,
108
+ useCustomError: false,
109
+ customErrors: {},
110
+ isValid: false,
111
+ isDirty: false,
112
+ type: 'string',
113
+ previousValue: null,
114
114
  };
115
+ modelValue.value!.formFieldsC12[name.value] = formFieldC12;
116
+
117
+ const { initFormFieldsC12 } = useFormControl();
118
+ initFormFieldsC12(props.name, formFieldC12);
115
119
 
116
- watchEffect(() => {
117
- console.log('watchEffect()');
118
- modelValue.value.dirtyFields[name.value] = modelValue.value.data[name.value] !== '';
120
+ const fieldValue = computed(() => {
121
+ return modelValue.value.data[name.value];
122
+ });
119
123
 
124
+ watch(fieldValue, () => {
125
+ if (!modelValue.value!.formFieldsC12[name.value].isDirty) {
126
+ modelValue.value!.formFieldsC12[name.value].isDirty = modelValue.value.data[name.value] !== '';
127
+ }
128
+ modelValue.value!.formFieldsC12[name.value].isValid = inputField.value?.validity.valid ?? false;
120
129
  modelValue.value!.validityState[name.value] = inputField.value?.validity.valid ?? false;
121
- if (hasCustomError()) {
122
- removeCustomError(inputField.value?.validity.valid);
130
+
131
+ if (modelValue.value!.formFieldsC12[name.value].useCustomError && modelValue.value.data[props.name] === modelValue.value.formFieldsC12[props.name].previousValue) {
132
+ modelValue.value!.validityState[name.value] = false;
133
+ modelValue.value!.formFieldsC12[name.value].isValid = false;
134
+ modelValue.value.displayErrorMessages = true;
123
135
  }
124
136
  });
125
137
 
126
138
  const isValid = () => {
127
139
  setTimeout(() => {
128
140
  modelValue.value!.validityState[name.value] = inputField.value?.validity.valid ?? false;
141
+ modelValue.value!.formFieldsC12[name.value].isValid = inputField.value?.validity.valid ?? false;
142
+ if (!modelValue.value!.formFieldsC12[name.value].isDirty) {
143
+ modelValue.value!.formFieldsC12[name.value].isDirty = modelValue.value.data[name.value] !== '';
144
+ }
129
145
  }, 0);
130
146
  };
131
147
 
@@ -136,10 +152,12 @@ onMounted(() => {
136
152
 
137
153
  <style lang="css">
138
154
  .input-text-wrapper {
139
- display: flex;
140
155
  align-items: center;
156
+ display: grid;
157
+ grid-template-columns: 1fr;
141
158
 
142
159
  &.has-left-content {
160
+ grid-template-columns: auto 1fr;
143
161
  margin-left: var(--_gutter);
144
162
 
145
163
  .left-content {
@@ -147,5 +165,17 @@ onMounted(() => {
147
165
  align-items: center;
148
166
  }
149
167
  }
168
+
169
+ &.has-right-content {
170
+ display: grid;
171
+ grid-template-columns: 1fr auto;
172
+ /* display: flex; */
173
+ margin-right: var(--_gutter);
174
+
175
+ .right-content {
176
+ display: flex;
177
+ align-items: center;
178
+ }
179
+ }
150
180
  }
151
181
  </style>
@@ -3,7 +3,16 @@
3
3
  <template #input>
4
4
  <InputTextCore :id :name :type :validation :required v-model="modelValue" :c12 :style-class-passthrough="styleClassPassthrough">
5
5
  <template #right>
6
- <InputButtonCore @click.stop.prevent="toggleDisplayPassword" :is-pending="false" button-text="Submit" theme="ghost" size="x-small">
6
+ <InputButtonCore
7
+ type="button"
8
+ @click.stop.prevent="toggleDisplayPassword"
9
+ :is-pending="false"
10
+ :buttonText
11
+ theme="ghost"
12
+ size="x-small"
13
+ @focusin="updateFocus(name, true)"
14
+ @focusout="updateFocus(name, false)"
15
+ >
7
16
  <template #iconOnly>
8
17
  <Icon v-if="displayPassword" name="radix-icons:eye-none" class="icon" />
9
18
  <Icon v-else name="radix-icons:eye-open" class="icon" />
@@ -75,6 +84,10 @@ const name = computed(() => {
75
84
 
76
85
  const modelValue = defineModel() as Ref<IFormData>;
77
86
 
87
+ const updateFocus = (name: string, isFocused: boolean) => {
88
+ modelValue.value.focusedField = isFocused ? name : '';
89
+ };
90
+
78
91
  const displayPassword = ref(false);
79
92
 
80
93
  const type = computed(() => {
@@ -82,7 +95,20 @@ const type = computed(() => {
82
95
  return displayPassword.value ? 'text' : 'password';
83
96
  });
84
97
 
98
+ const buttonText = computed(() => {
99
+ return displayPassword.value ? 'Hide password' : 'Show password';
100
+ });
101
+
85
102
  const toggleDisplayPassword = () => {
86
103
  displayPassword.value = !displayPassword.value;
87
104
  };
88
105
  </script>
106
+
107
+ <style lang="css" scoped>
108
+ /* :deep(.input-text-core:not(.active)),
109
+ :deep(.input-text-core:not(.dirty)) {
110
+ + .right-content {
111
+ display: none !important;
112
+ }
113
+ } */
114
+ </style>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <InputTextMaterialCore :type :id :name :required :c12 :styleClassPassthrough :theme v-model="modelValue">
2
+ <InputTextMaterialCore type="text" :id :name :required :c12 :styleClassPassthrough :theme v-model="modelValue">
3
3
  <template #input>
4
4
  <InputTextCore :id :name type="text" :validation :required v-model="modelValue" :c12 :style-class-passthrough="styleClassPassthrough" />
5
5
  </template>
@@ -33,13 +33,6 @@ const props = defineProps({
33
33
  return propValidators.theme.includes(value);
34
34
  },
35
35
  },
36
- type: {
37
- // type: String as PropType<"text" | "password" | "tel" | "number" | "email" | "url">, // This breaks props setup in unit tests
38
- type: String,
39
- validator(value: string) {
40
- return ['text', 'password', 'tel', 'number', 'email', 'url'].includes(value);
41
- },
42
- },
43
36
  id: {
44
37
  // type: String as PropType<string>,
45
38
  type: String,
@@ -1,11 +1,12 @@
1
1
  <template>
2
2
  <div class="input-text-material" :class="[`theme-${theme}`, { error: fieldHasError }, { compact: compact }]">
3
- <label class="label" :class="[{ active: isFocused }, { error: fieldHasError }, { dirty: isDirty }, { compact: compact }]" :for="id">
4
- <span>{{ labelText }}</span>
3
+ <label class="input-text-label" :for="id" :class="[{ active: isFocused }, { error: fieldHasError }, { dirty: fieldIsDirty }, { compact: compact }]">
4
+ <span>{{ c12.label }}</span>
5
5
  </label>
6
- <div class="input-text-container" :class="[{ active: isFocused }, { error: fieldHasError }, { dirty: isDirty }, { compact: compact }]">
6
+ <div class="input-text-container" :class="[{ active: isFocused }, { error: fieldHasError }, { dirty: fieldIsDirty }, { compact: compact }]">
7
7
  <slot name="input"></slot>
8
8
  </div>
9
+ <InputError :errorMessaging :fieldHasError :id :isDetached="false" />
9
10
  </div>
10
11
  </template>
11
12
 
@@ -38,8 +39,9 @@ const props = defineProps({
38
39
  },
39
40
  type: {
40
41
  type: String,
42
+ required: true,
41
43
  validator(value: string) {
42
- return ['text', 'password', 'tel', 'number', 'email', 'url'].includes(value);
44
+ return propValidators.inputTypesText.includes(value);
43
45
  },
44
46
  },
45
47
  id: {
@@ -68,8 +70,16 @@ const props = defineProps({
68
70
  },
69
71
  });
70
72
 
71
- const labelText = computed(() => {
72
- return fieldHasError.value ? errorMessage.value : props.c12.label;
73
+ const errorMessaging = computed(() => {
74
+ if (
75
+ typeof modelValue.value!.formFieldsC12[props.name] !== 'undefined' &&
76
+ modelValue.value!.formFieldsC12[props.name].useCustomError &&
77
+ modelValue.value.data[props.name] === modelValue.value.formFieldsC12[props.name].previousValue
78
+ ) {
79
+ return modelValue.value.formFieldsC12[props.name]?.customErrors || [];
80
+ } else {
81
+ return props.c12.errorMessage;
82
+ }
73
83
  });
74
84
 
75
85
  const modelValue = defineModel() as Ref<IFormData>;
@@ -78,17 +88,58 @@ const isFocused = computed(() => {
78
88
  return modelValue.value.focusedField == props.name;
79
89
  });
80
90
 
81
- const isDirty = computed(() => {
82
- return modelValue.value.dirtyFields[props.name];
91
+ const fieldIsDirty = computed(() => {
92
+ if (typeof modelValue.value!.formFieldsC12[props.name] !== 'undefined') {
93
+ return modelValue.value!.formFieldsC12[props.name].isDirty;
94
+ } else {
95
+ return false;
96
+ }
83
97
  });
84
98
 
85
- const { errorMessage, setDefaultError, fieldHasError } = useErrorMessage(props.name, modelValue);
86
- setDefaultError(props.c12.errorMessage);
99
+ const fieldHasError = computed(() => {
100
+ if (typeof modelValue.value!.formFieldsC12[props.name] !== 'undefined') {
101
+ return modelValue.value!.submitAttempted && !modelValue.value!.formFieldsC12[props.name].isValid;
102
+ }
103
+ return false;
104
+ });
87
105
  </script>
88
106
 
89
107
  <style lang="css">
90
108
  .input-text-material {
91
- input {
109
+ --_form-theme: var(--theme-form-primary);
110
+ --_focus-colour: var(--theme-form-primary-focus);
111
+ --_gutter: 12px;
112
+ --_border-width: var(--input-border-width-thin);
113
+ --_border-color: var(--_form-theme);
114
+ --_outline-width: var(--input-outline-width-thin);
115
+
116
+ &.theme-secondary {
117
+ --_form-theme: var(--theme-form-secondary);
118
+ --_focus-colour: var(--theme-form-secondary-focus);
119
+ }
120
+
121
+ &.error {
122
+ --_form-theme: var(--theme-error);
123
+ }
124
+
125
+ /*
126
+ &:has(.input-text:invalid),
127
+ &:has(.input-textarea:invalid) {
128
+ --_form-theme: green;
129
+ }
130
+ */
131
+
132
+ /*
133
+ &:not(:placeholder-shown):invalid {
134
+ --_form-theme: green;
135
+ }
136
+ &:has(.text-input:not(:placeholder-shown):invalid) {
137
+ --_form-theme: red;
138
+ }
139
+
140
+ */
141
+
142
+ .input-text-core {
92
143
  background-color: transparent;
93
144
  line-height: var(--line-height);
94
145
 
@@ -104,43 +155,46 @@ setDefaultError(props.c12.errorMessage);
104
155
  }
105
156
  }
106
157
 
107
- label {
158
+ .input-text-label {
159
+ color: var(--_form-theme);
108
160
  margin: initial;
109
161
  line-height: var(--line-height);
110
162
  padding: initial;
163
+ transition: color 0.2s ease-in-out;
111
164
  }
112
165
 
113
- --_gutter: 12px;
114
- --_form-theme: var(--theme-form-primary);
115
- --_border-width: var(--input-border-width-default);
116
-
117
166
  display: grid;
118
167
  border-radius: 2px;
119
- border: var(--_border-width) solid var(--_form-theme);
168
+ border: var(--_border-width) solid var(--_border-color);
120
169
 
121
170
  margin-bottom: 20px;
122
171
  overflow: hidden;
123
172
 
124
- &.theme-secondary {
125
- --_form-theme: var(--theme-form-secondary);
126
- }
127
-
128
173
  &:focus-within {
129
- border: var(--_border-width) solid var(--_form-theme);
130
- outline: var(--_border-width) solid hsl(from var(--_form-theme) h s 50%);
174
+ --_border-color: white;
175
+ outline: var(--_outline-width) solid hsl(from var(--_form-theme) h s 50%);
131
176
  background-color: hsl(from var(--_form-theme) h s 95%);
132
177
  }
133
178
 
179
+ &:has(.input-text-core:focus-visible),
180
+ &:has(.input-button-core:focus-visible) {
181
+ /* box-shadow: 0 0 2px 3px var(--_focus-colour);
182
+ outline-color: var(--_focus-colour); */
183
+
184
+ outline: var(--focus-visible-outline);
185
+ box-shadow: var(--focus-visible-box-shadow);
186
+ }
187
+
134
188
  &.error {
135
189
  /* outline: calc(var(--_border-width) * 2) solid var(--theme-error); */
136
190
 
137
191
  border: var(--_border-width) solid var(--theme-error);
138
- outline: var(--_border-width) solid hsl(from var(--theme-error) h s 75%);
192
+ outline: var(--_outline-width) solid hsl(from var(--theme-error) h s 75%);
139
193
 
140
194
  background-color: hsl(from var(--theme-error) h s 95%);
141
195
  }
142
196
 
143
- .label {
197
+ .input-text-label {
144
198
  grid-row: 1;
145
199
  grid-column: 1;
146
200
 
@@ -157,6 +211,7 @@ setDefaultError(props.c12.errorMessage);
157
211
  &.dirty,
158
212
  &.error {
159
213
  font-size: 16px;
214
+ height: 1.5em;
160
215
  transform: translateY(-2px);
161
216
  z-index: auto;
162
217
  }
@@ -177,7 +232,7 @@ setDefaultError(props.c12.errorMessage);
177
232
  opacity: 1;
178
233
  }
179
234
 
180
- .input-text {
235
+ .input-text-core {
181
236
  font-family: var(--font-family);
182
237
  border: 0px solid green;
183
238
  padding: calc(var(--_gutter) / 2) var(--_gutter);
@@ -223,7 +278,7 @@ setDefaultError(props.c12.errorMessage);
223
278
  }
224
279
  }
225
280
 
226
- .label {
281
+ .input-text-label {
227
282
  &.compact {
228
283
  align-content: center;
229
284
  font-size: 16px;
@@ -239,7 +294,7 @@ setDefaultError(props.c12.errorMessage);
239
294
  &.error {
240
295
  font-size: 16px;
241
296
  font-weight: 500;
242
- transform: translateY(-26px);
297
+ transform: translateY(-14px);
243
298
  z-index: auto;
244
299
 
245
300
  span {
@@ -0,0 +1,170 @@
1
+ <template>
2
+ <div class="input-textarea-wrapper" :class="[{ 'has-left-content': hasLeftContent }, { 'has-right-content': hasRightContent }]">
3
+ <span v-if="hasLeftContent" class="left-content">
4
+ <slot name="left"></slot>
5
+ </span>
6
+
7
+ <textarea
8
+ :id
9
+ :name
10
+ :pattern="componentValidation.pattern"
11
+ :maxlength="componentValidation.maxlength"
12
+ :required
13
+ :class="['input-textarea-core', 'text-normal', styleClassPassthrough, { active: isFocused }, { dirty: fieldIsDirty }, { error: fieldHasError }]"
14
+ v-model="<string>modelValue.data[name]"
15
+ ref="inputField"
16
+ :placeholder="c12.placeholder"
17
+ :aria-invalid="fieldHasError"
18
+ :aria-describedby="`${id}-error-message`"
19
+ @focusin="updateFocus(name, true)"
20
+ @focusout="updateFocus(name, false)"
21
+ ></textarea>
22
+
23
+ <span v-if="hasRightContent" class="right-content">
24
+ <slot name="right"></slot>
25
+ </span>
26
+ </div>
27
+ </template>
28
+
29
+ <script setup lang="ts">
30
+ import type { InpuTextC12, IFormFieldC12, IFormData } from '@/types/types.forms';
31
+ import { validationConfig } from '@/components/forms/c12/validation-patterns';
32
+
33
+ const props = defineProps({
34
+ id: {
35
+ type: String,
36
+ required: true,
37
+ },
38
+ name: {
39
+ type: String,
40
+ required: true,
41
+ },
42
+ validation: {
43
+ type: String,
44
+ default: null,
45
+ },
46
+ required: {
47
+ type: Boolean,
48
+ value: false,
49
+ },
50
+ c12: {
51
+ type: Object as PropType<InpuTextC12>,
52
+ required: true,
53
+ },
54
+ styleClassPassthrough: {
55
+ type: String,
56
+ default: '',
57
+ },
58
+ });
59
+
60
+ const slots = useSlots();
61
+ const hasLeftContent = computed(() => slots.left !== undefined);
62
+ const hasRightContent = computed(() => slots.right !== undefined);
63
+
64
+ const modelValue = defineModel() as Ref<IFormData>;
65
+
66
+ const updateFocus = (name: string, isFocused: boolean) => {
67
+ console.log(`textarea | updateFocus: ${name} ${isFocused}`);
68
+ modelValue.value.focusedField = isFocused ? name : '';
69
+ };
70
+
71
+ const isFocused = computed(() => {
72
+ return modelValue.value.focusedField == name.value;
73
+ });
74
+
75
+ const name = computed(() => {
76
+ return props.name !== null ? props.name : props.id;
77
+ });
78
+
79
+ const validatorLocale = toRef(useRuntimeConfig().public.validatorLocale);
80
+
81
+ const componentValidation = validationConfig[validatorLocale.value][props.validation];
82
+ const inputField = ref<HTMLInputElement | null>(null);
83
+
84
+ const fieldIsDirty = computed(() => {
85
+ return modelValue.value!.formFieldsC12[name.value].isDirty;
86
+ });
87
+ const fieldHasError = computed(() => {
88
+ return modelValue.value!.submitAttempted && !modelValue.value!.formFieldsC12[name.value].isValid;
89
+ });
90
+
91
+ // const { fieldHasError } = useFormControl(name.value);
92
+
93
+ const formFieldC12 = <IFormFieldC12>{
94
+ label: props.c12.label,
95
+ placeholder: props.c12.placeholder,
96
+ errorMessage: props.c12.errorMessage,
97
+ useCustomError: false,
98
+ customErrors: {},
99
+ isValid: false,
100
+ isDirty: false,
101
+ type: 'string',
102
+ previousValue: null,
103
+ };
104
+ modelValue.value!.formFieldsC12[name.value] = formFieldC12;
105
+
106
+ const { initFormFieldsC12 } = useFormControl();
107
+ initFormFieldsC12(props.name, formFieldC12);
108
+
109
+ const fieldValue = computed(() => {
110
+ return modelValue.value.data[name.value];
111
+ });
112
+
113
+ watch(fieldValue, () => {
114
+ if (!modelValue.value!.formFieldsC12[name.value].isDirty) {
115
+ modelValue.value!.formFieldsC12[name.value].isDirty = modelValue.value.data[name.value] !== '';
116
+ }
117
+ modelValue.value!.formFieldsC12[name.value].isValid = inputField.value?.validity.valid ?? false;
118
+ modelValue.value!.validityState[name.value] = inputField.value?.validity.valid ?? false;
119
+
120
+ if (modelValue.value!.formFieldsC12[name.value].useCustomError && modelValue.value.data[props.name] === modelValue.value.formFieldsC12[props.name].previousValue) {
121
+ modelValue.value!.validityState[name.value] = false;
122
+ modelValue.value!.formFieldsC12[name.value].isValid = false;
123
+ modelValue.value.displayErrorMessages = true;
124
+ }
125
+ });
126
+
127
+ const isValid = () => {
128
+ setTimeout(() => {
129
+ modelValue.value!.validityState[name.value] = inputField.value?.validity.valid ?? false;
130
+ modelValue.value!.formFieldsC12[name.value].isValid = inputField.value?.validity.valid ?? false;
131
+ if (!modelValue.value!.formFieldsC12[name.value].isDirty) {
132
+ modelValue.value!.formFieldsC12[name.value].isDirty = modelValue.value.data[name.value] !== '';
133
+ }
134
+ }, 0);
135
+ };
136
+
137
+ onMounted(() => {
138
+ isValid();
139
+ });
140
+ </script>
141
+
142
+ <style lang="css">
143
+ .input-textarea-wrapper {
144
+ align-items: center;
145
+ display: grid;
146
+ grid-template-columns: 1fr;
147
+
148
+ &.has-left-content {
149
+ grid-template-columns: auto 1fr;
150
+ margin-left: var(--_gutter);
151
+
152
+ .left-content {
153
+ display: flex;
154
+ align-items: center;
155
+ }
156
+ }
157
+
158
+ &.has-right-content {
159
+ display: grid;
160
+ grid-template-columns: 1fr auto;
161
+ /* display: flex; */
162
+ margin-right: var(--_gutter);
163
+
164
+ .right-content {
165
+ display: flex;
166
+ align-items: center;
167
+ }
168
+ }
169
+ }
170
+ </style>
@@ -0,0 +1,75 @@
1
+ <template>
2
+ <InputTextareaMaterialCore :type :id :name :required :c12 :styleClassPassthrough :theme v-model="modelValue">
3
+ <template #input>
4
+ <InputTextareaCore :id :name :validation :required v-model="modelValue" :c12 :style-class-passthrough="styleClassPassthrough" />
5
+ </template>
6
+ </InputTextareaMaterialCore>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ import type { InpuTextC12, IFormData } from '@/types/types.forms';
11
+
12
+ import propValidators from '../../../c12/prop-validators';
13
+
14
+ const props = defineProps({
15
+ size: {
16
+ type: String as PropType<string>,
17
+ default: 'normal',
18
+ validator(value: string) {
19
+ return propValidators.size.includes(value);
20
+ },
21
+ },
22
+ weight: {
23
+ type: String as PropType<string>,
24
+ default: 'wght-400',
25
+ validator(value: string) {
26
+ return propValidators.weight.includes(value);
27
+ },
28
+ },
29
+ theme: {
30
+ type: String as PropType<string>,
31
+ default: 'primary',
32
+ validator(value: string) {
33
+ return propValidators.theme.includes(value);
34
+ },
35
+ },
36
+ type: {
37
+ // type: String as PropType<"text" | "password" | "tel" | "number" | "email" | "url">, // This breaks props setup in unit tests
38
+ type: String,
39
+ validator(value: string) {
40
+ return ['text', 'password', 'tel', 'number', 'email', 'url'].includes(value);
41
+ },
42
+ },
43
+ id: {
44
+ // type: String as PropType<string>,
45
+ type: String,
46
+ required: true,
47
+ },
48
+ name: {
49
+ type: String,
50
+ default: null,
51
+ },
52
+ validation: {
53
+ type: String,
54
+ default: '',
55
+ },
56
+ required: {
57
+ type: Boolean,
58
+ value: false,
59
+ },
60
+ c12: {
61
+ type: Object as PropType<InpuTextC12>,
62
+ required: true,
63
+ },
64
+ styleClassPassthrough: {
65
+ type: String,
66
+ default: '',
67
+ },
68
+ });
69
+
70
+ const name = computed(() => {
71
+ return props.name !== null ? props.name : props.id;
72
+ });
73
+
74
+ const modelValue = defineModel() as Ref<IFormData>;
75
+ </script>