Skip to content

Commit

Permalink
Remove remaining dead code for phx-feedback-for, update docs
Browse files Browse the repository at this point in the history
  • Loading branch information
SteffenDE committed May 10, 2024
1 parent d84b19c commit 35d72c7
Show file tree
Hide file tree
Showing 6 changed files with 15 additions and 115 deletions.
1 change: 0 additions & 1 deletion assets/js/phoenix_live_view/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export const PHX_PRUNE = "data-phx-prune"
export const PHX_PAGE_LOADING = "page-loading"
export const PHX_CONNECTED_CLASS = "phx-connected"
export const PHX_LOADING_CLASS = "phx-loading"
export const PHX_NO_FEEDBACK_CLASS = "phx-no-feedback"
export const PHX_ERROR_CLASS = "phx-error"
export const PHX_CLIENT_ERROR_CLASS = "phx-client-error"
export const PHX_SERVER_ERROR_CLASS = "phx-server-error"
Expand Down
69 changes: 1 addition & 68 deletions assets/js/phoenix_live_view/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
PHX_HAS_FOCUSED,
PHX_HAS_SUBMITTED,
PHX_MAIN,
PHX_NO_FEEDBACK_CLASS,
PHX_PARENT_ID,
PHX_PRIVATE,
PHX_REF,
Expand Down Expand Up @@ -305,84 +304,18 @@ let DOM = {
}
},

isFeedbackContainer(el, phxFeedbackFor){
return el.hasAttribute && el.hasAttribute(phxFeedbackFor)
},

maybeHideFeedback(container, feedbackContainers, phxFeedbackFor, phxFeedbackGroup){
// because we can have multiple containers with the same phxFeedbackFor value
// we perform the check only once and store the result;
// we often have multiple containers, because we push both fromEl and toEl in dompatch
// when a container is updated
const feedbackResults = {}
feedbackContainers.forEach(el => {
// skip elements that are not in the DOM
if(!container.contains(el)) return
const feedback = el.getAttribute(phxFeedbackFor)
if(!feedback){
// the container previously had phx-feedback-for, but now it doesn't
// remove the class from the container (if it exists)
JS.addOrRemoveClasses(el, [], [PHX_NO_FEEDBACK_CLASS])
return
}
if(feedbackResults[feedback] === true){
this.hideFeedback(el)
return
}
feedbackResults[feedback] = this.shouldHideFeedback(container, feedback, phxFeedbackGroup)
if(feedbackResults[feedback] === true){
this.hideFeedback(el)
}
})
},

hideFeedback(container){
JS.addOrRemoveClasses(container, [PHX_NO_FEEDBACK_CLASS], [])
},

isUsedInput(el){
return(el.nodeType === Node.ELEMENT_NODE &&
return (el.nodeType === Node.ELEMENT_NODE &&
(this.private(el, PHX_HAS_FOCUSED) || this.private(el, PHX_HAS_SUBMITTED)))
},

shouldHideFeedback(container, nameOrGroup, phxFeedbackGroup){
const query = `[name="${nameOrGroup}"],
[name="${nameOrGroup}[]"],
[${phxFeedbackGroup}="${nameOrGroup}"]`
let focused = false
DOM.all(container, query, (input) => {
if(this.private(input, PHX_HAS_FOCUSED) || this.private(input, PHX_HAS_SUBMITTED)){
focused = true
}
})
return !focused
},

feedbackSelector(input, phxFeedbackFor, phxFeedbackGroup){
let query = `[${phxFeedbackFor}="${input.name}"],
[${phxFeedbackFor}="${input.name.replace(/\[\]$/, "")}"]`
if(input.getAttribute(phxFeedbackGroup)){
query += `,[${phxFeedbackFor}="${input.getAttribute(phxFeedbackGroup)}"]`
}
return query
},

resetForm(form){
Array.from(form.elements).forEach(input => {
this.deletePrivate(input, PHX_HAS_FOCUSED)
this.deletePrivate(input, PHX_HAS_SUBMITTED)
})
},

showError(inputEl, phxFeedbackFor, phxFeedbackGroup){
if(inputEl.name){
let query = this.feedbackSelector(inputEl, phxFeedbackFor, phxFeedbackGroup)
this.all(document, query, (el) => {
JS.addOrRemoveClasses(el, [], [PHX_NO_FEEDBACK_CLASS])
})
}
},

isPhxChild(node){
return node.getAttribute && node.getAttribute(PHX_PARENT_ID)
},
Expand Down
4 changes: 2 additions & 2 deletions assets/test/view_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1028,11 +1028,11 @@ describe("View + Component", function(){
<form id="form" phx-change="validate">
<label for="first_name">First Name</label>
<input id="first_name" value="" name="user[first_name]" />
<span class="feedback" phx-feedback-for="user[first_name]">can't be blank</span>
<span class="feedback">can't be blank</span>
<label for="last_name">Last Name</label>
<input id="last_name" value="" name="user[last_name]" />
<span class="feedback" phx-feedback-for="user[last_name]">can't be blank</span>
<span class="feedback">can't be blank</span>
</form>
`],
fingerprint: 345
Expand Down
9 changes: 0 additions & 9 deletions guides/cheatsheets/html-attrs.cheatmd
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,6 @@ Attribute values can be:
|------------------------------------------------------------------------------|--------------------------------------|
| [`phx-disable-with`](../client/form-bindings.md#javascript-client-specifics) | Text to show during event submission |

### On container elements

LiveView applies the `phx-no-feedback` CSS class before user interaction.

| Attribute | Value |
|-------------------------------------------------------------------|------------------------------------------------|
| [`phx-feedback-for`](../client/form-bindings.md#error-feedback) | Form field or group name |
| [`phx-feedback-group`](../client/form-bindings.md#error-feedback) | Arbitrary string to identify a group of fields |

### Form Example

#### lib/hello_web/live/hello_live.html.heex
Expand Down
2 changes: 1 addition & 1 deletion guides/client/bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ callback, for example:
|------------------------|------------|
| [Params](#click-events) | `phx-value-*` |
| [Click Events](#click-events) | `phx-click`, `phx-click-away` |
| [Form Events](form-bindings.md) | `phx-change`, `phx-submit`, `phx-feedback-for`, `phx-feedback-group`, `phx-disable-with`, `phx-trigger-action`, `phx-auto-recover` |
| [Form Events](form-bindings.md) | `phx-change`, `phx-submit`, `phx-disable-with`, `phx-trigger-action`, `phx-auto-recover` |
| [Focus Events](#focus-and-blur-events) | `phx-blur`, `phx-focus`, `phx-window-blur`, `phx-window-focus` |
| [Key Events](#key-events) | `phx-keydown`, `phx-keyup`, `phx-window-keydown`, `phx-window-keyup`, `phx-key` |
| [Scroll Events](#scroll-events-and-infinite-stream-pagination) | `phx-viewport-top`, `phx-viewport-bottom` |
Expand Down
45 changes: 11 additions & 34 deletions guides/client/form-bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,46 +106,23 @@ _Note_: only the individual input is sent as params for an input marked with `ph

## Error Feedback

For proper error feedback on form updates, the error tags must specify which
input they belong to. This is accomplished with `phx-feedback-for`.
For proper error feedback on form updates, LiveView sends special parameters on form events
starting with `_unused_` to indicate that the input for the specific field has not been interacted with yet.

The `phx-feedback-for` annotation specifies the name (or id, for backwards compatibility) of the input it belongs to. Failing to add the `phx-feedback-for` attribute will result in displaying error messages for form fields that the user has not changed yet (e.g. required
fields further down on the page).
When creating a form from these parameters through `Phoenix.Component.to_form/2` or `Phoenix.Component.form/1`,
`Phoenix.Component.used_input?/1` can be used to filter error messages.

For example, your `MyAppWeb.CoreComponents` may use this function:

def input(assigns) do
~H"""
<div phx-feedback-for={@name}>
<input
type={@type}
name={@name}
id={@id || @name}
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
class={[
"phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
"border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5",
]}
{@rest}
/>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []

def error(assigns) do
~H"""
<p class="phx-no-feedback:hidden">
<Heroicons.exclamation_circle mini class="mt-0.5 h-5 w-5 flex-none fill-rose-500" />
<%= render_slot(@inner_block) %>
</p>
"""
end
assigns
|> assign(field: nil, id: assigns.id || field.id)
|> assign(:errors, Enum.map(field.errors, &translate_error(&1)))
|> assign(:errors, Enum.map(errors, &translate_error(&1)))

Now, any DOM container with the `phx-feedback-for` attribute will receive a
`phx-no-feedback` class in cases where the form fields has yet to receive
user input/focus. Using new CSS rules or tailwindcss variants allows you
errors to be shown, hidden, and styled as feedback changes.
Now, only errors for fields that were interacted with are shown.

## Number inputs

Expand Down

0 comments on commit 35d72c7

Please sign in to comment.