-
Notifications
You must be signed in to change notification settings - Fork 4.3k
/
Copy pathobject-list-input.js
103 lines (91 loc) · 3.85 KB
/
object-list-input.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { assert } from '@ember/debug';
import { removeFromArray } from 'vault/helpers/remove-from-array';
/**
* @module ObjectListInput
* ObjectListInput components are used to render a variable number of text inputs in a single row
* with an "Add" button at the end of the row. Clicking 'add' generates a new row of empty inputs.
* Each input field is generated by an object in the @objectKeys array. Labels render above each column.
* @description
* ```
* sample object:
* {
* label: 'Input label', // required key
* key: 'attrKey', // required key
* placeholder: 'Enter input here'
* }
* ```
*
* @example
* <ObjectListInput @objectKeys={{hash label="Input label" key="attrKey" placeholder="Enter input here"}} @onChange={{this.handleChange}} @inputValue={{this.inputValue}}/>
*
* @param {array} objectKeys - an array of objects (sample above), the length determines the number of columns the component renders
* @param {function} onChange - callback triggered when any input changes or when a row is deleted, called with array of objects containing each input's key and value ex: `[ { attrKey: 'some input value' } ]`
* @param {string} [inputValue] - an array of objects to pre-fill the component inputs, key name must match objectKey[key]
* @param {array} [validationErrors] - an array of validation objects, the index of each object corresponds to the row with an invalid input. each object has a key that matches a key in objectKeys
* ex: (the nested object with 'errors' and 'isValid' matches the structure returned by the model validations decorator)
* `{ "attrKey": { "errors": ["Name is required."], "isValid": false } }`
*/
export default class ObjectListInput extends Component {
@tracked inputList = [];
@tracked inputKeys;
@tracked disableAdd = true;
constructor() {
super(...arguments);
const requiredKeys = ['label', 'key'];
this.assertKeys(this.args.objectKeys, requiredKeys);
this.inputKeys = this.args.objectKeys.map((e) => e.key);
if (this.args.inputValue) {
this.assertKeys(this.args.inputValue, this.inputKeys);
}
const emptyRow = this.createEmptyRow(this.inputKeys);
this.inputList = this.args.inputValue ? [...this.args.inputValue, emptyRow] : [emptyRow];
}
assertKeys(arrayOfObjects, requiredKeys) {
const argName = requiredKeys.includes('label') ? '@objectKeys' : '@inputValue';
return assert(
`objects in the ${argName} array must include keys called: ${requiredKeys.join(', ')}`,
arrayOfObjects.every((obj) => requiredKeys.every((k) => Object.keys(obj).includes(k)))
);
}
createEmptyRow(keys) {
// create a new object from input keys that have empty values
return Object.fromEntries(keys.map((key) => [key, '']));
}
@action
handleInput(idx, { target }) {
const inputObj = this.inputList[idx];
inputObj[target.name] = target.value;
this.handleChange();
}
@action
addRow() {
const emptyRow = this.createEmptyRow(this.inputKeys);
this.inputList = [...this.inputList, emptyRow];
this.disableAdd = true;
}
@action
removeRow(idx) {
const row = this.inputList[idx];
this.inputList = removeFromArray(this.inputList, row);
this.handleChange();
}
@action
handleChange() {
// disable/enable "add" button based on last row
const lastObject = this.inputList[this.inputList.length - 1];
this.disableAdd = Object.values(lastObject).any((input) => input === '') ? true : false;
// don't send an empty last object to parent
if (Object.values(lastObject).every((input) => input === '')) {
this.args.onChange(this.inputList.slice(0, -1));
} else {
this.args.onChange(this.inputList);
}
}
}