Unverified Commit 91751a31 authored by Jordan Reimer's avatar Jordan Reimer Committed by GitHub
Browse files

Form Field Glimmer (#15026)

* glimmerizes form field components

* updates model validations handling in components that use form field
parent 642a60ac
Branches unavailable
No related merge requests found
Showing with 577 additions and 624 deletions
+577 -624
import AdapterError from '@ember-data/adapter/error'; import AdapterError from '@ember-data/adapter/error';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import Component from '@ember/component'; import Component from '@ember/component';
import { computed, set } from '@ember/object'; import { computed } from '@ember/object';
import { task } from 'ember-concurrency'; import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters'; import { waitFor } from '@ember/test-waiters';
...@@ -25,7 +25,7 @@ export default Component.extend({ ...@@ -25,7 +25,7 @@ export default Component.extend({
itemType: null, itemType: null,
flashMessages: service(), flashMessages: service(),
router: service(), router: service(),
validationMessages: null, modelValidations: null,
isFormInvalid: false, isFormInvalid: false,
props: computed('model', function () { props: computed('model', function () {
return this.model.serialize(); return this.model.serialize();
...@@ -48,7 +48,6 @@ export default Component.extend({ ...@@ -48,7 +48,6 @@ export default Component.extend({
), ),
init() { init() {
this._super(...arguments); this._super(...arguments);
this.set('validationMessages', {});
if (this.mode === 'edit') { if (this.mode === 'edit') {
// For validation to work in edit mode, // For validation to work in edit mode,
// reconstruct the model values from field group // reconstruct the model values from field group
...@@ -70,13 +69,10 @@ export default Component.extend({ ...@@ -70,13 +69,10 @@ export default Component.extend({
if (this.model.validate) { if (this.model.validate) {
// Set validation error message for updated attribute // Set validation error message for updated attribute
const { isValid, state } = this.model.validate(); const { isValid, state } = this.model.validate();
if (state[name]) { this.setProperties({
state[name].isValid modelValidations: state,
? set(this.validationMessages, name, '') isFormInvalid: !isValid,
: set(this.validationMessages, name, state[name].errors.join('. ')); });
}
// Set form button state
this.set('isFormInvalid', !isValid);
} else { } else {
this.set('isFormInvalid', false); this.set('isFormInvalid', false);
} }
......
import Ember from 'ember'; import Ember from 'ember';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { computed, set } from '@ember/object'; import { computed } from '@ember/object';
import Component from '@ember/component'; import Component from '@ember/component';
import { task } from 'ember-concurrency'; import { task } from 'ember-concurrency';
import { methods } from 'vault/helpers/mountable-auth-methods'; import { methods } from 'vault/helpers/mountable-auth-methods';
...@@ -46,7 +46,7 @@ export default Component.extend({ ...@@ -46,7 +46,7 @@ export default Component.extend({
showEnable: false, showEnable: false,
// validation related properties // validation related properties
validationMessages: null, modelValidations: null,
isFormInvalid: false, isFormInvalid: false,
mountIssue: false, mountIssue: false,
...@@ -57,10 +57,6 @@ export default Component.extend({ ...@@ -57,10 +57,6 @@ export default Component.extend({
const modelType = type === 'secret' ? 'secret-engine' : 'auth-method'; const modelType = type === 'secret' ? 'secret-engine' : 'auth-method';
const model = this.store.createRecord(modelType); const model = this.store.createRecord(modelType);
this.set('mountModel', model); this.set('mountModel', model);
this.set('validationMessages', {
path: '',
});
}, },
mountTypes: computed('engines', 'mountType', function () { mountTypes: computed('engines', 'mountType', function () {
...@@ -167,23 +163,11 @@ export default Component.extend({ ...@@ -167,23 +163,11 @@ export default Component.extend({
actions: { actions: {
onKeyUp(name, value) { onKeyUp(name, value) {
this.mountModel.set(name, value); this.mountModel.set(name, value);
const { const { isValid, state } = this.mountModel.validate();
isValid, this.setProperties({
state: { path, maxVersions }, modelValidations: state,
} = this.mountModel.validate(); isFormInvalid: !isValid,
// validate path });
if (name === 'path') {
path.isValid
? set(this.validationMessages, 'path', '')
: set(this.validationMessages, 'path', path.errors.join('. '));
}
// check maxVersions is a number
if (name === 'maxVersions') {
maxVersions.isValid
? set(this.validationMessages, 'maxVersions', '')
: set(this.validationMessages, 'maxVersions', maxVersions.errors.join('. '));
}
this.set('isFormInvalid', !isValid);
}, },
onTypeChange(path, value) { onTypeChange(path, value) {
if (path === 'type') { if (path === 'type') {
......
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
/** /**
* @module SecretEditMetadata * @module SecretEditMetadata
* *
...@@ -15,24 +20,12 @@ ...@@ -15,24 +20,12 @@
* @param {Function} [updateValidationErrorCount] - function on parent that handles disabling the save button. * @param {Function} [updateValidationErrorCount] - function on parent that handles disabling the save button.
*/ */
import Component from '@glimmer/component';
import { action, set } from '@ember/object';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
export default class SecretEditMetadata extends Component { export default class SecretEditMetadata extends Component {
@service router; @service router;
@service store; @service store;
@tracked validationErrorCount = 0; @tracked validationErrorCount = 0;
@tracked modelValidations;
constructor() {
super(...arguments);
this.validationMessages = {
customMetadata: '',
maxVersions: '',
};
}
async save() { async save() {
let model = this.args.model; let model = this.args.model;
...@@ -51,29 +44,32 @@ export default class SecretEditMetadata extends Component { ...@@ -51,29 +44,32 @@ export default class SecretEditMetadata extends Component {
return this.save(); return this.save();
} }
@action onKeyUp(name, value) { @action onKeyUp(name, value) {
let state = {};
if (value) { if (value) {
if (name === 'customMetadata') { if (name === 'customMetadata') {
// cp validations won't work on an object so performing validations here // atypical case where property is not set on model on change - validate independently
// JLR TODO: review this and incorporate into model-validations system
/* eslint-disable no-useless-escape */ /* eslint-disable no-useless-escape */
let regex = /^[^\\]+$/g; // looking for a backward slash let regex = /^[^\\]+$/g; // looking for a backward slash
value.match(regex) if (!value.match(regex)) {
? set(this.validationMessages, name, '') state[name] = {
: set(this.validationMessages, name, 'Custom values cannot contain a backward slash.'); errors: ['Custom values cannot contain a backward slash.'],
isValid: false,
};
}
} }
if (name === 'maxVersions') { if (name === 'maxVersions') {
this.args.model.maxVersions = value; this.args.model.maxVersions = value;
const { state = this.args.model.validate().state;
state: { maxVersions },
} = this.args.model.validate();
maxVersions.isValid
? set(this.validationMessages, name, '')
: set(this.validationMessages, name, maxVersions.errors.join('. '));
} }
} }
let count = 0;
let values = Object.values(this.validationMessages); for (let key in state) {
this.validationErrorCount = values.filter(Boolean).length; if (!state[key].isValid) {
count++;
}
}
this.modelValidations = state;
this.validationErrorCount = count;
// when mode is "update" this works, but on mode "create" we need to bubble up the count // when mode is "update" this works, but on mode "create" we need to bubble up the count
if (this.args.updateValidationErrorCount) { if (this.args.updateValidationErrorCount) {
this.args.updateValidationErrorCount(this.validationErrorCount); this.args.updateValidationErrorCount(this.validationErrorCount);
......
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
@model={{this.model}} @model={{this.model}}
@mode={{this.mode}} @mode={{this.mode}}
@onKeyUp={{action "onKeyUp"}} @onKeyUp={{action "onKeyUp"}}
@validationMessages={{this.validationMessages}} @modelValidations={{this.modelValidations}}
/> />
</div> </div>
<div class="field is-grouped-split box is-fullwidth is-bottomless"> <div class="field is-grouped-split box is-fullwidth is-bottomless">
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
@model={{this.mountModel}} @model={{this.mountModel}}
@onChange={{action "onTypeChange"}} @onChange={{action "onTypeChange"}}
@renderGroup="default" @renderGroup="default"
@validationMessages={{this.validationMessages}} @modelValidations={{this.modelValidations}}
@onKeyUp={{action "onKeyUp"}} @onKeyUp={{action "onKeyUp"}}
/> />
<FormFieldGroups @model={{this.mountModel}} @onChange={{action "onTypeChange"}} @renderGroup="Method Options" /> <FormFieldGroups @model={{this.mountModel}} @onChange={{action "onTypeChange"}} @renderGroup="Method Options" />
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
@attr={{attr}} @attr={{attr}}
@model={{@model}} @model={{@model}}
@onKeyUp={{action "onKeyUp"}} @onKeyUp={{action "onKeyUp"}}
@validationMessages={{this.validationMessages}} @modelValidations={{this.modelValidations}}
@mode={{@mode}} @mode={{@mode}}
/> />
<label class={{if (eq @mode "create") "title has-padding-top is-5" "title has-padding-top is-4"}}> <label class={{if (eq @mode "create") "title has-padding-top is-5" "title has-padding-top is-4"}}>
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
@attr={{attr}} @attr={{attr}}
@model={{@model}} @model={{@model}}
@onKeyUp={{action "onKeyUp"}} @onKeyUp={{action "onKeyUp"}}
@validationMessages={{this.validationMessages}} @modelValidations={{this.modelValidations}}
/> />
{{/if}} {{/if}}
{{/each}} {{/each}}
......
{{#each @model.fieldGroups as |fieldGroup|}}
{{#each-in fieldGroup as |group fields|}}
{{#if (or (not @renderGroup) (and @renderGroup (eq group @renderGroup)))}}
{{#if (eq group "default")}}
{{#each fields as |attr|}}
{{! template-lint-configure simple-unless "warn" }}
{{#unless (and (not-eq @mode "create") (eq attr.name "name"))}}
<FormField
data-test-field
@attr={{attr}}
@mode={{@mode}}
@model={{@model}}
@onChange={{@onChange}}
@onKeyUp={{@onKeyUp}}
@modelValidations={{@modelValidations}}
/>
{{/unless}}
{{/each}}
{{else}}
<ToggleButton
@isOpen={{eq this.showGroup group}}
@openLabel={{concat "Hide " group}}
@closedLabel={{group}}
@onClick={{fn this.toggleGroup group}}
class="is-block"
data-test-toggle-group={{group}}
/>
{{#if (eq this.showGroup group)}}
<div class="box is-marginless">
{{#each fields as |attr|}}
<FormField data-test-field @attr={{attr}} @mode={{@mode}} @model={{@model}} />
{{/each}}
</div>
{{/if}}
{{/if}}
{{/if}}
{{/each-in}}
{{/each}}
\ No newline at end of file
import Component from '@ember/component'; import Component from '@glimmer/component';
import { computed } from '@ember/object'; import { tracked } from '@glimmer/tracking';
import layout from '../templates/components/form-field-groups'; import { action } from '@ember/object';
/** /**
* @module FormFieldGroups * @module FormFieldGroups
...@@ -22,24 +22,21 @@ import layout from '../templates/components/form-field-groups'; ...@@ -22,24 +22,21 @@ import layout from '../templates/components/form-field-groups';
* @validationMessages={{validationMessages}} * @validationMessages={{validationMessages}}
* /> * />
* ``` * ```
* * @callback onChangeCallback
* @param [renderGroup=null] {String} - An allow list of groups to include in the render. * @callback onKeyUpCallback
* @param model=null {DS.Model} - Model to be passed down to form-field component. If `fieldGroups` is present on the model then it will be iterated over and groups of `FormField` components will be rendered. * @param {Model} model- Model to be passed down to form-field component. If `fieldGroups` is present on the model then it will be iterated over and groups of `FormField` components will be rendered.
* @param onChange=null {Func} - Handler that will get set on the `FormField` component. * @param {string} [renderGroup] - An allow list of groups to include in the render.
* @param onKeyUp=null {Func} - Handler that will set the value and trigger validation on input changes * @param {onChangeCallback} [onChange] - Handler that will get set on the `FormField` component.
* @param validationMessages=null {Object} Object containing validation message for each property * @param {onKeyUpCallback} [onKeyUp] - Handler that will set the value and trigger validation on input changes
* @param {ModelValidations} [modelValidations] - Object containing validation message for each property
* *
*/ */
export default Component.extend({ export default class FormFieldGroupsComponent extends Component {
layout, @tracked showGroup = null;
tagName: '',
renderGroup: computed(function () {
return null;
}),
model: null,
onChange: () => {}, @action
}); toggleGroup(group, isOpen) {
this.showGroup = isOpen ? group : null;
}
}
{{! template-lint-configure simple-unless "warn" }}
<div class="field" data-test-field>
{{#unless
(or
(eq @attr.type "boolean")
(contains
@attr.options.editType
(array "boolean" "optionalText" "searchSelect" "mountAccessor" "kv" "file" "ttl" "stringArray" "json" "regex")
)
)
}}
{{#unless (eq @attr.type "object")}}
<FormFieldLabel
for={{@attr.name}}
@label={{this.labelString}}
@helpText={{@attr.options.helpText}}
@subText={{@attr.options.subText}}
@docLink={{@attr.options.docLink}}
/>
{{/unless}}
{{/unless}}
{{#if @attr.options.possibleValues}}
<div class="control is-expanded">
<div class="select is-fullwidth">
<select name={{@attr.name}} id={{@attr.name}} onchange={{this.onChangeWithEvent}} data-test-input={{@attr.name}}>
{{#if @attr.options.noDefault}}
<option value="">
Select one
</option>
{{/if}}
{{#each (path-or-array @attr.options.possibleValues @model) as |val|}}
<option selected={{eq (get @model this.valuePath) (or val.value val)}} value={{or val.value val}}>
{{or val.displayName val}}
</option>
{{/each}}
</select>
</div>
</div>
{{else if (and (eq @attr.type "string") (eq @attr.options.editType "boolean"))}}
<div class="b-checkbox">
<input
type="checkbox"
id={{@attr.name}}
class="styled"
checked={{eq (get @model this.valuePath) @attr.options.trueValue}}
onchange={{fn this.setAndBroadcastBool @attr.options.trueValue @attr.options.falseValue}}
data-test-input={{@attr.name}}
/>
<label for={{@attr.name}} class="is-label">
{{this.labelString}}
{{#if @attr.options.helpText}}
<InfoTooltip>{{@attr.options.helpText}}</InfoTooltip>
{{/if}}
</label>
</div>
{{else if (eq @attr.options.editType "searchSelect")}}
<div class="form-section">
<SearchSelect
@id={{@attr.name}}
@models={{@attr.options.models}}
@onChange={{this.setAndBroadcast}}
@inputValue={{get @model this.valuePath}}
@helpText={{@attr.options.helpText}}
@subText={{@attr.options.subText}}
@wildcardLabel={{@attr.options.wildcardLabel}}
@label={{this.labelString}}
@subLabel={{@attr.options.subLabel}}
@fallbackComponent={{@attr.options.fallbackComponent}}
@selectLimit={{@attr.options.selectLimit}}
@backend={{@model.backend}}
@disallowNewItems={{@attr.options.onlyAllowExisting}}
/>
</div>
{{else if (eq @attr.options.editType "mountAccessor")}}
<MountAccessorSelect
@name={{@attr.name}}
@label={{this.labelString}}
@warning={{@attr.options.warning}}
@helpText={{@attr.options.helpText}}
@value={{get @model this.valuePath}}
@onChange={{this.setAndBroadcast}}
/>
{{else if (eq @attr.options.editType "kv")}}
{{! KV Object Editor }}
<KvObjectEditor
@value={{get @model this.valuePath}}
@onChange={{this.setAndBroadcast}}
@label={{this.labelString}}
@labelClass="title {{if (eq @mode 'create') 'is-5' 'is-4'}}"
@helpText={{@attr.options.helpText}}
@subText={{@attr.options.subText}}
@onKeyUp={{this.handleKeyUp}}
@validationError={{this.validationError}}
class={{if (not-eq @mode "customMetadata") "form-section"}}
/>
{{else if (eq @attr.options.editType "file")}}
{{! File Input }}
<TextFile
@helpText={{@attr.options.helpText}}
@file={{this.file}}
@onChange={{this.setFile}}
@label={{this.labelString}}
/>
{{else if (eq @attr.options.editType "ttl")}}
{{! TTL Picker }}
<div class="field">
{{#let (or (get @model this.valuePath) @attr.options.setDefault) as |initialValue|}}
<TtlPicker2
@onChange={{this.setAndBroadcastTtl}}
@label={{this.labelString}}
@helperTextDisabled={{or @attr.options.helperTextDisabled "Vault will use the default lease duration."}}
@helperTextEnabled={{or @attr.options.helperTextEnabled "Lease will expire after"}}
@description={{@attr.helpText}}
@initialValue={{initialValue}}
@initialEnabled={{if (eq initialValue "0s") false initialValue}}
/>
{{/let}}
</div>
{{else if (eq @attr.options.editType "regex")}}
{{! Regex Validated Input }}
<RegexValidator
@attr={{@attr}}
@labelString={{this.labelString}}
@value={{or (get @model this.valuePath) @attr.options.defaultValue}}
@onChange={{this.onChangeWithEvent}}
/>
{{else if (eq @attr.options.editType "optionalText")}}
{{! Togglable Text Input }}
<Toggle
@name="show-{{@attr.name}}"
@status="success"
@size="small"
@onChange={{this.toggleShow}}
@checked={{this.showInput}}
data-test-toggle={{@attr.name}}
>
<span class="ttl-picker-label is-large">{{this.labelString}}</span><br />
<div class="description has-text-grey">
{{#if this.showInput}}
<span>
{{@attr.options.subText}}
{{#if @attr.options.docLink}}
<a href={{@attr.options.docLink}} target="_blank" rel="noopener noreferrer">
See our documentation
</a>
for help.
{{/if}}
</span>
{{else}}
<span>
{{or @attr.options.defaultSubText "Vault will use the engine default."}}
{{#if @attr.options.docLink}}
<a href={{@attr.options.docLink}} target="_blank" rel="noopener noreferrer">
See our documentation
</a>
for help.
{{/if}}
</span>
{{/if}}
</div>
{{#if this.showInput}}
<input
data-test-input={{@attr.name}}
id={{@attr.name}}
autocomplete="off"
spellcheck="false"
value={{or (get @model this.valuePath) @attr.options.defaultValue}}
onchange={{this.onChangeWithEvent}}
class="input"
maxLength={{@attr.options.characterLimit}}
/>
{{/if}}
</Toggle>
{{else if (eq @attr.options.editType "stringArray")}}
<StringList
@data-test-input={{@attr.name}}
@label={{this.labelString}}
@warning={{@attr.options.warning}}
@helpText={{@attr.options.helpText}}
@inputValue={{get @model this.valuePath}}
@onChange={{this.setAndBroadcast}}
/>
{{else if (eq @attr.options.sensitive true)}}
{{! Masked Input }}
<MaskedInput
@value={{or (get @model this.valuePath) @attr.options.defaultValue}}
@allowCopy="true"
@onChange={{this.setAndBroadcast}}
@name={{@attr.name}}
@onKeyUp={{@onKeyUp}}
/>
{{#if this.validationError}}
<AlertInline @type="danger" @message={{this.validationError}} @paddingTop={{true}} />
{{/if}}
{{else if (or (eq @attr.type "number") (eq @attr.type "string"))}}
<div class="control">
{{#if (eq @attr.options.editType "textarea")}}
{{! Text area }}
<textarea
data-test-input={{@attr.name}}
id={{@attr.name}}
value={{or (get @model this.valuePath) @attr.options.defaultValue}}
oninput={{this.onChangeWithEvent}}
class="textarea"
></textarea>
{{else if (eq @attr.options.editType "password")}}
<Input
data-test-input={{@attr.name}}
@type="password"
@value={{get @model this.valuePath}}
name={{@attr.name}}
class="input"
{{! Prevents browsers from auto-filling }}
autocomplete="new-password"
spellcheck="false"
/>
{{else if (eq @attr.options.editType "json")}}
{{! JSON Editor }}
<JsonEditor
data-test-input={{@attr.name}}
@title={{this.labelString}}
@value={{if
(get @model this.valuePath)
(stringify (jsonify (get @model this.valuePath)))
@attr.options.defaultValue
}}
@valueUpdated={{fn this.codemirrorUpdated true}}
@theme={{or @attr.options.theme "hashi"}}
@helpText={{@attr.options.helpText}}
>
{{#if @attr.options.allowReset}}
<button
type="button"
class="toolbar-link"
disabled={{not (get @model this.valuePath)}}
onClick={{this.setAndBroadcast}}
data-test-json-clear-button
>
Clear
<Icon @name="reload" />
</button>
{{/if}}
</JsonEditor>
{{#if @attr.options.subText}}
<p class="sub-text">
{{@attr.options.subText}}
{{#if @attr.options.docLink}}
<a href={{@attr.options.docLink}} target="_blank" rel="noopener noreferrer">
See our documentation
</a>
for help.
{{/if}}
</p>
{{/if}}
{{else}}
{{! Regular Text Input }}
<input
data-test-input={{@attr.name}}
id={{@attr.name}}
readonly={{this.isReadOnly}}
autocomplete="off"
spellcheck="false"
value={{or (get @model this.valuePath) @attr.options.defaultValue}}
onchange={{this.onChangeWithEvent}}
onkeyup={{this.handleKeyUp}}
class="input {{if this.validationError 'has-error-border'}}"
maxLength={{@attr.options.characterLimit}}
/>
{{#if @attr.options.validationAttr}}
{{#if (and (get @model this.valuePath) (not (get @model @attr.options.validationAttr)))}}
<AlertInline @type="danger" @message={{@attr.options.invalidMessage}} />
{{/if}}
{{/if}}
{{#if this.validationError}}
<AlertInline @type="danger" @message={{this.validationError}} @paddingTop={{true}} />
{{/if}}
{{/if}}
</div>
{{else if (eq @attr.type "boolean")}}
<div class="b-checkbox">
<input
disabled={{this.disabled}}
type="checkbox"
id={{@attr.name}}
class="styled"
checked={{get @model @attr.name}}
onchange={{this.onChangeWithEvent}}
data-test-input={{@attr.name}}
/>
<label for={{@attr.name}} class="is-label">
{{this.labelString}}
{{#if (and this.showHelpText @attr.options.helpText)}}
<InfoTooltip>{{@attr.options.helpText}}</InfoTooltip>
{{/if}}
</label>
{{#if @attr.options.subText}}
<p class="sub-text">{{@attr.options.subText}}</p>
{{/if}}
</div>
{{else if (eq @attr.type "object")}}
<JsonEditor
@title={{this.labelString}}
@value={{if (get @model this.valuePath) (stringify (get @model this.valuePath)) this.emptyData}}
@valueUpdated={{fn this.codemirrorUpdated false}}
@helpText={{@attr.options.helpText}}
/>
{{/if}}
</div>
\ No newline at end of file
import { or } from '@ember/object/computed'; import Component from '@glimmer/component';
import Component from '@ember/component'; import { tracked } from '@glimmer/tracking';
import { computed } from '@ember/object'; import { action, get } from '@ember/object';
import { capitalize } from 'vault/helpers/capitalize'; import { capitalize } from 'vault/helpers/capitalize';
import { humanize } from 'vault/helpers/humanize'; import { humanize } from 'vault/helpers/humanize';
import { dasherize } from 'vault/helpers/dasherize'; import { dasherize } from 'vault/helpers/dasherize';
import layout from '../templates/components/form-field';
/** /**
* @module FormField * @module FormField
* `FormField` components are field elements associated with a particular model. * `FormField` components are field elements associated with a particular model.
...@@ -16,152 +14,140 @@ import layout from '../templates/components/form-field'; ...@@ -16,152 +14,140 @@ import layout from '../templates/components/form-field';
* <FormField data-test-field @attr={{attr}} @model={{this.model}} /> * <FormField data-test-field @attr={{attr}} @model={{this.model}} />
* {{/each}} * {{/each}}
* ``` * ```
* * example attr object:
* @param [onChange=null] {Func} - Called whenever a value on the model changes via the component. * attr = {
* @param [onKeyUp=null] {Func} - A function passed through into MaskedInput to handle validation. It is also handled for certain form-field types here in the action handleKeyUp. * name: "foo", // name of attribute -- used to populate various fields and pull value from model
* @param attr=null {Object} - This is usually derived from ember model `attributes` lookup, and all members of `attr.options` are optional. * options: {
* @param model=null {DS.Model} - The Ember Data model that `attr` is defined on * label: "Foo", // custom label to be shown, otherwise attr.name will be displayed
* @param [disabled=false] {Boolean} - whether the field is disabled * defaultValue: "", // default value to display if model value is not present
* @param [showHelpText=true] {Boolean} - whether to show the tooltip with help text from OpenAPI * fieldValue: "bar", // used for value lookup on model over attr.name
* @param [subText] {String} - Text to be displayed below the label * editType: "ttl", type of field to use -- example boolean, searchSelect, etc.
* @param [validationMessages] {Object} - Object of errors. If attr.name is in object and has error message display in AlertInline. * helpText: "This will be in a tooltip",
* readOnly: true
* },
* type: "boolean" // type of attribute value -- string, boolean, etc.
* }
* @param {Object} attr - usually derived from ember model `attributes` lookup, and all members of `attr.options` are optional
* @param {Model} model - Ember Data model that `attr` is defined on
* @param {boolean} [disabled=false] - whether the field is disabled
* @param {boolean} [showHelpText=true] - whether to show the tooltip with help text from OpenAPI
* @param {string} [subText] - text to be displayed below the label
* @param {string} [mode] - used when editType is 'kv'
* @param {ModelValidations} [modelValidations] - Object of errors. If attr.name is in object and has error message display in AlertInline.
* @callback onChangeCallback
* @param {onChangeCallback} [onChange] - called whenever a value on the model changes via the component
* @callback onKeyUpCallback
* @param {onKeyUpCallback} [onKeyUp] - function passed through into MaskedInput to handle validation. It is also handled for certain form-field types here in the action handleKeyUp.
* *
*/ */
export default Component.extend({ export default class FormFieldComponent extends Component {
layout, @tracked showInput = false;
'data-test-field': true, @tracked file = { value: '' }; // used by the pgp-file component when an attr is editType of 'file'
attributeBindings: ['data-test-field'], emptyData = '{\n}';
classNames: ['field'],
disabled: false, constructor() {
showHelpText: true, super(...arguments);
subText: '', const { attr, model } = this.args;
// This is only used internally for `optional-text` editType const valuePath = attr.options?.fieldValue || attr.name;
showInput: false, const modelValue = model[valuePath];
this.showInput = !!modelValue;
init() { }
this._super(...arguments);
const valuePath = this.attr.options?.fieldValue || this.attr.name; get disabled() {
const modelValue = this.model[valuePath]; return this.args.disabled || false;
this.set('showInput', !!modelValue); }
}, get showHelpText() {
return this.args.showHelpText || true;
onChange() {}, }
get subText() {
/* return this.args.subText || '';
* @public }
* @param Object // used in the label element next to the form element
* in the form of get labelString() {
* { const label = this.args.attr.options?.label || '';
* name: "foo",
* options: {
* label: "Foo",
* defaultValue: "",
* editType: "ttl",
* helpText: "This will be in a tooltip"
* },
* type: "boolean"
* }
*
*/
attr: null,
mode: null,
/*
* @private
* @param string
* Computed property used in the label element next to the form element
*
*/
labelString: computed('attr.{name,options.label}', function () {
const label = this.attr.options ? this.attr.options.label : '';
const name = this.attr.name;
if (label) { if (label) {
return label; return label;
} }
if (name) { if (this.args.attr.name) {
return capitalize([humanize([dasherize([name])])]); return capitalize([humanize([dasherize([this.args.attr.name])])]);
} }
return ''; return '';
}), }
// both the path to mutate on the model, and the path to read the value from // both the path to mutate on the model, and the path to read the value from
/* get valuePath() {
* @private return this.args.attr.options?.fieldValue || this.args.attr.name;
* @param string }
* get isReadOnly() {
* Computed property used to set values on the passed model const readonly = this.args.attr.options?.readOnly || false;
* return readonly && this.args.mode === 'edit';
*/ }
valuePath: or('attr.options.fieldValue', 'attr.name'), get validationError() {
const validations = this.args.modelValidations || {};
isReadOnly: computed('attr.options.readOnly', 'mode', function () { const state = get(validations, this.valuePath);
let readonly = this.attr.options?.readOnly || false; return state && !state.isValid ? state.errors.join('. ') : null;
return readonly && this.mode === 'edit'; }
}),
onChange() {
model: null, if (this.args.onChange) {
this.args.onChange(...arguments);
/* }
* @private }
* @param object
* @action
* Used by the pgp-file component when an attr is editType of 'file' setFile(_, keyFile) {
*/ const path = this.valuePath;
file: computed(function () { const { value } = keyFile;
return { value: '' }; this.args.model.set(path, value);
}), this.onChange(path, value);
emptyData: '{\n}', this.file = keyFile;
}
actions: { @action
setFile(_, keyFile) { setAndBroadcast(value) {
const path = this.valuePath; this.args.model.set(this.valuePath, value);
const { value } = keyFile; this.onChange(this.valuePath, value);
this.model.set(path, value); }
this.onChange(path, value); @action
this.set('file', keyFile); setAndBroadcastBool(trueVal, falseVal, event) {
}, let valueToSet = event.target.checked === true ? trueVal : falseVal;
this.setAndBroadcast(valueToSet);
setAndBroadcast(path, value) { }
this.model.set(path, value); @action
this.onChange(path, value); setAndBroadcastTtl(value) {
}, const alwaysSendValue = this.valuePath === 'expiry' || this.valuePath === 'safetyBuffer';
let valueToSet = value.enabled === true || alwaysSendValue ? `${value.seconds}s` : 0;
setAndBroadcastBool(path, trueVal, falseVal, value) { this.setAndBroadcast(`${valueToSet}`);
let valueToSet = value === true ? trueVal : falseVal; }
this.send('setAndBroadcast', path, valueToSet); @action
}, codemirrorUpdated(isString, value, codemirror) {
codemirror.performLint();
setAndBroadcastTtl(path, value) { const hasErrors = codemirror.state.lint.marked.length > 0;
const alwaysSendValue = path === 'expiry' || path === 'safetyBuffer'; let valToSet = isString ? value : JSON.parse(value);
let valueToSet = value.enabled === true || alwaysSendValue ? `${value.seconds}s` : 0;
this.send('setAndBroadcast', path, `${valueToSet}`); if (!hasErrors) {
}, this.args.model.set(this.valuePath, valToSet);
this.onChange(this.valuePath, valToSet);
codemirrorUpdated(path, isString, value, codemirror) { }
codemirror.performLint(); }
const hasErrors = codemirror.state.lint.marked.length > 0; @action
let valToSet = isString ? value : JSON.parse(value); toggleShow() {
const value = !this.showInput;
if (!hasErrors) { this.showInput = value;
this.model.set(path, valToSet); if (!value) {
this.onChange(path, valToSet); this.setAndBroadcast(null);
} }
}, }
@action
toggleShow(path) { handleKeyUp(maybeEvent) {
const value = !this.showInput; const value = typeof maybeEvent === 'object' ? maybeEvent.target.value : maybeEvent;
this.set('showInput', value); if (!this.args.onKeyUp) {
if (!value) { return;
this.send('setAndBroadcast', path, null); }
} this.args.onKeyUp(this.valuePath, value);
}, }
handleKeyUp(name, value) { @action
if (!this.onKeyUp) { onChangeWithEvent(event) {
return; const prop = event.target.type === 'checkbox' ? 'checked' : 'value';
} this.setAndBroadcast(event.target[prop]);
this.onKeyUp(name, value); }
}, }
},
});
{{#each this.model.fieldGroups as |fieldGroup|}}
{{#each-in fieldGroup as |group fields|}}
{{#if (or (not this.renderGroup) (and this.renderGroup (eq group this.renderGroup)))}}
{{#if (eq group "default")}}
{{#each fields as |attr|}}
{{! template-lint-configure simple-unless "warn" }}
{{#unless (and (not-eq this.mode "create") (eq attr.name "name"))}}
<FormField
data-test-field
@attr={{attr}}
@mode={{this.mode}}
@model={{this.model}}
@onChange={{this.onChange}}
@onKeyUp={{this.onKeyUp}}
@validationMessages={{this.validationMessages}}
/>
{{/unless}}
{{/each}}
{{else}}
{{#let (camelize (concat "show" group)) as |prop|}}
<ToggleButton
@isOpen={{get this prop}}
@openLabel={{concat "Hide " group}}
@closedLabel={{group}}
@onClick={{fn (mut (get this prop))}}
class="is-block"
data-test-toggle-group={{group}}
/>
{{#if (get this prop)}}
<div class="box is-marginless">
{{#each fields as |attr|}}
<FormField data-test-field @attr={{attr}} @mode={{this.mode}} @model={{this.model}} />
{{/each}}
</div>
{{/if}}
{{/let}}
{{/if}}
{{/if}}
{{/each-in}}
{{/each}}
\ No newline at end of file
{{! template-lint-configure simple-unless "warn" }}
{{#unless
(or
(eq this.attr.type "boolean")
(contains
this.attr.options.editType
(array "boolean" "optionalText" "searchSelect" "mountAccessor" "kv" "file" "ttl" "stringArray" "json" "regex")
)
)
}}
{{#unless (eq this.attr.type "object")}}
<FormFieldLabel
for={{this.attr.name}}
@label={{this.labelString}}
@helpText={{this.attr.options.helpText}}
@subText={{this.attr.options.subText}}
@docLink={{this.attr.options.docLink}}
/>
{{/unless}}
{{/unless}}
{{#if this.attr.options.possibleValues}}
<div class="control is-expanded">
<div class="select is-fullwidth">
<select
name={{this.attr.name}}
id={{this.attr.name}}
onchange={{action (action "setAndBroadcast" this.valuePath) value="target.value"}}
data-test-input={{this.attr.name}}
>
{{#if this.attr.options.noDefault}}
<option value="">
Select one
</option>
{{/if}}
{{#each (path-or-array this.attr.options.possibleValues this.model) as |val|}}
<option selected={{eq (get this.model this.valuePath) (or val.value val)}} value={{or val.value val}}>
{{or val.displayName val}}
</option>
{{/each}}
</select>
</div>
</div>
{{else if (and (eq this.attr.type "string") (eq this.attr.options.editType "boolean"))}}
<div class="b-checkbox">
<input
type="checkbox"
id={{this.attr.name}}
class="styled"
checked={{eq (get this.model this.valuePath) this.attr.options.trueValue}}
onchange={{action
(action "setAndBroadcastBool" this.valuePath this.attr.options.trueValue this.attr.options.falseValue)
value="target.checked"
}}
data-test-input={{this.attr.name}}
/>
<label for={{this.attr.name}} class="is-label">
{{this.labelString}}
{{#if this.attr.options.helpText}}
<InfoTooltip>{{this.attr.options.helpText}}</InfoTooltip>
{{/if}}
</label>
</div>
{{else if (eq this.attr.options.editType "searchSelect")}}
<div class="form-section">
<SearchSelect
@id={{this.attr.name}}
@models={{this.attr.options.models}}
@onChange={{action (action "setAndBroadcast" this.valuePath)}}
@inputValue={{get this.model this.valuePath}}
@helpText={{this.attr.options.helpText}}
@subText={{this.attr.options.subText}}
@wildcardLabel={{this.attr.options.wildcardLabel}}
@label={{this.labelString}}
@subLabel={{this.attr.options.subLabel}}
@fallbackComponent={{this.attr.options.fallbackComponent}}
@selectLimit={{this.attr.options.selectLimit}}
@backend={{this.model.backend}}
@disallowNewItems={{this.attr.options.onlyAllowExisting}}
/>
</div>
{{else if (eq this.attr.options.editType "mountAccessor")}}
<MountAccessorSelect
@name={{this.attr.name}}
@label={{this.labelString}}
@warning={{this.attr.options.warning}}
@helpText={{this.attr.options.helpText}}
@value={{get this.model this.valuePath}}
@onChange={{action "setAndBroadcast" this.valuePath}}
/>
{{else if (eq this.attr.options.editType "kv")}}
{{! KV Object Editor }}
<KvObjectEditor
@value={{get this.model this.valuePath}}
@onChange={{action "setAndBroadcast" this.valuePath}}
@label={{this.labelString}}
@labelClass="title {{if (eq this.mode 'create') 'is-5' 'is-4'}}"
@helpText={{this.attr.options.helpText}}
@subText={{this.attr.options.subText}}
@onKeyUp={{action "handleKeyUp" this.valuePath}}
@validationError={{get this.validationMessages this.valuePath}}
class={{if (not-eq this.mode "customMetadata") "form-section"}}
/>
{{else if (eq this.attr.options.editType "file")}}
{{! File Input }}
<TextFile
@helpText={{this.attr.options.helpText}}
@file={{this.file}}
@onChange={{action "setFile"}}
@label={{this.labelString}}
/>
{{else if (eq this.attr.options.editType "ttl")}}
{{! TTL Picker }}
<div class="field">
{{#let (or (get this.model this.valuePath) this.attr.options.setDefault) as |initialValue|}}
<TtlPicker2
@onChange={{action (action "setAndBroadcastTtl" this.valuePath)}}
@label={{this.labelString}}
@helperTextDisabled={{or this.attr.options.helperTextDisabled "Vault will use the default lease duration."}}
@helperTextEnabled={{or this.attr.options.helperTextEnabled "Lease will expire after"}}
@description={{this.attr.helpText}}
@initialValue={{initialValue}}
@initialEnabled={{if (eq initialValue "0s") false initialValue}}
/>
{{/let}}
</div>
{{else if (eq this.attr.options.editType "regex")}}
{{! Regex Validated Input }}
<RegexValidator
@attr={{this.attr}}
@labelString={{this.labelString}}
@value={{or (get this.model this.valuePath) this.attr.options.defaultValue}}
@onChange={{action (action "setAndBroadcast" this.valuePath) value="target.value"}}
/>
{{else if (eq this.attr.options.editType "optionalText")}}
{{! Togglable Text Input }}
<Toggle
@name="show-{{this.attr.name}}"
@status="success"
@size="small"
@onChange={{action "toggleShow" this.attr.name}}
@checked={{this.showInput}}
data-test-toggle={{this.attr.name}}
>
<span class="ttl-picker-label is-large">{{this.labelString}}</span><br />
<div class="description has-text-grey">
{{#if this.showInput}}
<span>
{{this.attr.options.subText}}
{{#if this.attr.options.docLink}}
<a href={{this.attr.options.docLink}} target="_blank" rel="noopener noreferrer">
See our documentation
</a>
for help.
{{/if}}
</span>
{{else}}
<span>
{{or this.attr.options.defaultSubText "Vault will use the engine default."}}
{{#if this.attr.options.docLink}}
<a href={{this.attr.options.docLink}} target="_blank" rel="noopener noreferrer">
See our documentation
</a>
for help.
{{/if}}
</span>
{{/if}}
</div>
{{#if this.showInput}}
<input
data-test-input={{this.attr.name}}
id={{this.attr.name}}
autocomplete="off"
spellcheck="false"
value={{or (get this.model this.valuePath) this.attr.options.defaultValue}}
onChange={{action (action "setAndBroadcast" this.valuePath) value="target.value"}}
class="input"
maxLength={{this.attr.options.characterLimit}}
/>
{{/if}}
</Toggle>
{{else if (eq this.attr.options.editType "stringArray")}}
<StringList
@data-test-input={{this.attr.name}}
@label={{this.labelString}}
@warning={{this.attr.options.warning}}
@helpText={{this.attr.options.helpText}}
@inputValue={{get this.model this.valuePath}}
@onChange={{action (action "setAndBroadcast" this.valuePath)}}
/>
{{else if (eq this.attr.options.sensitive true)}}
{{! Masked Input }}
<MaskedInput
@value={{or (get this.model this.valuePath) this.attr.options.defaultValue}}
@allowCopy="true"
@onChange={{action (action "setAndBroadcast" this.valuePath)}}
@name={{this.attr.name}}
@onKeyUp={{@onKeyUp}}
/>
{{#if (get this.validationMessages this.attr.name)}}
<AlertInline @type="danger" @message={{get this.validationMessages this.attr.name}} @paddingTop={{true}} />
{{/if}}
{{else if (or (eq this.attr.type "number") (eq this.attr.type "string"))}}
<div class="control">
{{#if (eq this.attr.options.editType "textarea")}}
{{! Text area }}
<textarea
data-test-input={{this.attr.name}}
id={{this.attr.name}}
value={{or (get this.model this.valuePath) this.attr.options.defaultValue}}
oninput={{action (action "setAndBroadcast" this.valuePath) value="target.value"}}
class="textarea"
></textarea>
{{else if (eq this.attr.options.editType "password")}}
<Input
data-test-input={{this.attr.name}}
@type="password"
@value={{get this.model this.valuePath}}
name={{this.attr.name}}
class="input"
{{! Prevents browsers from auto-filling }}
autocomplete="new-password"
spellcheck="false"
/>
{{else if (eq this.attr.options.editType "json")}}
{{! JSON Editor }}
<JsonEditor
data-test-input={{this.attr.name}}
@title={{this.labelString}}
@value={{if
(get this.model this.valuePath)
(stringify (jsonify (get this.model this.valuePath)))
this.attr.options.defaultValue
}}
@valueUpdated={{action "codemirrorUpdated" this.attr.name "string"}}
@theme={{or this.attr.options.theme "hashi"}}
@helpText={{this.attr.options.helpText}}
>
{{#if this.attr.options.allowReset}}
<button
type="button"
class="toolbar-link"
disabled={{not (get this.model this.valuePath)}}
onClick={{action (action "setAndBroadcast" this.valuePath) null}}
data-test-json-clear-button
>
Clear
<Icon @name="reload" />
</button>
{{/if}}
</JsonEditor>
{{#if this.attr.options.subText}}
<p class="sub-text">
{{this.attr.options.subText}}
{{#if this.attr.options.docLink}}
<a href={{this.attr.options.docLink}} target="_blank" rel="noopener noreferrer">
See our documentation
</a>
for help.
{{/if}}
</p>
{{/if}}
{{else}}
{{! Regular Text Input }}
<input
data-test-input={{this.attr.name}}
id={{this.attr.name}}
readonly={{this.isReadOnly}}
autocomplete="off"
spellcheck="false"
value={{or (get this.model this.valuePath) this.attr.options.defaultValue}}
onChange={{action (action "setAndBroadcast" this.valuePath) value="target.value"}}
onkeyup={{action (action "handleKeyUp" this.attr.name) value="target.value"}}
class="input {{if (get this.validationMessages this.attr.name) 'has-error-border'}}"
maxLength={{this.attr.options.characterLimit}}
/>
{{#if this.attr.options.validationAttr}}
{{#if (and (get this.model this.valuePath) (not (get this.model this.attr.options.validationAttr)))}}
<AlertInline @type="danger" @message={{this.attr.options.invalidMessage}} />
{{/if}}
{{/if}}
{{#if (get this.validationMessages this.attr.name)}}
<AlertInline @type="danger" @message={{get this.validationMessages this.attr.name}} @paddingTop={{true}} />
{{/if}}
{{/if}}
</div>
{{else if (eq this.attr.type "boolean")}}
<div class="b-checkbox">
<input
disabled={{this.disabled}}
type="checkbox"
id={{this.attr.name}}
class="styled"
checked={{get this.model this.attr.name}}
onchange={{action (action "setAndBroadcast" this.valuePath) value="target.checked"}}
data-test-input={{this.attr.name}}
/>
<label for={{this.attr.name}} class="is-label">
{{this.labelString}}
{{#if (and this.showHelpText this.attr.options.helpText)}}
<InfoTooltip>{{this.attr.options.helpText}}</InfoTooltip>
{{/if}}
</label>
{{#if this.attr.options.subText}}
<p class="sub-text">{{this.attr.options.subText}}</p>
{{/if}}
</div>
{{else if (eq this.attr.type "object")}}
<JsonEditor
@title={{this.labelString}}
@value={{if (get this.model this.valuePath) (stringify (get this.model this.valuePath)) this.emptyData}}
@valueUpdated={{action "codemirrorUpdated" this.attr.name false}}
@helpText={{this.attr.options.helpText}}
/>
{{/if}}
\ No newline at end of file
<!--THIS FILE IS AUTO GENERATED. This file is generated from JSDoc comments in app/components/form-field-groups.js. To make changes, first edit that file and run "yarn gen-story-md form-field-groups" to re-generate the content.--> <!--THIS FILE IS AUTO GENERATED. This file is generated from JSDoc comments in lib/core/addon/components/form-field-groups.js. To make changes, first edit that file and run "yarn gen-story-md form-field-groups" to re-generate the content.-->
## FormFieldGroups ## onKeyUpCallback : <code>function</code>
`FormFieldGroups` components are field groups associated with a particular model. They render individual `FormField` components.
**Params**
| Param | Type | Default | Description | | Param | Type | Description |
| --- | --- | --- | --- | | --- | --- | --- |
| [renderGroup] | <code>String</code> | <code></code> | An allow list of groups to include in the render. | | model- | <code>Model</code> | Model to be passed down to form-field component. If `fieldGroups` is present on the model then it will be iterated over and groups of `FormField` components will be rendered. |
| model | <code>DS.Model</code> | <code></code> | Model to be passed down to form-field component. If `fieldGroups` is present on the model then it will be iterated over and groups of `FormField` components will be rendered. | | [renderGroup] | <code>string</code> | An allow list of groups to include in the render. |
| onChange | <code>Func</code> | <code></code> | Handler that will get set on the `FormField` component. | | [onChange] | <code>onChangeCallback</code> | Handler that will get set on the `FormField` component. |
| [onKeyUp] | [<code>onKeyUpCallback</code>](#onKeyUpCallback) | Handler that will set the value and trigger validation on input changes |
| [modelValidations] | <code>ModelValidations</code> | Object containing validation message for each property |
**Example** **Example**
...@@ -20,15 +22,11 @@ ...@@ -20,15 +22,11 @@
... ...
<FormFieldGroups <FormFieldGroups
@model={{mountModel}}
@onChange={{action "onTypeChange"}}
@renderGroup="Method Options"
/>
```
**See** **See**
- [Uses of FormFieldGroups](https://github.com/hashicorp/vault/search?l=Handlebars&q=FormFieldGroups) - [Uses of FormFieldGroups](https://github.com/hashicorp/vault/search?l=Handlebars&q=FormFieldGroups+OR+form-field-groups)
- [FormFieldGroups Source Code](https://github.com/hashicorp/vault/blob/main/ui/app/components/form-field-groups.js) - [FormFieldGroups Source Code](https://github.com/hashicorp/vault/blob/main/ui/lib/core/addon/components/form-field-groups.js)
--- ---
<!--THIS FILE IS AUTO GENERATED. This file is generated from JSDoc comments in app/components/form-field.js. To make changes, first edit that file and run "yarn gen-story-md form-field" to re-generate the content.--> <!--THIS FILE IS AUTO GENERATED. This file is generated from JSDoc comments in lib/core/addon/components/form-field.js. To make changes, first edit that file and run "yarn gen-story-md form-field" to re-generate the content.-->
## FormField ## onKeyUpCallback : <code>function</code>
`FormField` components are field elements associated with a particular model.
**Params**
| Param | Type | Default | Description | | Param | Type | Default | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| [onChange] | <code>Func</code> | <code></code> | Called whenever a value on the model changes via the component. | | attr | <code>Object</code> | | usually derived from ember model `attributes` lookup, and all members of `attr.options` are optional |
| attr | <code>Object</code> | <code></code> | This is usually derived from ember model `attributes` lookup, and all members of `attr.options` are optional. | | model | <code>Model</code> | | Ember Data model that `attr` is defined on |
| model | <code>DS.Model</code> | <code></code> | The Ember Data model that `attr` is defined on | | [disabled] | <code>boolean</code> | <code>false</code> | whether the field is disabled |
| [showHelpText] | <code>boolean</code> | <code>true</code> | whether to show the tooltip with help text from OpenAPI |
### Example Attr | [subText] | <code>string</code> | | text to be displayed below the label |
| [mode] | <code>string</code> | | used when editType is 'kv' |
```js | [modelValidations] | <code>ModelValidations</code> | | Object of errors. If attr.name is in object and has error message display in AlertInline. |
{ | [onChange] | <code>onChangeCallback</code> | | called whenever a value on the model changes via the component |
name: "foo", | [onKeyUp] | [<code>onKeyUpCallback</code>](#onKeyUpCallback) | | function passed through into MaskedInput to handle validation. It is also handled for certain form-field types here in the action handleKeyUp. |
options: {
label: "Foo",
defaultValue: "",
editType: "ttl",
helpText: "This will be in a tooltip"
},
type: "boolean"
}
```
**Example** **Example**
```js ```js
{{#each @model.fields as |attr|}} {{#each @model.fields as |attr|}}
<FormField data-test-field @attr={{attr}} @model={{this.model}} /> <FormField data-test-field @attr={{attr}} @model={{this.model}} />
{{/each}} {{/each}}
``` ```
example attr object:
attr = {
name: "foo", // name of attribute -- used to populate various fields and pull value from model
options: {
label: "Foo", // custom label to be shown, otherwise attr.name will be displayed
defaultValue: "", // default value to display if model value is not present
fieldValue: "bar", // used for value lookup on model over attr.name
editType: "ttl", type of field to use -- example boolean, searchSelect, etc.
helpText: "This will be in a tooltip",
readOnly: true
},
type: "boolean" // type of attribute value -- string, boolean, etc.
}
**See** **See**
- [Uses of FormField](https://github.com/hashicorp/vault/search?l=Handlebars&q=form-field) - [Uses of FormField](https://github.com/hashicorp/vault/search?l=Handlebars&q=FormField+OR+form-field)
- [FormField Source Code](https://github.com/hashicorp/vault/blob/main/ui/app/components/form-field.js) - [FormField Source Code](https://github.com/hashicorp/vault/blob/main/ui/lib/core/addon/components/form-field.js)
--- ---
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment