diff --git a/playbook/app/pb_kits/playbook/pb_form/docs/_form_form_with.html.erb b/playbook/app/pb_kits/playbook/pb_form/docs/_form_form_with.html.erb
index 2e81609bcc..4f94d1c4a1 100644
--- a/playbook/app/pb_kits/playbook/pb_form/docs/_form_form_with.html.erb
+++ b/playbook/app/pb_kits/playbook/pb_form/docs/_form_form_with.html.erb
@@ -23,7 +23,7 @@
%>
<%= pb_form_with(scope: :example, url: "", method: :get) do |form| %>
- <%= form.typeahead :example_user, props: { data: { typeahead_example1: true, user: {} }, placeholder: "Search for a user" } %>
+ <%= form.typeahead :example_typeahead, props: { data: { typeahead_example1: true, user: {} }, label: true, placeholder: "Search for a user" } %>
<%= form.text_field :example_text_field, props: { label: true } %>
<%= form.phone_number_field :example_phone_number_field, props: { label: "Example phone field" } %>
<%= form.email_field :example_email_field, props: { label: true } %>
@@ -92,7 +92,7 @@
const selectedUserData = JSON.parse(selectedUserJSON)
// set the input field's value
- event.target.querySelector('input[name=example_user]').value = selectedUserData.login
+ event.target.querySelector('input[name=example_typeahead]').value = selectedUserData.login
// log the selected option's dataset
console.log('The selected user data:')
diff --git a/playbook/app/pb_kits/playbook/pb_form/docs/_form_form_with_loading.html.erb b/playbook/app/pb_kits/playbook/pb_form/docs/_form_form_with_loading.html.erb
index e0702b1303..4b8e99ba54 100644
--- a/playbook/app/pb_kits/playbook/pb_form/docs/_form_form_with_loading.html.erb
+++ b/playbook/app/pb_kits/playbook/pb_form/docs/_form_form_with_loading.html.erb
@@ -1,5 +1,5 @@
<%= pb_form_with(scope: :example, url: "", method: :get, loading: true) do |form| %>
- <%= form.text_field :example_text_field, props: { label: true } %>
+ <%= form.text_field :example_text_field_loading, props: { label: true } %>
<%= form.actions do |action| %>
<%= action.submit %>
diff --git a/playbook/app/pb_kits/playbook/pb_form/docs/_form_form_with_validate.html.erb b/playbook/app/pb_kits/playbook/pb_form/docs/_form_form_with_validate.html.erb
index 254749c364..8ec58d1b19 100644
--- a/playbook/app/pb_kits/playbook/pb_form/docs/_form_form_with_validate.html.erb
+++ b/playbook/app/pb_kits/playbook/pb_form/docs/_form_form_with_validate.html.erb
@@ -22,23 +22,74 @@
%>
<%= pb_form_with(scope: :example, method: :get, url: "", validate: true) do |form| %>
- <%= form.text_field :example_text_field, props: { label: true, required: true } %>
- <%= form.phone_number_field :example_phone_number_field, props: { label: "Example phone field" } %>
- <%= form.email_field :example_email_field, props: { label: true, required: true } %>
- <%= form.number_field :example_number_field, props: { label: true, required: true } %>
- <%= form.search_field :example_project_number, props: { label: true, required: true, validation: { pattern: "[0-9]{2}-[0-9]{5}", message: "Please enter a valid project number (example: 33-12345)." } } %>
- <%= form.password_field :example_password_field, props: { label: true, required: true } %>
- <%= form.url_field :example_url_field, props: { label: true, required: true } %>
- <%= form.text_area :example_text_area, props: { label: true, required: true } %>
- <%= form.dropdown_field :example_dropdown, props: { label: true, options: example_dropdown_options, required: true } %>
- <%= form.select :example_select, [ ["Yes", 1], ["No", 2] ], props: { label: true, blank_selection: "Select One...", required: true } %>
- <%= form.collection_select :example_collection_select, example_collection, :value, :name, props: { label: true, blank_selection: "Select One...", required: true } %>
+ <%= form.typeahead :example_typeahead_validation, props: { data: { typeahead_example2: true, user: {} }, label: true, placeholder: "Search for a user", required: true, validation: { message: "Please select a user." } } %>
+ <%= form.text_field :example_text_field_validation, props: { label: true, required: true } %>
+ <%= form.phone_number_field :example_phone_number_field_validation, props: { label: "Example phone field" } %>
+ <%= form.email_field :example_email_field_validation, props: { label: true, required: true } %>
+ <%= form.number_field :example_number_field_validation, props: { label: true, required: true } %>
+ <%= form.search_field :example_project_number_validation, props: { label: true, required: true, validation: { pattern: "[0-9]{2}-[0-9]{5}", message: "Please enter a valid project number (example: 33-12345)." } } %>
+ <%= form.password_field :example_password_field_validation, props: { label: true, required: true } %>
+ <%= form.url_field :example_url_field_validation, props: { label: true, required: true } %>
+ <%= form.text_area :example_text_area_validation, props: { label: true, required: true } %>
+ <%= form.dropdown_field :example_dropdown_validation, props: { label: true, options: example_dropdown_options, required: true } %>
+ <%= form.select :example_select_validation, [ ["Yes", 1], ["No", 2] ], props: { label: true, blank_selection: "Select One...", required: true } %>
+ <%= form.collection_select :example_collection_select_validation, example_collection, :value, :name, props: { label: true, blank_selection: "Select One...", required: true } %>
<%= form.check_box :example_checkbox, props: { text: "Example Checkbox", label: true, required: true } %>
<%= form.date_picker :example_date_picker_2, props: { label: true, required: true } %>
- <%= form.star_rating_field :example_star_rating, props: { variant: "interactive", label: true, required: true } %>
+ <%= form.star_rating_field :example_star_rating_validation, props: { variant: "interactive", label: true, required: true } %>
<%= form.actions do |action| %>
<%= action.submit %>
<%= action.button props: { type: "reset", text: "Cancel", variant: "secondary" } %>
<% end %>
<% end %>
+
+
+
+ <%= pb_rails("user", props: {
+ name: tag(:slot, name: "name"),
+ orientation: "horizontal",
+ align: "left",
+ avatar_url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGP6zwAAAgcBApocMXEAAAAASUVORK5CYII=",
+ avatar: true
+ }) %>
+
+
+
+<%= javascript_tag defer: "defer" do %>
+ document.addEventListener("pb-typeahead-kit-search", function(event) {
+ if (!event.target.dataset || !event.target.dataset.typeaheadExample2) return
+
+ fetch(`https://api.github.com/search/users?q=${encodeURIComponent(event.detail.searchingFor)}`)
+ .then(response => response.json())
+ .then((result) => {
+ const resultOptionTemplate = document.querySelector("[data-typeahead-example-result-option]")
+
+ event.detail.setResults((result.items || []).map((user) => {
+ const wrapper = resultOptionTemplate.content.cloneNode(true)
+ wrapper.children[0].dataset.user = JSON.stringify(user)
+ wrapper.querySelector('slot[name="name"]').replaceWith(user.login)
+ wrapper.querySelector('img').dataset.src = user.avatar_url
+ return wrapper
+ }))
+ })
+ })
+
+
+ document.addEventListener("pb-typeahead-kit-result-option-selected", function(event) {
+ if (!event.target.dataset.typeaheadExample2) return
+
+ const selectedUserJSON = event.detail.selected.firstElementChild.dataset.user
+ const selectedUserData = JSON.parse(selectedUserJSON)
+
+ // set the input field's value
+ event.target.querySelector('input[name=example_typeahead_validation]').value = selectedUserData.login
+
+ // log the selected option's dataset
+ console.log('The selected user data:')
+ console.dir(selectedUserData)
+
+ // do even more with the data later - TBD
+ event.target.dataset.user = selectedUserJSON
+ })
+<% end %>
\ No newline at end of file
diff --git a/playbook/app/pb_kits/playbook/pb_typeahead/index.ts b/playbook/app/pb_kits/playbook/pb_typeahead/index.ts
index a246f88770..a9e820f6bf 100644
--- a/playbook/app/pb_kits/playbook/pb_typeahead/index.ts
+++ b/playbook/app/pb_kits/playbook/pb_typeahead/index.ts
@@ -4,11 +4,12 @@ import { debounce } from 'lodash'
export default class PbTypeahead extends PbEnhancedElement {
_searchInput: HTMLInputElement
_resultsElement: HTMLElement
- _debouncedSearch: Function
+ _debouncedSearch: () => void
_resultsLoadingIndicator: HTMLElement
_resultOptionTemplate: HTMLElement
_resultsOptionCache: Map>
_searchContext: string
+ _validSelection: boolean
static get selector() {
return '[data-pb-typeahead-kit]'
@@ -19,6 +20,7 @@ export default class PbTypeahead extends PbEnhancedElement {
this.searchInput.addEventListener('focus', () => this.debouncedSearch())
this.searchInput.addEventListener('input', () => this.debouncedSearch())
this.resultsElement.addEventListener('click', (event: MouseEvent) => this.optionSelected(event))
+ this.element.closest('form')?.addEventListener('submit', (event) => this.handleFormSubmission(event))
}
handleKeydown(event: KeyboardEvent) {
@@ -86,6 +88,9 @@ export default class PbTypeahead extends PbEnhancedElement {
const resultOption = (event.target as Element).closest('[data-result-option-item]')
if (!resultOption) return
+ this._validSelection = true
+ this.removeValidationError()
+
this.resultsCacheClear()
this.searchInputClear()
this.clearResults()
@@ -93,6 +98,35 @@ export default class PbTypeahead extends PbEnhancedElement {
this.element.dispatchEvent(new CustomEvent('pb-typeahead-kit-result-option-selected', { bubbles: true, detail: { selected: resultOption, typeahead: this } }))
}
+ removeValidationError() {
+ const inputWrapper = this.searchInput.closest('.text_input_wrapper')
+ if (inputWrapper) {
+ const errorMessage = inputWrapper.querySelector('.pb_body_kit_negative') as HTMLElement
+ if (errorMessage) {
+ errorMessage.style.display = 'none'
+ }
+ this.searchInput.classList.remove('error')
+ }
+ }
+
+ showValidationError() {
+ const inputWrapper = this.searchInput.closest('.text_input_wrapper')
+ if (inputWrapper) {
+ const errorMessage = inputWrapper.querySelector('.pb_body_kit_negative') as HTMLElement
+ if (errorMessage) {
+ errorMessage.style.display = 'block'
+ }
+ this.searchInput.classList.add('error')
+ }
+ }
+
+ handleFormSubmission(event: Event) {
+ if (!this._validSelection) {
+ this.showValidationError()
+ event.preventDefault()
+ }
+ }
+
clearResults() {
this.resultsElement.innerHTML = ''
}
@@ -201,7 +235,7 @@ export default class PbTypeahead extends PbEnhancedElement {
}
toggleResultsLoadingIndicator(visible: boolean) {
- var visibilityProperty = '0'
+ let visibilityProperty = '0'
if (visible) visibilityProperty = '1'
this.resultsLoadingIndicator.style.opacity = visibilityProperty
}
diff --git a/playbook/app/pb_kits/playbook/pb_typeahead/typeahead.html.erb b/playbook/app/pb_kits/playbook/pb_typeahead/typeahead.html.erb
index 71f4622f11..342a900276 100644
--- a/playbook/app/pb_kits/playbook/pb_typeahead/typeahead.html.erb
+++ b/playbook/app/pb_kits/playbook/pb_typeahead/typeahead.html.erb
@@ -17,11 +17,14 @@
<%= pb_rails("text_input", props: {
type: "search",
input_options: object.input_options,
- label: object.label,
name: object.name,
value: object.value,
placeholder: object.placeholder,
margin_bottom: "none",
+ required: object.required,
+ validation: object.validation,
+ label: object.label,
+ id: object.input_options[:id],
}) %>
<%= pb_rails("list", props: { ordered: false, borderless: false, xpadding: true, role: "status", aria: { live: "polite" }, data: { pb_typeahead_kit_results: true } }) do %>
<% end %>
@@ -33,4 +36,4 @@
<% end %>
<% end %>
-<% end %>
+<% end %>
\ No newline at end of file
diff --git a/playbook/app/pb_kits/playbook/pb_typeahead/typeahead.rb b/playbook/app/pb_kits/playbook/pb_typeahead/typeahead.rb
index db547fcb8f..14a08f6d62 100644
--- a/playbook/app/pb_kits/playbook/pb_typeahead/typeahead.rb
+++ b/playbook/app/pb_kits/playbook/pb_typeahead/typeahead.rb
@@ -40,6 +40,10 @@ class Typeahead < Playbook::KitBase
prop :pill_color, type: Playbook::Props::Enum,
values: %w[primary neutral success warning error info data_1 data_2 data_3 data_4 data_5 data_6 data_7 data_8 windows siding roofing doors gutters solar insulation accessories],
default: "primary"
+ prop :required, type: Playbook::Props::Boolean,
+ default: false
+ prop :validation, type: Playbook::Props::HashProp,
+ default: {}
def classname
default_margin_bottom = margin_bottom.present? ? "" : " mb_sm"
diff --git a/playbook/lib/playbook/forms/builder/typeahead_field.rb b/playbook/lib/playbook/forms/builder/typeahead_field.rb
index a5d6c43411..43f341f39e 100644
--- a/playbook/lib/playbook/forms/builder/typeahead_field.rb
+++ b/playbook/lib/playbook/forms/builder/typeahead_field.rb
@@ -5,6 +5,19 @@ module Forms
class Builder
def typeahead(name, _options = {}, _html_options = {}, props: {})
props[:name] = name
+
+ input_id = "#{name}_input"
+ props[:input_options] ||= {}
+ props[:input_options][:id] ||= input_id
+
+ props[:label] = @template.label(@object_name, name, for: props[:input_options][:id]) if props[:label] == true
+
+ if props[:validation]
+ validation_message = props[:validation][:message]
+ props[:input_options][:data] ||= {}
+ props[:input_options][:data][:validation_message] = validation_message
+ end
+
@template.pb_rails("typeahead", props: props)
end
end