@milaboratories/uikit 2.0.13 → 2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/uikit",
3
- "version": "2.0.13",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "main": "dist/pl-uikit.umd.js",
6
6
  "module": "dist/pl-uikit.js",
@@ -41,7 +41,7 @@ const props = withDefaults(
41
41
  );
42
42
 
43
43
  const defaultData = () => ({
44
- dirPath: '',
44
+ dirPath: '' as string,
45
45
  storageEntry: undefined as StorageEntry | undefined,
46
46
  items: [] as FileDialogItem[],
47
47
  error: '',
@@ -119,12 +119,12 @@ const query = (handle: StorageHandle, dirPath: string) => {
119
119
 
120
120
  const load = () => {
121
121
  const { storageHandle, dirPath, modelValue } = lookup.value;
122
- if (storageHandle && modelValue) {
122
+ if (storageHandle && modelValue && dirPath) {
123
123
  query(storageHandle, dirPath);
124
124
  }
125
125
  };
126
126
 
127
- const updateDirPathDebounced = debounce((v: string | undefined) => {
127
+ const updateDirPathDebounced = debounce((v: string) => {
128
128
  if (v) {
129
129
  data.dirPath = v;
130
130
  }
@@ -5,32 +5,29 @@ export default {
5
5
  };
6
6
  </script>
7
7
 
8
- <script lang="ts" setup generic="M extends string | null | undefined = string, E extends M = M">
8
+ <script lang="ts" setup generic="M, E = string, C = E">
9
9
  import './pl-text-field.scss';
10
- import { computed, ref, useSlots } from 'vue';
10
+ import { computed, reactive, ref, useSlots } from 'vue';
11
11
  import { PlTooltip } from '@/components/PlTooltip';
12
12
  import DoubleContour from '@/utils/DoubleContour.vue';
13
13
  import { useLabelNotch } from '@/utils/useLabelNotch';
14
14
  import { useValidation } from '@/utils/useValidation';
15
15
  import { PlIcon16 } from '../PlIcon16';
16
16
  import { PlIcon24 } from '../PlIcon24';
17
+ import type { Equal } from '@milaboratories/helpers';
17
18
 
18
19
  const slots = useSlots();
19
20
 
20
- const emit = defineEmits<{
21
- /**
22
- * Emitted when the model value is updated.
23
- *
24
- * @param value - The new value of the input, which can be a string or undefined if cleared.
25
- */
26
- (e: 'update:modelValue', value: string | E): void;
27
- }>();
21
+ type Model = Equal<M, E | C> extends true ? M : never; // basically in === out
22
+
23
+ /**
24
+ * The current value of the input field.
25
+ */
26
+ const model = defineModel<Model>({
27
+ required: true,
28
+ });
28
29
 
29
30
  const props = defineProps<{
30
- /**
31
- * The current value of the input field.
32
- */
33
- modelValue: M;
34
31
  /**
35
32
  * The label to display above the input field.
36
33
  */
@@ -39,7 +36,12 @@ const props = defineProps<{
39
36
  * If `true`, a clear icon will appear in the input field to clear the value (set it to empty string).
40
37
  * Or you can pass a callback that returns a custom "empty" value (null | undefined | string)
41
38
  */
42
- clearable?: boolean | (() => E);
39
+ clearable?: boolean | (() => C);
40
+ /**
41
+ * An optional callback to parse and/or cast the value, the return type overrides the model type.
42
+ * The callback must throw an exception if the value is invalid
43
+ */
44
+ parse?: (v: string) => E;
43
45
  /**
44
46
  * If `true`, the input field is marked as required.
45
47
  */
@@ -75,7 +77,7 @@ const props = defineProps<{
75
77
  /**
76
78
  * The string specifies whether the field should be a password or not, value could be "password" or undefined.
77
79
  */
78
- type?: 'password';
80
+ type?: 'password' | 'number';
79
81
  }>();
80
82
 
81
83
  const rootRef = ref<HTMLInputElement | undefined>(undefined);
@@ -84,12 +86,32 @@ const inputRef = ref<HTMLInputElement | undefined>();
84
86
 
85
87
  const showPassword = ref(false);
86
88
 
89
+ const data = reactive({
90
+ cached: undefined as { error: string; value: string } | undefined,
91
+ });
92
+
87
93
  const valueRef = computed<string>({
88
94
  get() {
89
- return props.modelValue ?? '';
95
+ if (data.cached) {
96
+ return data.cached.value;
97
+ }
98
+ return model.value === undefined || model.value === null ? '' : String(model.value);
90
99
  },
91
- set(v) {
92
- emit('update:modelValue', v);
100
+ set(value) {
101
+ data.cached = undefined;
102
+
103
+ if (props.parse) {
104
+ try {
105
+ model.value = props.parse(value) as Model;
106
+ } catch (err) {
107
+ data.cached = {
108
+ error: err instanceof Error ? err.message : String(err),
109
+ value,
110
+ };
111
+ }
112
+ } else {
113
+ model.value = value as Model;
114
+ }
93
115
  },
94
116
  });
95
117
 
@@ -105,7 +127,8 @@ const passwordIcon = computed(() => (showPassword.value ? 'view-on' : 'view-off'
105
127
 
106
128
  const clear = () => {
107
129
  if (props.clearable) {
108
- emit('update:modelValue', props.clearable === true ? '' : props.clearable());
130
+ data.cached = undefined;
131
+ model.value = props.clearable === true ? ('' as Model) : (props.clearable() as Model);
109
132
  }
110
133
  };
111
134
 
@@ -113,10 +136,10 @@ const validationData = useValidation(valueRef, props.rules || []);
113
136
 
114
137
  const isEmpty = computed(() => {
115
138
  if (props.clearable) {
116
- return props.clearable === true ? props.modelValue === '' : props.modelValue === props.clearable();
139
+ return props.clearable === true ? model.value === '' : model.value === props.clearable();
117
140
  }
118
141
 
119
- return props.modelValue === '';
142
+ return model.value === '';
120
143
  });
121
144
 
122
145
  const nonEmpty = computed(() => !isEmpty.value);
@@ -126,6 +149,9 @@ const displayErrors = computed(() => {
126
149
  if (props.error) {
127
150
  errors.push(props.error);
128
151
  }
152
+ if (data.cached) {
153
+ errors.push(data.cached.error);
154
+ }
129
155
  if (!validationData.value.isValid) {
130
156
  errors.push(...validationData.value.errors);
131
157
  }
@@ -136,11 +162,13 @@ const hasErrors = computed(() => displayErrors.value.length > 0);
136
162
 
137
163
  const canShowClearable = computed(() => props.clearable && nonEmpty.value && props.type !== 'password');
138
164
 
139
- useLabelNotch(rootRef);
165
+ const togglePasswordVisibility = () => (showPassword.value = !showPassword.value);
166
+
167
+ const onFocusOut = () => {
168
+ data.cached = undefined;
169
+ };
140
170
 
141
- function togglePasswordVisibility() {
142
- showPassword.value = !showPassword.value;
143
- }
171
+ useLabelNotch(rootRef);
144
172
  </script>
145
173
 
146
174
  <template>
@@ -167,7 +195,15 @@ function togglePasswordVisibility() {
167
195
  <div v-if="prefix" class="pl-text-field__prefix">
168
196
  {{ prefix }}
169
197
  </div>
170
- <input ref="inputRef" v-model="valueRef" :disabled="disabled" :placeholder="placeholder || '...'" :type="fieldType" spellcheck="false" />
198
+ <input
199
+ ref="inputRef"
200
+ v-model="valueRef"
201
+ :disabled="disabled"
202
+ :placeholder="placeholder || '...'"
203
+ :type="fieldType"
204
+ spellcheck="false"
205
+ @focusout="onFocusOut"
206
+ />
171
207
  <div class="pl-text-field__append">
172
208
  <PlIcon16 v-if="canShowClearable" name="delete-clear" @click="clear" />
173
209
  <PlIcon24 v-if="type === 'password'" :name="passwordIcon" style="cursor: pointer" @click="togglePasswordVisibility" />
@@ -14,8 +14,8 @@ describe('TextField', () => {
14
14
  expect(wrapper.text()).toContain('TextField Label');
15
15
  });
16
16
 
17
- it('modelValue', async () => {
18
- const wrapper = mount(PlTextField, {
17
+ it('modelValue:string', async () => {
18
+ const wrapper = mount(PlTextField<string>, {
19
19
  props: {
20
20
  modelValue: 'initialText',
21
21
  'onUpdate:modelValue': (e: string) => wrapper.setProps({ modelValue: e }),
@@ -23,8 +23,19 @@ describe('TextField', () => {
23
23
  });
24
24
 
25
25
  await wrapper.find('input').setValue('test');
26
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
27
- // @ts-expect-error @TODO generic component issue
26
+ expect(wrapper.props('modelValue')).toBe('test');
27
+ });
28
+
29
+ it('modelValue:string?', async () => {
30
+ const wrapper = mount(PlTextField, {
31
+ props: {
32
+ modelValue: 'initialText' as string | undefined,
33
+ clearable: () => undefined,
34
+ 'onUpdate:modelValue': (e: unknown) => wrapper.setProps({ modelValue: e }),
35
+ },
36
+ });
37
+
38
+ await wrapper.find('input').setValue('test');
28
39
  expect(wrapper.props('modelValue')).toBe('test');
29
40
  });
30
41
  });