@stonecrop/aform 0.9.2 → 0.10.1

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.
@@ -1,9 +1,14 @@
1
1
  <template>
2
- <div v-on-click-outside="onClickOutside" class="autocomplete" :class="{ isOpen: dropdown.open }">
2
+ <div v-if="mode === 'display'" class="input-wrapper">
3
+ <span class="aform_display-value">{{ search ?? '' }}</span>
4
+ <label>{{ label }}</label>
5
+ </div>
6
+ <div v-else v-on-click-outside="onClickOutside" class="autocomplete" :class="{ isOpen: dropdown.open }">
3
7
  <div class="input-wrapper">
4
8
  <input
5
9
  v-model="search"
6
10
  type="text"
11
+ :disabled="mode === 'read'"
7
12
  @input="filter"
8
13
  @focus="openDropdown"
9
14
  @keydown.down="selectNextResult"
@@ -31,26 +36,33 @@
31
36
 
32
37
  <script setup lang="ts">
33
38
  import { vOnClickOutside } from '@vueuse/components'
34
- import { reactive } from 'vue'
39
+ import { reactive, ref } from 'vue'
40
+
41
+ import type { ComponentProps } from '../../types'
35
42
 
36
43
  const {
37
44
  label,
38
- items = [],
45
+ options = [],
39
46
  isAsync = false,
40
47
  filterFunction = undefined,
41
- } = defineProps<{
42
- label: string
43
- items?: string[]
44
- isAsync?: boolean
45
- filterFunction?: (search: string) => string[] | Promise<string[]>
46
- }>()
48
+ mode,
49
+ } = defineProps<
50
+ ComponentProps & {
51
+ options?: string[]
52
+ isAsync?: boolean
53
+ filterFunction?: (search: string) => string[] | Promise<string[]>
54
+ }
55
+ >()
47
56
  const search = defineModel<string>()
48
57
 
58
+ // tracks the last explicitly-committed value so outside-click reverts instead of clears
59
+ const committedValue = ref(search.value ?? '')
60
+
49
61
  const dropdown = reactive({
50
62
  activeItemIndex: null as number | null,
51
63
  open: false,
52
64
  loading: false,
53
- results: items,
65
+ results: options,
54
66
  })
55
67
 
56
68
  const onClickOutside = () => closeDropdown()
@@ -75,29 +87,31 @@ const filter = async () => {
75
87
 
76
88
  const setResult = (result: string) => {
77
89
  search.value = result
90
+ committedValue.value = result
78
91
  closeDropdown(result)
79
92
  }
80
93
 
81
94
  const openDropdown = () => {
82
- dropdown.activeItemIndex = isAsync ? null : search.value ? items?.indexOf(search.value) || null : null
95
+ const idx = options?.indexOf(search.value ?? '') ?? -1
96
+ dropdown.activeItemIndex = isAsync ? null : idx >= 0 ? idx : null
83
97
  dropdown.open = true
84
98
  // TODO: this should probably call the async function if it's async
85
- dropdown.results = isAsync ? [] : items
99
+ dropdown.results = isAsync ? [] : options
86
100
  }
87
101
 
88
102
  const closeDropdown = (result?: string) => {
89
103
  dropdown.activeItemIndex = null
90
104
  dropdown.open = false
91
- if (!items?.includes(result || search.value || '')) {
92
- search.value = ''
105
+ if (!options?.includes(result || search.value || '')) {
106
+ search.value = committedValue.value
93
107
  }
94
108
  }
95
109
 
96
110
  const filterResults = () => {
97
111
  if (!search.value) {
98
- dropdown.results = items
112
+ dropdown.results = options
99
113
  } else {
100
- dropdown.results = items?.filter(item => item.toLowerCase().includes((search.value ?? '').toLowerCase()))
114
+ dropdown.results = options?.filter(item => item.toLowerCase().includes((search.value ?? '').toLowerCase()))
101
115
  }
102
116
  }
103
117
 
@@ -5,7 +5,7 @@
5
5
  <CollapseButton v-if="collapsible" :collapsed="collapsed" />
6
6
  </legend>
7
7
  <slot :collapsed="collapsed">
8
- <AForm v-show="!collapsed" :schema="formSchema" v-model:data="formData" />
8
+ <AForm v-show="!collapsed" v-model:data="formData" :schema="formSchema" :mode="mode" />
9
9
  </slot>
10
10
  </fieldset>
11
11
  </template>
@@ -15,18 +15,21 @@ import { ref } from 'vue'
15
15
 
16
16
  import CollapseButton from '../base/CollapseButton.vue'
17
17
  import AForm from '../AForm.vue'
18
- import { SchemaTypes } from '../../types'
18
+ import type { SchemaTypes, FormMode } from '../../types'
19
19
 
20
20
  const {
21
21
  schema,
22
22
  label,
23
23
  collapsible,
24
24
  data = {},
25
+ mode = 'edit',
25
26
  } = defineProps<{
26
27
  schema: SchemaTypes[]
27
28
  label: string
28
29
  collapsible?: boolean
29
30
  data?: Record<string, any>
31
+ /** Rendering mode forwarded to the inner AForm */
32
+ mode?: FormMode
30
33
  }>()
31
34
 
32
35
  const collapsed = ref(false)
@@ -1,20 +1,32 @@
1
1
  <template>
2
2
  <div class="aform_form-element aform_file-attach aform__grid--full">
3
- <template v-if="files">
4
- <div class="aform_file-attach-feedback">
5
- <p>
6
- You have selected: <b>{{ fileLengthText }}</b>
7
- </p>
8
- <li v-for="file of files" :key="file.name">
9
- {{ file.name }}
10
- </li>
11
- </div>
3
+ <template v-if="mode === 'display'">
4
+ <template v-if="files">
5
+ <div class="aform_file-attach-feedback">
6
+ <p>
7
+ <b>{{ fileLengthText }}</b>
8
+ </p>
9
+ <li v-for="file of files" :key="file.name">{{ file.name }}</li>
10
+ </div>
11
+ </template>
12
+ <span v-else class="aform_display-value">No file selected</span>
13
+ </template>
14
+ <template v-else>
15
+ <template v-if="files">
16
+ <div class="aform_file-attach-feedback">
17
+ <p>
18
+ You have selected: <b>{{ fileLengthText }}</b>
19
+ </p>
20
+ <li v-for="file of files" :key="file.name">
21
+ {{ file.name }}
22
+ </li>
23
+ </div>
24
+ </template>
25
+ <button type="button" class="aform_form-btn" :disabled="mode === 'read'" @click="open()">
26
+ {{ label }}
27
+ </button>
28
+ <button type="button" :disabled="!files || mode === 'read'" class="aform_form-btn" @click="reset()">Reset</button>
12
29
  </template>
13
-
14
- <button type="button" class="aform_form-btn" @click="open()">
15
- {{ label }}
16
- </button>
17
- <button type="button" :disabled="!files" class="aform_form-btn" @click="reset()">Reset</button>
18
30
  </div>
19
31
  </template>
20
32
 
@@ -22,11 +34,14 @@
22
34
  import { useFileDialog } from '@vueuse/core'
23
35
  import { computed } from 'vue'
24
36
 
25
- const { label } = defineProps<{ label: string }>()
37
+ import type { ComponentProps } from '../../types'
38
+
39
+ const { label, mode } = defineProps<ComponentProps>()
26
40
  const { files, open, reset, onChange } = useFileDialog()
27
41
 
28
42
  const fileLengthText = computed(() => {
29
- return `${files.value.length} ${files.value.length === 1 ? 'file' : 'files'}`
43
+ const count = files.value?.length ?? 0
44
+ return `${count} ${count === 1 ? 'file' : 'files'}`
30
45
  })
31
46
 
32
47
  onChange(files => files)
@@ -1,20 +1,26 @@
1
1
  <template>
2
2
  <div class="aform_form-element">
3
- <input
4
- :id="uuid"
5
- v-model="inputNumber"
6
- class="aform_input-field"
7
- type="number"
8
- :disabled="readOnly"
9
- :required="required" />
10
- <label class="aform_field-label" :for="uuid">{{ label }}</label>
11
- <p v-show="validation.errorMessage" class="aform_error" v-html="validation.errorMessage"></p>
3
+ <template v-if="mode === 'display'">
4
+ <span class="aform_display-value">{{ inputNumber ?? '' }}</span>
5
+ <label class="aform_field-label">{{ label }}</label>
6
+ </template>
7
+ <template v-else>
8
+ <input
9
+ :id="uuid"
10
+ v-model="inputNumber"
11
+ class="aform_input-field"
12
+ type="number"
13
+ :disabled="mode === 'read'"
14
+ :required="required" />
15
+ <label class="aform_field-label" :for="uuid">{{ label }}</label>
16
+ <p v-show="validation.errorMessage" class="aform_error" v-html="validation.errorMessage"></p>
17
+ </template>
12
18
  </div>
13
19
  </template>
14
20
 
15
21
  <script setup lang="ts">
16
22
  import { ComponentProps } from '../../types'
17
23
 
18
- const { label, required, readOnly, uuid, validation = { errorMessage: '&nbsp;' } } = defineProps<ComponentProps>()
24
+ const { label, required, mode, uuid, validation = { errorMessage: '&nbsp;' } } = defineProps<ComponentProps>()
19
25
  const inputNumber = defineModel<number>()
20
26
  </script>
@@ -1,15 +1,21 @@
1
1
  <template>
2
2
  <div class="aform_form-element">
3
- <input
4
- :id="uuid"
5
- v-model="inputText"
6
- v-mask="mask"
7
- class="aform_input-field"
8
- :disabled="readOnly"
9
- :maxlength="mask ? (maskFilled ? mask.length : undefined) : undefined"
10
- :required="required" />
11
- <label class="aform_field-label" :for="uuid">{{ label }} </label>
12
- <p v-show="validation.errorMessage" class="aform_error" v-html="validation.errorMessage"></p>
3
+ <template v-if="mode === 'display'">
4
+ <span class="aform_display-value">{{ inputText ?? '' }}</span>
5
+ <label class="aform_field-label">{{ label }}</label>
6
+ </template>
7
+ <template v-else>
8
+ <input
9
+ :id="uuid"
10
+ v-model="inputText"
11
+ v-mask="mask"
12
+ class="aform_input-field"
13
+ :disabled="mode === 'read'"
14
+ :maxlength="mask ? (maskFilled ? mask.length : undefined) : undefined"
15
+ :required="required" />
16
+ <label class="aform_field-label" :for="uuid">{{ label }} </label>
17
+ <p v-show="validation.errorMessage" class="aform_error" v-html="validation.errorMessage"></p>
18
+ </template>
13
19
  </div>
14
20
  </template>
15
21
 
@@ -19,7 +25,7 @@ import { /* inject, */ ref } from 'vue'
19
25
  import { useStringMask as vMask } from '../../directives/mask'
20
26
  import { ComponentProps } from '../../types'
21
27
 
22
- const { label, mask, required, readOnly, uuid, validation = { errorMessage: '&nbsp;' } } = defineProps<ComponentProps>()
28
+ const { label, mask, required, mode, uuid, validation = { errorMessage: '&nbsp;' } } = defineProps<ComponentProps>()
23
29
 
24
30
  // TODO: setup maskFilled as a computed property
25
31
  const maskFilled = ref(true)
@@ -1,19 +1,5 @@
1
1
  import type { DirectiveBinding } from 'vue'
2
2
 
3
- import type { FormSchema } from '../types'
4
-
5
- /**
6
- * Named masks for common input types
7
- */
8
- const NAMED_MASKS = {
9
- date: '##/##/####',
10
- datetime: '####/##/## ##:##',
11
- time: '##:##',
12
- fulltime: '##:##:##',
13
- phone: '(###) ### - ####',
14
- card: '#### #### #### ####',
15
- }
16
-
17
3
  /**
18
4
  * Extracts a mask function from a stringified function
19
5
  * @param mask - Mask string
@@ -36,23 +22,15 @@ function extractMaskFn(mask: string): ((args: any) => string) | void {
36
22
  * @returns Mask string
37
23
  */
38
24
  function getMask(binding: DirectiveBinding<string>) {
39
- let mask = binding.value
40
-
41
- if (mask) {
42
- const maskFn = extractMaskFn(mask)
43
- if (maskFn) {
44
- // TODO: (state) replace with state management;
45
- // pass the entire form/table data to the function
46
- const locale = binding.instance?.['locale']
47
- mask = maskFn(locale)
48
- }
49
- } else {
50
- // TODO: (state) handle using state management
51
- const schema = binding.instance?.['schema'] as FormSchema
52
- const fieldType: string | undefined = schema?.fieldtype?.toLowerCase()
53
- if (fieldType && NAMED_MASKS[fieldType]) {
54
- mask = NAMED_MASKS[fieldType]
55
- }
25
+ const mask = binding.value
26
+ if (!mask) return undefined
27
+
28
+ const maskFn = extractMaskFn(mask)
29
+ if (maskFn) {
30
+ // TODO: (state) replace with state management;
31
+ // pass the entire form/table data to the function
32
+ const locale = binding.instance?.['locale']
33
+ return maskFn(locale) as string
56
34
  }
57
35
 
58
36
  return mask
@@ -1,5 +1,11 @@
1
1
  import type { TableColumn, TableConfig, TableRow } from '@stonecrop/atable'
2
2
 
3
+ /**
4
+ * The rendering mode for AForm components
5
+ * @public
6
+ */
7
+ export type FormMode = 'edit' | 'read' | 'display'
8
+
3
9
  /**
4
10
  * Defined props for AForm components
5
11
  * @public
@@ -18,7 +24,10 @@ export type ComponentProps = {
18
24
  label?: string
19
25
 
20
26
  /**
21
- * The masking string to apply to inputs inside the component
27
+ * The mask to apply to inputs inside the component. Accepts either a plain
28
+ * mask string (e.g. `"(###) ###-####"`) or a stringified arrow function that
29
+ * receives `locale` and returns a mask string
30
+ * (e.g. `"(locale) => locale === 'en-US' ? '(###) ###-####' : '####-######'"`).
22
31
  * @public
23
32
  */
24
33
  mask?: string
@@ -30,10 +39,10 @@ export type ComponentProps = {
30
39
  required?: boolean
31
40
 
32
41
  /**
33
- * Indicate whether elements inside the component are read-only
42
+ * The rendering mode for the component
34
43
  * @public
35
44
  */
36
- readOnly?: boolean
45
+ mode?: FormMode
37
46
 
38
47
  /**
39
48
  * Set a unique identifier for elements inside the component
@@ -79,10 +88,10 @@ export type BaseSchema = {
79
88
  component?: string
80
89
 
81
90
  /**
82
- * A placeholder value for the field
83
- * @beta
91
+ * Per-field rendering mode override; takes precedence over the AForm-level `mode` prop
92
+ * @public
84
93
  */
85
- value?: any
94
+ mode?: FormMode
86
95
  }
87
96
 
88
97
  /**
@@ -104,17 +113,6 @@ export type FormSchema = BaseSchema & {
104
113
 
105
114
  /**
106
115
  * The field type for the schema field
107
- *
108
- * @remarks
109
- * This must be a string that represents the field type. A mask string will be automatically
110
- * applied for the following field types:
111
- * - Date ('##/##/####')
112
- * - Datetime ('####/##/## ##:##')
113
- * - Time ('##:##')
114
- * - Fulltime ('##:##:##')
115
- * - Phone ('(###) ### - ####')
116
- * - Card ('#### #### #### ####')
117
- *
118
116
  * @public
119
117
  */
120
118
  fieldtype?: string
@@ -138,8 +136,11 @@ export type FormSchema = BaseSchema & {
138
136
  width?: string
139
137
 
140
138
  /**
141
- * The mask string for the field
142
- * @beta
139
+ * The mask to apply to the field. Accepts either a plain mask string
140
+ * (e.g. `"##/##/####"`) or a stringified arrow function that receives `locale`
141
+ * and returns a mask string
142
+ * (e.g. `"(locale) => locale === 'en-US' ? '(###) ###-####' : '####-######'"`).
143
+ * @public
143
144
  */
144
145
  mask?: string
145
146
  }
@@ -232,12 +233,6 @@ export type DoctypeSchema = BaseSchema & {
232
233
  * @public
233
234
  */
234
235
  schema?: SchemaTypes[]
235
-
236
- /**
237
- * Indicate whether the nested form is read-only
238
- * @public
239
- */
240
- readOnly?: boolean
241
236
  }
242
237
 
243
238
  /**
@@ -291,12 +286,6 @@ export type TableDoctypeSchema = BaseSchema & {
291
286
  * @public
292
287
  */
293
288
  rows?: TableRow[]
294
-
295
- /**
296
- * Indicate whether the table is read-only
297
- * @public
298
- */
299
- readOnly?: boolean
300
289
  }
301
290
 
302
291
  /**