Skip to content

Commit

Permalink
Merge pull request #194 from Jenesius/issue_193
Browse files Browse the repository at this point in the history
Issue 193
  • Loading branch information
Jenesius authored Feb 20, 2024
2 parents 691210e + 475dfdd commit faff9cc
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 15 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
#### 3.0.13 (2024-02-20)

##### New Features

* add limit attr to input-select ([03b2a72d](https://github.com/Jenesius/vue-form/commit/03b2a72db66e7ed6f3e5bb6fb3590b937af51052))
* add `multiple` attr, update title and select handler. ([907b3fe2](https://github.com/Jenesius/vue-form/commit/907b3fe2f1b9b031428dfb4f9b5fbaf28723bc87))

##### Tests

* add tests for multiple select. ([900ac31f](https://github.com/Jenesius/vue-form/commit/900ac31fe93d3268c7af187eb4d1b8212249f4a6))

#### 3.0.12 (2024-02-01)

##### Documentation Changes
Expand Down
28 changes: 27 additions & 1 deletion docs/inputs/input-select.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ If this array is specified, then the selection will not contain the elements spe

If no value is selected, a text label is shown.

### multiple <Badge type = "info">Optional</Badge>

- Type `boolean`

Данный параметр позволяет использовать множественную выборку. В таком случае modelValue будет обрабатываться как массив.

### limit <Badge type = "info">Optional</Badge>

- Type `number`

Данный параметр устанавливает предельное количество выбираемых элементов, если используется атрибут `multiple`.


____

Also, all parameters common to all `FormField`. Information about them can be viewed
Expand All @@ -60,7 +73,8 @@ on [this page](./form-field.md#params).
## Value

On clicking or selecting the corresponding `select` element, the value will be set to
according to the value in the `value` field of the passed `options`.
according to the value in the `value` field of the passed `options`. Если установлен атрибут `multiple`, то значение
будет проверяться на наличие в modelValue: если не было найдено, то будет добавлено, иначе - исключено.

## Specification

Expand All @@ -73,6 +87,8 @@ according to the value in the `value` field of the passed `options`.
- An additional search controller is shown for a long list.
- Blocking fields cancels navigation using `Tab`. There is also a change in the style of `select`.
- If the validation fails, the field should change the style of the `select`.
- При использовании аттрибута `multiple` выборка не должна закрывать выпадающий список.


## Examples

Expand Down Expand Up @@ -133,6 +149,16 @@ ____
Using `hiddenValues` and setting the value to `['blue', 'purple', 'pink', 'brown', 'grey']`:
<FormField :options = "colors" hiddenValues = "['blue', 'purple', 'pink', 'brown', 'grey']" type = "select" name = "color" label = "Filtered colors" />

----

Использование `multiple`:
<FormField :options = "colors" type = "select" name = "multiple-color" multiple label = "Multiple colors" />

----

Использование `limit` = `2` вместе с `multiple`:
<FormField :options = "colors" type = "select" name = "multiple-color" multiple label = "Multiple colors" limit = "2" />


----
The current state of the form:
Expand Down
4 changes: 4 additions & 0 deletions examples/input-select/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@
<div class="wrap-app ">
<input-field type="select" name = "sex" :options = "sexOptions"/>
<input-field type = "select" name = "language" :options = "languageOptions"/>
<input-field type = "select" name = "languages" :options = "languageOptions" multiple label = "Multi languages"/>
<input-field type = "select" name = "languages" :options = "languageOptions" multiple label = "Multi languages" disabled/>
<input-field type = "select" name = "languages" :options = "languageOptions" multiple label = "Multi languages" limit = "2"/>


<input-field type= "select" name="programming-language" :options = "programLanguageOptions" :hidden-values = "['2','1']"/>
<input-field type = "select" name = "year" :options = "yearOptions" label = "Year"/>
<input-field type = "select" name = "year" :options = "yearOptions" label = "Year"/>

<button class="button" @click="form.cleanValues()">Clean Form</button>
</div>
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "jenesius-vue-form",
"version": "3.0.12",
"version": "3.0.13",
"description": "Heavy form system for Vue.js",
"main": "dist/jenesius-vue-form.cjs.js",
"module": "dist/jenesius-vue-form.es.js",
Expand Down
17 changes: 17 additions & 0 deletions src/utils/toggle-value-from-array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* @description The method is used to add an element to the array if it does not exist, and to remove an element from the array when
* Help with splicing in any case.
* @param array
* @param value
* @param limit Предельное число элементов в массиве.
*/
export default function toggleValueFromArray<T>(array: T[], value: T, limit?: number) {
const index = array.indexOf(value);
if (index === -1) {
if (limit === undefined || (typeof limit === 'number' && Number.isFinite(limit) && array.length < limit))
array.push(value);
}
else array.splice(index, 1);

return array;
}
57 changes: 46 additions & 11 deletions src/widgets/inputs/input-select/input-select.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
'input-select_error': errors.length,
'input-select_active': isActive
}"
:tabindex="!disabled ? 0 : 'none'"
:tabindex="!disabled ? 0 : 'none' "

@focusout = "deactivate()"
@keyup.enter="setActive()"
Expand All @@ -34,11 +34,11 @@
<p
v-for = "option in filteredOptions"
:key = "option.value"
:class="{'input-select-option-list-item_active': modelValue === option.value}"
:class="{'input-select-option-list-item_active': isActiveItem(option.value)}"
class="input-select-option-list-item"
:title = "option.value"

@click = "onInput(option.value), setActive(false)"
@click = "handleSelect(option.value)"
>{{getLabelFromOptionRow(option)}}</p>
</div>

Expand All @@ -59,6 +59,7 @@ import getLabelFromOptionRow from "../../../utils/get-label-from-option-row";
import FieldWrap from "../field-wrap.vue";
import debounce from "../../../utils/debounce";
import store from "../../../config/store";
import toggleValueFromArray from "../../../utils/toggle-value-from-array";
const props = defineProps<{
label?: string,
Expand All @@ -67,7 +68,9 @@ const props = defineProps<{
options: OptionRow[],
placeholder?: string,
errors: string[],
hiddenValues?: OptionRow['value'][]
hiddenValues?: OptionRow['value'][],
multiple?: boolean,
limit?: number | string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: any): void
Expand All @@ -87,16 +90,23 @@ function setActive(v = !isActive.value) {
if (!v) filter.value = '';
if (v) nextTick(scrollToActiveItem.bind(null,'auto'))
}
function onInput(v: any) {
if (props.disabled) return;
emit('update:modelValue', v)
}
/**
* @description Метка отображаемая в поле.
* @description Метка отображаемая в поле. В случае с одиночной выборкой отображается либо текущий элемент, либо placeholder.
* В случае множественной выборки (multiple) - отображается первый выбранный элемент. Если элементов больше одного,
* то отображается ещё + N, где N - количество выбранных элементов - 1
* */
const inputTitle = computed(() => {
const selected = props.options.find(x => x.value === props.modelValue);
if (selected) return getLabelFromOptionRow(selected);
const value = props.multiple ? props.modelValue?.[0] : props.modelValue;
const selected = props.options.find(x => x.value === value);
if (selected) {
const resultLabel = getLabelFromOptionRow(selected);
if (!props.multiple) return resultLabel;
return resultLabel + (props.modelValue.length > 1 ? ` + ${props.modelValue.length - 1}` : '')
}
return props.disabled ? '' : props.placeholder || '';
})
Expand Down Expand Up @@ -145,6 +155,30 @@ const filteredOptions = computed(() => {
)
})
/**
* @description Wrapper over data input.
*/
function handleSelect(value: any) {
onInput(value);
if (!props.multiple) setActive(false)
}
function onInput(value: any) {
if (props.disabled) return;
const limit = typeof props.limit === 'number' ? props.limit : (typeof props.limit === 'string' ? Number.parseInt(props.limit, 10) : undefined);
const resultValue = props.multiple ? toggleValueFromArray(Array.isArray(props.modelValue) ? props.modelValue : [], value, limit) : value
emit('update:modelValue', resultValue)
}
/**
* @description Является ли данный элемент активным(modelValue) содержит значение
*/
function isActiveItem(value: any) {
return props.multiple ? (Array.isArray(props.modelValue) ? props.modelValue : []).includes(value) : props.modelValue === value
}
</script>

<style scoped>
Expand Down Expand Up @@ -204,6 +238,7 @@ const filteredOptions = computed(() => {
.input-select-option-list-item_active {
background-color: #f4f4f4;
border-radius: 3px;
color: var(--vf-input-active);
}
.input-select-option-list {
overflow: auto;
Expand Down
79 changes: 79 additions & 0 deletions tests/integrations/inputs/input-select.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,83 @@ describe("Input Select Testing", () => {
await app.vm.$nextTick();
expect(input.element.getAttribute('tabindex')).toBe("none")
})

test("Multiple attr should show first selected item as title", async () => {
const wrap = defaultMount(defineSelectComponent({
multiple: true,
options: defaultOptions
}))
const form = (wrap.vm as any).form;
form.setValues({
[name]: [defaultOptions[0].value]
})
await wrap.vm.$nextTick();
expect(wrap.text()).toBe(defaultOptions[0].label);
})
test("Multiple attr should show first selected item + N as title if was selected more then one", async () => {
const wrap = defaultMount(defineSelectComponent({
multiple: true,
options: defaultOptions
}))
const form = (wrap.vm as any).form;
form.setValues({
[name]: defaultOptions.map(i => i.value)
})
await wrap.vm.$nextTick();
expect(wrap.text()).toBe(defaultOptions[0].label + ' + ' + (defaultOptions.length - 1));
})

test("Multiple attr should show selected items", async () => {
const wrap = defaultMount(defineSelectComponent({
multiple: true,
options: defaultOptions
}))
const form = (wrap.vm as any).form as Form;
form.setValues({
[name]: [defaultOptions[1].value]
})
currentItem = wrap.find('.container-input-select-current')
expect(currentItem.exists()).toBe(true);
await currentItem.trigger('click');

expect(wrap.findAll('.input-select-option-list-item_active').map(item => item.text())).toEqual([defaultOptions[1].label])
})
test("Selecting items should update value(multiple attr", async () => {
const wrap = defaultMount(defineSelectComponent({
multiple: true,
options: defaultOptions
}))
const form = (wrap.vm as any).form as Form;
currentItem = wrap.find('.container-input-select-current')
await currentItem.trigger('click');

await wrap.findAll('.input-select-option-list-item').reduce((acc, item) => {
return acc.then(() => item.trigger('click'))
}, Promise.resolve())

expect(form.getValueByName(name)).toEqual(defaultOptions.map(item => item.value))

await wrap.findAll('.input-select-option-list-item').reduce((acc, item) => {
return acc.then(() => item.trigger('click'))
}, Promise.resolve())

expect(form.getValueByName(name)).toEqual([])
})

test("Using limit should reject selecting more then provided in limit attr.", async () => {
const wrap = defaultMount(defineSelectComponent({
multiple: true,
options: defaultOptions,
limit: 2
}))
const form = (wrap.vm as any).form as Form;
currentItem = wrap.find('.container-input-select-current')
await currentItem.trigger('click');

await wrap.findAll('.input-select-option-list-item').reduce((acc, item) => {
return acc.then(() => item.trigger('click'))
}, Promise.resolve())

expect(form.getValueByName(name)).toEqual(defaultOptions.map(item => item.value).slice(0, 2))
})
})

0 comments on commit faff9cc

Please sign in to comment.