From 0562690341b3123da5c93446b3fa99dc7a43fcb1 Mon Sep 17 00:00:00 2001 From: Christopher Fraser Date: Tue, 10 Sep 2024 20:54:30 +1000 Subject: [PATCH] Checkbox internal state (#131) * Set checkbox internal state instead of attribute * Move checked state to NodeSpecificData instead of simply reflecting in checked attribute --- examples/form.rs | 9 ++- packages/blitz/src/renderer/render.rs | 8 ++- .../src/documents/dioxus_document.rs | 62 ++++++++++++++++--- packages/dom/src/document.rs | 26 ++------ packages/dom/src/layout/construct.rs | 17 +++++ packages/dom/src/node.rs | 17 +++++ packages/dom/src/stylo.rs | 6 +- 7 files changed, 110 insertions(+), 35 deletions(-) diff --git a/examples/form.rs b/examples/form.rs index f9dbc307..22610dc7 100644 --- a/examples/form.rs +++ b/examples/form.rs @@ -20,11 +20,10 @@ fn app() -> Element { id: "check1", name: "check1", value: "check1", - checked: Some("").filter(|_| checkbox_checked()), - oninput: move |ev| { - dbg!(ev); - checkbox_checked.set(!checkbox_checked()); - }, + checked: checkbox_checked(), + // This works too + // checked: "{checkbox_checked}", + oninput: move |ev| checkbox_checked.set(!ev.checked()), } label { r#for: "check1", diff --git a/packages/blitz/src/renderer/render.rs b/packages/blitz/src/renderer/render.rs index f07894d7..b46a8ba5 100644 --- a/packages/blitz/src/renderer/render.rs +++ b/packages/blitz/src/renderer/render.rs @@ -1475,7 +1475,13 @@ impl ElementCx<'_> { if self.element.local_name() == "input" && matches!(self.element.attr(local_name!("type")), Some("checkbox")) { - let checked = self.element.attr(local_name!("checked")).is_some(); + let Some(checked) = self + .element + .element_data() + .and_then(|data| data.checkbox_input_checked()) + else { + return; + }; let disabled = self.element.attr(local_name!("disabled")).is_some(); // TODO this should be coming from css accent-color, but I couldn't find how to retrieve it diff --git a/packages/dioxus-blitz/src/documents/dioxus_document.rs b/packages/dioxus-blitz/src/documents/dioxus_document.rs index f5239163..f933a4db 100644 --- a/packages/dioxus-blitz/src/documents/dioxus_document.rs +++ b/packages/dioxus-blitz/src/documents/dioxus_document.rs @@ -3,8 +3,11 @@ use std::{collections::HashMap, rc::Rc}; use blitz_dom::{ - events::EventData, local_name, namespace_url, node::Attribute, ns, Atom, Document, - DocumentLike, ElementNodeData, Node, NodeData, QualName, TextNodeData, Viewport, DEFAULT_CSS, + events::EventData, + local_name, namespace_url, + node::{Attribute, NodeSpecificData}, + ns, Atom, Document, DocumentLike, ElementNodeData, Node, NodeData, QualName, TextNodeData, + Viewport, DEFAULT_CSS, }; use dioxus::{ @@ -188,7 +191,10 @@ impl DioxusDocument { // - if value is not specified, it defaults to 'on' if let Some(name) = form_input.attr(local_name!("name")) { if form_input.attr(local_name!("type")) == Some("checkbox") - && form_input.attr(local_name!("checked")) == Some("true") + && form_input + .element_data() + .and_then(|data| data.checkbox_input_checked()) + .unwrap_or(false) { let value = form_input .attr(local_name!("value")) @@ -202,13 +208,14 @@ impl DioxusDocument { } else { Default::default() }; - let form_data = NativeFormData { - value: element_node_data + let value = match element_node_data.node_specific_data { + NodeSpecificData::CheckboxInput(checked) => checked.to_string(), + _ => element_node_data .attr(local_name!("value")) .unwrap_or_default() .to_string(), - values, }; + let form_data = NativeFormData { value, values }; Rc::new(PlatformEventData::new(Box::new(form_data))) } @@ -561,8 +568,11 @@ impl WriteMutations for MutationWriter<'_> { let node_id = self.state.element_to_node_id(id); let node = self.doc.get_node_mut(node_id).unwrap(); if let NodeData::Element(ref mut element) = node.raw_dom_data { - // FIXME: support non-text attributes - if let AttributeValue::Text(val) = value { + if element.name.local == local_name!("input") && name == "checked" { + set_input_checked_state(element, value); + } + // FIXME: support other non-text attributes + else if let AttributeValue::Text(val) = value { // FIXME check namespace let existing_attr = element .attrs @@ -668,6 +678,42 @@ impl WriteMutations for MutationWriter<'_> { } } +/// Set 'checked' state on an input based on given attributevalue +fn set_input_checked_state(element: &mut ElementNodeData, value: &AttributeValue) { + let checked: bool; + match value { + AttributeValue::Bool(checked_bool) => { + checked = *checked_bool; + } + AttributeValue::Text(val) => { + if let Ok(checked_bool) = val.parse() { + checked = checked_bool; + } else { + return; + }; + } + _ => { + return; + } + }; + match element.node_specific_data { + NodeSpecificData::CheckboxInput(ref mut checked_mut) => *checked_mut = checked, + // If we have just constructed the element, set the node attribute, + // and NodeSpecificData will be created from that later + // this simulates the checked attribute being set in html, + // and the element's checked property being set from that + NodeSpecificData::None => element.attrs.push(Attribute { + name: QualName { + prefix: None, + ns: ns!(html), + local: local_name!("checked"), + }, + value: checked.to_string(), + }), + _ => {} + } +} + fn create_template_node(doc: &mut Document, node: &TemplateNode) -> NodeId { match node { TemplateNode::Element { diff --git a/packages/dom/src/document.rs b/packages/dom/src/document.rs index 866d4684..1a8e0dac 100644 --- a/packages/dom/src/document.rs +++ b/packages/dom/src/document.rs @@ -1,8 +1,8 @@ use crate::events::{EventData, HitResult, RendererEvent}; -use crate::node::{Attribute, NodeSpecificData, TextBrush}; +use crate::node::{NodeSpecificData, TextBrush}; use crate::{ElementNodeData, Node, NodeData, TextNodeData, Viewport}; use app_units::Au; -use html5ever::{local_name, namespace_url, ns, QualName}; +use html5ever::local_name; use peniko::kurbo; // use quadtree_rs::Quadtree; use parley::editor::{PointerButton, TextEvent}; @@ -313,24 +313,10 @@ impl Document { } pub fn toggle_checkbox(el: &mut ElementNodeData) { - let is_checked = el - .attrs - .iter() - .any(|attr| attr.name.local == local_name!("checked")); - - if is_checked { - el.attrs - .retain(|attr| attr.name.local != local_name!("checked")) - } else { - el.attrs.push(Attribute { - name: QualName { - prefix: None, - ns: ns!(html), - local: local_name!("checked"), - }, - value: String::new(), - }) - } + let Some(is_checked) = el.checkbox_input_checked_mut() else { + return; + }; + *is_checked = !*is_checked; } pub fn root_node(&self) -> &Node { diff --git a/packages/dom/src/layout/construct.rs b/packages/dom/src/layout/construct.rs index bf62b0a3..2b61e2b2 100644 --- a/packages/dom/src/layout/construct.rs +++ b/packages/dom/src/layout/construct.rs @@ -44,6 +44,9 @@ pub(crate) fn collect_layout_children( ) { create_text_editor(doc, container_node_id, false); return; + } else if type_attr == Some("checkbox") { + create_checkbox_input(doc, container_node_id); + return; } } @@ -303,6 +306,20 @@ fn create_text_editor(doc: &mut Document, input_element_id: usize, is_multiline: } } +fn create_checkbox_input(doc: &mut Document, input_element_id: usize) { + let node = &mut doc.nodes[input_element_id]; + + let element = &mut node.raw_dom_data.downcast_element_mut().unwrap(); + if !matches!( + element.node_specific_data, + NodeSpecificData::CheckboxInput(_) + ) { + let checked = element.attr_parsed(local_name!("checked")).unwrap_or(false); + + element.node_specific_data = NodeSpecificData::CheckboxInput(checked); + } +} + pub(crate) fn build_inline_layout( doc: &mut Document, inline_context_root_node_id: usize, diff --git a/packages/dom/src/node.rs b/packages/dom/src/node.rs index 870ba44b..1a0da2ba 100644 --- a/packages/dom/src/node.rs +++ b/packages/dom/src/node.rs @@ -391,6 +391,20 @@ impl ElementNodeData { } } + pub fn checkbox_input_checked(&self) -> Option { + match self.node_specific_data { + NodeSpecificData::CheckboxInput(checked) => Some(checked), + _ => None, + } + } + + pub fn checkbox_input_checked_mut(&mut self) -> Option<&mut bool> { + match self.node_specific_data { + NodeSpecificData::CheckboxInput(ref mut checked) => Some(checked), + _ => None, + } + } + pub fn inline_layout_data(&self) -> Option<&TextLayout> { match self.node_specific_data { NodeSpecificData::InlineRoot(ref data) => Some(data), @@ -503,6 +517,8 @@ pub enum NodeSpecificData { TableRoot(Arc), /// Parley text editor (text inputs) TextInput(TextInputData), + /// Checkbox checked state + CheckboxInput(bool), /// No data (for nodes that don't need any node-specific data) None, } @@ -515,6 +531,7 @@ impl std::fmt::Debug for NodeSpecificData { NodeSpecificData::InlineRoot(_) => f.write_str("NodeSpecificData::InlineRoot"), NodeSpecificData::TableRoot(_) => f.write_str("NodeSpecificData::TableRoot"), NodeSpecificData::TextInput(_) => f.write_str("NodeSpecificData::TextInput"), + NodeSpecificData::CheckboxInput(_) => f.write_str("NodeSpecificData::CheckboxInput"), NodeSpecificData::None => f.write_str("NodeSpecificData::None"), } } diff --git a/packages/dom/src/stylo.rs b/packages/dom/src/stylo.rs index 7ea9e0eb..408483a5 100644 --- a/packages/dom/src/stylo.rs +++ b/packages/dom/src/stylo.rs @@ -407,7 +407,11 @@ impl<'a> selectors::Element for BlitzNode<'a> { && elem.attr(local_name!("href")).is_some() }) .unwrap_or(false), - NonTSPseudoClass::Checked => false, + NonTSPseudoClass::Checked => self + .raw_dom_data + .downcast_element() + .and_then(|elem| elem.checkbox_input_checked()) + .unwrap_or(false), NonTSPseudoClass::Valid => false, NonTSPseudoClass::Invalid => false, NonTSPseudoClass::Defined => false,