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 %> + + + + + +<%= 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