-
Notifications
You must be signed in to change notification settings - Fork 84
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This PR changes the meaning of `checked` as specified in the `html!` macro. Originally, specifying `checked` would add or remove the `checked` HTML attribute. This changed the default checkedness of the input element, but may or may not actually change the rendered state of the element. Now, specifying `checked` directly specifies the rendered state of the element. (The default checkedness is not modified, and can be set manually.) --- ## Motivation This change makes it easier to do the more commonly-desired task of specifying the rendered state of the checkbox (likely according to application state) rather than merely the default checkedness. This change mitigates shooting ourselves in the foot by assuming `checked` sets a rendered checkbox's checkedness. --- Further discussion is available in the issue #205 as well as in the new book entry and tests https://github.com/SFBdragon/percy/tree/checked_sets_checkedness/book/src/html-macro/special-attributes https://github.com/SFBdragon/percy/blob/checked_sets_checkedness/crates/percy-dom/tests/checked_attribute.rs This resolves #205 for the time being.
- Loading branch information
Showing
15 changed files
with
418 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# Special Virtual DOM Attributes | ||
|
||
Some virtual DOM attributes do not merely set or remove the corresponding HTML attribute of the same name. | ||
|
||
## `checked` | ||
|
||
According to the [HTML spec](https://html.spec.whatwg.org/multipage/input.html#attr-input-checked), the `checked` HTML attribute only controls the default checkedness. | ||
Changing the `checked` HTML attribute may not cause the checkbox's checkedness to change. | ||
|
||
By contrast: specifying `html! { <input checked={bool} /> }` causes `percy` to always render the checkbox with the specified checkedness. | ||
- If the VDOM is updated from `html! { <input checked=true /> }` to `html { <input checked=false /> }`, the input element's checkedness will definitely change. | ||
- If the VDOM is updated from `html! { <input checked=true /> }` to `html { <input checked=true /> }`, the input element's checkedness will be reverted to `true` even if the user interacted with the checkbox in between. | ||
|
||
`percy-dom` updates the `checked` property (current checkedness, not reflected in HTML). | ||
|
||
This behavior is more desirable because `percy` developers are accustomed to declaratively controlling the DOM and rendered HTML. | ||
|
||
`percy-dom` does not affect the presence of the `checked` attribute (default checkedness, reflected in HTML). | ||
|
||
This means that if you need to configure the `checked` attribute (this should almost never be the case if your GUI state is driven by the backend state) then `percy-dom` won't get in your way. | ||
|
||
## `value` | ||
|
||
According to the [HTML spec](https://html.spec.whatwg.org/multipage/input.html#attr-input-value), the `value` HTML attribute only controls the default value. | ||
Changing the `value` HTML attribute may not cause the input element's value to change. | ||
|
||
By contrast: specifying `html! { <input value="..." /> }` causes `percy` to always render the input element with the specified value. | ||
- If the VDOM is updated from `html! { <input value="hello" /> }` to `html { <input value="goodbye" /> }`, the input element's value will definitely change. | ||
- If the VDOM is updated from `html! { <input value="hello" /> }` to `html { <input value="hello" /> }`, the input element's value will be reverted to `"hello"` even if the user interacted with the input element in between. | ||
|
||
`percy` updates both | ||
- the `value` attribute (default value, reflected in HTML) and, | ||
- the `value` property (current value, not reflected in HTML). | ||
|
||
Setting the `value` property is desirable because `percy` developers are accustomed to declaratively controlling the DOM and rendered HTML. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
//! `percy-dom` treats the `checked` virtual node attribute specially. | ||
//! `percy-dom` sets the `checked` element property (actual checkedness), | ||
//! not the `checked` HTML attribute (default checkedness). | ||
//! | ||
//! Developers, are likely to assume that `checked` specifies the state of the checkbox | ||
//! directly. `percy-dom` ensures that this is true. | ||
//! | ||
//! See the tests for more details. Start with [`patch_uses_set_checked_function`]. | ||
//! | ||
//! To run all tests in this file: | ||
//! | ||
//! ```sh | ||
//! wasm-pack test --chrome --headless crates/percy-dom --test checked_property | ||
//! ``` | ||
|
||
use wasm_bindgen_test::*; | ||
|
||
use percy_dom::event::VirtualEvents; | ||
use wasm_bindgen::JsCast; | ||
use web_sys::*; | ||
|
||
use percy_dom::prelude::*; | ||
|
||
wasm_bindgen_test_configure!(run_in_browser); | ||
|
||
/// Verify that `percy_dom::patch` uses `set_checked` to set the checkedness | ||
/// of an input element when specified. | ||
/// | ||
/// ## Why? | ||
/// | ||
/// The `checked` HTML attribute only determines the default checkedness. | ||
/// The browser uses the default checkedness as the checkbox's | ||
/// checkedness until the user clicks on the checkbox, setting the "dirty checked" browser | ||
/// flag https://html.spec.whatwg.org/multipage/input.html#concept-input-checked-dirty | ||
/// which results in the browser maintaining the checkbox's state INDEPENDENTLY from the | ||
/// default checked state. i.e. Changing the default checkedness no longer affects the actual | ||
/// checkedness after the user has pressed the input. | ||
/// | ||
/// We want `html!{ ... checked=val ... }` to specify the current checkedness of the checkbox | ||
/// directly - avoiding the checkbox being toggled differently to what the developer | ||
/// specified in the virtual DOM. | ||
/// | ||
/// Using web-sys's `set_checked` sets the actual checkbox's checkedness. Futhermore, it enables the | ||
/// dirty-checked flag (NB: BUT ONLY WHEN THE CHECKBOX STATE IS CHANGED), which we can test for. | ||
/// | ||
/// ## Test approach | ||
/// | ||
/// - Create a virtual node with the checkbox having checkedness !C, and patch it to have checkedness C. | ||
/// (This should cause the dirty flag to be set IF `set_checked` is used.) | ||
/// - Assert that the corresponding DOM element has checkedness of C. | ||
/// | ||
/// - Now, remove the attribute if the checkbox is checked, or set the attribute if not. | ||
/// (The checkbox should hold its state as the dirty flag is checked, therefore | ||
/// changing the default checkedness through the `checked` attribute no longer | ||
/// should affect the checkedness of the checkbox.) | ||
/// - Assert that the checkedness of the checkbox element is still B. | ||
#[wasm_bindgen_test] | ||
fn patch_sets_checked_property() { | ||
for checkedness in [false, true] { | ||
let start_input = html! {<input checked={!checkedness}>}; | ||
let end_input = html! {<input checked=checkedness>}; | ||
|
||
let mut events = VirtualEvents::new(); | ||
let (input_node, enode) = start_input.create_dom_node(&mut events); | ||
events.set_root(enode); | ||
|
||
let input_elem = input_node.dyn_ref::<HtmlInputElement>().unwrap(); | ||
|
||
let patches = percy_dom::diff(&start_input, &end_input); | ||
percy_dom::patch(input_node.clone(), &end_input, &mut events, &patches).unwrap(); | ||
assert_eq!(input_elem.checked(), checkedness); | ||
|
||
if checkedness { | ||
input_elem.remove_attribute("checked").unwrap(); | ||
} else { | ||
input_elem.set_attribute("checked", "").unwrap(); | ||
} | ||
assert_eq!(input_elem.checked(), checkedness); | ||
} | ||
} | ||
|
||
/// Verify that `percy_dom::patch` uses `set_checked` to set the `checked` property | ||
/// of an input element even if the the specified `checked` value does not change | ||
/// between the `old` and `new` virtual nodes. | ||
/// | ||
/// ## Why? | ||
/// | ||
/// Note: the rationale given in [`patch_sets_checked_property`] is prerequisite reading. | ||
/// | ||
/// The user might interact with the checkbox in between the previous render and the next | ||
/// one, changing the checkedness state in the browser, but `diff` would not realize this | ||
/// assuming the rendered `checked` value does not change. | ||
/// | ||
/// For example: | ||
/// - Developer renders `html! { ... checked=true ... }` | ||
/// - User clicks on the checkbox, changing the browser's checkbox checkedness to false. | ||
/// - Developer renders `html! { ... checked=true ... }` | ||
/// - `diff` doesn't realize anything needs to change, so it doesn't issue any changes. | ||
/// - Developer is still trying to render the checkbox as checked but the browser checkbox | ||
/// stays unchecked. | ||
/// | ||
/// If `percy_dom::diff` always specifies that `percy_dom::patch` should set the `checked` | ||
/// property if its specified, then the above cannot happen. The element's checked state | ||
/// will be fixed when `percy_dom::patch` is called, keeping the developer-specified `checked` | ||
/// value and the checkbox element's visual state in sync. | ||
/// | ||
/// ## Test approach | ||
/// | ||
/// - Create a a DOM node with the checkbox having checkedness C. | ||
/// - Set it's checkedness to be !C. | ||
/// - Diff and patch with the virtual node still specifying it's checkedness as C. | ||
/// - Assert that the checkedness has been reset to C, even though the virtual node did not change. | ||
#[wasm_bindgen_test] | ||
fn patch_always_sets_checked_property() { | ||
for checkedness in [false, true] { | ||
let start_input = html! {<input checked=checkedness>}; | ||
let end_input = html! {<input checked=checkedness>}; | ||
|
||
let mut events = VirtualEvents::new(); | ||
let (input_node, enode) = start_input.create_dom_node(&mut events); | ||
events.set_root(enode); | ||
|
||
let input_elem = input_node.dyn_ref::<HtmlInputElement>().unwrap(); | ||
assert_eq!(input_elem.checked(), checkedness); | ||
|
||
input_elem.set_checked(!checkedness); // modify checked property | ||
|
||
let patches = percy_dom::diff(&start_input, &end_input); | ||
percy_dom::patch(input_node.clone(), &end_input, &mut events, &patches).unwrap(); | ||
|
||
assert_eq!(input_elem.checked(), checkedness); | ||
} | ||
} | ||
|
||
/// Verify that `percy_dom::patch` does not set the default checkedness. | ||
/// That is to say: it does NOT manipulate the HTML `checked` attribute. | ||
/// | ||
/// ## Why? | ||
/// | ||
/// Note: the rationale given in [`patch_sets_checked_property`] is prerequisite reading. | ||
/// | ||
/// `percy` is intended to serve the developer who is making a nontrivial app who's state | ||
/// is driven by the backend of the application. The application state is _not_ driven by | ||
/// the frontend. Therefore the state of checkboxes is specified explicitly idiomatically. | ||
/// | ||
/// `percy-dom` does not currently allow for a way to directly manipulate the default | ||
/// checkedness of an input element. Default checkedness is not useful unless the state | ||
/// of an input element is driven by the frontend. | ||
/// | ||
/// Users may want to break out of this mold however, and modify the default checkedness. | ||
/// In this case, it's much simpler if `percy` doesn't change the default checkedness | ||
/// unnecessarily. Equivalently, `percy-dom` does not manipulate the presence of the HTML | ||
/// `checked` attribute. | ||
/// | ||
/// ## Test approach | ||
/// | ||
/// - Create an input element with checkedness C. | ||
/// - Set the presence of the HTML attribute (default checkedness) to be D. | ||
/// - Update the input element's checkedness to be E. | ||
/// - Assert that the default checkedness is still D. | ||
#[wasm_bindgen_test] | ||
fn patch_does_not_set_default_checkedness() { | ||
for prev_checkedness in [false, true] { | ||
for next_checkedness in [false, true] { | ||
for default_checkedness in [false, true] { | ||
let start_input = html! {<input checked=prev_checkedness>}; | ||
let end_input = html! {<input checked=next_checkedness>}; | ||
|
||
let mut events = VirtualEvents::new(); | ||
let (input_node, enode) = start_input.create_dom_node(&mut events); | ||
events.set_root(enode); | ||
|
||
let input_elem = input_node.dyn_ref::<HtmlInputElement>().unwrap(); | ||
assert_eq!(input_elem.checked(), prev_checkedness); | ||
|
||
// Modify presence of `checked`` attribute. | ||
input_elem.set_default_checked(default_checkedness); | ||
|
||
let patches = percy_dom::diff(&start_input, &end_input); | ||
percy_dom::patch(input_node.clone(), &end_input, &mut events, &patches).unwrap(); | ||
|
||
assert_eq!(input_elem.default_checked(), default_checkedness); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.