mirror of https://github.com/linebender/xilem
add class and style interfaces (#193)
* add class interface * add style interface * remove parse style/class logic, and panic instead (in dev) * combine attributes, styles, classes into single struct * make `class` and `style` methods polymorphic * fix clippy * Update Cargo.toml Co-authored-by: Philipp Mildenberger <philipp@mildenberger.me> * Update crates/xilem_web/src/context.rs Co-authored-by: Philipp Mildenberger <philipp@mildenberger.me> * Update crates/xilem_web/src/context.rs Co-authored-by: Philipp Mildenberger <philipp@mildenberger.me> * Update crates/xilem_web/src/class.rs Co-authored-by: Philipp Mildenberger <philipp@mildenberger.me> --------- Co-authored-by: Philipp Mildenberger <philipp@mildenberger.me>
This commit is contained in:
parent
072358e293
commit
74bc6fcee8
|
@ -34,7 +34,9 @@ peniko = { git = "https://github.com/linebender/peniko", rev = "629fc3325b016a8c
|
|||
version = "0.3.4"
|
||||
features = [
|
||||
"console",
|
||||
"CssStyleDeclaration",
|
||||
"Document",
|
||||
"DomTokenList",
|
||||
"Element",
|
||||
"Event",
|
||||
"HtmlElement",
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
use std::{borrow::Cow, marker::PhantomData};
|
||||
|
||||
use xilem_core::{Id, MessageResult};
|
||||
|
||||
use crate::{
|
||||
interfaces::{sealed::Sealed, Element},
|
||||
ChangeFlags, Cx, View, ViewMarker,
|
||||
};
|
||||
|
||||
/// A trait to make the class adding functions generic over collection type
|
||||
pub trait IntoClasses {
|
||||
fn into_classes(self, classes: &mut Vec<Cow<'static, str>>);
|
||||
}
|
||||
|
||||
impl IntoClasses for String {
|
||||
fn into_classes(self, classes: &mut Vec<Cow<'static, str>>) {
|
||||
classes.push(self.into());
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoClasses for &'static str {
|
||||
fn into_classes(self, classes: &mut Vec<Cow<'static, str>>) {
|
||||
classes.push(self.into());
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoClasses for Cow<'static, str> {
|
||||
fn into_classes(self, classes: &mut Vec<Cow<'static, str>>) {
|
||||
classes.push(self);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoClasses for Option<T>
|
||||
where
|
||||
T: IntoClasses,
|
||||
{
|
||||
fn into_classes(self, classes: &mut Vec<Cow<'static, str>>) {
|
||||
if let Some(t) = self {
|
||||
t.into_classes(classes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoClasses for Vec<T>
|
||||
where
|
||||
T: IntoClasses,
|
||||
{
|
||||
fn into_classes(self, classes: &mut Vec<Cow<'static, str>>) {
|
||||
for itm in self {
|
||||
itm.into_classes(classes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_tuple_intoclasses {
|
||||
($($name:ident : $type:ident),* $(,)?) => {
|
||||
impl<$($type),*> IntoClasses for ($($type,)*)
|
||||
where
|
||||
$($type: IntoClasses),*
|
||||
{
|
||||
#[allow(unused_variables)]
|
||||
fn into_classes(self, classes: &mut Vec<Cow<'static, str>>) {
|
||||
let ($($name,)*) = self;
|
||||
$(
|
||||
$name.into_classes(classes);
|
||||
)*
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_tuple_intoclasses!();
|
||||
impl_tuple_intoclasses!(t1: T1);
|
||||
impl_tuple_intoclasses!(t1: T1, t2: T2);
|
||||
impl_tuple_intoclasses!(t1: T1, t2: T2, t3: T3);
|
||||
impl_tuple_intoclasses!(t1: T1, t2: T2, t3: T3, t4: T4);
|
||||
|
||||
/// Applies a class to the underlying element.
|
||||
pub struct Class<E, T, A> {
|
||||
pub(crate) element: E,
|
||||
pub(crate) class_names: Vec<Cow<'static, str>>,
|
||||
pub(crate) phantom: PhantomData<fn() -> (T, A)>,
|
||||
}
|
||||
|
||||
impl<E, T, A> ViewMarker for Class<E, T, A> {}
|
||||
impl<E, T, A> Sealed for Class<E, T, A> {}
|
||||
|
||||
impl<E: Element<T, A>, T, A> View<T, A> for Class<E, T, A> {
|
||||
type State = E::State;
|
||||
type Element = E::Element;
|
||||
|
||||
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
|
||||
for class_name in &self.class_names {
|
||||
cx.add_class_to_element(class_name);
|
||||
}
|
||||
self.element.build(cx)
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
&self,
|
||||
cx: &mut Cx,
|
||||
prev: &Self,
|
||||
id: &mut Id,
|
||||
state: &mut Self::State,
|
||||
element: &mut Self::Element,
|
||||
) -> ChangeFlags {
|
||||
for class_name in &self.class_names {
|
||||
cx.add_class_to_element(class_name);
|
||||
}
|
||||
self.element.rebuild(cx, &prev.element, id, state, element)
|
||||
}
|
||||
|
||||
fn message(
|
||||
&self,
|
||||
id_path: &[Id],
|
||||
state: &mut Self::State,
|
||||
message: Box<dyn std::any::Any>,
|
||||
app_state: &mut T,
|
||||
) -> MessageResult<A> {
|
||||
self.element.message(id_path, state, message, app_state)
|
||||
}
|
||||
}
|
||||
|
||||
crate::interfaces::impl_dom_interfaces_for_ty!(Element, Class);
|
|
@ -16,6 +16,131 @@ use crate::{
|
|||
|
||||
type CowStr = std::borrow::Cow<'static, str>;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct HtmlProps {
|
||||
pub(crate) attributes: VecMap<CowStr, AttributeValue>,
|
||||
pub(crate) classes: VecMap<CowStr, ()>,
|
||||
pub(crate) styles: VecMap<CowStr, CowStr>,
|
||||
}
|
||||
|
||||
impl HtmlProps {
|
||||
fn apply(&mut self, el: &web_sys::Element) -> Self {
|
||||
let attributes = self.apply_attributes(el);
|
||||
let classes = self.apply_classes(el);
|
||||
let styles = self.apply_styles(el);
|
||||
Self {
|
||||
attributes,
|
||||
classes,
|
||||
styles,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_attributes(&mut self, element: &web_sys::Element) -> VecMap<CowStr, AttributeValue> {
|
||||
let mut attributes = VecMap::default();
|
||||
std::mem::swap(&mut attributes, &mut self.attributes);
|
||||
for (name, value) in attributes.iter() {
|
||||
set_attribute(element, name, &value.serialize());
|
||||
}
|
||||
attributes
|
||||
}
|
||||
|
||||
fn apply_classes(&mut self, element: &web_sys::Element) -> VecMap<CowStr, ()> {
|
||||
let mut classes = VecMap::default();
|
||||
std::mem::swap(&mut classes, &mut self.classes);
|
||||
for (class_name, ()) in classes.iter() {
|
||||
set_class(element, class_name);
|
||||
}
|
||||
classes
|
||||
}
|
||||
|
||||
fn apply_styles(&mut self, element: &web_sys::Element) -> VecMap<CowStr, CowStr> {
|
||||
let mut styles = VecMap::default();
|
||||
std::mem::swap(&mut styles, &mut self.styles);
|
||||
for (name, value) in styles.iter() {
|
||||
set_style(element, name, value);
|
||||
}
|
||||
styles
|
||||
}
|
||||
|
||||
fn apply_changes(&mut self, element: &web_sys::Element, props: &mut HtmlProps) -> ChangeFlags {
|
||||
self.apply_attribute_changes(element, &mut props.attributes)
|
||||
| self.apply_class_changes(element, &mut props.classes)
|
||||
| self.apply_style_changes(element, &mut props.styles)
|
||||
}
|
||||
|
||||
pub(crate) fn apply_attribute_changes(
|
||||
&mut self,
|
||||
element: &web_sys::Element,
|
||||
attributes: &mut VecMap<CowStr, AttributeValue>,
|
||||
) -> ChangeFlags {
|
||||
let mut changed = ChangeFlags::empty();
|
||||
// update attributes
|
||||
for itm in diff_kv_iterables(&*attributes, &self.attributes) {
|
||||
match itm {
|
||||
Diff::Add(name, value) | Diff::Change(name, value) => {
|
||||
set_attribute(element, name, &value.serialize());
|
||||
changed |= ChangeFlags::OTHER_CHANGE;
|
||||
}
|
||||
Diff::Remove(name) => {
|
||||
remove_attribute(element, name);
|
||||
changed |= ChangeFlags::OTHER_CHANGE;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::mem::swap(attributes, &mut self.attributes);
|
||||
self.attributes.clear();
|
||||
changed
|
||||
}
|
||||
|
||||
pub(crate) fn apply_class_changes(
|
||||
&mut self,
|
||||
element: &web_sys::Element,
|
||||
classes: &mut VecMap<CowStr, ()>,
|
||||
) -> ChangeFlags {
|
||||
let mut changed = ChangeFlags::empty();
|
||||
// update attributes
|
||||
for itm in diff_kv_iterables(&*classes, &self.classes) {
|
||||
match itm {
|
||||
Diff::Add(class_name, ()) | Diff::Change(class_name, ()) => {
|
||||
set_class(element, class_name);
|
||||
changed |= ChangeFlags::OTHER_CHANGE;
|
||||
}
|
||||
Diff::Remove(class_name) => {
|
||||
remove_class(element, class_name);
|
||||
changed |= ChangeFlags::OTHER_CHANGE;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::mem::swap(classes, &mut self.classes);
|
||||
self.classes.clear();
|
||||
changed
|
||||
}
|
||||
|
||||
pub(crate) fn apply_style_changes(
|
||||
&mut self,
|
||||
element: &web_sys::Element,
|
||||
styles: &mut VecMap<CowStr, CowStr>,
|
||||
) -> ChangeFlags {
|
||||
let mut changed = ChangeFlags::empty();
|
||||
// update attributes
|
||||
for itm in diff_kv_iterables(&*styles, &self.styles) {
|
||||
match itm {
|
||||
Diff::Add(name, value) | Diff::Change(name, value) => {
|
||||
set_style(element, name, value);
|
||||
changed |= ChangeFlags::OTHER_CHANGE;
|
||||
}
|
||||
Diff::Remove(name) => {
|
||||
remove_style(element, name);
|
||||
changed |= ChangeFlags::OTHER_CHANGE;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::mem::swap(styles, &mut self.styles);
|
||||
self.styles.clear();
|
||||
changed
|
||||
}
|
||||
}
|
||||
|
||||
fn set_attribute(element: &web_sys::Element, name: &str, value: &str) {
|
||||
// we have to special-case `value` because setting the value using `set_attribute`
|
||||
// doesn't work after the value has been changed.
|
||||
|
@ -41,12 +166,52 @@ fn remove_attribute(element: &web_sys::Element, name: &str) {
|
|||
}
|
||||
}
|
||||
|
||||
fn set_class(element: &web_sys::Element, class_name: &str) {
|
||||
debug_assert!(
|
||||
!class_name.is_empty(),
|
||||
"class names cannot be the empty string"
|
||||
);
|
||||
debug_assert!(
|
||||
!class_name.contains(' '),
|
||||
"class names cannot contain the ascii space character"
|
||||
);
|
||||
element.class_list().add_1(class_name).unwrap_throw();
|
||||
}
|
||||
|
||||
fn remove_class(element: &web_sys::Element, class_name: &str) {
|
||||
debug_assert!(
|
||||
!class_name.is_empty(),
|
||||
"class names cannot be the empty string"
|
||||
);
|
||||
debug_assert!(
|
||||
!class_name.contains(' '),
|
||||
"class names cannot contain the ascii space character"
|
||||
);
|
||||
element.class_list().remove_1(class_name).unwrap_throw();
|
||||
}
|
||||
|
||||
fn set_style(element: &web_sys::Element, name: &str, value: &str) {
|
||||
if let Some(el) = element.dyn_ref::<web_sys::HtmlElement>() {
|
||||
el.style().set_property(name, value).unwrap_throw();
|
||||
} else if let Some(el) = element.dyn_ref::<web_sys::SvgElement>() {
|
||||
el.style().set_property(name, value).unwrap_throw();
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_style(element: &web_sys::Element, name: &str) {
|
||||
if let Some(el) = element.dyn_ref::<web_sys::HtmlElement>() {
|
||||
el.style().remove_property(name).unwrap_throw();
|
||||
} else if let Some(el) = element.dyn_ref::<web_sys::SvgElement>() {
|
||||
el.style().remove_property(name).unwrap_throw();
|
||||
}
|
||||
}
|
||||
|
||||
// Note: xilem has derive Clone here. Not sure.
|
||||
pub struct Cx {
|
||||
id_path: IdPath,
|
||||
document: Document,
|
||||
// TODO There's likely a cleaner more robust way to propagate the attributes to an element
|
||||
pub(crate) current_element_attributes: VecMap<CowStr, AttributeValue>,
|
||||
pub(crate) current_element_props: HtmlProps,
|
||||
app_ref: Option<Box<dyn AppRunner>>,
|
||||
}
|
||||
|
||||
|
@ -69,7 +234,7 @@ impl Cx {
|
|||
id_path: Vec::new(),
|
||||
document: crate::document(),
|
||||
app_ref: None,
|
||||
current_element_attributes: Default::default(),
|
||||
current_element_props: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -141,73 +306,62 @@ impl Cx {
|
|||
&self.document
|
||||
}
|
||||
|
||||
pub(crate) fn build_element(
|
||||
&mut self,
|
||||
ns: &str,
|
||||
name: &str,
|
||||
) -> (web_sys::Element, VecMap<CowStr, AttributeValue>) {
|
||||
pub(crate) fn build_element(&mut self, ns: &str, name: &str) -> (web_sys::Element, HtmlProps) {
|
||||
let el = self
|
||||
.document
|
||||
.create_element_ns(Some(ns), name)
|
||||
.expect("could not create element");
|
||||
let attributes = self.apply_attributes(&el);
|
||||
(el, attributes)
|
||||
let props = self.current_element_props.apply(&el);
|
||||
(el, props)
|
||||
}
|
||||
|
||||
pub(crate) fn rebuild_element(
|
||||
&mut self,
|
||||
element: &web_sys::Element,
|
||||
attributes: &mut VecMap<CowStr, AttributeValue>,
|
||||
props: &mut HtmlProps,
|
||||
) -> ChangeFlags {
|
||||
self.apply_attribute_changes(element, attributes)
|
||||
self.current_element_props.apply_changes(element, props)
|
||||
}
|
||||
|
||||
// TODO Not sure how multiple attribute definitions with the same name should be handled (e.g. `e.attr("class", "a").attr("class", "b")`)
|
||||
// Currently the outer most (in the example above "b") defines the attribute (when it isn't `None`, in that case the inner attr defines the value)
|
||||
pub(crate) fn add_attr_to_element(&mut self, name: &CowStr, value: &Option<AttributeValue>) {
|
||||
// Panic in dev if "class" is used as an attribute. In production the result is undefined.
|
||||
debug_assert!(
|
||||
name != "class",
|
||||
"classes should be set using the `class` method"
|
||||
);
|
||||
// Panic in dev if "style" is used as an attribute. In production the result is undefined.
|
||||
debug_assert!(
|
||||
name != "style",
|
||||
"styles should be set using the `style` method"
|
||||
);
|
||||
|
||||
if let Some(value) = value {
|
||||
// could be slightly optimized via something like this: `new_attrs.entry(name).or_insert_with(|| value)`
|
||||
if !self.current_element_attributes.contains_key(name) {
|
||||
self.current_element_attributes
|
||||
if !self.current_element_props.attributes.contains_key(name) {
|
||||
self.current_element_props
|
||||
.attributes
|
||||
.insert(name.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn apply_attributes(
|
||||
&mut self,
|
||||
element: &web_sys::Element,
|
||||
) -> VecMap<CowStr, AttributeValue> {
|
||||
let mut attributes = VecMap::default();
|
||||
std::mem::swap(&mut attributes, &mut self.current_element_attributes);
|
||||
for (name, value) in attributes.iter() {
|
||||
set_attribute(element, name, &value.serialize());
|
||||
pub(crate) fn add_class_to_element(&mut self, class_name: &CowStr) {
|
||||
// Don't strictly need this check but I assume its better for perf (might not be though)
|
||||
if !self.current_element_props.classes.contains_key(class_name) {
|
||||
self.current_element_props
|
||||
.classes
|
||||
.insert(class_name.clone(), ());
|
||||
}
|
||||
attributes
|
||||
}
|
||||
|
||||
pub(crate) fn apply_attribute_changes(
|
||||
&mut self,
|
||||
element: &web_sys::Element,
|
||||
attributes: &mut VecMap<CowStr, AttributeValue>,
|
||||
) -> ChangeFlags {
|
||||
let mut changed = ChangeFlags::empty();
|
||||
// update attributes
|
||||
for itm in diff_kv_iterables(&*attributes, &self.current_element_attributes) {
|
||||
match itm {
|
||||
Diff::Add(name, value) | Diff::Change(name, value) => {
|
||||
set_attribute(element, name, &value.serialize());
|
||||
changed |= ChangeFlags::OTHER_CHANGE;
|
||||
}
|
||||
Diff::Remove(name) => {
|
||||
remove_attribute(element, name);
|
||||
changed |= ChangeFlags::OTHER_CHANGE;
|
||||
}
|
||||
}
|
||||
pub(crate) fn add_style_to_element(&mut self, name: &CowStr, value: &CowStr) {
|
||||
if !self.current_element_props.styles.contains_key(name) {
|
||||
self.current_element_props
|
||||
.styles
|
||||
.insert(name.clone(), value.clone());
|
||||
}
|
||||
std::mem::swap(attributes, &mut self.current_element_attributes);
|
||||
self.current_element_attributes.clear();
|
||||
changed
|
||||
}
|
||||
|
||||
pub fn message_thunk(&self) -> MessageThunk {
|
||||
|
|
|
@ -4,8 +4,8 @@ use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
|||
use xilem_core::{Id, MessageResult, VecSplice};
|
||||
|
||||
use crate::{
|
||||
interfaces::sealed::Sealed, vecmap::VecMap, view::DomNode, AttributeValue, ChangeFlags, Cx,
|
||||
ElementsSplice, Pod, View, ViewMarker, ViewSequence, HTML_NS,
|
||||
context::HtmlProps, interfaces::sealed::Sealed, view::DomNode, ChangeFlags, Cx, ElementsSplice,
|
||||
Pod, View, ViewMarker, ViewSequence, HTML_NS,
|
||||
};
|
||||
|
||||
use super::interfaces::Element;
|
||||
|
@ -17,7 +17,7 @@ type CowStr = std::borrow::Cow<'static, str>;
|
|||
/// Stores handles to the child elements and any child state, as well as attributes and event listeners
|
||||
pub struct ElementState<ViewSeqState> {
|
||||
pub(crate) children_states: ViewSeqState,
|
||||
pub(crate) attributes: VecMap<CowStr, AttributeValue>,
|
||||
pub(crate) props: HtmlProps,
|
||||
pub(crate) child_elements: Vec<Pod>,
|
||||
/// This is temporary cache for elements while updating/diffing,
|
||||
/// after usage it shouldn't contain any elements,
|
||||
|
@ -150,7 +150,7 @@ where
|
|||
type Element = web_sys::HtmlElement;
|
||||
|
||||
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
|
||||
let (el, attributes) = cx.build_element(HTML_NS, &self.name);
|
||||
let (el, props) = cx.build_element(HTML_NS, &self.name);
|
||||
|
||||
let mut child_elements = vec![];
|
||||
let mut scratch = vec![];
|
||||
|
@ -171,7 +171,7 @@ where
|
|||
children_states,
|
||||
child_elements,
|
||||
scratch,
|
||||
attributes,
|
||||
props,
|
||||
};
|
||||
(id, state, el)
|
||||
}
|
||||
|
@ -193,8 +193,8 @@ where
|
|||
.parent_element()
|
||||
.expect_throw("this element was mounted and so should have a parent");
|
||||
parent.remove_child(element).unwrap_throw();
|
||||
let (new_element, attributes) = cx.build_element(HTML_NS, self.node_name());
|
||||
state.attributes = attributes;
|
||||
let (new_element, props) = cx.build_element(HTML_NS, self.node_name());
|
||||
state.props = props;
|
||||
// TODO could this be combined with child updates?
|
||||
while let Some(child) = element.child_nodes().get(0) {
|
||||
new_element.append_child(&child).unwrap_throw();
|
||||
|
@ -203,7 +203,7 @@ where
|
|||
changed |= ChangeFlags::STRUCTURE;
|
||||
}
|
||||
|
||||
changed |= cx.rebuild_element(element, &mut state.attributes);
|
||||
changed |= cx.rebuild_element(element, &mut state.props);
|
||||
|
||||
// update children
|
||||
let mut splice =
|
||||
|
@ -280,7 +280,7 @@ macro_rules! define_element {
|
|||
type Element = web_sys::$dom_interface;
|
||||
|
||||
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
|
||||
let (el, attributes) = cx.build_element($ns, $tag_name);
|
||||
let (el, props) = cx.build_element($ns, $tag_name);
|
||||
|
||||
let mut child_elements = vec![];
|
||||
let mut scratch = vec![];
|
||||
|
@ -300,7 +300,7 @@ macro_rules! define_element {
|
|||
children_states,
|
||||
child_elements,
|
||||
scratch,
|
||||
attributes,
|
||||
props,
|
||||
};
|
||||
(id, state, el)
|
||||
}
|
||||
|
@ -315,7 +315,7 @@ macro_rules! define_element {
|
|||
) -> ChangeFlags {
|
||||
let mut changed = ChangeFlags::empty();
|
||||
|
||||
changed |= cx.rebuild_element(element, &mut state.attributes);
|
||||
changed |= cx.rebuild_element(element, &mut state.props);
|
||||
|
||||
// update children
|
||||
let mut splice = ChildrenSplice::new(&mut state.child_elements, &mut state.scratch, element);
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
use crate::{Pointer, PointerMsg, View, ViewMarker};
|
||||
use std::borrow::Cow;
|
||||
use crate::{
|
||||
class::{Class, IntoClasses},
|
||||
style::{IntoStyles, Style},
|
||||
Pointer, PointerMsg, View, ViewMarker,
|
||||
};
|
||||
use std::{borrow::Cow, marker::PhantomData};
|
||||
|
||||
use gloo::events::EventListenerOptions;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
@ -91,11 +95,30 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
// TODO should some methods extend some properties automatically,
|
||||
// instead of overwriting the (possibly set) inner value
|
||||
// or should there be (extra) "modifier" methods like `add_class` and/or `remove_class`
|
||||
fn class(self, class: impl Into<Cow<'static, str>>) -> Attr<Self, T, A> {
|
||||
self.attr("class", class.into())
|
||||
/// Add 0 or more classes to the wrapped element.
|
||||
///
|
||||
/// Can pass a string, &'static str, Option, tuple, or vec
|
||||
///
|
||||
/// If multiple classes are added, all will be applied to the element.
|
||||
fn class(self, class: impl IntoClasses) -> Class<Self, T, A> {
|
||||
let mut class_names = vec![];
|
||||
class.into_classes(&mut class_names);
|
||||
Class {
|
||||
element: self,
|
||||
class_names,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a style attribute
|
||||
fn style(self, style: impl IntoStyles) -> Style<Self, T, A> {
|
||||
let mut styles = vec![];
|
||||
style.into_styles(&mut styles);
|
||||
Style {
|
||||
element: self,
|
||||
styles,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
// event list from
|
||||
|
|
|
@ -10,6 +10,7 @@ use wasm_bindgen::JsCast;
|
|||
mod app;
|
||||
mod attribute;
|
||||
mod attribute_value;
|
||||
mod class;
|
||||
mod context;
|
||||
mod diff;
|
||||
pub mod elements;
|
||||
|
@ -18,6 +19,7 @@ pub mod interfaces;
|
|||
mod one_of;
|
||||
mod optional_action;
|
||||
mod pointer;
|
||||
mod style;
|
||||
pub mod svg;
|
||||
mod vecmap;
|
||||
mod view;
|
||||
|
@ -35,6 +37,7 @@ pub use one_of::{
|
|||
};
|
||||
pub use optional_action::{Action, OptionalAction};
|
||||
pub use pointer::{Pointer, PointerDetails, PointerMsg};
|
||||
pub use style::style;
|
||||
pub use view::{
|
||||
memoize, static_view, Adapt, AdaptState, AdaptThunk, AnyView, BoxedView, ElementsSplice,
|
||||
Memoize, MemoizeState, Pod, View, ViewMarker, ViewSequence,
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::marker::PhantomData;
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
use xilem_core::{Id, MessageResult};
|
||||
|
||||
use crate::{interfaces::sealed::Sealed, ChangeFlags, Cx, View, ViewMarker};
|
||||
|
||||
use super::interfaces::Element;
|
||||
|
||||
/// A trait to make the class adding functions generic over collection type
|
||||
pub trait IntoStyles {
|
||||
fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>);
|
||||
}
|
||||
|
||||
struct StyleTuple<T1, T2>(T1, T2);
|
||||
|
||||
/// Create a style from a style name and its value.
|
||||
pub fn style<T1, T2>(name: T1, value: T2) -> impl IntoStyles
|
||||
where
|
||||
T1: Into<Cow<'static, str>>,
|
||||
T2: Into<Cow<'static, str>>,
|
||||
{
|
||||
StyleTuple(name, value)
|
||||
}
|
||||
|
||||
impl<T1, T2> IntoStyles for StyleTuple<T1, T2>
|
||||
where
|
||||
T1: Into<Cow<'static, str>>,
|
||||
T2: Into<Cow<'static, str>>,
|
||||
{
|
||||
fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>) {
|
||||
let StyleTuple(key, value) = self;
|
||||
styles.push((key.into(), value.into()));
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoStyles for Option<T>
|
||||
where
|
||||
T: IntoStyles,
|
||||
{
|
||||
fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>) {
|
||||
if let Some(t) = self {
|
||||
t.into_styles(styles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoStyles for Vec<T>
|
||||
where
|
||||
T: IntoStyles,
|
||||
{
|
||||
fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>) {
|
||||
for itm in self {
|
||||
itm.into_styles(styles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T1, T2, S> IntoStyles for HashMap<T1, T2, S>
|
||||
where
|
||||
T1: Into<Cow<'static, str>>,
|
||||
T2: Into<Cow<'static, str>>,
|
||||
{
|
||||
fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>) {
|
||||
for (key, value) in self {
|
||||
styles.push((key.into(), value.into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T1, T2> IntoStyles for BTreeMap<T1, T2>
|
||||
where
|
||||
T1: Into<Cow<'static, str>>,
|
||||
T2: Into<Cow<'static, str>>,
|
||||
{
|
||||
fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>) {
|
||||
for (key, value) in self {
|
||||
styles.push((key.into(), value.into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_tuple_intostyles {
|
||||
($($name:ident : $type:ident),* $(,)?) => {
|
||||
impl<$($type),*> IntoStyles for ($($type,)*)
|
||||
where
|
||||
$($type: IntoStyles),*
|
||||
{
|
||||
#[allow(unused_variables)]
|
||||
fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>) {
|
||||
let ($($name,)*) = self;
|
||||
$(
|
||||
$name.into_styles(styles);
|
||||
)*
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_tuple_intostyles!();
|
||||
impl_tuple_intostyles!(t1: T1);
|
||||
impl_tuple_intostyles!(t1: T1, t2: T2);
|
||||
impl_tuple_intostyles!(t1: T1, t2: T2, t3: T3);
|
||||
impl_tuple_intostyles!(t1: T1, t2: T2, t3: T3, t4: T4);
|
||||
|
||||
pub struct Style<E, T, A> {
|
||||
pub(crate) element: E,
|
||||
pub(crate) styles: Vec<(Cow<'static, str>, Cow<'static, str>)>,
|
||||
pub(crate) phantom: PhantomData<fn() -> (T, A)>,
|
||||
}
|
||||
|
||||
impl<E, T, A> ViewMarker for Style<E, T, A> {}
|
||||
impl<E, T, A> Sealed for Style<E, T, A> {}
|
||||
|
||||
impl<E: Element<T, A>, T, A> View<T, A> for Style<E, T, A> {
|
||||
type State = E::State;
|
||||
type Element = E::Element;
|
||||
|
||||
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
|
||||
for (key, value) in &self.styles {
|
||||
cx.add_style_to_element(key, value);
|
||||
}
|
||||
self.element.build(cx)
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
&self,
|
||||
cx: &mut Cx,
|
||||
prev: &Self,
|
||||
id: &mut Id,
|
||||
state: &mut Self::State,
|
||||
element: &mut Self::Element,
|
||||
) -> ChangeFlags {
|
||||
for (key, value) in &self.styles {
|
||||
cx.add_style_to_element(key, value);
|
||||
}
|
||||
self.element.rebuild(cx, &prev.element, id, state, element)
|
||||
}
|
||||
|
||||
fn message(
|
||||
&self,
|
||||
id_path: &[Id],
|
||||
state: &mut Self::State,
|
||||
message: Box<dyn std::any::Any>,
|
||||
app_state: &mut T,
|
||||
) -> MessageResult<A> {
|
||||
self.element.message(id_path, state, message, app_state)
|
||||
}
|
||||
}
|
||||
|
||||
crate::interfaces::impl_dom_interfaces_for_ty!(Element, Style);
|
|
@ -9,11 +9,10 @@ use std::borrow::Cow;
|
|||
use xilem_core::{Id, MessageResult};
|
||||
|
||||
use crate::{
|
||||
context::{ChangeFlags, Cx},
|
||||
context::{ChangeFlags, Cx, HtmlProps},
|
||||
interfaces::sealed::Sealed,
|
||||
vecmap::VecMap,
|
||||
view::{View, ViewMarker},
|
||||
AttributeValue, IntoAttributeValue, SVG_NS,
|
||||
IntoAttributeValue, SVG_NS,
|
||||
};
|
||||
|
||||
macro_rules! generate_dom_interface_impl {
|
||||
|
@ -29,7 +28,7 @@ impl ViewMarker for Line {}
|
|||
impl Sealed for Line {}
|
||||
|
||||
impl<T, A> View<T, A> for Line {
|
||||
type State = VecMap<Cow<'static, str>, AttributeValue>;
|
||||
type State = HtmlProps;
|
||||
type Element = web_sys::Element;
|
||||
|
||||
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
|
||||
|
@ -37,9 +36,9 @@ impl<T, A> View<T, A> for Line {
|
|||
cx.add_attr_to_element(&"y1".into(), &self.p0.y.into_attr_value());
|
||||
cx.add_attr_to_element(&"x2".into(), &self.p1.x.into_attr_value());
|
||||
cx.add_attr_to_element(&"y2".into(), &self.p1.y.into_attr_value());
|
||||
let (el, attributes) = cx.build_element(SVG_NS, "line");
|
||||
let (el, props) = cx.build_element(SVG_NS, "line");
|
||||
let id = Id::next();
|
||||
(id, attributes, el)
|
||||
(id, props, el)
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
|
@ -47,14 +46,14 @@ impl<T, A> View<T, A> for Line {
|
|||
cx: &mut Cx,
|
||||
_prev: &Self,
|
||||
_id: &mut Id,
|
||||
attributes: &mut Self::State,
|
||||
props: &mut Self::State,
|
||||
element: &mut Self::Element,
|
||||
) -> ChangeFlags {
|
||||
cx.add_attr_to_element(&"x1".into(), &self.p0.x.into_attr_value());
|
||||
cx.add_attr_to_element(&"y1".into(), &self.p0.y.into_attr_value());
|
||||
cx.add_attr_to_element(&"x2".into(), &self.p1.x.into_attr_value());
|
||||
cx.add_attr_to_element(&"y2".into(), &self.p1.y.into_attr_value());
|
||||
cx.rebuild_element(element, attributes)
|
||||
cx.rebuild_element(element, props)
|
||||
}
|
||||
|
||||
fn message(
|
||||
|
@ -75,7 +74,7 @@ impl ViewMarker for Rect {}
|
|||
impl Sealed for Rect {}
|
||||
|
||||
impl<T, A> View<T, A> for Rect {
|
||||
type State = VecMap<Cow<'static, str>, AttributeValue>;
|
||||
type State = HtmlProps;
|
||||
type Element = web_sys::Element;
|
||||
|
||||
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
|
||||
|
@ -84,9 +83,9 @@ impl<T, A> View<T, A> for Rect {
|
|||
let size = self.size();
|
||||
cx.add_attr_to_element(&"width".into(), &size.width.into_attr_value());
|
||||
cx.add_attr_to_element(&"height".into(), &size.height.into_attr_value());
|
||||
let (el, attributes) = cx.build_element(SVG_NS, "rect");
|
||||
let (el, props) = cx.build_element(SVG_NS, "rect");
|
||||
let id = Id::next();
|
||||
(id, attributes, el)
|
||||
(id, props, el)
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
|
@ -94,7 +93,7 @@ impl<T, A> View<T, A> for Rect {
|
|||
cx: &mut Cx,
|
||||
_prev: &Self,
|
||||
_id: &mut Id,
|
||||
attributes: &mut Self::State,
|
||||
props: &mut Self::State,
|
||||
element: &mut Self::Element,
|
||||
) -> ChangeFlags {
|
||||
cx.add_attr_to_element(&"x".into(), &self.x0.into_attr_value());
|
||||
|
@ -102,7 +101,7 @@ impl<T, A> View<T, A> for Rect {
|
|||
let size = self.size();
|
||||
cx.add_attr_to_element(&"width".into(), &size.width.into_attr_value());
|
||||
cx.add_attr_to_element(&"height".into(), &size.height.into_attr_value());
|
||||
cx.rebuild_element(element, attributes)
|
||||
cx.rebuild_element(element, props)
|
||||
}
|
||||
|
||||
fn message(
|
||||
|
@ -123,16 +122,16 @@ impl ViewMarker for Circle {}
|
|||
impl Sealed for Circle {}
|
||||
|
||||
impl<T, A> View<T, A> for Circle {
|
||||
type State = VecMap<Cow<'static, str>, AttributeValue>;
|
||||
type State = HtmlProps;
|
||||
type Element = web_sys::Element;
|
||||
|
||||
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
|
||||
cx.add_attr_to_element(&"cx".into(), &self.center.x.into_attr_value());
|
||||
cx.add_attr_to_element(&"cy".into(), &self.center.y.into_attr_value());
|
||||
cx.add_attr_to_element(&"r".into(), &self.radius.into_attr_value());
|
||||
let (el, attributes) = cx.build_element(SVG_NS, "circle");
|
||||
let (el, props) = cx.build_element(SVG_NS, "circle");
|
||||
let id = Id::next();
|
||||
(id, attributes, el)
|
||||
(id, props, el)
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
|
@ -140,13 +139,13 @@ impl<T, A> View<T, A> for Circle {
|
|||
cx: &mut Cx,
|
||||
_prev: &Self,
|
||||
_id: &mut Id,
|
||||
attributes: &mut Self::State,
|
||||
props: &mut Self::State,
|
||||
element: &mut Self::Element,
|
||||
) -> ChangeFlags {
|
||||
cx.add_attr_to_element(&"cx".into(), &self.center.x.into_attr_value());
|
||||
cx.add_attr_to_element(&"cy".into(), &self.center.y.into_attr_value());
|
||||
cx.add_attr_to_element(&"r".into(), &self.radius.into_attr_value());
|
||||
cx.rebuild_element(element, attributes)
|
||||
cx.rebuild_element(element, props)
|
||||
}
|
||||
|
||||
fn message(
|
||||
|
@ -167,15 +166,15 @@ impl ViewMarker for BezPath {}
|
|||
impl Sealed for BezPath {}
|
||||
|
||||
impl<T, A> View<T, A> for BezPath {
|
||||
type State = (Cow<'static, str>, VecMap<Cow<'static, str>, AttributeValue>);
|
||||
type State = (Cow<'static, str>, HtmlProps);
|
||||
type Element = web_sys::Element;
|
||||
|
||||
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
|
||||
let svg_repr = Cow::from(self.to_svg());
|
||||
cx.add_attr_to_element(&"d".into(), &svg_repr.clone().into_attr_value());
|
||||
let (el, attributes) = cx.build_element(SVG_NS, "path");
|
||||
let (el, props) = cx.build_element(SVG_NS, "path");
|
||||
let id = Id::next();
|
||||
(id, (svg_repr, attributes), el)
|
||||
(id, (svg_repr, props), el)
|
||||
}
|
||||
|
||||
fn rebuild(
|
||||
|
@ -183,7 +182,7 @@ impl<T, A> View<T, A> for BezPath {
|
|||
cx: &mut Cx,
|
||||
prev: &Self,
|
||||
_id: &mut Id,
|
||||
(svg_repr, attributes): &mut Self::State,
|
||||
(svg_repr, props): &mut Self::State,
|
||||
element: &mut Self::Element,
|
||||
) -> ChangeFlags {
|
||||
// slight optimization to avoid serialization/allocation
|
||||
|
@ -191,7 +190,7 @@ impl<T, A> View<T, A> for BezPath {
|
|||
*svg_repr = Cow::from(self.to_svg());
|
||||
}
|
||||
cx.add_attr_to_element(&"d".into(), &svg_repr.clone().into_attr_value());
|
||||
cx.rebuild_element(element, attributes)
|
||||
cx.rebuild_element(element, props)
|
||||
}
|
||||
|
||||
fn message(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::{borrow::Borrow, ops::Index};
|
||||
use std::{borrow::Borrow, fmt, ops::Index};
|
||||
|
||||
/// Basically an ordered Map (similar as BTreeMap) with a Vec as backend for very few elements
|
||||
/// As it uses linear search instead of a tree traversal,
|
||||
|
@ -11,6 +11,12 @@ impl<K, V> Default for VecMap<K, V> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<K: fmt::Debug, V: fmt::Debug> fmt::Debug for VecMap<K, V> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.debug_map().entries(self.iter()).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> VecMap<K, V> {
|
||||
/// Returns a reference to the value corresponding to the key.
|
||||
///
|
||||
|
|
|
@ -8,7 +8,7 @@ use xilem_web::{
|
|||
#[derive(Default)]
|
||||
struct AppState {
|
||||
clicks: i32,
|
||||
class: &'static str,
|
||||
class: Option<&'static str>,
|
||||
text: String,
|
||||
}
|
||||
|
||||
|
@ -23,10 +23,10 @@ impl AppState {
|
|||
self.clicks = 0;
|
||||
}
|
||||
fn change_class(&mut self) {
|
||||
if self.class == "gray" {
|
||||
self.class = "green";
|
||||
if self.class == Some("gray") {
|
||||
self.class = Some("green");
|
||||
} else {
|
||||
self.class = "gray";
|
||||
self.class = Some("gray");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,7 +49,7 @@ fn btn(
|
|||
|
||||
fn app_logic(state: &mut AppState) -> impl View<AppState> {
|
||||
el::div((
|
||||
el::span(format!("clicked {} times", state.clicks)).attr("class", state.class),
|
||||
el::span(format!("clicked {} times", state.clicks)).class(state.class),
|
||||
el::br(()),
|
||||
btn("+1 click", |state, _| state.increment()),
|
||||
btn("-1 click", |state, _| state.decrement()),
|
||||
|
|
|
@ -4,7 +4,8 @@ use state::{AppState, Filter, Todo};
|
|||
|
||||
use wasm_bindgen::JsCast;
|
||||
use xilem_web::{
|
||||
elements::html as el, get_element_by_id, interfaces::*, Action, Adapt, App, MessageResult, View,
|
||||
elements::html as el, get_element_by_id, interfaces::*, style as s, Action, Adapt, App,
|
||||
MessageResult, View,
|
||||
};
|
||||
|
||||
// All of these actions arise from within a `Todo`, but we need access to the full state to reduce
|
||||
|
@ -19,16 +20,8 @@ enum TodoAction {
|
|||
impl Action for TodoAction {}
|
||||
|
||||
fn todo_item(todo: &mut Todo, editing: bool) -> impl Element<Todo, TodoAction> {
|
||||
let mut class = String::new();
|
||||
if todo.completed {
|
||||
class.push_str(" completed");
|
||||
}
|
||||
if editing {
|
||||
class.push_str(" editing");
|
||||
}
|
||||
|
||||
let checkbox = el::input(())
|
||||
.attr("class", "toggle")
|
||||
.class("toggle")
|
||||
.attr("type", "checkbox")
|
||||
.attr("checked", todo.completed)
|
||||
.on_click(|state: &mut Todo, _| state.completed = !state.completed);
|
||||
|
@ -39,13 +32,13 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl Element<Todo, TodoAction> {
|
|||
el::label(todo.title.clone())
|
||||
.on_dblclick(|state: &mut Todo, _| TodoAction::SetEditing(state.id)),
|
||||
el::button(())
|
||||
.attr("class", "destroy")
|
||||
.class("destroy")
|
||||
.on_click(|state: &mut Todo, _| TodoAction::Destroy(state.id)),
|
||||
))
|
||||
.attr("class", "view"),
|
||||
.class("view"),
|
||||
el::input(())
|
||||
.attr("value", todo.title_editing.clone())
|
||||
.attr("class", "edit")
|
||||
.class("edit")
|
||||
.on_keydown(|state: &mut Todo, evt| {
|
||||
let key = evt.key();
|
||||
if key == "Enter" {
|
||||
|
@ -70,7 +63,8 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl Element<Todo, TodoAction> {
|
|||
.passive(true)
|
||||
.on_blur(|_, _| TodoAction::CancelEditing),
|
||||
))
|
||||
.attr("class", class)
|
||||
.class(todo.completed.then_some("completed"))
|
||||
.class(editing.then_some("editing"))
|
||||
}
|
||||
|
||||
fn footer_view(state: &mut AppState, should_display: bool) -> impl Element<AppState> {
|
||||
|
@ -82,7 +76,7 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl Element<AppSt
|
|||
|
||||
let clear_button = (state.todos.iter().filter(|todo| todo.completed).count() > 0).then(|| {
|
||||
Element::on_click(
|
||||
el::button("Clear completed").attr("class", "clear-completed"),
|
||||
el::button("Clear completed").class("clear-completed"),
|
||||
|state: &mut AppState, _| {
|
||||
state.todos.retain(|todo| !todo.completed);
|
||||
},
|
||||
|
@ -96,12 +90,12 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl Element<AppSt
|
|||
el::strong(state.todos.len().to_string()),
|
||||
format!(" {} left", item_str),
|
||||
))
|
||||
.attr("class", "todo-count"),
|
||||
.class("todo-count"),
|
||||
el::ul((
|
||||
el::li(Element::on_click(
|
||||
el::a("All")
|
||||
.attr("href", "#/")
|
||||
.attr("class", filter_class(Filter::All)),
|
||||
.class(filter_class(Filter::All)),
|
||||
|state: &mut AppState, _| {
|
||||
state.filter = Filter::All;
|
||||
},
|
||||
|
@ -110,7 +104,7 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl Element<AppSt
|
|||
el::li(Element::on_click(
|
||||
el::a("Active")
|
||||
.attr("href", "#/active")
|
||||
.attr("class", filter_class(Filter::Active)),
|
||||
.class(filter_class(Filter::Active)),
|
||||
|state: &mut AppState, _| {
|
||||
state.filter = Filter::Active;
|
||||
},
|
||||
|
@ -119,17 +113,17 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl Element<AppSt
|
|||
el::li(Element::on_click(
|
||||
el::a("Completed")
|
||||
.attr("href", "#/completed")
|
||||
.attr("class", filter_class(Filter::Completed)),
|
||||
.class(filter_class(Filter::Completed)),
|
||||
|state: &mut AppState, _| {
|
||||
state.filter = Filter::Completed;
|
||||
},
|
||||
)),
|
||||
))
|
||||
.attr("class", "filters"),
|
||||
.class("filters"),
|
||||
clear_button,
|
||||
))
|
||||
.attr("class", "footer")
|
||||
.attr("style", (!should_display).then_some("display:none;"))
|
||||
.class("footer")
|
||||
.style((!should_display).then_some(s("display", "none")))
|
||||
}
|
||||
|
||||
fn main_view(state: &mut AppState, should_display: bool) -> impl Element<AppState> {
|
||||
|
@ -158,17 +152,17 @@ fn main_view(state: &mut AppState, should_display: bool) -> impl Element<AppStat
|
|||
.collect();
|
||||
let toggle_all = el::input(())
|
||||
.attr("id", "toggle-all")
|
||||
.attr("class", "toggle-all")
|
||||
.class("toggle-all")
|
||||
.attr("type", "checkbox")
|
||||
.attr("checked", state.are_all_complete());
|
||||
|
||||
el::section((
|
||||
toggle_all.on_click(|state: &mut AppState, _| state.toggle_all_complete()),
|
||||
el::label(()).attr("for", "toggle-all"),
|
||||
el::ul(todos).attr("class", "todo-list"),
|
||||
el::ul(todos).class("todo-list"),
|
||||
))
|
||||
.attr("class", "main")
|
||||
.attr("style", (!should_display).then_some("display:none;"))
|
||||
.class("main")
|
||||
.style((!should_display).then_some(s("display", "none")))
|
||||
}
|
||||
|
||||
fn app_logic(state: &mut AppState) -> impl View<AppState> {
|
||||
|
@ -177,7 +171,7 @@ fn app_logic(state: &mut AppState) -> impl View<AppState> {
|
|||
let main = main_view(state, some_todos);
|
||||
let footer = footer_view(state, some_todos);
|
||||
let input = el::input(())
|
||||
.attr("class", "new-todo")
|
||||
.class("new-todo")
|
||||
.attr("placeholder", "What needs to be done?")
|
||||
.attr("value", state.new_todo.clone())
|
||||
.attr("autofocus", true);
|
||||
|
@ -202,7 +196,7 @@ fn app_logic(state: &mut AppState) -> impl View<AppState> {
|
|||
})
|
||||
.passive(false),
|
||||
))
|
||||
.attr("class", "header"),
|
||||
.class("header"),
|
||||
main,
|
||||
footer,
|
||||
))
|
||||
|
|
Loading…
Reference in New Issue