srcdev-nuxt-forms 0.0.22 → 0.1.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 (37) hide show
  1. package/.prettierrc +2 -1
  2. package/assets/styles/forms/themes/_error.css +12 -0
  3. package/assets/styles/forms/themes/_ghost.css +11 -0
  4. package/assets/styles/forms/themes/_primary.css +9 -1
  5. package/assets/styles/forms/themes/_secondary.css +11 -0
  6. package/assets/styles/forms/themes/_success.css +12 -0
  7. package/assets/styles/forms/themes/_tertiary.css +11 -0
  8. package/assets/styles/forms/themes/_warning.css +11 -0
  9. package/assets/styles/forms/themes/index.css +6 -0
  10. package/assets/styles/forms/variables/_theme.css +64 -1
  11. package/assets/styles/variables/colors/_orange.css +1 -1
  12. package/assets/styles/variables/colors/_red.css +1 -1
  13. package/components/forms/c12/prop-validators/index.ts +25 -0
  14. package/components/forms/c12/validation-patterns/en.json +1 -1
  15. package/components/forms/input-button/InputButtonCore.vue +367 -0
  16. package/components/forms/input-button/variants/InputButtonConfirm.vue +78 -0
  17. package/components/forms/input-button/variants/InputButtonSubmit.vue +74 -0
  18. package/components/forms/input-text/InputTextCore.vue +77 -59
  19. package/components/forms/input-text/variants/material/InputEmailMaterial.vue +72 -0
  20. package/components/forms/input-text/variants/material/InputPasswordMaterial.vue +88 -0
  21. package/components/forms/input-text/variants/material/InputTextMaterial.vue +75 -0
  22. package/components/forms/input-text/variants/material/InputTextMaterialCore.vue +258 -0
  23. package/components/forms/ui/FormField.vue +7 -2
  24. package/components/forms/ui/FormWrapper.vue +2 -2
  25. package/composables/useErrorMessages.ts +4 -4
  26. package/composables/useFormControl.ts +36 -16
  27. package/composables/useUpdateStyleClassPassthrough.ts +29 -0
  28. package/layouts/default.vue +33 -2
  29. package/nuxt.config.ts +4 -3
  30. package/package.json +3 -1
  31. package/pages/forms/examples/buttons/index.vue +154 -0
  32. package/pages/forms/examples/material/text-fields-compact.vue +136 -0
  33. package/pages/forms/examples/material/text-fields.vue +136 -0
  34. package/pages/index.vue +2 -70
  35. package/types/types.forms.ts +6 -11
  36. package/components/forms/input-text/InputTextField.vue +0 -22
  37. package/components/forms/input-text/variants/InputTextMaterial.vue +0 -159
@@ -0,0 +1,258 @@
1
+ <template>
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>
5
+ </label>
6
+ <div class="input-text-container" :class="[{ active: isFocused }, { error: fieldHasError }, { dirty: isDirty }, { compact: compact }]">
7
+ <slot name="input"></slot>
8
+ </div>
9
+ </div>
10
+ </template>
11
+
12
+ <script setup lang="ts">
13
+ import type { InpuTextC12, IFormData } from '@/types/types.forms';
14
+
15
+ import propValidators from '../../../c12/prop-validators';
16
+
17
+ const props = defineProps({
18
+ size: {
19
+ type: String as PropType<string>,
20
+ default: 'normal',
21
+ validator(value: string) {
22
+ return propValidators.size.includes(value);
23
+ },
24
+ },
25
+ weight: {
26
+ type: String as PropType<string>,
27
+ default: 'wght-400',
28
+ validator(value: string) {
29
+ return propValidators.weight.includes(value);
30
+ },
31
+ },
32
+ theme: {
33
+ type: String as PropType<string>,
34
+ default: 'primary',
35
+ validator(value: string) {
36
+ return propValidators.theme.includes(value);
37
+ },
38
+ },
39
+ type: {
40
+ type: String,
41
+ validator(value: string) {
42
+ return ['text', 'password', 'tel', 'number', 'email', 'url'].includes(value);
43
+ },
44
+ },
45
+ id: {
46
+ type: String,
47
+ required: true,
48
+ },
49
+ name: {
50
+ type: String,
51
+ default: null,
52
+ },
53
+ required: {
54
+ type: Boolean,
55
+ value: false,
56
+ },
57
+ c12: {
58
+ type: Object as PropType<InpuTextC12>,
59
+ required: true,
60
+ },
61
+ styleClassPassthrough: {
62
+ type: String,
63
+ default: '',
64
+ },
65
+ compact: {
66
+ type: Boolean,
67
+ value: false,
68
+ },
69
+ });
70
+
71
+ const labelText = computed(() => {
72
+ return fieldHasError.value ? errorMessage.value : props.c12.label;
73
+ });
74
+
75
+ const modelValue = defineModel() as Ref<IFormData>;
76
+
77
+ const isFocused = computed(() => {
78
+ return modelValue.value.focusedField == props.name;
79
+ });
80
+
81
+ const isDirty = computed(() => {
82
+ return modelValue.value.dirtyFields[props.name];
83
+ });
84
+
85
+ const { errorMessage, setDefaultError, fieldHasError } = useErrorMessage(props.name, modelValue);
86
+ setDefaultError(props.c12.errorMessage);
87
+ </script>
88
+
89
+ <style lang="css">
90
+ .input-text-material {
91
+ input {
92
+ background-color: transparent;
93
+ line-height: var(--line-height);
94
+
95
+ &:focus {
96
+ outline: none;
97
+ box-shadow: none;
98
+ border: none;
99
+ }
100
+
101
+ &:-internal-autofill-selected,
102
+ &:autofill {
103
+ background-color: transparent !important;
104
+ }
105
+ }
106
+
107
+ label {
108
+ margin: initial;
109
+ line-height: var(--line-height);
110
+ padding: initial;
111
+ }
112
+
113
+ --_gutter: 12px;
114
+ --_form-theme: var(--theme-form-primary);
115
+ --_border-width: var(--input-border-width-default);
116
+
117
+ display: grid;
118
+ border-radius: 2px;
119
+ border: var(--_border-width) solid var(--_form-theme);
120
+
121
+ margin-bottom: 20px;
122
+ overflow: hidden;
123
+
124
+ &.theme-secondary {
125
+ --_form-theme: var(--theme-form-secondary);
126
+ }
127
+
128
+ &: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%);
131
+ background-color: hsl(from var(--_form-theme) h s 95%);
132
+ }
133
+
134
+ &.error {
135
+ /* outline: calc(var(--_border-width) * 2) solid var(--theme-error); */
136
+
137
+ border: var(--_border-width) solid var(--theme-error);
138
+ outline: var(--_border-width) solid hsl(from var(--theme-error) h s 75%);
139
+
140
+ background-color: hsl(from var(--theme-error) h s 95%);
141
+ }
142
+
143
+ .label {
144
+ grid-row: 1;
145
+ grid-column: 1;
146
+
147
+ font-family: var(--font-family);
148
+ font-size: 20px;
149
+ font-weight: 700;
150
+ padding: var(--_gutter);
151
+ transform: translateY(12px);
152
+ transition: all linear 0.2s;
153
+ background-color: transparent;
154
+ z-index: 2;
155
+
156
+ &.active,
157
+ &.dirty,
158
+ &.error {
159
+ font-size: 16px;
160
+ transform: translateY(-2px);
161
+ z-index: auto;
162
+ }
163
+ }
164
+
165
+ .input-text-container {
166
+ display: grid;
167
+ grid-row: 1;
168
+ grid-column: 1;
169
+ margin-top: 2rem;
170
+ background-color: transparent;
171
+ opacity: 0;
172
+ transition: opacity linear 0.2s;
173
+
174
+ &.active,
175
+ &.dirty,
176
+ &.error {
177
+ opacity: 1;
178
+ }
179
+
180
+ .input-text {
181
+ font-family: var(--font-family);
182
+ border: 0px solid green;
183
+ padding: calc(var(--_gutter) / 2) var(--_gutter);
184
+ font-size: var(--font-size);
185
+ margin: 0;
186
+ /* opacity: 0;
187
+ transition: opacity linear 0.2s;
188
+
189
+ &.active,
190
+ &.dirty {
191
+ opacity: 1;
192
+ } */
193
+ /*
194
+ &::placeholder,
195
+ &:-ms-input-placeholder,
196
+ &::-moz-placeholder, */
197
+ &::-webkit-input-placeholder {
198
+ font-family: var(--font-family);
199
+ /* color: var(--gray-5); */
200
+ color: hsl(from var(--_form-theme) h s 50%);
201
+ font-size: var(--font-size);
202
+ font-style: italic;
203
+ font-weight: 500;
204
+ }
205
+ }
206
+ }
207
+ }
208
+
209
+ /*
210
+ * Compact UI
211
+ **/
212
+
213
+ .input-text-material {
214
+ &.compact {
215
+ overflow: initial;
216
+
217
+ &:focus-within {
218
+ background-color: initial;
219
+ }
220
+
221
+ &.error {
222
+ background-color: initial;
223
+ }
224
+ }
225
+
226
+ .label {
227
+ &.compact {
228
+ align-content: center;
229
+ font-size: 16px;
230
+ padding: 0 12px;
231
+ transform: translateY(0);
232
+
233
+ span {
234
+ padding: 0 8px;
235
+ }
236
+
237
+ &.active,
238
+ &.dirty,
239
+ &.error {
240
+ font-size: 16px;
241
+ font-weight: 500;
242
+ transform: translateY(-26px);
243
+ z-index: auto;
244
+
245
+ span {
246
+ background-color: white;
247
+ }
248
+ }
249
+ }
250
+ }
251
+
252
+ .input-text-container {
253
+ &.compact {
254
+ margin: 10px 8px 6px 8px;
255
+ }
256
+ }
257
+ }
258
+ </style>
@@ -1,6 +1,5 @@
1
1
  <template>
2
- <div class="form-field" :class="[width, { 'has-gutter': hasGutter }]">
3
- <h1>FormField.vue</h1>
2
+ <div class="form-field" :class="[width, styleClassPassthrough, { 'has-gutter': hasGutter }]">
4
3
  <slot name="default"></slot>
5
4
  </div>
6
5
  </template>
@@ -16,6 +15,10 @@ defineProps({
16
15
  type: Boolean as PropType<boolean>,
17
16
  default: true,
18
17
  },
18
+ styleClassPassthrough: {
19
+ type: String,
20
+ default: '',
21
+ },
19
22
  });
20
23
  </script>
21
24
 
@@ -25,6 +28,8 @@ defineProps({
25
28
  --_max-width: 400px;
26
29
 
27
30
  margin-inline: auto;
31
+ margin-bottom: 16px;
32
+
28
33
  width: min(100% - calc(2 * var(--_gutter-width)), var(--_max-width));
29
34
  outline: 0px solid var(--gray-5);
30
35
 
@@ -1,6 +1,5 @@
1
1
  <template>
2
2
  <div class="form-wrapper" :class="width">
3
- <h1>FormWrapper.vue</h1>
4
3
  <slot name="default"></slot>
5
4
  </div>
6
5
  </template>
@@ -17,7 +16,8 @@ defineProps({
17
16
 
18
17
  <style lang="css">
19
18
  .form-wrapper {
20
- outline: 1px solid var(--gray-12);
19
+ outline: 0px solid var(--gray-12);
20
+ padding-bottom: 20px;
21
21
 
22
22
  &.narrow {
23
23
  max-width: 400px;
@@ -1,7 +1,7 @@
1
- import type { IFormData } from "@/types/types.forms";
1
+ import type { IFormData } from '@/types/types.forms';
2
2
 
3
3
  export function useErrorMessage(name: string, formData: Ref<IFormData>) {
4
- const defaultError = ref("");
4
+ const defaultError = ref('');
5
5
  const customErrorMessages = ref(toRaw(formData.value.customErrorMessages));
6
6
 
7
7
  const hasCustomError = () => {
@@ -21,7 +21,7 @@ export function useErrorMessage(name: string, formData: Ref<IFormData>) {
21
21
  };
22
22
 
23
23
  const fieldHasError = computed(() => {
24
- if (formData.value.isPending) {
24
+ if (formData.value.submitDisabled) {
25
25
  if (hasCustomError()) {
26
26
  return true;
27
27
  } else if (Object.keys(formData.value.validityState).length > 0 && formData.value.validityState[name] !== undefined) {
@@ -42,6 +42,6 @@ export function useErrorMessage(name: string, formData: Ref<IFormData>) {
42
42
  errorMessage,
43
43
  setDefaultError,
44
44
  fieldHasError,
45
- removeCustomError
45
+ removeCustomError,
46
46
  };
47
47
  }
@@ -1,4 +1,4 @@
1
- import type { IFormData, IFieldsInitialState, ICustomErrorMessage, ICustomErrorMessagesArr } from "@/types/types.forms";
1
+ import type { IFormData, IFieldsInitialState, ICustomErrorMessage, ICustomErrorMessagesArr } from '@/types/types.forms';
2
2
 
3
3
  export function useFormControl(fieldsInitialState: IFieldsInitialState | Ref<IFieldsInitialState | null>) {
4
4
  let savedInitialState = {};
@@ -6,13 +6,15 @@ export function useFormControl(fieldsInitialState: IFieldsInitialState | Ref<IFi
6
6
  const formData = ref<IFormData>({
7
7
  data: {} as IFieldsInitialState,
8
8
  validityState: {},
9
+ dirtyFields: {},
10
+ focusedField: '',
9
11
  isPending: false,
10
12
  errorCount: 0,
11
13
  customErrorMessages: {},
12
14
  hasCustomErrorMessages: false,
13
15
  formIsValid: false,
14
- showErrors: false,
15
- submitSuccess: false
16
+ submitSuccess: false,
17
+ submitDisabled: false,
16
18
  });
17
19
 
18
20
  const initValidationState = async () => {
@@ -34,12 +36,21 @@ export function useFormControl(fieldsInitialState: IFieldsInitialState | Ref<IFi
34
36
  return;
35
37
  };
36
38
 
37
- const getErrorCount = async () => {
39
+ const getErrorCount = async (updateState: boolean = false) => {
38
40
  await nextTick();
39
41
 
40
42
  const errorCount = Object.values(formData.value.validityState).filter((value) => !value).length;
41
43
  formData.value.errorCount = errorCount;
42
44
  formData.value.formIsValid = errorCount === 0;
45
+
46
+ if (updateState) {
47
+ formData.value.submitDisabled = true;
48
+ }
49
+
50
+ if (formData.value.submitDisabled) {
51
+ formData.value.submitDisabled = !formData.value.formIsValid;
52
+ }
53
+
43
54
  return formData.value.errorCount;
44
55
  };
45
56
 
@@ -73,7 +84,7 @@ export function useFormControl(fieldsInitialState: IFieldsInitialState | Ref<IFi
73
84
  formData.value.validityState[name] = valid;
74
85
  formData.value.customErrorMessages[name] = {
75
86
  useCustomError: true,
76
- message
87
+ message,
77
88
  };
78
89
  }
79
90
  formData.value.hasCustomErrorMessages = countItemsWithCustomError(formData.value.customErrorMessages) > 0;
@@ -88,15 +99,20 @@ export function useFormControl(fieldsInitialState: IFieldsInitialState | Ref<IFi
88
99
  formData.value.formIsValid = false;
89
100
  };
90
101
 
91
- const showErrors = computed(() => {
92
- return formData.value.errorCount > 0 && formData.value.isPending;
93
- });
94
-
95
102
  const formIsValid = computed(() => {
96
103
  return formData.value.errorCount === 0;
97
104
  });
98
105
 
106
+ const submitDisabled = computed(() => {
107
+ return formData.value.submitDisabled;
108
+ });
109
+
99
110
  // Keep an eye on this for performance issue
111
+
112
+ watchEffect(() => {
113
+ console.log('watchEffect: formData.value', formData.value.validityState);
114
+ });
115
+
100
116
  watch(
101
117
  () => formData.value.validityState,
102
118
  () => {
@@ -105,12 +121,16 @@ export function useFormControl(fieldsInitialState: IFieldsInitialState | Ref<IFi
105
121
  { deep: true }
106
122
  );
107
123
 
108
- // watch(
109
- // () => savedInitialState,
110
- // () => {
111
- // console.log("savedInitialState UPDATED", savedInitialState);
112
- // }
113
- // );
124
+ onMounted(() => {
125
+ initFormData();
126
+ });
114
127
 
115
- return { formData, initFormData, getErrorCount, updateCustomErrors, resetForm, showErrors, formIsValid };
128
+ return {
129
+ formData,
130
+ getErrorCount,
131
+ updateCustomErrors,
132
+ resetForm,
133
+ formIsValid,
134
+ submitDisabled,
135
+ };
116
136
  }
@@ -0,0 +1,29 @@
1
+ export const useUpdateStyleClassPassthrough = (classes: string) => {
2
+ const styleClassPassthroughRef = ref(classes);
3
+
4
+ function updateClasses(add: boolean, cssClass: string) {
5
+ let classesArray = classes.split(' ');
6
+
7
+ if (add && !classesArray.includes(cssClass)) {
8
+ classesArray.push(cssClass);
9
+ } else if (!add && classesArray.includes(cssClass)) {
10
+ classesArray = classesArray.filter((className) => className !== cssClass);
11
+ }
12
+
13
+ // if (classesArray.includes(cssClass)) {
14
+ // // Remove the value if it's already in the array
15
+ // classesArray = classesArray.filter(className => className !== cssClass);
16
+ // } else {
17
+ // // Add the value if it's not in the array
18
+ // classesArray.push(cssClass);
19
+ // }
20
+
21
+ // Join the array back into a string and assign it back
22
+ styleClassPassthroughRef.value = classesArray.join(' ');
23
+ }
24
+
25
+ return {
26
+ styleClassPassthroughRef,
27
+ updateClasses,
28
+ };
29
+ };
@@ -1,8 +1,26 @@
1
1
  <template>
2
2
  <div class="page-layout">
3
3
  <div>
4
- <h1>Header</h1>
4
+ <h1><NuxtLink to="/">Home</NuxtLink></h1>
5
+ <ul class="flex-group">
6
+ <li>
7
+ <NuxtLink to="/forms/examples/material/text-fields"
8
+ >Material UI text fields</NuxtLink
9
+ >
10
+ </li>
11
+ <li>
12
+ <NuxtLink to="/forms/examples/material/text-fields-compact"
13
+ >Material UI text fields (compact)</NuxtLink
14
+ >
15
+ </li>
16
+ </ul>
5
17
  </div>
18
+ <h2>Buttons</h2>
19
+ <ul class="flex-group">
20
+ <li>
21
+ <NuxtLink to="/forms/examples/buttons">Buttons</NuxtLink>
22
+ </li>
23
+ </ul>
6
24
 
7
25
  <div>
8
26
  <slot name="layout-content"></slot>
@@ -20,7 +38,7 @@ useHead({
20
38
  class: 'body-default',
21
39
  id: 'body',
22
40
  },
23
- })
41
+ });
24
42
  </script>
25
43
 
26
44
  <style lang="css">
@@ -28,4 +46,17 @@ useHead({
28
46
  display: grid;
29
47
  grid-template-rows: auto 1fr auto;
30
48
  }
49
+
50
+ .flex-group {
51
+ align-items: flex-start;
52
+ display: flex;
53
+ flex-wrap: wrap;
54
+ gap: 24px;
55
+ margin-bottom: 32px;
56
+ }
57
+
58
+ ul.flex-group {
59
+ list-style-type: none;
60
+ padding: 0;
61
+ }
31
62
  </style>
package/nuxt.config.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // https://nuxt.com/docs/api/configuration/nuxt-config
2
- import { createResolver } from '@nuxt/kit'
3
- const { resolve } = createResolver(import.meta.url)
2
+ import { createResolver } from '@nuxt/kit';
3
+ const { resolve } = createResolver(import.meta.url);
4
4
 
5
5
  export default defineNuxtConfig({
6
6
  devtools: { enabled: true },
@@ -11,10 +11,11 @@ export default defineNuxtConfig({
11
11
  },
12
12
  },
13
13
 
14
+ modules: ['@nuxt/icon'],
14
15
  components: [
15
16
  {
16
17
  path: './components',
17
18
  pathPrefix: false,
18
19
  },
19
20
  ],
20
- })
21
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "srcdev-nuxt-forms",
3
3
  "type": "module",
4
- "version": "0.0.22",
4
+ "version": "0.1.0",
5
5
  "main": "./nuxt.config.ts",
6
6
  "scripts": {
7
7
  "reinstall": "rm -rf node_modules && npm install",
@@ -14,7 +14,9 @@
14
14
  "release": "release-it"
15
15
  },
16
16
  "devDependencies": {
17
+ "@iconify-json/material-symbols": "^1.1.83",
17
18
  "@nuxt/eslint-config": "^0.3.13",
19
+ "@nuxt/icon": "^1.0.0",
18
20
  "eslint": "^9.5.0",
19
21
  "nuxt": "^3.12.2",
20
22
  "release-it": "^17.4.0",