<%= pb_rails("passphrase", props: { classname: "pass_input_1" }) %> <%= pb_rails("passphrase", props: { confirmation: true, classname: "pass_input_2"}) %> <div id="match"> </div> <script> window.addEventListener("load", () => { const useState = (defaultValue) => { let value = defaultValue; const getValue = () => value const setValue = (newValue) => { return value = newValue } return [getValue, setValue]; } const [input, setInput] = useState('') const [confirmationInput, setConfirmationInput] = useState('') const match = document.querySelector("#match") const input1 = document.querySelector(".pass_input_1").querySelector("input") const input2 = document.querySelector(".pass_input_2").querySelector("input") input1.addEventListener('input', (e) => { setInput(e.target.value) setMatchText() }); input2.addEventListener('input', (e) => { setConfirmationInput(e.target.value) setMatchText() }); const setMatchText = () => { if (input() && confirmationInput()) { if (input() === confirmationInput()) { match.textContent = "They match!" } else { match.textContent = "They don't match!" } } else { match.textContent = "" } } }) </script>
This example shows how to enhance the passphrase strenght by setting diferent thresholds and lengths.
The meterSettings array contains different settings for each rendered input. The handleStrengthCalculation handles the strength calculation using those settings, showing different results for the same passphrase input.
By default, minLength is 12. Try typing any value in the Default Example input. Notice that the bar won't change from red until the minimum is met.
Adjust these props to tune the sensitivity of the bar.
Note: minimum length trumps strength and will set the bar to a red color, despite whatever strength is calculated.
This example depends on the zxcvbn library.
You can use any library to achieve the same result, this example only intends to show how to add more features to the Passphrase kit.
<%= pb_rails("passphrase", props: { label: "Default Settings", classname: "def_passphrase" }) %> <%= pb_rails("progress_simple", props: { percent: 0, id: "def_bar" }) %> <%= pb_rails("caption", props: { size: 'xs', text: "hello", id: "def_caption" }) %> <%= pb_rails("text_input", props: { label: "Calculated Strength", value: "0", disabled: true, id: "calc_strength" }) %> <%= pb_rails("passphrase", props: { label: "Min length = 5", classname: "min_5" }) %> <%= pb_rails("progress_simple", props: { percent: 0, id: "min_5_bar" }) %> <%= pb_rails("caption", props: { size: 'xs', text: "hello", id: "min_5_caption" }) %> <%= pb_rails("passphrase", props: { label: "Min length = 30", classname: "min_30" }) %> <%= pb_rails("progress_simple", props: { percent: 0, id: "min_30_bar" }) %> <%= pb_rails("caption", props: { size: 'xs', text: "hello", id: "min_30_caption" }) %> <%= pb_rails("passphrase", props: { label: "Average Threshold = 1", classname: "avg_1" }) %> <%= pb_rails("progress_simple", props: { percent: 0, id: "avg_1_bar" }) %> <%= pb_rails("caption", props: { size: 'xs', text: "hello", id: "avg_1_caption" }) %> <%= pb_rails("passphrase", props: { label: "Strong Threshold = 4", classname: "strong_4" }) %> <%= pb_rails("progress_simple", props: { percent: 0, id: "strong_4_bar" }) %> <%= pb_rails("caption", props: { size: 'xs', text: "hello", id: "strong_4_caption" }) %> <script> window.addEventListener("load", () => { // variables for the passphrase kits you are targeting const defPassphrase = document.querySelector(".def_passphrase").querySelector("input") const min5 = document.querySelector(".min_5").querySelector("input") const min30 = document.querySelector(".min_30").querySelector("input") const avg1 = document.querySelector(".avg_1").querySelector("input") const strong4 = document.querySelector(".strong_4").querySelector("input") // variable for the text_input kit you are targeting const calcStrength = document.querySelector("#calc_strength") // variables for the progress_simple kits you are targeting const defBarVariant = document.getElementById("def_bar") const defBarPercent = document.getElementById("def_bar").querySelector("div") const min5BarVariant = document.getElementById("min_5_bar") const min5BarPercent = document.getElementById("min_5_bar").querySelector("div") const min30BarVariant = document.getElementById("min_30_bar") const min30BarPercent = document.getElementById("min_30_bar").querySelector("div") const avg1BarVariant = document.getElementById("avg_1_bar") const avg1BarPercent = document.getElementById("avg_1_bar").querySelector("div") const strong4BarVariant = document.getElementById("strong_4_bar") const strong4BarPercent = document.getElementById("strong_4_bar").querySelector("div") // hide all the progress_simple bars defBarVariant.style.display = 'none'; defBarPercent.style.display = 'none'; min5BarVariant.style.display = 'none'; min5BarPercent.style.display = 'none'; min30BarVariant.style.display = 'none'; min30BarPercent.style.display = 'none'; avg1BarVariant.style.display = 'none'; avg1BarPercent.style.display = 'none'; strong4BarVariant.style.display = 'none'; strong4BarPercent.style.display = 'none'; // variables for the caption kits you are targeting const defCaption = document.getElementById("def_caption") const min5Caption = document.getElementById("min_5_caption") const min30Caption = document.getElementById("min_30_caption") const avg1Caption = document.getElementById("avg_1_caption") const strong4Caption = document.getElementById("strong_4_caption") // hide all the captions defCaption.style.display = 'none'; min5Caption.style.display = 'none'; min30Caption.style.display = 'none'; avg1Caption.style.display = 'none'; strong4Caption.style.display = 'none'; // funtion that determines strenght of user passowrd using zxcvbn const handleStrengthCalculation = (settings) => { // define the settings object with its defaults const { passphrase = "", common = false, isPwned = false, averageThreshold = 2, minLength = 12, strongThreshold = 3, } = settings // define the resultsByScore objects, these return an object with a variant and percentage, // depending on the score of the password const resultByScore = { 0: { variant: 'negative', label: '', percent: 0, }, 1: { variant: 'negative', label: 'This passphrase is too common', percent: 25, }, 2: { variant: 'negative', label: 'Too weak', percent: 25, }, 3: { variant: 'warning', label: 'Almost there, keep going!', percent: 50, }, 4: { variant: 'positive', label: 'Success! Strong passphrase', percent: 100, } } const { score } = zxcvbn(passphrase); const noPassphrase = passphrase.length <= 0 const commonPassphrase = common || isPwned const weakPassphrase = passphrase.length < minLength || score < averageThreshold const averagePassphrase = score < strongThreshold const strongPassphrase = score >= strongThreshold // conditional that returns the score of the password, along with the resultByScore object // so we can change the percantage and variant of the progress_simple kit if (noPassphrase) { return {...resultByScore[0], score} } else if (commonPassphrase) { return {...resultByScore[1], score} } else if (weakPassphrase) { return {...resultByScore[2], score} } else if (averagePassphrase){ return {...resultByScore[3], score} } else if (strongPassphrase) { return {...resultByScore[4], score} } } // event listeners attached to the input field min5.addEventListener('input', (e) => { const passphrase = e.target.value; // defining setting object to spread into handleStrengthCalculation setting = { minLength: 5, } // pass in passphrase and setting object to handleStrengthCalculation and set that equal to result variable const result = handleStrengthCalculation({passphrase, ...setting}) // set the value of the text_input to the score calcStrength.value = result.score // conditional statment to show or hide progress_simple bar and caption if user has entered a password if (passphrase) { min5BarVariant.style.display = 'block'; min5BarPercent.style.display = 'block'; min5Caption.style.display = 'block'; } else { min5BarVariant.style.display = 'none'; min5BarPercent.style.display = 'none'; min5Caption.style.display = 'none'; } // set the width of the progress_simple kit min5BarPercent.style.width = result.percent.toString()+ "%" // set the variant of the progress_simple kit min5BarVariant.setAttribute("class", "pb_progress_simple_kit_"+ result.variant +"_left"); // set the text of the caption kit min5Caption.textContent = result.label }); defPassphrase.addEventListener('input', (e) => { const passphrase = e.target.value; const result = handleStrengthCalculation({passphrase}) calcStrength.value = result.score if (passphrase) { defBarVariant.style.display = 'block'; defBarPercent.style.display = 'block'; defCaption.style.display = 'block'; } else { defBarVariant.style.display = 'none'; defBarPercent.style.display = 'none'; defCaption.style.display = 'none'; } defBarPercent.style.width = result.percent.toString()+ "%" defBarVariant.setAttribute("class", "pb_progress_simple_kit_"+ result.variant +"_left"); defCaption.textContent = result.label }); min30.addEventListener('input', (e) => { const passphrase = e.target.value; setting = { minLength: 30, } const result = handleStrengthCalculation({passphrase, ...setting}) calcStrength.value = result.score if (passphrase) { min30BarVariant.style.display = 'block'; min30BarPercent.style.display = 'block'; min30Caption.style.display = 'block'; } else { min30BarVariant.style.display = 'none'; min30BarPercent.style.display = 'none'; min30Caption.style.display = 'none'; } min30BarPercent.style.width = result.percent.toString()+ "%" min30BarVariant.setAttribute("class", "pb_progress_simple_kit_"+ result.variant +"_left"); min30Caption.textContent = result.label }); avg1.addEventListener('input', (e) => { const passphrase = e.target.value; setting = { averageThreshold: 1, } const result = handleStrengthCalculation({passphrase, ...setting}) calcStrength.value = result.score if (passphrase) { avg1BarVariant.style.display = 'block'; avg1BarPercent.style.display = 'block'; avg1Caption.style.display = 'block'; } else { avg1BarVariant.style.display = 'none'; avg1BarPercent.style.display = 'none'; avg1Caption.style.display = 'none'; } avg1BarPercent.style.width = result.percent.toString()+ "%" avg1BarVariant.setAttribute("class", "pb_progress_simple_kit_"+ result.variant +"_left"); avg1Caption.textContent = result.label }); strong4.addEventListener('input', (e) => { const passphrase = e.target.value; setting = { strongThreshold: 4, } const result = handleStrengthCalculation({passphrase, ...setting}) calcStrength.value = result.score if (passphrase) { strong4BarVariant.style.display = 'block'; strong4BarPercent.style.display = 'block'; strong4Caption.style.display = 'block'; } else { strong4BarVariant.style.display = 'none'; strong4BarPercent.style.display = 'none'; strong4Caption.style.display = 'none'; } strong4BarPercent.style.width = result.percent.toString()+ "%" strong4BarVariant.setAttribute("class", "pb_progress_simple_kit_"+ result.variant +"_left"); strong4Caption.textContent = result.label }); }) </script>
inputProps is passed directly to an underlying Text Input kit. See the specific docs here for more details.
<%= pb_rails("passphrase", props: { input_props: { disabled: true, id: "my-disabled-passphrase", name: "my-disabled-field", }, label: "Pass props directly to input kit" }) %> <%= pb_rails("passphrase", props: { input_props: { id: "my-custome-id", name: "my-value-name", }, label: "Set name and ID for use in form libraries" }) %>
showTipsBelow(react) / show_tips_below(rails) takes 'xs', 'sm', 'md', 'lg', 'xl' and only show the tips below the given screen size. Similar to the responsive table breakpoints. Omit this prop to always show.
<%= pb_rails("passphrase", props: { label: "Pass an array of strings to the tips prop", tips: ['And the info icon will appear.', 'Each string will be displayed as its own tip'], }) %> <%= pb_rails("passphrase", props: { label: "Omit the prop to hide the icon" }) %> <%= pb_rails("passphrase", props: { label: "Only show tips at small screen size", show_tips_below: "sm", tips: ['Make the password longer', 'Type more things', 'Use something else'], }) %> <%= pb_rails("passphrase", props: { label: "Only show tips at medium screen size", show_tips_below: "md", tips: ['Make the password longer', 'Type more things', 'Use something else'], }) %> <%= pb_rails("passphrase", props: { label: "Only show tips at large screen size", show_tips_below: "lg", tips: ['Make the password longer', 'Type more things', 'Use something else'], }) %>
Strength is calculated on a 0-4 scale by the Zxcvbn package.
This example depends on the zxcvbn library.
You can use any library to achieve the same result, this example only intends to show how to add more features to the Passphrase kit.
<%= pb_rails("passphrase", props: { label: "Passphrase", classname: "passphrase_change" }) %> <%= pb_rails("progress_simple", props: { percent: 0, id: "bar_change" }) %> <%= pb_rails("caption", props: { size: 'xs', text: "hello", id: "caption_change" }) %> <%= pb_rails("text_input", props: { label: "Passphrase Strength", value: "0", disabled: true, id: "calc_strength_change" }) %> <script> window.addEventListener("load", () => { // variables for the kits you are targeting const passphrase = document.querySelector(".passphrase_change").querySelector("input") const calcStrength = document.querySelector("#calc_strength_change") const barVariant = document.getElementById("bar_change") const barPercent = document.getElementById("bar_change").querySelector("div") const caption = document.getElementById("caption_change") // hide the bar and captions barVariant.style.display = 'none'; barPercent.style.display = 'none'; caption.style.display = 'none'; const handleStrengthCalculation = (settings) => { const { passphrase = "", common = false, isPwned = false, averageThreshold = 2, minLength = 12, strongThreshold = 3, } = settings const resultByScore = { 0: { variant: 'negative', label: '', percent: 0, }, 1: { variant: 'negative', label: 'This passphrase is too common', percent: 25, }, 2: { variant: 'negative', label: 'Too weak', percent: 25, }, 3: { variant: 'warning', label: 'Almost there, keep going!', percent: 50, }, 4: { variant: 'positive', label: 'Success! Strong passphrase', percent: 100, } } const { score } = zxcvbn(passphrase); const noPassphrase = passphrase.length <= 0 const commonPassphrase = common || isPwned const weakPassphrase = passphrase.length < minLength || score < averageThreshold const averagePassphrase = score < strongThreshold const strongPassphrase = score >= strongThreshold if (noPassphrase) { return {...resultByScore[0], score} } else if (commonPassphrase) { return {...resultByScore[1], score} } else if (weakPassphrase) { return {...resultByScore[2], score} } else if (averagePassphrase){ return {...resultByScore[3], score} } else if (strongPassphrase) { return {...resultByScore[4], score} } } // event listeners attached to the input field passphrase.addEventListener('input', (e) => { const passphrase = e.target.value; // pass in passphrase to the handleStrengthCalculation and set that equal to result variable const result = handleStrengthCalculation({passphrase: passphrase}) // set the value of the text_input to the score calcStrength.value = result.score // conditional statment to show or hide progress_simple bar and caption if user has entered a password if (passphrase) { barVariant.style.display = 'block'; barPercent.style.display = 'block'; caption.style.display = 'block'; } else { barVariant.style.display = 'none'; barPercent.style.display = 'none'; caption.style.display = 'none'; } // set the width of the progress_simple kit barPercent.style.width = result.percent.toString()+ "%" // set the variant of the progress_simple kit barVariant.setAttribute("class", "pb_progress_simple_kit_"+ result.variant +"_left"); // set the text of the caption kit caption.textContent = result.label }); }) </script>
This example depends on the zxcvbn library.
You can use any library to achieve the same result, this example only intends to show how to add more features to the Passphrase kit.
<%= pb_rails("body", props: { margin_bottom: "md", id: "body_common" }) %> <%= pb_rails("passphrase", props: { label: "Passphrase", classname: "passphrase_common" }) %> <%= pb_rails("progress_simple", props: { percent: 0, id: "bar_common" }) %> <%= pb_rails("caption", props: { size: 'xs', text: "hello", id: "caption_common" }) %> <script> window.addEventListener("load", () => { const commonText = document.querySelector("#body_common") // variables for the kits you are targeting const passphrase = document.querySelector(".passphrase_common").querySelector("input") const barVariant = document.getElementById("bar_common") const barPercent = document.getElementById("bar_common").querySelector("div") const caption = document.getElementById("caption_common") // hide the bar and captions barVariant.style.display = 'none'; barPercent.style.display = 'none'; caption.style.display = 'none'; const handleStrengthCalculation = (settings) => { const { passphrase = "", common = false, isPwned = false, averageThreshold = 2, minLength = 12, strongThreshold = 3, } = settings const resultByScore = { 0: { variant: 'negative', label: '', percent: 0, }, 1: { variant: 'negative', label: 'This passphrase is too common', percent: 25, }, 2: { variant: 'negative', label: 'Too weak', percent: 25, }, 3: { variant: 'warning', label: 'Almost there, keep going!', percent: 50, }, 4: { variant: 'positive', label: 'Success! Strong passphrase', percent: 100, } } const { score } = zxcvbn(passphrase); const noPassphrase = passphrase.length <= 0 const commonPassphrase = common || isPwned const weakPassphrase = passphrase.length < minLength || score < averageThreshold const averagePassphrase = score < strongThreshold const strongPassphrase = score >= strongThreshold if (noPassphrase) { return {...resultByScore[0], score} } else if (commonPassphrase) { return {...resultByScore[1], score} } else if (weakPassphrase) { return {...resultByScore[2], score} } else if (averagePassphrase){ return {...resultByScore[3], score} } else if (strongPassphrase) { return {...resultByScore[4], score} } } // array that holds the common passwords you wish to target const COMMON_PASSPHRASES = ['passphrase', 'apple', 'password', 'p@55w0rd'] commonText.textContent = `Try typing any of the following: ${COMMON_PASSPHRASES.join(', ')}` // function that checks if the user password is in the common password list const isCommon = (passphrase) => { if (COMMON_PASSPHRASES.includes(passphrase)) return true return false } // event listeners attached to the input field passphrase.addEventListener('input', (e) => { const passphrase = e.target.value; // pass in passphrase to the handleStrengthCalculation and set that equal to result variable const result = handleStrengthCalculation({ passphrase: passphrase, common: isCommon(passphrase) }) // conditional statment to show or hide progress_simple bar and caption if user has entered a password if (passphrase) { barVariant.style.display = 'block'; barPercent.style.display = 'block'; caption.style.display = 'block'; } else { barVariant.style.display = 'none'; barPercent.style.display = 'none'; caption.style.display = 'none'; } // set the width of the progress_simple kit barPercent.style.width = result.percent.toString()+ "%" // set the variant of the progress_simple kit barVariant.setAttribute("class", "pb_progress_simple_kit_"+ result.variant +"_left"); // set the text of the caption kit caption.textContent = result.label }); }) </script>
Use HaveIBeenPwned's API to check for breached passwords.
As the passphrase is typed, it is checked against more than half a billion breached passwords, to help ensure its not compromised.
Should it fail, the feedback will express the passphrase is too common, prompting the user to change.
This uses their k-Anonymity model, so only the first 5 characters of a hashed copy of the passphrase are sent.
This example depends on the zxcvbn library and haveibeenpwned API.
You can use any library to achieve the same result, this example only intends to show how to add more features to the Passphrase kit.
<%= pb_rails("passphrase", props: { label: "Passphrase", classname: "passphrase_breached" }) %> <%= pb_rails("progress_simple", props: { percent: 0, id: "bar_breached" }) %> <%= pb_rails("caption", props: { size: 'xs', text: "hello", id: "caption_breached" }) %> <script> window.addEventListener("load", () => { // variables for the kits you are targeting const passphrase = document.querySelector(".passphrase_breached").querySelector("input") const barVariant = document.getElementById("bar_breached") const barPercent = document.getElementById("bar_breached").querySelector("div") const caption = document.getElementById("caption_breached") // hide the bar and captions barVariant.style.display = 'none'; barPercent.style.display = 'none'; caption.style.display = 'none'; const handleStrengthCalculation = (settings) => { const { passphrase = "", common = false, isPwned = false, averageThreshold = 2, minLength = 12, strongThreshold = 3, } = settings const resultByScore = { 0: { variant: 'negative', label: '', percent: 0, }, 1: { variant: 'negative', label: 'This passphrase is too common', percent: 25, }, 2: { variant: 'negative', label: 'Too weak', percent: 25, }, 3: { variant: 'warning', label: 'Almost there, keep going!', percent: 50, }, 4: { variant: 'positive', label: 'Success! Strong passphrase', percent: 100, } } const { score } = zxcvbn(passphrase); const noPassphrase = passphrase.length <= 0 const commonPassphrase = common || isPwned const weakPassphrase = passphrase.length < minLength || score < averageThreshold const averagePassphrase = score < strongThreshold const strongPassphrase = score >= strongThreshold if (noPassphrase) { return {...resultByScore[0], score} } else if (commonPassphrase) { return {...resultByScore[1], score} } else if (weakPassphrase) { return {...resultByScore[2], score} } else if (averagePassphrase){ return {...resultByScore[3], score} } else if (strongPassphrase) { return {...resultByScore[4], score} } } // event listeners attached to the input field passphrase.addEventListener('input', (e) => { const passphrase = e.target.value; let pwndMatch = false const checkHaveIBeenPwned = async function (passphrase) { const buffer = new TextEncoder('utf-8').encode(passphrase) const digest = await crypto.subtle.digest('SHA-1', buffer) const hashArray = Array.from(new Uint8Array(digest)) const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('') const firstFive = hashHex.slice(0, 5) const endOfHash = hashHex.slice(5) const resp = await fetch(`https://api.pwnedpasswords.com/range/${firstFive}`) const text = await resp.text() if (passphrase.length < 5) { pwndMatch = false } else { pwndMatch = text.split('\n').some((line) => { return line.split(':')[0] === endOfHash.toUpperCase() }) } // pass in passphrase and isPwnd match to the handleStrengthCalculation and set that equal to result variable const result = handleStrengthCalculation({ passphrase: passphrase, isPwned: pwndMatch }); // conditional statment to show or hide progress_simple bar and caption if user has entered a password if (passphrase) { barVariant.style.display = 'block'; barPercent.style.display = 'block'; caption.style.display = 'block'; } else { barVariant.style.display = 'none'; barPercent.style.display = 'none'; caption.style.display = 'none'; } // set the width of the progress_simple kit barPercent.style.width = result.percent.toString()+ "%" // set the variant of the progress_simple kit barVariant.setAttribute("class", "pb_progress_simple_kit_"+ result.variant +"_left"); // set the text of the caption kit caption.textContent = result.label } checkHaveIBeenPwned(passphrase) }); }) </script>
NOTE: Error state is handled by default, validating length (too long or too short) relative to the selected country’s phone format and enforcing numeric-only values for all countries.
Preferred countries will display in the order they are listed with the first country preselected, and all non-preferred countries listed alphabetically below in the remaining dropdown.
Setting an initial country preselects that country within the input as well as the country dropdown.
Limiting countries removes all countries from the dropdown except your selections.
Excluding countries removes the selected countries from the dropdown.
<form id="example-form-validation" action="" method="get"> <%= pb_rails("phone_number_input", props: { error: "Missing phone number", id: "validation", initial_country: "af", value: "", required: true }) %> <%= pb_rails("button", props: {html_type: "submit", text: "Save Phone Number"}) %> </form> <%= javascript_tag do %> document.addEventListener('DOMContentLoaded', function () { document.querySelector('#example-form-validation').addEventListener('submit', function (e) { if (e.target.querySelectorAll('[error]:not([error=""])').length > 0) e.preventDefault(); }) }) <% end %>
NOTE: the number in the React onChange event will not include formatting (no spaces, dashes, and parentheses). For Rails, the value will include formatting and its value must be sanitized manually.
<%= pb_rails("phone_number_input", props: { id: "phone_number_input", format_as_you_type: true }) %> <%= pb_rails("button", props: {id: "clickable", text: "Save Phone Number"}) %> <%= javascript_tag do %> document.querySelector('#clickable').addEventListener('click', () => { const formattedPhoneNumber = document.querySelector('#phone_number_input').value const unformattedPhoneNumber = formattedPhoneNumber.replace(/\D/g, "") alert(`Formatted: ${formattedPhoneNumber}. Unformatted: ${unformattedPhoneNumber}`) }) <% end %>
Ignore any irrelevant characters and cap the length at the maximum valid number length.
This can be combined with format_as_you_type / formatAsYouType.
The hidden_inputs boolean prop generates two hidden input fields, {field_name}_full and {field_name}_country_code. The value passed when the form is submitted contains the full phone number including the country code. Because it requires the submission of a form to function, only use this prop on Rails phone number elements within html form tags <form>/<form> or pb_forms. Read the intl-tel-input docs for more information.
Set country_search / countrySearch to true to allow users to search for a specific Country within the dropdown. If the range of countries has been limited, only the selected countries will be searchable.
Add an id to your Text Input so that clicking the label will move focus directly to the input.
<%= pb_rails("text_input", props: { label: "First Name", placeholder: "Enter first name", value: "Timothy Wenhold", data: { say: "hi", yell: "go" }, aria: { something: "hello"}, id: "unique_id" }) %> <%= pb_rails("text_input", props: { label: "Last Name", placeholder: "Enter last name", id: "last-name" }) %> <%= pb_rails("text_input", props: { label: "Phone Number", type: "phone", placeholder: "Enter phone number", id: "phone" }) %> <%= pb_rails("text_input", props: { label: "Email Address", type: "email", placeholder: "Enter email address", id: "email" }) %> <%= pb_rails("text_input", props: { label: "Zip Code", type: "number", placeholder: "Enter zip code", id: "zip" }) %>
Text Input w/ Error shows that the radio option must be selected or is invalid (ie when used in a form it signals a user to fix an error).
<%= pb_rails("text_input", props: { add_on: { alignment: "left", border: true, icon: "user" }, error: raw(pb_rails("icon", props: { icon: "warning" }) + " Please enter a valid email address"), label: "Email Address", placeholder: "Enter email address", type: "email" }) %> <%= pb_rails("text_input", props: { add_on: { alignment: "left", border: true, icon: "user" }, label: "Confirm Email Address", placeholder: "Confirm email address", type: "email" }) %>
<%= pb_rails("text_input", props: { label: "Add On With Defaults", add_on: { icon: "bat" } }) %> <%= pb_rails("text_input", props: { label: "Right-Aligned Add On With Border", add_on: { icon: "user", alignment: 'right', border: true } }) %> <%= pb_rails("text_input", props: { label: "Right-Aligned Add On With No Border", add_on: { icon: "percent", alignment: 'right', border: false } }) %> <%= pb_rails("text_input", props: { label: "Left-Aligned Add On With No Border", add_on: { icon: "percent", alignment: 'left', border: false } }) %> <%= pb_rails("text_input", props: { label: "Left-Aligned Add On With Border", add_on: { icon: "user", alignment: 'left', border: true } }) %>
The mask prop lets you style your inputs while maintaining the value that the user typed in.
It uses a hidden input field to submit the unformatted value as it will have the proper name attribute. It will also copy the id field with a "#{your-id-sanitized}"
<%= pb_rails("text_input", props: { label: "Currency", mask: "currency", margin_bottom: "md", name: "currency_name", placeholder:"$0.00" }) %> <%= pb_rails("text_input", props: { label: "ZIP Code", mask: "zip_code", margin_bottom: "md", placeholder: "12345" }) %> <%= pb_rails("text_input", props: { label: "Postal Code", mask: "postal_code", placeholder: "12345-6789", margin_bottom: "md", }) %> <%= pb_rails("text_input", props: { label: "SSN", mask: "ssn", margin_bottom: "md", placeholder: "123-45-6789" }) %> <%= pb_rails("text_input", props: { label: "Credit Card", mask: "credit_card", margin_bottom: "md", placeholder: "1234 5678 9012 3456" }) %> <%= pb_rails("text_input", props: { label: "CVV", mask: "cvv", margin_bottom: "md", placeholder: "123" }) %> <%= pb_rails("title" , props: { text: "Hidden Input Under The Hood", padding_bottom: "sm" })%> <%= pb_rails("text_input", props: { label: "Currency", mask: "currency", margin_bottom: "md", name: "currency_name", id: "example-currency", value: "$99.99", }) %> <style> #example-currency-sanitized {display: flex !important;} </style>
Set this prop to false or "off" to remove autocomplete from text inputs. You can also set it to a string, but browsers will often defer to other attributes like name.
<%= pb_rails("text_input", props: { autocomplete: false, label: "autocomplete='off'", name: "firstName", placeholder: "Enter first name", }) %> <%= pb_rails("text_input", props: { label: "no autocomplete attribute (let browser decide- basically 'on')", name: "lastName", placeholder: "Enter last name" }) %> <%= pb_rails("text_input", props: { autocomplete: true, label: "autocomplete='on'", name: "phone", type: "phone", placeholder: "Enter phone number" }) %> <%= pb_rails("body", props: { margin_bottom: "sm" }) do %> The following have the same autocomplete attributes (email), but have different name attributes (email and emailAlt). Many browsers will open autocomplete based on name attributes instead of autocomplete: <% end %> <%= pb_rails("text_input", props: { autocomplete: "email", label: "autocomplete='email' name='email'", name: "email", placeholder: "Enter email address" }) %> <%= pb_rails("text_input", props: { autocomplete: "email", label: "autocomplete='email' name='emailAlt'", name: "emailAlt", type: "email", placeholder: "Enter email address" }) %>
<%= pb_rails("rich_text_editor", props: {id: "sticky", sticky: true, value: "In this example, when you scroll down, the rich text editor's toolbar will scroll along with the page and it will no longer be visible at the top of the page. Dummy text to enable scroll.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ornare lorem ut pellentesque tempor. Vivamus ut ex vestibulum velit rich text editor eleifend fringilla. Sed non metus dictum, elementum mauris wysiwyg html editor non, sagittis odio. Nullam pellentesque leo sit amet ante suscipit wysiwyg html editor sagittis. Donec tempus vulputate suscipit. Ut non felis rich text editor ac dolor pulvinar lacinia eu eget urna. Sed tincidunt sapien vulputate tellus fringilla sodales. Morbi accumsan dui wysiwyg html editor sed massa pellentesque, quis vestibulum lectus scelerisque. Nulla ultrices mi id felis luctus aliquet. Donec nec ligula wysiwyg html editor pretium sapien semper dictum eu id quam. Etiam ut sollicitudin nibh. Quisque eu ultrices dui. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus rich text editor mi eu consequat. Nullam tincidunt erat et placerat mattis. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus mi eu consequat. Nullam tincidunt erat et placerat mattis.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ornare lorem ut pellentesque tempor. Vivamus ut ex vestibulum velit rich text editor eleifend fringilla. Sed non metus dictum, elementum mauris wysiwyg html editor non, sagittis odio. Nullam pellentesque leo sit amet ante suscipit wysiwyg html editor sagittis. Donec tempus vulputate suscipit. Ut non felis rich text editor ac dolor pulvinar lacinia eu eget urna. Sed tincidunt sapien vulputate tellus fringilla sodales. Morbi accumsan dui wysiwyg html editor sed massa pellentesque, quis vestibulum lectus scelerisque. Nulla ultrices mi id felis luctus aliquet. Donec nec ligula wysiwyg html editor pretium sapien semper dictum eu id quam. Etiam ut sollicitudin nibh. Quisque eu ultrices dui. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor vulputate suscipit. Ut non felis rich text editor ac dolor pulvinar lacinia eu eget urna. Sed tincidunt sapien vulputate tellus fringilla sodales. Morbi accumsan dui wysiwyg html editor sed massa pellentesque, quis vestibulum lectus scelerisque. Nulla ultrices mi id felis luctus aliquet. Donec nec ligula wysiwyg html editor pretium sapien semper dictum eu id quam. Etiam ut sollicitudin nibh. Quisque eu ultrices dui. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus rich text editor mi eu consequat. Nullam tincidunt erat et placerat mattis. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus mi eu consequat. Nullam tincidunt erat et placerat mattis.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ornare lorem ut pellentesque tempor. Vivamus ut ex vestibulum velit rich text editor eleifend fringilla. Sed non metus dictum, elementum mauris wysiwyg html editor non, sagittis odio. Nullam pellentesque leo sit amet ante suscipit wysiwyg html editor sagittis. Donec tempus vulputate suscipit. Ut non felis rich text editor ac dolor pulvinar lacinia eu eget urna. Sed tincidunt sapien vulputate tellus fringilla sodales. Morbi accumsan dui wysiwyg html editor sed massa pellentesque, quis vestibulum lectus scelerisque. Nulla ultrices mi id felis luctus aliquet. Donec nec ligula wysiwyg html editor pretium sapien semper dictum eu id quam. Etiam ut sollicitudin nibh. Quisque eu ultrices dui. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus rich text editor mi eu consequat. Nullam tincidunt erat et placerat mattis. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus mi eu consequat. Nullam tincidunt erat et placerat mattis.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ornare lorem ut pellentesque tempor. Vivamus ut ex vestibulum velit rich text editor eleifend fringilla. Sed non metus dictum, elementum mauris wysiwyg html editor non, sagittis odio. Nullam pellentesque leo sit amet ante suscipit wysiwyg html editor sagittis. Donec tempus vulputate suscipit. Ut non felis rich text editor ac dolor pulvinar lacinia eu eget urna. Sed tincidunt sapien vulputate tellus fringilla sodales. Morbi accumsan dui wysiwyg html editor sed massa pellentesque, quis vestibulum lectus scelerisque. Nulla ultrices mi id felis luctus aliquet. Donec nec ligula wysiwyg html editor pretium sapien semper dictum eu id quam. Etiam ut sollicitudin nibh. Quisque eu ultrices dui. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus rich text editor mi eu consequat. Nullam tincidunt erat et placerat mattis. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus mi eu consequat. Nullam tincidunt erat et placerat mattis.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ornare lorem ut pellentesque tempor. Vivamus ut ex vestibulum velit rich text editor eleifend fringilla. Sed non metus dictum, elementum mauris wysiwyg html editor non, sagittis odio. Nullam pellentesque leo sit amet ante suscipit wysiwyg html editor sagittis. Donec tempus vulputate suscipit. Ut non felis rich text editor ac dolor pulvinar lacinia eu eget urna. Sed tincidunt sapien vulputate tellus fringilla sodales. Morbi accumsan dui wysiwyg html editor sed massa pellentesque, quis vestibulum lectus scelerisque. Nulla ultrices mi id felis luctus aliquet. Donec nec ligula wysiwyg html editor pretium sapien semper dictum eu id quam. Etiam ut sollicitudin nibh. Quisque eu ultrices dui. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus rich text editor mi eu consequat. Nullam tincidunt erat et placerat mattis. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus mi eu consequat. Nullam tincidunt erat et placerat mattis.sit amet ex. Aliquam cursus maximus rich text editor mi eu consequat. Nullam tincidunt erat et placerat mattis. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus mi eu consequat. Nullam tincidunt erat et placerat mattis.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ornare lorem ut pellentesque tempor. Vivamus ut ex vestibulum velit rich text editor eleifend fringilla. Sed non metus dictum, elementum mauris wysiwyg html editor non, sagittis odio. Nullam pellentesque leo sit amet ante suscipit wysiwyg html editor sagittis. Donec tempus vulputate suscipit. Ut non felis rich text editor ac dolor pulvinar lacinia eu eget urna. Sed tincidunt sapien vulputate tellus fringilla sodales. Morbi accumsan dui wysiwyg html editor sed massa pellentesque, quis vestibulum lectus scelerisque. Nulla ultrices mi id felis luctus aliquet. Donec nec ligula wysiwyg html editor pretium sapien semper dictum eu id quam. Etiam ut sollicitudin nibh. Quisque eu ultrices dui. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus rich text editor mi eu consequat. Nullam tincidunt erat et placerat mattis. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus mi eu consequat. Nullam tincidunt erat et placerat mattis.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ornare lorem ut pellentesque tempor. Vivamus ut ex vestibulum velit rich text editor eleifend fringilla. Sed non metus dictum, elementum mauris wysiwyg html editor non, sagittis odio. Nullam pellentesque leo sit amet ante suscipit wysiwyg html editor sagittis. Donec tempus vulputate suscipit. Ut non felis rich text editor ac dolor pulvinar lacinia eu eget urna. Sed tincidunt sapien vulputate tellus fringilla sodales. Morbi accumsan dui wysiwyg html editor sed massa pellentesque, quis vestibulum lectus scelerisque. Nulla ultrices mi id felis luctus aliquet. Donec nec ligula wysiwyg html editor pretium sapien semper dictum eu id quam. Etiam ut sollicitudin nibh. Quisque eu ultrices dui. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus rich text editor mi eu consequat. Nullam tincidunt erat et placerat mattis. Nunc rich text editor congue, enim vitae dictum dignissim, libero nisl sagittis augue, non aliquet nibh tortor sit amet ex. Aliquam cursus maximus mi eu consequat. Nullam tincidunt erat et placerat mattis."}) %>
<% changelog = "<div> <strong>Changelog:<br></strong> [INSERT LINK]<br><br> You can test the normal spots of Playbook rails and react on dev docs plus the following: </div> <div> <br> </div>" %> <% release = "<div> <div> <strong>Story Background</strong> </div> <div> Follow the{' '} <a href='https://github.com/powerhome/playbook/wiki/Release-Team-Guide'> release process </a>{' '} to create a new version, create a gem, and package. Create a Ninja testing plan, then update Nitro with the new version. </div> <div> <br /> </div> <div> <strong>Timeline / Due Date</strong> </div> <div> <em>Release End of business Thursday</em> </div> <div> <em>Testing on Nitro End of business Friday</em> </div> <div> <br /> </div> <div> <strong>Definition of done</strong> </div> <ol> <li>Merge all PR’s</li> <li>Update the final CHANGELOG</li> <li>Version up and generate NPM, and RubyGem</li> <li>Create next version branch and milestone</li> <li>Update default branch and branch protection rules </li> <li>Notify Everyone of new version</li> <li> Generate testing plan and pages to test for Ninjas (update runway ticket) </li> <li>Update version on Nitro and get on Demo</li> <li>Send Ninjas demo and runway ticket for testing</li> <li>Ninja Approved + PR Approved</li> </ol> <div> <br /> </div> <div> <strong>Stakeholders / Sign-off</strong> </div> <ul> <li>Code Owners</li> </ul> <div> <br /> <strong>Cadence</strong> <br /> Jason, Jon, Stephen, Jasper, Brendan, Cole </div> </div>" %> <%= pb_rails("select", props: { id: "rails-select-dropdown", label: "", name: "", blank_selection: "Select a template", options: [ { value: release, value_text: "Playbook Release", }, { value: changelog, value_text: "Changelog", }, ] }) %> <%= pb_rails("rich_text_editor", props: {classname: 'template-test', id: "template", template: '' }) %> <script> const updateContent = (template) => { const trix = document.querySelector('.template-test trix-editor'); const editor = trix.editor; console.log(editor) editor.loadHTML("") editor.setSelectedRange([0, 0]) editor.insertHTML(template) } window.addEventListener('DOMContentLoaded', () => { const editor = document.querySelector("#rails-select-dropdown") editor.addEventListener('change', function() { console.log('You selected: ', this.value); const template = this.value updateContent(template); }); }); </script>
<%= pb_rails("rich_text_editor", props: { id: "content-preview-editor" }) %> <div id="card-obfuscation" style="display:none"> <%= pb_rails("card", props: { margin_top: "md", max_width: "md", padding: "sm" }) do %> <div id="content-preview" class="trix-content"> </div> <% end %> </div> <%= pb_rails("button", props: { id: "preview-button", variant: "secondary", margin_top: "md" }) do %> <span>Preview Output</span> <% end %> <script> document.addEventListener('DOMContentLoaded', () => { function handleButtonClick() { const editorContainer = [...document.querySelectorAll('[data-pb-react-props]')] .find(element => element.getAttribute('data-pb-react-props')?.includes('"id":"content-preview-editor"')) const editorElement = editorContainer?.querySelector('trix-editor') const inputId = editorElement?.getAttribute('input') const inputElement = inputId && document.getElementById(inputId) const editorContent = inputElement?.value || '' const previewArea = document.getElementById('content-preview') const cardDiv = document.getElementById('card-obfuscation') if (previewArea && cardDiv) { previewArea.innerHTML = editorContent cardDiv.style.display = 'block' } } document.getElementById('preview-button')?.addEventListener('click', handleButtonClick) }) </script>
<%= pb_rails("textarea", props: { label: "auto", placeholder: "Resize Auto", resize: "auto" }) %> <br/> <%= pb_rails("textarea", props: { label: "vertical", placeholder: "Resize Vertical", resize: "vertical" }) %> <br/> <%= pb_rails("textarea", props: { label: "both", placeholder: "Resize Both", resize: "both" }) %> <br/> <%= pb_rails("textarea", props: { label: "horizontal", placeholder: "Resize Horizontal", resize: "horizontal" }) %>
Textarea w/ Error shows that the radio option must be selected or is invalid (ie when used in a form it signals a user to fix an error).
<% text_area_2_value = "Counting characters!" %> <% text_area_3_value = "This counter prevents the user from exceeding the maximum number of allowed characters. Just try it!" %> <% text_area_4_value = "This counter alerts the user that they have exceeded the maximum number of allowed characters." %> <%= pb_rails("textarea", props: { character_count: 0, id: "text-area-1", label: "Count Only", onkeyup: "handleChange1(event)", }) %> <%= pb_rails("textarea", props: { character_count: text_area_2_value.length, id: "text-area-2", label: "Max Characters", max_characters: 100, onkeyup: "handleChange2(event, 100)", value: text_area_2_value, }) %> <%= pb_rails("textarea", props: { character_count: text_area_3_value.length, id: "text-area-3", label: "Max Characters w/ Blocker", max_characters: 100, onkeyup: "handleChange3(event, 100)", value: text_area_3_value, }) %> <%= pb_rails("textarea", props: { character_count: text_area_4_value.length, error: "Too many characters!", id: "text-area-4", label: "Max Characters w/ Error", max_characters: 75, onkeyup: "handleChange4(event, 75)", value: text_area_4_value, }) %> <script type="text/javascript"> const characterCount1 = document.querySelector("#text-area-1 .pb_caption_kit_xs") const characterCount2 = document.querySelector("#text-area-2 .pb_caption_kit_xs") const characterCount3 = document.querySelector("#text-area-3 .pb_caption_kit_xs") const characterCount4 = document.querySelector("#text-area-4 .pb_caption_kit_xs") const textArea3 = document.querySelector("#text-area-3 textarea") const textArea4 = document.querySelector("#text-area-4 textarea") const handleChange1 = (event) => { characterCount1.innerHTML = event.target.value.length } const handleChange2 = (event, maxCharacters) => { characterCount2.innerHTML = `${event.target.value.length} / ${maxCharacters}` } const handleChange3 = (event, maxCharacters) => { if (event.target.value.length > maxCharacters) { textArea3.value = event.target.value.substring(0, maxCharacters) } characterCount3.innerHTML = `${textArea3.value.length} / ${maxCharacters}` } const handleChange4 = (event, maxCharacters) => { characterCount4.innerHTML = `${textArea4.value.length} / ${maxCharacters}` } </script>
<% options = [ { label: 'Orange', value: '#FFA500' }, { label: 'Red', value: '#FF0000' }, { label: 'Green', value: '#00FF00' }, { label: 'Blue', value: '#0000FF' }, ] %> <%= pb_rails("typeahead", props: { id: "typeahead-default", placeholder: "All Colors", options: options, label: "Colors", name: :foo, is_multi: false }) %> <%= javascript_tag defer: "defer" do %> document.addEventListener("pb-typeahead-kit-typeahead-default-result-option-select", function(event) { console.log('Single Option selected') console.dir(event.detail) }) document.addEventListener("pb-typeahead-kit-typeahead-default-result-clear", function() { console.log('All options cleared') }) <% end %>
Grouped options can be rendered via structuring the options in the way shown in the code snippet below.
<% grouped_options = [ { label: "Warm Colors", options: [ { label: "Red", value: "#FF0000" }, { label: "Orange", value: "#FFA500" }, { label: "Yellow", value: "#FFFF00" }, { label: "Coral", value: "#FF7F50" }, { label: "Gold", value: "#FFD700" } ] }, { label: "Cool Colors", options: [ { label: "Blue", value: "#0000FF" }, { label: "Teal", value: "#008080" }, { label: "Cyan", value: "#00FFFF" }, { label: "Navy", value: "#000080" }, { label: "Turquoise", value: "#40E0D0" } ] }, { label: "Neutrals", options: [ { label: "White", value: "#FFFFFF" }, { label: "Black", value: "#000000" }, { label: "Gray", value: "#808080" }, { label: "Beige", value: "#F5F5DC" }, { label: "Silver", value: "#C0C0C0" } ] }, { label: "Earth Tones", options: [ { label: "Brown", value: "#A52A2A" }, { label: "Olive", value: "#808000" }, { label: "Forest Green", value: "#228B22" }, { label: "Tan", value: "#D2B48C" }, { label: "Maroon", value: "#800000" } ] }, { label: "Fun Shades", options: [ { label: "Pink", value: "#FFC0CB" }, { label: "Magenta", value: "#FF00FF" }, { label: "Lime", value: "#00FF00" }, { label: "Purple", value: "#800080" }, { label: "Indigo", value: "#4B0082" } ] } ] %> <br /> <%= pb_rails("typeahead", props: { id: "typeahead-custom-options", options: grouped_options, label: "Colors", name: :foo, placeholder: "Select a Color...", is_multi: false }) %>
The optional default_options prop can be used to set a default value for the kit. When a default value is set, focus will be automatically set to the selected option and the dropdown container will scroll to bring the selected option into view.
<% options = [ { label: 'Orange', value: '#FFA500' }, { label: 'Red', value: '#FF0000' }, { label: 'Green', value: '#1e3d1eff' }, { label: 'Blue', value: '#0000FF' }, { label: 'Purple', value: '#800080' }, { label: 'Yellow', value: '#FFFF00' }, { label: 'Pink', value: '#FFC0CB' }, { label: 'Brown', value: '#A52A2A' }, { label: 'Black', value: '#000000' }, { label: 'White', value: '#FFFFFF' }, { label: 'Gray', value: '#808080' }, { label: 'Cyan', value: '#00FFFF' }, { label: 'Magenta', value: '#FF00FF' }, { label: 'Lime', value: '#00FF00' }, { label: 'Maroon', value: '#800000' }, { label: 'Olive', value: '#808000' }, { label: 'Navy', value: '#000080' }, { label: 'Teal', value: '#008080' }, { label: 'Silver', value: '#C0C0C0' }, { label: 'Gold', value: '#FFD700' }, { label: 'Beige', value: '#F5F5DC' }, { label: 'Coral', value: '#FF7F50' } ] %> <% grouped_options = [ { label: "Warm Colors", options: [ { label: "Red", value: "#FF0000" }, { label: "Orange", value: "#FFA500" }, { label: "Yellow", value: "#FFFF00" }, { label: "Coral", value: "#FF7F50" }, { label: "Gold", value: "#FFD700" } ] }, { label: "Cool Colors", options: [ { label: "Blue", value: "#0000FF" }, { label: "Teal", value: "#008080" }, { label: "Cyan", value: "#00FFFF" }, { label: "Navy", value: "#000080" }, { label: "Turquoise", value: "#40E0D0" } ] }, { label: "Neutrals", options: [ { label: "White", value: "#FFFFFF" }, { label: "Black", value: "#000000" }, { label: "Gray", value: "#808080" }, { label: "Beige", value: "#F5F5DC" }, { label: "Silver", value: "#C0C0C0" } ] }, { label: "Earth Tones", options: [ { label: "Brown", value: "#A52A2A" }, { label: "Olive", value: "#808000" }, { label: "Forest Green", value: "#228B22" }, { label: "Tan", value: "#D2B48C" }, { label: "Maroon", value: "#800000" } ] }, { label: "Fun Shades", options: [ { label: "Pink", value: "#FFC0CB" }, { label: "Magenta", value: "#FF00FF" }, { label: "Lime", value: "#00FF00" }, { label: "Purple", value: "#800080" }, { label: "Indigo", value: "#4B0082" } ] } ] %> <%= pb_rails("typeahead", props: { default_options: [{ label: 'Gray', value: '#808080' }], id: "typeahead-default-value", options: options, label: "Default Value with basic options", name: :foo, is_multi: false }) %> <%= pb_rails("typeahead", props: { default_options:[{ label: "Pink", value: "#FFC0CB" }], id: "typeahead-default-value-grouped-options", options: grouped_options, label: "Default Value with grouped options", name: :foo, is_multi: false }) %>
<%= pb_rails("select", props: { label: "Colors", name: "foo", data: { context_select: true }, options: [ { value: "red", value_text: "Red" }, { value: "orange", value_text: "Orange" }, { value: "yellow", value_text: "Yellow" }, { value: "green", value_text: "Green" }, { value: "blue", value_text: "Blue" }, { value: "purple", value_text: "Purple" }, ] }) %> <%= pb_rails("typeahead", props: { label: "Crayola Crayons", name: :foo, data: { typeahead_example2: true, search_context_value_selector: "[data-context-select] select" } }) %> <br><br><br> <%= javascript_tag defer: "defer" do %> document.addEventListener("pb-typeahead-kit-search", function(event) { if (!event.target.dataset || !event.target.dataset.typeaheadExample2) return; const fuzzyMatch = function(string, term) { const ratio = 0.5; string = string.toLowerCase(); const compare = term.toLowerCase(); let matches = 0; if (string.indexOf(compare) > -1) return true; for (let index = 0; index < compare.length; index++) { if (string.indexOf(compare[index]) > -1) { matches += 1 } else { matches -=1; } } return (matches / string.length >= ratio || term == "") }; const colors = { red: ["Red", "Scarlet", "Chestnut", "Mahogany"], orange: ["Orange", "Apricot", "Peach", "Melon", "Burnt Sienna", "Macaroni and Cheese"], yellow: ["Yellow", "Gold", "Goldenrod", "Canary", "Laser Lemon"], green: ["Green", "Olive Green", "Granny Smith Apple", "Spring Green", "Sea Green"], blue: ["Blue", "Cerulean", "Bluetiful", "Sky Blue", "Cadet Blue", "Cornflower"], purple: ["Violet", "Indigo", "Wisteria", "Purple Mountain Majesty", "Lavender"] }; event.detail.setResults(colors[event.detail.searchingContext].filter((color) => fuzzyMatch(color, event.detail.searchingFor)).map((color) => document.createTextNode(color))); }) <% end %>
Typeahead kit is data-driven. The minimum default fields are label and value.
This is an example of an option: { label: 'Windows', value: '#FFA500' }
You can also pass default_options which will populate the initial pill selections:
default_options: [{ label: 'Windows', value: '#FFA500' }]
JavaScript events are triggered based on actions you take within the kit such as selection, removal and clearing.
This kit utilizes a default id prop named react-select-input. It is highly advised to send your own unique id prop when using this kit to ensure these events do not unintentionally affect other instances of the kit in the same view. The examples below will use the unique id prop named typeahead-pills-example1:
pb-typeahead-kit-typeahead-pills-example1-result-option-select event to perform custom work when an option is clicked.
pb-typeahead-kit-typeahead-pills-example1-result-option-remove event to perform custom work when a pill is clicked.
pb-typeahead-kit-typeahead-pills-example1-result-option-clear event to perform custom work when all pills are removed upon clicking the X.
The same rule regarding the id prop applies to publishing JS events. The examples below will use the unique id prop named typeahead-pills-example1:
pb-typeahead-kit-typeahead-pills-example1:clear event to clear all options.
<% options = [ { label: 'Windows', value: '#FFA500' }, { label: 'Siding', value: '#FF0000' }, { label: 'Doors', value: '#00FF00' }, { label: 'Roofs', value: '#0000FF' }, ] %> <%= pb_rails("typeahead", props: { id: "typeahead-pills-example1", default_options: [options.first], options: options, label: "Products", name: :foo, pills: true }) %> <%= pb_rails("button", props: {id: "clear-pills", text: "Clear All Options", variant: "secondary"}) %> <!-- This section is an example of the available JavaScript event hooks --> <%= javascript_tag defer: "defer" do %> document.addEventListener("pb-typeahead-kit-typeahead-pills-example1-result-option-select", function(event) { console.log('Option selected') console.dir(event.detail) }) document.addEventListener("pb-typeahead-kit-typeahead-pills-example1-result-option-remove", function(event) { console.log('Option removed') console.dir(event.detail) }) document.addEventListener("pb-typeahead-kit-typeahead-pills-example1-result-clear", function() { console.log('All options cleared') }) document.querySelector('#clear-pills').addEventListener('click', function() { document.dispatchEvent(new CustomEvent('pb-typeahead-kit-typeahead-pills-example1:clear')) }) <% end %>
Passing is_multi: false will allow the user to only select one option from the drop down. Note: this will disable pills prop.
<% options = [ { label: 'Windows', value: '#FFA500' }, { label: 'Siding', value: '#FF0000' }, { label: 'Doors', value: '#00FF00' }, { label: 'Roofs', value: '#0000FF' }, ] %> <%= pb_rails("typeahead", props: { id: "typeahead-without-pills-example1", placeholder: "All Products", options: options, label: "Products", name: :foo, is_multi: false }) %> <!-- This section is an example of the available JavaScript event hooks --> <%= javascript_tag defer: "defer" do %> document.addEventListener("pb-typeahead-kit-typeahead-without-pills-example1-result-option-select", function(event) { console.log('Single Option selected') console.dir(event.detail) }) document.addEventListener("pb-typeahead-kit-typeahead-without-pills-example1-result-clear", function() { console.log('All options cleared') }) <% end %>
load_options Promise *Additional required props: * async: true, pills: true
The prop load_options, when used in conjunction with async: true and pills: true, points to a JavaScript function located within the global window namespace. This function should return a Promise which resolves with the list of formatted options as described in prior examples above. This function is identical to the function provided to the React version of this kit. See the code example for more details.
loadOptions *Additional required props: * async: true
As outlined in the react-select Async docs, loadOptions expects to return a Promise that resolves resolves with the list of formatted options as described in prior examples above. See the code example for more details.
getOptionLabel + getOptionValue If your server returns data that requires differing field names other than label and value
See react-select docs for more information: https://react-select.com/advanced#replacing-builtins
<%= pb_rails("typeahead", props: { async: true, get_option_label: 'getOptionLabel', get_option_value: 'getOptionValue', load_options: 'asyncPillsPromiseOptions', label: "Github Users", name: :foo, pills: true, placeholder: "type the name of a Github user" }) %> <!-- This section is an example of how to provide load_options prop for using dynamic options --> <%= javascript_tag defer: "defer" do %> // Simple filter function, providing a list of results in the expected data format const filterResults = function(results) { return results.items.map(function(result) { return { name: result.login, id: result.id, } }) } /* Note: Make sure you assign this to window or a namespace within window or it will not be accessible to the kit! */ window.asyncPillsPromiseOptions = function(inputValue) { return new Promise(function(resolve) { if (inputValue) { fetch(`https://api.github.com/search/users?q=${inputValue}`) .then(function(response) { return response.json() }) .then(function(results) { resolve(filterResults(results))}) } else { resolve([]) } }) } window.getOptionLabel = function(option) { return option.name; } window.getOptionValue = function(option) { return option.id; } <% end %>
If the data field imageUrl is present, FormPill will receive that field as a prop and display the image.
<%= pb_rails("typeahead", props: { async: true, load_options: 'asyncPillsPromiseOptionsUsers', label: "Github Users", name: :foo, pills: true, placeholder: "type the name of a Github user" }) %> <%= javascript_tag defer: "defer" do %> const filterUserResults = function(results) { return results.items.map(function(result) { return { imageUrl: result.avatar_url, label: result.login, value: result.id, } }) } window.asyncPillsPromiseOptionsUsers = function(inputValue) { return new Promise(function(resolve) { if (inputValue) { fetch(`https://api.github.com/search/users?q=${inputValue}`) .then(function(response) { return response.json() }) .then(function(results) { resolve(filterUserResults(results))}) } else { resolve([]) } }) } <% end %>
<% synths = [ { label: 'Oberheim', value: 'OBXa' }, { label: 'Moog', value: 'Minimoog' }, { label: 'Roland', value: 'Juno' }, { label: 'Korg', value: 'MS-20' }, ] %> <% cities = [ { label: 'Budapest', value: 'Hungary' }, { label: 'Singapore', value: 'Singapore' }, { label: 'Oslo', value: 'Norway' }, { label: 'Lagos', value: 'Nigeria' }, ] %> <%= pb_rails("typeahead", props: { default_options: [synths.first], id: "typeahead-inline-example1", inline: true, options: synths, label: "Synths", placeholder: "Add synths", pills: true }) %> <%= pb_rails("typeahead", props: { id: "typeahead-inline-example2", inline: true, options: cities, label: "Cities", pills: true, placeholder: "Add cities", plus_icon: true }) %>
<% labels = [ { label: 'Verve', value: '1956' }, { label: 'Stax', value: '1957' }, { label: 'Motown', value: '1959' }, { label: 'Kudu', value: '1971' }, { label: 'Stones Throw', value: '1996' }, ] %> <% expressionists = [ { label: 'Kandinsky', value: 'Russia' }, { label: 'Klee', value: 'Switzerland' }, { label: 'Kokoschka', value: 'Austria' }, { label: 'Kirchner', value: 'Germany' }, ] %> <%= pb_rails("typeahead", props: { default_options: [labels.first], id: "typeahead-multi-kit-example1", options: labels, label: "Badges", multi_kit: "badge", pills: true }) %> <%= pb_rails("typeahead", props: { default_options: [expressionists.first], id: "typeahead-multi-kit-example2", options: expressionists, label: "Small Pills", multi_kit: "smallPill", pills: true }) %>
Typeahead w/ Error shows that an option must be selected or the selected option is invalid (i.e., when used in a form it signals a user to fix an error).
<% options = [ { label: 'Windows', value: '#FFA500' }, { label: 'Siding', value: '#FF0000' }, { label: 'Doors', value: '#00FF00' }, { label: 'Roofs', value: '#0000FF' }, ] %> <%= pb_rails("typeahead", props: { id: "typeahead-error-example", options: options, error: "Please make a valid selection", label: "Products", name: :foo, is_multi: false }) %> <!-- This section is an example of the available JavaScript event hooks --> <%= javascript_tag defer: "defer" do %> document.addEventListener("pb-typeahead-kit-typeahead-error-example-result-option-select", function(event) { console.log('Option selected') console.dir(event.detail) }) <% end %>
<% options = [ { label: 'Orange', value: '#FFA500' }, { label: 'Red', value: '#FF0000' }, { label: 'Green', value: '#00FF00' }, { label: 'Blue', value: '#0000FF' }, ] %> <%= pb_rails("typeahead", props: { id: "typeahead-default", placeholder: "All Colors", options: options, label: "None", name: :foo, is_multi: false, margin_bottom: "none", }) %> <%= pb_rails("typeahead", props: { id: "typeahead-default", placeholder: "All Colors", options: options, label: "XXS", name: :foo, is_multi: false, margin_bottom: "xxs", }) %> <%= pb_rails("typeahead", props: { id: "typeahead-default", placeholder: "All Colors", options: options, label: "XS", name: :foo, is_multi: false, margin_bottom: "xs", }) %> <%= pb_rails("typeahead", props: { id: "typeahead-default", placeholder: "All Colors", options: options, label: "Default - SM", name: :foo, is_multi: false, }) %> <%= pb_rails("typeahead", props: { id: "typeahead-default", placeholder: "All Colors", options: options, label: "MD", name: :foo, is_multi: false, margin_bottom: "md", }) %> <%= pb_rails("typeahead", props: { id: "typeahead-default", placeholder: "All Colors", options: options, label: "LG", name: :foo, is_multi: false, margin_bottom: "lg", }) %> <%= pb_rails("typeahead", props: { id: "typeahead-default", placeholder: "All Colors", options: options, label: "XL", name: :foo, is_multi: false, margin_bottom: "xl", }) %> <%= javascript_tag defer: "defer" do %> document.addEventListener("pb-typeahead-kit-typeahead-default-result-option-select", function(event) { console.log('Single Option selected') console.dir(event.detail) }) document.addEventListener("pb-typeahead-kit-typeahead-default-result-clear", function() { console.log('All options cleared') }) <% end %>
Change the form pill color by passing the optional pill_color prop. Product, Data, and Status colors are available options. Check them out here in the Form Pill colors example.
<% options = [ { label: 'Windows', value: '#FFA500' }, { label: 'Siding', value: '#FF0000' }, { label: 'Doors', value: '#00FF00' }, { label: 'Roofs', value: '#0000FF' }, ] %> <%= pb_rails("typeahead", props: { id: "typeahead-pills-example2", pill_color: "neutral", options: options, label: "Products", name: :foo, pills: true }) %>
For Form Pills with longer text, the truncate global prop can be used to truncate the label within each Form Pill. Hover over the truncated Form Pill and a Tooltip containing the text or tag section of the Form Pill will appear. See here for more information on the truncate global prop.
<% names = [ { label: 'Alexander Nathaniel Montgomery', value: 'Alexander Nathaniel Montgomery' }, { label: 'Isabella Anastasia Wellington', value: 'Isabella Anastasia Wellington' }, { label: 'Christopher Maximilian Harrington', value: 'Christopher Maximilian Harrington' }, { label: 'Elizabeth Seraphina Kensington', value: 'Elizabeth Seraphina Kensington' }, { label: 'Theodore Jonathan Abernathy', value: 'Theodore Jonathan Abernathy' }, ] %> <%= pb_rails("typeahead", props: { html_options: { style: { maxWidth: "240px" }}, id: "typeahead-form-pill", is_multi: true, options: names, label: "Truncation Within Typeahead", pills: true, truncate: 1, }) %>
You can also set up a typeahead to render options dynamically based on input from a select. To achieve this:
idsearch_context_selector prop on the typeahead. The value here must match the id of the select so the Typeahead knows where to read the current "context" from.options_by_context to pass in a hash whose keys match the possible values of your “context” select. Each key maps to an array of { label, value } objects. The typeahead automatically displays only the subset of options matching the current context.clear_on_context_change prop controls whether the typeahead clears or not when a change happens in the linked select. This prop is set to true by default so that whenever a selection is made in the select, the Typeahead automatically clears its current input/selection.<%= pb_rails("select", props: { id:"color_context_2", label: "Choose a Color", name: "color_name", options: [ { value: "red", value_text: "Red" }, { value: "blue", value_text: "Blue" }, { value: "green", value_text: "Green" } ], }) %> <%= pb_rails("typeahead", props: { label: "Pick a Shade", is_multi: false, search_context_selector: "color_context_2", options_by_context: { "red": [ { label: "Scarlet", value: "scarlet" }, { label: "Mahogany", value: "mahogany" }, { label: "Crimson", value: "crimson" } ], "blue": [ { label: "Sky Blue", value: "sky" }, { label: "Cerulean", value: "cerulean" }, { label: "Navy", value: "navy" } ], "green": [ { label: "Emerald", value: "emerald" }, { label: "Mint", value: "mint" }, { label: "Olive", value: "olive" } ] }, id: "typeahead-dynamic-options", }) %> <%= javascript_tag defer: "defer" do %> document.addEventListener("pb-typeahead-kit-typeahead-dynamic-options-result-option-select", function(event) { console.log('Single Option selected') console.dir(event.detail) }) document.addEventListener("pb-typeahead-kit-typeahead-dynamic-options-result-clear", function() { console.log('All options cleared') }) <% end %>
The dynamic rendering of options for the typeahead can also be achieved with a pure Rails implementation (not react rendered). For this implementation, use all the props as above with the following additions:
search_term_minimum_length: this sets the minimum input in the typeahead needed to display the dropdown. This is set to 3 by default. Set it to 0 for the dropdown to always display when the typeahead is interacted with. <%= pb_rails("select", props: { id:"color_context", label: "Choose a Color", name: "color_name_2", options: [ { value: "red", value_text: "Red" }, { value: "blue", value_text: "Blue" }, { value: "green", value_text: "Green" } ], }) %> <%= pb_rails("typeahead", props: { label: "Pick a Shade", search_context_selector: "color_context", options_by_context: { "red": [ { label: "Scarlet", value: "scarlet" }, { label: "Mahogany", value: "mahogany" }, { label: "Crimson", value: "crimson" } ], "blue": [ { label: "Sky Blue", value: "sky" }, { label: "Cerulean", value: "cerulean" }, { label: "Navy", value: "navy" } ], "green": [ { label: "Emerald", value: "emerald" }, { label: "Mint", value: "mint" }, { label: "Olive", value: "olive" } ] }, search_term_minimum_length: 0, }) %>
<% options = [ { label: 'Orange', value: '#FFA500' }, { label: 'Red', value: '#FF0000' }, { label: 'Green', value: '#00FF00' }, { label: 'Blue', value: '#0000FF' }, ] %> <%= pb_rails("typeahead", props: { id: "typeahead-disabled", disabled: true, placeholder: "All Colors", options: options, label: "Colors", name: :foo, is_multi: false }) %>
By default, text is not preserved in the typeahead kit when you click off of the input field. You can utilize the preserve_search_input prop in order to prevent text from being cleared when the field loses focus
<% options = [ { label: 'Orange', value: '#FFA500' }, { label: 'Red', value: '#FF0000' }, { label: 'Green', value: '#00FF00' }, { label: 'Blue', value: '#0000FF' }, ] %> <%= pb_rails("typeahead", props: { id: "typeahead-preserve-search-input", is_multi: false, label: "Colors", options: options, placeholder: "Select...", preserve_search_input: true, }) %>