aloha-vue 2.79.0 → 2.80.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": "aloha-vue",
3
- "version": "2.79.0",
3
+ "version": "2.80.0",
4
4
  "description": "Aloha-vue is a JavaScript library that provides a wide range of accessible components and directives for Vue.js. Accessibility is of paramount importance to us, and we have designed all our components to meet accessibility compliance criteria. This library is a valuable tool for frontend developers and has already been used in three projects, including two large-scale ones",
5
5
  "keywords": [
6
6
  "accessibility compliance criteria",
@@ -11,11 +11,9 @@ import {
11
11
  AKeyIndex,
12
12
  } from "../../const/AKeys";
13
13
  import {
14
+ cloneDeep,
14
15
  floor,
15
16
  forEach,
16
- } from "lodash";
17
- import {
18
- cloneDeep,
19
17
  isNil,
20
18
  min,
21
19
  round,
@@ -5,7 +5,7 @@ import {
5
5
 
6
6
  import {
7
7
  snakeCase,
8
- } from "lodash";
8
+ } from "lodash-es";
9
9
 
10
10
  export default function ClassAPI(props, {
11
11
  subTypeLocal = computed(() => undefined),
@@ -6,12 +6,10 @@ import {
6
6
  AKeyId,
7
7
  } from "../../../index";
8
8
 
9
- import {
10
- forEach,
11
- } from "lodash";
12
9
  import {
13
10
  cloneDeep,
14
11
  filter,
12
+ forEach,
15
13
  } from "lodash-es";
16
14
 
17
15
  export default function AllAPI(props, {
@@ -5,7 +5,7 @@ import {
5
5
 
6
6
  import {
7
7
  forEach,
8
- } from "lodash";
8
+ } from "lodash-es";
9
9
 
10
10
  export default function ReadonlyAPI(props, {
11
11
  dataKeyByKeyIdLocal = computed(() => ({})),
@@ -219,6 +219,11 @@ export default {
219
219
  required: false,
220
220
  default: () => [],
221
221
  },
222
+ valueKey: {
223
+ type: String,
224
+ required: false,
225
+ default: undefined,
226
+ },
222
227
  useFlatErrors: {
223
228
  type: Boolean,
224
229
  required: false,
@@ -289,6 +294,8 @@ export default {
289
294
  closeModalCreateListMode,
290
295
  deleteListMode,
291
296
  isModalCreateListModeVisible,
297
+ getListModeErrors,
298
+ listModeItems,
292
299
  moveListMode,
293
300
  modelListMode,
294
301
  openModalCreateListMode,
@@ -321,6 +328,8 @@ export default {
321
328
  open,
322
329
  closeModalCreateListMode,
323
330
  isModalCreateListModeVisible,
331
+ getListModeErrors,
332
+ listModeItems,
324
333
  modelListMode,
325
334
  modelJsonModeSorted,
326
335
  btnIdAdd,
@@ -422,7 +431,7 @@ export default {
422
431
  class: "a_list_group",
423
432
  }, [
424
433
  this.modelListMode.length > 0 ?
425
- this.modelListMode.map((item, index) => {
434
+ this.listModeItems.map((item, index) => {
426
435
  return h(AValidatedJsonItem, {
427
436
  key: index,
428
437
  children: this.childrenFiltered,
@@ -430,11 +439,11 @@ export default {
430
439
  deactivatePreview: this.deactivatePreview,
431
440
  elementLabel: this.elementLabelTranslated,
432
441
  elementTemplate: this.elementTemplate,
433
- errors: this.errors?.[index],
442
+ errors: this.getListModeErrors(index),
434
443
  hidePosition: this.hidePosition,
435
444
  isLast: index === this.modelListMode.length - 1,
436
445
  mode: this.mode,
437
- modelAll: this.modelListMode,
446
+ modelAll: this.listModeItems,
438
447
  modelIndex: index,
439
448
  modelItem: item,
440
449
  parentHtmlId: this.htmlIdLocal,
@@ -487,7 +496,7 @@ export default {
487
496
  elementLabelTranslated: this.elementLabelTranslated,
488
497
  isCreate: true,
489
498
  mode: "list",
490
- modelAll: this.modelListMode,
499
+ modelAll: this.listModeItems,
491
500
  typedBaseId: this.typedBaseId,
492
501
  typedChildren: this.typedChildren,
493
502
  uniqueChildrenIds: this.uniqueChildrenIds,
@@ -0,0 +1,45 @@
1
+ # AValidatedJson: lists of primitive values
2
+
3
+ Use `mode="list"` with the optional `valueKey` prop to edit a primitive array.
4
+ The matching child field defines the editor and its validation rules.
5
+
6
+ ```vue
7
+ <a-validated-json
8
+ v-model="tags"
9
+ mode="list"
10
+ value-key="value"
11
+ :children="[{ id: 'value', type: 'text', label: 'Tag', required: true }]"
12
+ element-template="{{ value }}"
13
+ />
14
+ ```
15
+
16
+ ```js
17
+ const tags = ref(["Aloha", "Vue"]);
18
+ ```
19
+
20
+ - `valueKey` defaults to `undefined`. Object lists, JSON mode and single mode
21
+ retain their existing behavior. `valueKey` only applies to list mode.
22
+ - The public `v-model`, `update:modelValue` event and `change` callback contain
23
+ primitive arrays, including `0`, `false`, empty strings and `null`.
24
+ - Internally the component wraps each value under `valueKey` for forms,
25
+ labels, previews and uniqueness checks. No wrapper component or plugin
26
+ registration is needed. `valueKey` should identify the list's editable field.
27
+ - Use an `integer` or `switch` child to edit numbers or booleans.
28
+ - `uniqueChildrenIds: ['value']` prevents duplicate values when requested.
29
+ - Errors such as `{ 0: ['Invalid value'] }` are passed to the corresponding
30
+ value field. Already structured errors such as
31
+ `{ 0: { value: ['Invalid value'] } }` are also supported.
32
+ - `readonly`, ordering, deleting and editing use the usual list controls.
33
+
34
+ With `AForm`, use the same options on a field of type `validatedJson`:
35
+
36
+ ```js
37
+ const data = [{
38
+ id: "tags",
39
+ type: "validatedJson",
40
+ mode: "list",
41
+ valueKey: "value",
42
+ children: [{ id: "value", type: "text", label: "Tag" }],
43
+ }];
44
+ // model: { tags: ["Aloha", "Vue"] }
45
+ ```
@@ -0,0 +1,103 @@
1
+ import {
2
+ nextTick,
3
+ reactive,
4
+ } from "vue";
5
+ import {
6
+ describe,
7
+ expect,
8
+ it,
9
+ vi,
10
+ } from "vitest";
11
+
12
+ import ListModeAPI from "../compositionAPI/ListModeAPI";
13
+
14
+ vi.mock("../../../utils/utilsDOM", () => ({
15
+ setFocusToElement: vi.fn(),
16
+ }));
17
+
18
+ function createList(modelValue, valueKey = "value", errors) {
19
+ const props = reactive({
20
+ id: "values",
21
+ change: vi.fn(),
22
+ modelValue,
23
+ valueKey,
24
+ errors,
25
+ });
26
+ const emit = vi.fn();
27
+ const api = ListModeAPI(props, { emit }, {});
28
+ return { api, emit, props };
29
+ }
30
+
31
+ describe("AValidatedJson primitive lists", () => {
32
+ it("wraps values for forms and reacts to external model and key changes", () => {
33
+ const { api, props } = createList(["Aloha", 0, false, null]);
34
+ expect(api.listModeItems.value).toEqual([
35
+ { value: "Aloha" }, { value: 0 }, { value: false }, { value: null },
36
+ ]);
37
+ expect(api.modelListMode.value).toEqual(["Aloha", 0, false, null]);
38
+ props.valueKey = "entry";
39
+ props.modelValue = ["Vue"];
40
+ expect(api.listModeItems.value).toEqual([{ entry: "Vue" }]);
41
+ });
42
+
43
+ it.each(["text", "", 0, false, null])("creates and updates the primitive %j", value => {
44
+ const { api, emit, props } = createList(["old"]);
45
+ api.openModalCreateListMode();
46
+ api.closeModalCreateListMode({ model: { value } });
47
+ expect(api.isModalCreateListModeVisible.value).toBe(false);
48
+ expect(emit).toHaveBeenLastCalledWith("update:modelValue", ["old", value]);
49
+ expect(props.change).toHaveBeenLastCalledWith({
50
+ currentModel: ["old", value], model: ["old", value], id: "values",
51
+ });
52
+ api.updateListMode({ index: 0, model: { value } });
53
+ expect(emit).toHaveBeenLastCalledWith("update:modelValue", [value]);
54
+ expect(props.modelValue).toEqual(["old"]);
55
+ });
56
+
57
+ it("supports an initially undefined model and cancelling the create dialog", () => {
58
+ const { api, emit } = createList(undefined);
59
+ expect(api.listModeItems.value).toEqual([]);
60
+ api.openModalCreateListMode();
61
+ api.closeModalCreateListMode();
62
+ expect(emit).not.toHaveBeenCalled();
63
+ api.closeModalCreateListMode({ model: { value: "first" } });
64
+ expect(emit).toHaveBeenLastCalledWith("update:modelValue", ["first"]);
65
+ });
66
+
67
+ it("keeps primitive values when moving and deleting without changing the input", async() => {
68
+ const { api, emit, props } = createList([0, false, "last"]);
69
+ api.moveListMode({ action: "up", index: 1 });
70
+ expect(emit).toHaveBeenLastCalledWith("update:modelValue", [false, 0, "last"]);
71
+ api.moveListMode({ action: "down", index: 0 });
72
+ expect(emit).toHaveBeenLastCalledWith("update:modelValue", [false, 0, "last"]);
73
+ api.deleteListMode({ index: 1 });
74
+ expect(emit).toHaveBeenLastCalledWith("update:modelValue", [0, "last"]);
75
+ expect(props.modelValue).toEqual([0, false, "last"]);
76
+ await nextTick();
77
+ });
78
+
79
+ it("associates primitive element errors with the configured field", () => {
80
+ const { api } = createList(["invalid"], "value", { 0: ["Invalid value"] });
81
+ expect(api.getListModeErrors(0)).toEqual({ value: ["Invalid value"] });
82
+ expect(api.getListModeErrors(1)).toBeUndefined();
83
+ });
84
+
85
+ it("preserves already structured errors", () => {
86
+ const errors = { 0: { value: ["Invalid value"] } };
87
+ const { api } = createList(["invalid"], "value", errors);
88
+ expect(api.getListModeErrors(0)).toEqual(errors[0]);
89
+ });
90
+
91
+ it("keeps the existing object-list contract when valueKey is omitted", () => {
92
+ const { api, emit, props } = createList([{ text: "old" }], undefined);
93
+ // Explicitly unset the helper default to exercise the public default.
94
+ props.valueKey = undefined;
95
+ expect(api.listModeItems.value).toEqual([{ text: "old" }]);
96
+ api.closeModalCreateListMode({ model: { text: "new" } });
97
+ expect(emit).toHaveBeenLastCalledWith("update:modelValue", [
98
+ { text: "old" }, { text: "new" },
99
+ ]);
100
+ api.updateListMode({ index: 0, model: { text: "updated" } });
101
+ expect(emit).toHaveBeenLastCalledWith("update:modelValue", [{ text: "updated" }]);
102
+ });
103
+ });
@@ -0,0 +1,121 @@
1
+ import {
2
+ h,
3
+ nextTick,
4
+ } from "vue";
5
+ import {
6
+ afterEach,
7
+ beforeEach,
8
+ describe,
9
+ expect,
10
+ it,
11
+ } from "vitest";
12
+ import {
13
+ mount,
14
+ } from "@vue/test-utils";
15
+ import {
16
+ AFieldset,
17
+ AForm,
18
+ AValidatedJson,
19
+ } from "../../../index";
20
+
21
+ import AValidatedJsonItem from "../AValidatedJsonItem/AValidatedJsonItem";
22
+ import AValidatedJsonModalCreateOrUpdate from "../AValidatedJsonModalCreateOrUpdate/AValidatedJsonModalCreateOrUpdate";
23
+
24
+ import {
25
+ setI18n,
26
+ updateTranslation,
27
+ } from "../../../ATranslation/compositionAPI/ATranslationAPI";
28
+
29
+ const wrappers = [];
30
+ beforeEach(async() => {
31
+ setI18n({ de: {} });
32
+ updateTranslation();
33
+ await nextTick();
34
+ });
35
+ afterEach(() => {
36
+ wrappers.forEach(wrapper => wrapper.unmount());
37
+ wrappers.length = 0;
38
+ });
39
+
40
+ function mountList(props = {}) {
41
+ const wrapper = mount(AValidatedJson, {
42
+ props: {
43
+ id: "values",
44
+ mode: "list",
45
+ valueKey: "value",
46
+ modelValue: ["Aloha", "Vue"],
47
+ elementTemplate: "{{ value }}",
48
+ children: [{ id: "value", type: "text", label: "Value", required: true }],
49
+ ...props,
50
+ },
51
+ global: {
52
+ stubs: {
53
+ AModalForm: { render() {
54
+ return h("div");
55
+ } },
56
+ },
57
+ },
58
+ });
59
+ wrappers.push(wrapper);
60
+ return wrapper;
61
+ }
62
+
63
+ describe("AValidatedJson valueKey rendering", () => {
64
+ it("displays primitive values in readonly labels and previews", async() => {
65
+ const wrapper = mountList({ readonly: true });
66
+ expect(wrapper.findAll("strong").map(label => label.text())).toEqual(["1. Aloha", "2. Vue"]);
67
+ expect(wrapper.find(".test_add").exists()).toBe(false);
68
+ expect(wrapper.findAllComponents(AValidatedJsonItem)[0].props("modelItem")).toEqual({ value: "Aloha" });
69
+ await wrapper.find(".test_toggle_details").trigger("click");
70
+ const preview = wrapper.findComponent(AValidatedJsonItem).findComponent(AFieldset);
71
+ expect(preview.props("readonly")).toBe(true);
72
+ expect(preview.props("modelValue")).toEqual({ value: "Aloha" });
73
+ });
74
+
75
+ it("supplies object models to editing and emits primitive arrays", async() => {
76
+ const wrapper = mountList({ errors: { 0: ["Invalid value"] } });
77
+ const item = wrapper.findAllComponents(AValidatedJsonItem)[0];
78
+ expect(item.props("errors")).toEqual({ value: ["Invalid value"] });
79
+ item.vm.openModalUpdate();
80
+ await wrapper.vm.$nextTick();
81
+ const modal = wrapper.findComponent(AValidatedJsonModalCreateOrUpdate);
82
+ expect(modal.props("currentModel")).toEqual({ value: "Aloha" });
83
+ modal.props("close")({ model: { value: "Updated" } });
84
+ expect(wrapper.emitted("update:modelValue").at(-1)).toEqual([["Updated", "Vue"]]);
85
+ });
86
+
87
+ it("checks uniqueness against the wrapped primitive values", async() => {
88
+ const wrapper = mountList({ uniqueChildrenIds: ["value"] });
89
+ await wrapper.find(".test_add").trigger("click");
90
+ const modal = wrapper.findComponent(AValidatedJsonModalCreateOrUpdate);
91
+ expect(modal.props("modelAll")).toEqual([{ value: "Aloha" }, { value: "Vue" }]);
92
+ modal.vm.changeDataForm({ fullModel: { value: "Aloha" } });
93
+ await modal.vm.$nextTick();
94
+ modal.vm.save();
95
+ expect(wrapper.emitted("update:modelValue")).toBeUndefined();
96
+ modal.vm.changeDataForm({ fullModel: { value: "New" } });
97
+ await modal.vm.$nextTick();
98
+ modal.vm.save();
99
+ expect(wrapper.emitted("update:modelValue").at(-1)).toEqual([["Aloha", "Vue", "New"]]);
100
+ });
101
+
102
+ it("works through AForm without registering a wrapper component", () => {
103
+ const wrapper = mount(AForm, {
104
+ props: {
105
+ modelValue: { tags: ["Aloha"] },
106
+ data: [{
107
+ id: "tags",
108
+ type: "validatedJson",
109
+ mode: "list",
110
+ valueKey: "value",
111
+ children: [{ id: "value", type: "text" }],
112
+ }],
113
+ },
114
+ });
115
+ wrappers.push(wrapper);
116
+ const list = wrapper.findComponent(AValidatedJson);
117
+ expect(list.findComponent(AValidatedJsonItem).props("modelItem")).toEqual({ value: "Aloha" });
118
+ list.vm.closeModalCreateListMode({ model: { value: "Vue" } });
119
+ expect(wrapper.emitted("update:modelValue").at(-1)).toEqual([{ tags: ["Aloha", "Vue"] }]);
120
+ });
121
+ });
@@ -10,7 +10,9 @@ import {
10
10
  } from "../../../utils/utilsDOM";
11
11
  import {
12
12
  cloneDeep,
13
+ get,
13
14
  isArray,
15
+ set,
14
16
  } from "lodash-es";
15
17
 
16
18
  export default function ListModeAPI(props, { emit }, {
@@ -18,6 +20,8 @@ export default function ListModeAPI(props, { emit }, {
18
20
  }) {
19
21
  const change = toRef(props, "change");
20
22
  const id = toRef(props, "id");
23
+ const errors = toRef(props, "errors");
24
+ const valueKey = toRef(props, "valueKey");
21
25
  const modelValue = toRef(props, "modelValue");
22
26
 
23
27
  const isModalCreateListModeVisible = ref(false);
@@ -28,6 +32,27 @@ export default function ListModeAPI(props, { emit }, {
28
32
  [];
29
33
  });
30
34
 
35
+ /*
36
+ * Forms, previews and uniqueness checks keep their object-based contract.
37
+ * Only the public model and change payloads contain primitive values.
38
+ */
39
+ const listModeItems = computed(() => {
40
+ if (!valueKey.value) {
41
+ return modelListMode.value;
42
+ }
43
+ return modelListMode.value.map(value => set({}, valueKey.value, value));
44
+ });
45
+
46
+ const getListModeValue = model => valueKey.value ? get(model, valueKey.value) : model;
47
+
48
+ const getListModeErrors = index => {
49
+ const ERRORS = errors.value?.[index];
50
+ if (valueKey.value && (typeof ERRORS === "string" || isArray(ERRORS))) {
51
+ return set({}, valueKey.value, ERRORS);
52
+ }
53
+ return ERRORS;
54
+ };
55
+
31
56
  const openModalCreateListMode = () => {
32
57
  isModalCreateListModeVisible.value = true;
33
58
  };
@@ -35,7 +60,7 @@ export default function ListModeAPI(props, { emit }, {
35
60
  const closeModalCreateListMode = ({ model } = {}) => {
36
61
  if (model) {
37
62
  const MODEL_VALUE = cloneDeep(modelListMode.value);
38
- MODEL_VALUE.push(model);
63
+ MODEL_VALUE.push(getListModeValue(model));
39
64
  emit("update:modelValue", cloneDeep(MODEL_VALUE));
40
65
  change.value({
41
66
  currentModel: MODEL_VALUE,
@@ -123,7 +148,7 @@ export default function ListModeAPI(props, { emit }, {
123
148
 
124
149
  const updateListMode = ({ index, model }) => {
125
150
  const MODEL = cloneDeep(modelListMode.value);
126
- MODEL.splice(index, 1, model);
151
+ MODEL.splice(index, 1, getListModeValue(model));
127
152
  emit("update:modelValue", cloneDeep(MODEL));
128
153
  change.value({
129
154
  currentModel: MODEL,
@@ -136,6 +161,8 @@ export default function ListModeAPI(props, { emit }, {
136
161
  closeModalCreateListMode,
137
162
  deleteListMode,
138
163
  isModalCreateListModeVisible,
164
+ getListModeErrors,
165
+ listModeItems,
139
166
  modelListMode,
140
167
  moveListMode,
141
168
  openModalCreateListMode,
@@ -1,6 +1,3 @@
1
- import {
2
- isArray,
3
- } from "lodash";
4
1
  import {
5
2
  typesContainer,
6
3
  typesModelArray,
@@ -10,6 +7,7 @@ import {
10
7
  filter,
11
8
  forEach,
12
9
  get,
10
+ isArray,
13
11
  isEmpty,
14
12
  isNil,
15
13
  } from "lodash-es";