Skip to content

Commit

Permalink
feat(ld-input, ld-select): hidden inputs inside form
Browse files Browse the repository at this point in the history
  • Loading branch information
renet committed Oct 28, 2021
1 parent 2b2d98d commit 958937c
Show file tree
Hide file tree
Showing 8 changed files with 400 additions and 48 deletions.
26 changes: 25 additions & 1 deletion src/liquid/components/ld-input/ld-input.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, Element, h, Host, Method, Prop } from '@stencil/core'
import { Component, Element, h, Host, Method, Prop, Watch } from '@stencil/core'
import { cloneAttributes } from '../../utils/cloneAttributes'

/**
Expand Down Expand Up @@ -28,8 +28,12 @@ import { cloneAttributes } from '../../utils/cloneAttributes'
})
export class LdInput implements InnerFocusable {
@Element() el: HTMLInputElement | HTMLTextAreaElement
private hiddenInput?: HTMLInputElement
private input: HTMLInputElement | HTMLTextAreaElement

/** Input tone. Use `'dark'` on white backgrounds. Default is a light tone. */
@Prop() name?: string

/** Input tone. Use `'dark'` on white backgrounds. Default is a light tone. */
@Prop() tone: 'dark'

Expand Down Expand Up @@ -64,7 +68,27 @@ export class LdInput implements InnerFocusable {
}
}

@Watch('value')
updateValue() {
if (this.hiddenInput) {
this.hiddenInput.value = this.value
}
}

componentWillLoad() {
const outerForm = this.el.closest('form')

if (outerForm && this.name) {
this.hiddenInput = document.createElement('input')
this.hiddenInput.type = 'hidden'
this.hiddenInput.name = this.name
if (this.value) {
this.hiddenInput.value = this.value
}

this.el.appendChild(this.hiddenInput)
}

this.el.querySelectorAll('ld-button').forEach((button) => {
if (this.size !== undefined) {
button.setAttribute('size', this.size)
Expand Down
15 changes: 8 additions & 7 deletions src/liquid/components/ld-input/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -939,24 +939,24 @@ The `ld-input` Web Component does not provide any properties or methods for vali
<form id="example-form" novalidate>
<ld-label>
Login*
<ld-input required placeholder="login"></ld-input>
<ld-input name="login" required placeholder="login"></ld-input>
<ld-input-message visible="false">This field is required.</ld-input-message>
</ld-label>
<ld-label>
Password*
<ld-input required placeholder="password" type="password"></ld-input>
<ld-input name="password" required placeholder="password" type="password"></ld-input>
<ld-input-message visible="false">This field is required.</ld-input-message>
</ld-label>
<ld-button>Submit</ld-button>
</form>
<script>
const form = document.querySelector('#example-form')
const username = document.querySelector('#example-form ld-label:first-of-type ld-input')
const usernameErrorMessage = document.querySelector('#example-form ld-label:first-of-type ld-input-message')
const password = document.querySelector('#example-form ld-label:last-of-type ld-input')
const passwordErrorMessage = document.querySelector('#example-form ld-label:last-of-type ld-input-message')
const submitButton = document.querySelector('#example-form ld-button')
function validateInput(ldInput, ldInputMessage) {
value = ldInput.value
function validateInput(ldInput, value, ldInputMessage) {
if (!value) {
ldInput.setAttribute('invalid', 'true')
ldInputMessage.style.visibility = 'inherit'
Expand All @@ -978,10 +978,10 @@ The `ld-input` Web Component does not provide any properties or methods for vali
password.addEventListener('blur', ev => {
validateInput(password, passwordErrorMessage)
})
submitButton.addEventListener('click', ev => {
form.addEventListener('submit', ev => {
ev.preventDefault()
const isUsernameValid = validateInput(username, usernameErrorMessage)
const isPasswordValid = validateInput(password, passwordErrorMessage)
const isUsernameValid = validateInput(username, form.login.value, usernameErrorMessage)
const isPasswordValid = validateInput(password, form.password.value, passwordErrorMessage)
setTimeout(() => {
if (isUsernameValid && isPasswordValid) {
window.alert('Form submitted.')
Expand All @@ -1004,6 +1004,7 @@ The `ld-input` Web Component does not provide any properties or methods for vali
| `invalid` | `invalid` | Set this property to `true` in order to mark the field visually as invalid. | `boolean` | `undefined` |
| `key` | `key` | for tracking the node's identity when working with lists | `string \| number` | `undefined` |
| `multiline` | `multiline` | Uses textarea instead of input internally. Setting this attribute to true disables the attribute type and both slots. | `boolean` | `undefined` |
| `name` | `name` | Input tone. Use `'dark'` on white backgrounds. Default is a light tone. | `string` | `undefined` |
| `placeholder` | `placeholder` | The input placeholder. | `string` | `undefined` |
| `ref` | `ref` | reference to component | `any` | `undefined` |
| `size` | `size` | Size of the input. | `"lg" \| "sm"` | `undefined` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,20 @@ exports[`ld-input removes size from ld-icon web component 1`] = `
</mock:shadow-root>
<ld-icon name="placeholder" slot="start"></ld-icon>
<ld-icon name="placeholder" slot="end"></ld-icon>
exports[`ld-input creates hidden input field, if inside a form 1`] = `
<ld-input class="ld-input" name="example">
<input name="example" type="hidden">
</ld-input>
`;
exports[`ld-input fills hidden input field with initial value 1`] = `
<ld-input class="ld-input" name="example" value="hello">
<mock:shadow-root>
<slot name="start"></slot>
<input part="input focusable" value="hello">
<slot name="end"></slot>
</mock:shadow-root>
<input name="example" type="hidden" value="hello">
</ld-input>
`;
Expand Down Expand Up @@ -203,6 +217,17 @@ exports[`ld-input sets size on ld-icon web component 1`] = `
</ld-input>
`;
exports[`ld-input updates hidden input field 1`] = `
<ld-input class="ld-input" name="example" value="test">
<mock:shadow-root>
<slot name="start"></slot>
<input part="input focusable" value="test">
<slot name="end"></slot>
</mock:shadow-root>
<input name="example" type="hidden" value="test">
</ld-input>
`;
exports[`ld-input updates value prop on value change 1`] = `
<ld-input class="ld-input" value="yoda-yoda">
<mock:shadow-root>
Expand Down
30 changes: 30 additions & 0 deletions src/liquid/components/ld-input/test/ld-input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,4 +275,34 @@ describe('ld-input', () => {
})
expect(page.root).toMatchSnapshot()
})

it('creates hidden input field, if inside a form', async () => {
const { root } = await newSpecPage({
components: [LdInput],
html: `<form><ld-input name="example" /></form>`,
})
expect(root).toMatchSnapshot()
})

it('fills hidden input field with initial value', async () => {
const { root } = await newSpecPage({
components: [LdInput],
html: `<form><ld-input name="example" value="hello" /></form>`,
})
expect(root).toMatchSnapshot()
})

it('updates hidden input field', async () => {
const { root, waitForChanges } = await newSpecPage({
components: [LdInput],
html: `<form><ld-input name="example" /></form>`,
})
const input = root.shadowRoot.querySelector('input')

input.value = 'test'
input.dispatchEvent(new Event('input'))
await waitForChanges()

expect(root).toMatchSnapshot()
})
})
106 changes: 71 additions & 35 deletions src/liquid/components/ld-select/ld-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { LdSelectPopper } from './ld-select-popper/ld-select-popper'
import { LdOptionInternal } from './ld-option-internal/ld-option-internal'
import { applyPropAliases } from '../../utils/applyPropAliases'

type SelectOption = { value: string; text: string }

/**
* @slot - the default slot contains the select options
* @slot icon - replaces caret with custom trigger button icon
Expand Down Expand Up @@ -92,35 +94,24 @@ export class LdSelect {
@Prop({ mutable: true }) tetherOptions = '{}'

@State() initialized = false

@State() expanded = false

@State() selected: { value: string; text: string }[] = []

@State() selected: SelectOption[] = []
@State() theme: string

@State() ariaDisabled = false

@State() typeAheadQuery: string

@State() typeAheadTimeout: number

@State() internalOptionsHTML: string

@State() hasMore = false

@State() hasCustomIcon = false
@State() renderHiddenInput = false

@Watch('selected')
emitEvents(
newSelection: { value: string; text: string }[],
oldSelection: { value: string; text: string }[]
) {
emitEvents(newSelection: SelectOption[], oldSelection: SelectOption[]) {
if (!this.initialized) return

const newValues = newSelection.map((option) => option.value)
const oldValues = oldSelection.map((option) => option.value)
if (JSON.stringify(newValues) === JSON.stringify(oldValues)) return
if (newValues.join() === oldValues.join()) return

this.updateTriggerMoreIndicator(true)

Expand Down Expand Up @@ -301,8 +292,8 @@ export class LdSelect {
this.theme = themeEl.classList
.toString()
.split(' ')
.find((cl) => cl.indexOf('ld-theme-') === 0)
?.split('ld-theme-')[1]
.find((cl) => cl.startsWith('ld-theme-'))
?.substr(9)
})
}

Expand Down Expand Up @@ -353,7 +344,17 @@ export class LdSelect {
)
}

const childrenArr = Array.from(children) as HTMLElement[]
const selectedChildren = Array.from<HTMLElement>(children).filter(
(child) => {
return ((child as unknown) as LdOptionInternal).selected
}
)

if (selectedChildren.length > 1 && !this.multiple) {
throw new TypeError(
'Multiple selected options are not allowed, if multiple option is not set.'
)
}

setTimeout(() => {
if (!initialized) {
Expand All @@ -369,18 +370,55 @@ export class LdSelect {
''
)
}
this.selected = childrenArr
.filter((child) => {
return ((child as unknown) as LdOptionInternal).selected
})
.map((child) => ({
value: child.getAttribute('value'),
text: child.innerText,
}))
this.selected = selectedChildren.map((child) => ({
value: child.getAttribute('value'),
text: child.innerText,
}))

if (this.renderHiddenInput) {
this.updateHiddenInput(this.selected)
}

this.updateTriggerMoreIndicator(true)
})
}

private updateHiddenInput = (selected: SelectOption[]) => {
const selectedValues = selected.map(({ value }) => value)
const inputs = this.el.querySelectorAll('input')

inputs.forEach((hiddenInput) => {
const index = selectedValues.indexOf(hiddenInput.value)
if (index >= 0) {
selectedValues.splice(index, 1)
} else {
hiddenInput.remove()
}
})

if (selected.length === 0) {
this.appendHiddenInput()
return
}

selectedValues.forEach(this.appendHiddenInput)
}

private appendHiddenInput = (value?: string) => {
const hiddenInput = document.createElement('input')

// Slot required to keep the hidden input outside the popper
hiddenInput.setAttribute('slot', 'hidden')
hiddenInput.name = this.name
hiddenInput.type = 'hidden'

if (value !== undefined) {
hiddenInput.value = value
}

this.el.appendChild(hiddenInput)
}

private handleSlotChange() {
this.initialized = false

Expand Down Expand Up @@ -782,6 +820,12 @@ export class LdSelect {
}

componentWillLoad() {
const outerForm = this.el.closest('form')

if (outerForm && this.name) {
this.renderHiddenInput = true
}

applyPropAliases.apply(this)
const customIcon = this.el.querySelector('ld-icon')
this.hasCustomIcon = !!customIcon
Expand Down Expand Up @@ -862,15 +906,7 @@ export class LdSelect {
: undefined
}
>
{this.name
? this.selected.map((selection) => (
<input
type="hidden"
name={this.name}
value={selection.value}
></input>
))
: ''}
{this.renderHiddenInput && <slot name="hidden" />}
<div
ref={(el) => (this.slotContainerRef = el as HTMLElement)}
class="ld-select__slot-container"
Expand Down
8 changes: 3 additions & 5 deletions src/liquid/components/ld-select/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -1185,13 +1185,13 @@ The `ld-select` Web Component provides a low level API for integrating it with t
<ld-button>Submit</ld-button>
</form>
<script>
const form = document.querySelector('#example-form')
const select = document.querySelector('#example-form ld-select')
const errorMessage = document.querySelector('#example-form ld-input-message')
const submitButton = document.querySelector('#example-form ld-button')
let selected = []
let selectDirty = false
function validateInput() {
if (selectDirty && selected.length < 3) {
if (selectDirty && (!form.fruits. || form.fruits.length < 3)) {
select.setAttribute('invalid', 'true')
errorMessage.style.visibility = 'inherit'
return false
Expand All @@ -1201,15 +1201,13 @@ The `ld-select` Web Component provides a low level API for integrating it with t
return true
}
select.addEventListener('change', ev => {
selected = ev.detail
validateInput()
})
select.addEventListener('blur', ev => {
selected = ev.detail
selectDirty = true
validateInput()
})
submitButton.addEventListener('click', ev => {
form.addEventListener('submit', ev => {
ev.preventDefault()
selectDirty = true
const isValid = validateInput()
Expand Down
Loading

0 comments on commit 958937c

Please sign in to comment.