Skip to content

Commit

Permalink
Immediate server side validation for text field form inputs (#1672)
Browse files Browse the repository at this point in the history
Co-authored-by: Cameron Dutro <camertron@gmail.com>
Co-authored-by: neall <neall@users.noreply.github.com>
Co-authored-by: Keith Cirkel <keithamus@users.noreply.github.com>
  • Loading branch information
4 people authored Dec 8, 2022
1 parent c1845c6 commit 1a7dadd
Show file tree
Hide file tree
Showing 26 changed files with 318 additions and 38 deletions.
5 changes: 5 additions & 0 deletions .changeset/fresh-countries-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/view-components': patch
---

Add auto_check_src option to forms framework text fields to run server-side validation on field change
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ app/components/**/*.css
app/components/**/*.css.json
app/components/**/*.css.map
app/components/**/*.d.ts
lib/primer/forms/**/*.js
lib/primer/forms/**/*.css
lib/primer/forms/**/*.css.json
lib/primer/forms/**/*.css.map
lib/primer/forms/**/*.d.ts
app/assets/

# Generated by demo npm post-install
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion Procfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
rails: cd demo; bin/rails s -p 4000
js: npx chokidar "app/components/**/*.ts" -i "app/components/**/*.d.ts" -c "npm run build:js"
js: npx chokidar "app/components/**/*.ts" "lib/primer/forms/**/*.ts" -i "app/components/**/*.d.ts" -i "lib/primer/forms/**/*.d.ts" -c "npm run build:js"
css: npx chokidar "app/components/**/*.pcss" -c "npm run build:css"
1 change: 1 addition & 0 deletions app/components/primer/primer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ import './beta/clipboard_copy'
import './local_time'
import './tab_container_component'
import './time_ago_component'
import '../../../lib/primer/forms/primer_text_field'
29 changes: 29 additions & 0 deletions app/forms/immediate_validation_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

# :nodoc:
# :nocov:
class ImmediateValidationForm < ApplicationForm
form do |validation_form|
validation_form.text_field(
name: :has_error,
label: "Will have error",
caption: "Every time this checks with the server, it returns an error",
auto_check_src: @view_context.example_check_error_path
)

validation_form.text_field(
name: :no_error,
label: "Will not error",
caption: "Will not have an error when it checks the server",
auto_check_src: @view_context.example_check_ok_path,
validation_message: "This message will go away once you type something"
)

validation_form.text_field(
name: :random_error,
label: "Random error",
caption: "Server checks will randomly respond with errors",
auto_check_src: @view_context.example_check_random_path
)
end
end
5 changes: 5 additions & 0 deletions demo/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2022.0105)
mini_mime (1.1.2)
mini_portile2 (2.8.0)
minitest (5.16.3)
ms_rest (0.7.6)
concurrent-ruby (~> 1.0)
Expand All @@ -270,6 +271,9 @@ GEM
net-protocol
netrc (0.11.0)
nio4r (2.5.8)
nokogiri (1.13.10)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
nokogiri (1.13.10-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.13.10-x86_64-linux)
Expand Down Expand Up @@ -377,6 +381,7 @@ GEM
zeitwerk (2.6.6)

PLATFORMS
ruby
x86_64-darwin-19
x86_64-darwin-20
x86_64-linux
Expand Down
23 changes: 23 additions & 0 deletions demo/app/controllers/auto_check_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

# For auto-check previews
# :nocov:
class AutoCheckController < ApplicationController
skip_before_action :verify_authenticity_token

def error
render status: :unprocessable_entity, plain: "Error! Error!"
end

def ok
head :ok
end

def random
if rand > 0.5
head :ok
else
render status: :unprocessable_entity, plain: "Random error!"
end
end
end
3 changes: 3 additions & 0 deletions demo/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
get "/auto_complete", to: "auto_complete_test#index"
resources :toggle_switch, only: [:create]
resources :nav_list_items, only: [:index]
post "/example_check/ok", to: "auto_check#ok", as: :example_check_ok
post "/example_check/error", to: "auto_check#error", as: :example_check_error
post "/example_check/random", to: "auto_check#random", as: :example_check_random

mount Lookbook::Engine, at: "/lookbook" if defined?(Lookbook)
end
Expand Down
2 changes: 1 addition & 1 deletion demo/kuby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def install_from_image(image, dockerfile)
tsconfig.json
rollup.config.js
postcss.config.js
lib/postcss_mixins
lib/
app/
package.json
package-lock.json
Expand Down
16 changes: 16 additions & 0 deletions lib/primer/forms/dsl/input.rb
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,22 @@ def input?
true
end

def need_validation_element?
invalid?
end

def validation_arguments
{
class: "FormControl-inlineValidation",
id: validation_id,
hidden: valid?
}
end

def validation_message_arguments
{}
end

private

def input_data
Expand Down
32 changes: 31 additions & 1 deletion lib/primer/forms/dsl/text_field_input.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class TextFieldInput < Input
attr_reader(
*%i[
name label show_clear_button leading_visual clear_button_id
visually_hide_label inset monospace field_wrap_classes
visually_hide_label inset monospace field_wrap_classes auto_check_src
]
)

Expand All @@ -21,6 +21,7 @@ def initialize(name:, label:, **system_arguments)
@clear_button_id = system_arguments.delete(:clear_button_id)
@inset = system_arguments.delete(:inset)
@monospace = system_arguments.delete(:monospace)
@auto_check_src = system_arguments.delete(:auto_check_src)

super(**system_arguments)

Expand All @@ -29,6 +30,7 @@ def initialize(name:, label:, **system_arguments)
Primer::Forms::Dsl::Input::SIZE_MAPPINGS[size]
)

add_input_data(:target, "primer-text-field.inputElement") if auto_check_src.present?
add_input_classes("FormControl-inset") if inset?
add_input_classes("FormControl-monospace") if monospace?
end
Expand All @@ -52,6 +54,34 @@ def focusable?
def leading_visual?
!!@leading_visual
end

def need_validation_element?
super || auto_check_src.present?
end

def validation_arguments
if auto_check_src.present?
super.merge(
data: {
target: "primer-text-field.validationElement"
}
)
else
super
end
end

def validation_message_arguments
if auto_check_src.present?
super.merge(
data: {
target: "primer-text-field.validationMessageElement"
}
)
else
super
end
end
end
end
end
Expand Down
10 changes: 5 additions & 5 deletions lib/primer/forms/form_control.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<%= content_tag(:div, style: "flex-grow: 1", **@form_group_arguments) do %>
<%= content_tag(:div, **@form_group_arguments) do %>
<% if @input.label %>
<%= builder.label(@input.name, **@input.label_arguments) do %>
<%= @input.label %>
Expand All @@ -8,11 +8,11 @@
<% end %>
<% end %>
<%= content %>
<% if @input.invalid? && @input.validation_messages.present? %>
<div class="FormControl-inlineValidation" id="<%= @input.validation_id %>">
<% if @input.need_validation_element? %>
<%= content_tag(:div, **@input.validation_arguments) do %>
<%= render(Primer::Beta::Octicon.new(icon: :"alert-fill", size: :xsmall, aria: { hidden: true })) %>
<span><%= @input.validation_messages.first %></span>
</div>
<%= content_tag(:span, @input.validation_messages.first, **@input.validation_message_arguments) %>
<% end %>
<% end %>
<%= render(Caption.new(input: @input)) %>
<% end %>
2 changes: 2 additions & 0 deletions lib/primer/forms/form_control.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ def initialize(input:)
@form_group_arguments = {
class: class_names(
"FormControl",
"flex-1",
"width-full",
"FormControl--fullWidth" => @input.full_width?
)
}
Expand Down
48 changes: 48 additions & 0 deletions lib/primer/forms/primer_text_field.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import '@github/auto-check-element'
import {controller, target} from '@github/catalyst'

@controller
class PrimerTextFieldElement extends HTMLElement {
@target inputElement: HTMLInputElement
@target validationElement: HTMLElement
@target validationMessageElement: HTMLElement

#abortController: AbortController | null

connectedCallback(): void {
this.#abortController?.abort()
const {signal} = (this.#abortController = new AbortController())

this.inputElement.addEventListener(
'auto-check-success',
() => { this.clearError() },
{signal}
)

this.inputElement.addEventListener(
'auto-check-error',
(event: any) => {
event.detail.response.text().then(
(error_message: string) => { this.setError(error_message) }
)
},
{signal}
)
}

disconnectedCallback() {
this.#abortController?.abort()
}

clearError(): void {
this.inputElement.removeAttribute('invalid')
this.validationElement.hidden = true
this.validationMessageElement.innerText = ''
}

setError(message: string): void {
this.validationMessageElement.innerText = message
this.validationElement.hidden = false
this.inputElement.setAttribute('invalid', 'true')
}
}
12 changes: 6 additions & 6 deletions lib/primer/forms/text_field.html.erb
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
<%= render(FormControl.new(input: @input)) do %>
<% if @input.leading_visual || @input.show_clear_button? %>
<%= content_tag(:div, **@field_wrap_arguments) do %>
<%= render Primer::ConditionalWrapper.new(condition: @input.auto_check_src, tag: "primer-text-field") do %>
<%= render(FormControl.new(input: @input)) do %>
<%= render Primer::ConditionalWrapper.new(tag: :div, class: @input.field_wrap_classes, condition: @input.leading_visual || @input.show_clear_button?) do %>
<% if @input.leading_visual %>
<span class="FormControl-input-leadingVisualWrap">
<%= render(Primer::Beta::Octicon.new(**@input.leading_visual)) %>
</span>
<% end %>
<%= builder.text_field(@input.name, **@input.input_arguments) %>
<%= render Primer::ConditionalWrapper.new(condition: @input.auto_check_src, tag: "auto-check", csrf: auto_check_authenticity_token, src: @input.auto_check_src) do %>
<%= builder.text_field(@input.name, **@input.input_arguments) %>
<% end %>
<% if @input.show_clear_button? %>
<button id="<%= @input.clear_button_id %>" class="FormControl-input-trailingAction" aria-label="Clear">
<%= render(Primer::Beta::Octicon.new(icon: :"x-circle-fill")) %>
</button>
<% end %>
<% end %>
<% else %>
<%= builder.text_field(@input.name, **@input.input_arguments) %>
<% end %>
<% end %>
11 changes: 11 additions & 0 deletions lib/primer/forms/text_field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ def initialize(input:)
hidden: @input.hidden?
}
end

def auto_check_authenticity_token
return @auto_check_authenticity_token if defined?(@auto_check_authenticity_token)

@auto_check_authenticity_token =
if @input.auto_check_src
@view_context.form_authenticity_token(
form_options: { method: :post, action: @input.auto_check_src }
)
end
end
end
end
end
29 changes: 29 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 1a7dadd

Please sign in to comment.