xilem/xilem_web/src/modifiers/attribute.rs

363 lines
13 KiB
Rust

// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0
use crate::{
core::{MessageResult, Mut, View, ViewElement, ViewId, ViewMarker},
modifiers::With,
vecmap::VecMap,
AttributeValue, DomView, DynMessage, ElementProps, IntoAttributeValue, ViewCtx,
};
use std::marker::PhantomData;
use wasm_bindgen::{JsCast, UnwrapThrowExt};
type CowStr = std::borrow::Cow<'static, str>;
#[derive(Debug, PartialEq, Clone)]
/// An modifier element to either set or remove an attribute.
///
/// It's used in [`Attributes`].
pub enum AttributeModifier {
Set(CowStr, AttributeValue),
Remove(CowStr),
}
impl AttributeModifier {
/// Returns the attribute name of this modifier.
pub fn name(&self) -> &CowStr {
let (AttributeModifier::Set(name, _) | AttributeModifier::Remove(name)) = self;
name
}
/// Convert this modifier into its attribute name.
pub fn into_name(self) -> CowStr {
let (AttributeModifier::Set(name, _) | AttributeModifier::Remove(name)) = self;
name
}
}
impl<K: Into<CowStr>, V: IntoAttributeValue> From<(K, V)> for AttributeModifier {
fn from((name, value): (K, V)) -> Self {
match value.into_attr_value() {
Some(value) => AttributeModifier::Set(name.into(), value),
None => AttributeModifier::Remove(name.into()),
}
}
}
#[derive(Default)]
/// An Element modifier that manages all attributes of an Element.
pub struct Attributes {
// TODO think about using a `VecSplice` for more efficient insertion etc.,
// but this is an additional trade-off of memory-usage and complexity,
// while probably not helping much in the average case (of very few styles)...
modifiers: Vec<AttributeModifier>,
updated: VecMap<CowStr, ()>,
idx: u16,
in_hydration: bool,
was_created: bool,
}
impl With<Attributes> for ElementProps {
fn modifier(&mut self) -> &mut Attributes {
self.attributes()
}
}
impl Attributes {
/// Creates a new `Attributes` modifier.
///
/// `size_hint` is used to avoid unnecessary allocations while traversing up the view-tree when adding modifiers in [`View::build`].
pub(crate) fn new(size_hint: usize, in_hydration: bool) -> Self {
Self {
modifiers: Vec::with_capacity(size_hint),
was_created: true,
in_hydration,
..Default::default()
}
}
/// Applies potential changes of the attributes of an element to the underlying DOM node.
pub fn apply_changes(&mut self, element: &web_sys::Element) {
if self.in_hydration {
self.in_hydration = false;
self.was_created = false;
} else if self.was_created {
self.was_created = false;
for modifier in &self.modifiers {
match modifier {
AttributeModifier::Remove(n) => remove_attribute(element, n),
AttributeModifier::Set(n, v) => set_attribute(element, n, &v.serialize()),
}
}
} else if !self.updated.is_empty() {
for modifier in self.modifiers.iter().rev() {
match modifier {
AttributeModifier::Remove(name) if self.updated.remove(name).is_some() => {
remove_attribute(element, name);
}
AttributeModifier::Set(name, value) if self.updated.remove(name).is_some() => {
set_attribute(element, name, &value.serialize());
}
_ => {}
}
}
// if there's any remaining key in updated, it means these are deleted keys
for (name, ()) in self.updated.drain() {
remove_attribute(element, &name);
}
}
debug_assert!(self.updated.is_empty());
}
#[inline]
/// Rebuilds the current element, while ensuring that the order of the modifiers stays correct.
/// Any children should be rebuilt in inside `f`, *before* modifying any other properties of [`Attributes`].
pub fn rebuild<E: With<Self>>(mut element: E, prev_len: usize, f: impl FnOnce(E)) {
element.modifier().idx -= prev_len as u16;
f(element);
}
#[inline]
/// Returns whether the underlying element has been built or rebuilt, this could e.g. happen, when `OneOf` changes a variant to a different element.
pub fn was_created(&self) -> bool {
self.was_created
}
#[inline]
/// Pushes `modifier` at the end of the current modifiers.
///
/// Must only be used when `self.was_created() == true`.
pub fn push(&mut self, modifier: impl Into<AttributeModifier>) {
debug_assert!(
self.was_created(),
"This should never be called, when the underlying element wasn't (re)created. Use `Attributes::insert` instead."
);
let modifier = modifier.into();
self.modifiers.push(modifier);
self.idx += 1;
}
#[inline]
/// Inserts `modifier` at the current index.
///
/// Must only be used when `self.was_created() == false`.
pub fn insert(&mut self, modifier: impl Into<AttributeModifier>) {
debug_assert!(
!self.was_created(),
"This should never be called, when the underlying element was (re)created, use `Attributes::push` instead."
);
let modifier = modifier.into();
self.updated.insert(modifier.name().clone(), ());
// TODO this could potentially be expensive, maybe think about `VecSplice` again.
// Although in the average case, this is likely not relevant, as usually very few attributes are used, thus shifting is probably good enough
// I.e. a `VecSplice` is probably less optimal (either more complicated code, and/or more memory usage)
self.modifiers.insert(self.idx as usize, modifier);
self.idx += 1;
}
#[inline]
/// Mutates the next modifier.
///
/// Must only be used when `self.was_created() == false`.
pub fn mutate<R>(&mut self, f: impl FnOnce(&mut AttributeModifier) -> R) -> R {
debug_assert!(
!self.was_created(),
"This should never be called, when the underlying element was (re)created."
);
let modifier = &mut self.modifiers[self.idx as usize];
let old = modifier.name().clone();
let rv = f(modifier);
let new = modifier.name();
if *new != old {
self.updated.insert(new.clone(), ());
}
self.updated.insert(old, ());
self.idx += 1;
rv
}
/// Skips the next `count` modifiers.
///
/// Must only be used when `self.was_created() == false`.
pub fn skip(&mut self, count: usize) {
debug_assert!(
!self.was_created(),
"This should never be called, when the underlying element was (re)created."
);
self.idx += count as u16;
}
/// Deletes the next `count` modifiers.
///
/// Must only be used when `self.was_created() == false`.
pub fn delete(&mut self, count: usize) {
debug_assert!(
!self.was_created(),
"This should never be called, when the underlying element was (re)created."
);
let start = self.idx as usize;
for modifier in self.modifiers.drain(start..(start + count)) {
self.updated.insert(modifier.into_name(), ());
}
}
/// Updates the next modifier, based on the diff of `prev` and `next`.
pub fn update(&mut self, prev: &AttributeModifier, next: &AttributeModifier) {
if self.was_created() {
self.push(next.clone());
} else if next != prev {
self.mutate(|modifier| *modifier = next.clone());
} else {
self.skip(1);
}
}
/// Updates the next modifier, based on the diff of `prev` and `next`, this can be used only when the previous modifier has the same name `key`, and only its value has changed.
pub fn update_with_same_key<Value: IntoAttributeValue + PartialEq + Clone>(
&mut self,
key: impl Into<CowStr>,
prev: &Value,
next: &Value,
) {
if self.was_created() {
self.push((key, next.clone()));
} else if next != prev {
self.mutate(|modifier| *modifier = (key, next.clone()).into());
} else {
self.skip(1);
}
}
}
fn set_attribute(element: &web_sys::Element, name: &str, value: &str) {
debug_assert_ne!(
name, "class",
"Using `class` as attribute is not supported, use the `el.class()` modifier instead"
);
debug_assert_ne!(
name, "style",
"Using `style` as attribute is not supported, use the `el.style()` modifier instead"
);
// we have to special-case `value` because setting the value using `set_attribute`
// doesn't work after the value has been changed.
// TODO not sure, whether this is always a good idea, in case custom or other interfaces such as HtmlOptionElement elements are used that have "value" as an attribute name.
// We likely want to use the DOM attributes instead.
if name == "value" {
if let Some(input_element) = element.dyn_ref::<web_sys::HtmlInputElement>() {
input_element.set_value(value);
} else {
element.set_attribute("value", value).unwrap_throw();
}
} else if name == "checked" {
if let Some(input_element) = element.dyn_ref::<web_sys::HtmlInputElement>() {
input_element.set_checked(true);
} else {
element.set_attribute("checked", value).unwrap_throw();
}
} else {
element.set_attribute(name, value).unwrap_throw();
}
}
fn remove_attribute(element: &web_sys::Element, name: &str) {
debug_assert_ne!(
name, "class",
"Using `class` as attribute is not supported, use the `el.class()` modifier instead"
);
debug_assert_ne!(
name, "style",
"Using `style` as attribute is not supported, use the `el.style()` modifier instead"
);
// we have to special-case `checked` because setting the value using `set_attribute`
// doesn't work after the value has been changed.
if name == "checked" {
if let Some(input_element) = element.dyn_ref::<web_sys::HtmlInputElement>() {
input_element.set_checked(false);
} else {
element.remove_attribute("checked").unwrap_throw();
}
} else {
element.remove_attribute(name).unwrap_throw();
}
}
/// A view to add an attribute to [`Element`](`crate::interfaces::Element`) derived components.
///
/// See [`Element::attr`](`crate::interfaces::Element::attr`) for more usage information.
pub struct Attr<V, State, Action> {
inner: V,
modifier: AttributeModifier,
phantom: PhantomData<fn() -> (State, Action)>,
}
impl<V, State, Action> Attr<V, State, Action> {
/// Create an [`Attr`] view. When `value` is `None`, it means remove the `name` attribute.
///
/// Usually [`Element::attr`](`crate::interfaces::Element::attr`) should be used instead of this function.
pub fn new(el: V, name: CowStr, value: Option<AttributeValue>) -> Self {
let modifier = match value {
Some(value) => AttributeModifier::Set(name, value),
None => AttributeModifier::Remove(name),
};
Self {
inner: el,
modifier,
phantom: PhantomData,
}
}
}
impl<V, State, Action> ViewMarker for Attr<V, State, Action> {}
impl<V, State, Action> View<State, Action, ViewCtx, DynMessage> for Attr<V, State, Action>
where
State: 'static,
Action: 'static,
V: DomView<State, Action, Element: With<Attributes>>,
for<'a> <V::Element as ViewElement>::Mut<'a>: With<Attributes>,
{
type Element = V::Element;
type ViewState = V::ViewState;
fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) {
let (mut element, state) =
ctx.with_size_hint::<Attributes, _>(1, |ctx| self.inner.build(ctx));
element.modifier().push(self.modifier.clone());
(element, state)
}
fn rebuild(
&self,
prev: &Self,
view_state: &mut Self::ViewState,
ctx: &mut ViewCtx,
element: Mut<Self::Element>,
) {
Attributes::rebuild(element, 1, |mut element| {
self.inner
.rebuild(&prev.inner, view_state, ctx, element.reborrow_mut());
element.modifier().update(&prev.modifier, &self.modifier);
});
}
fn teardown(
&self,
view_state: &mut Self::ViewState,
ctx: &mut ViewCtx,
element: Mut<Self::Element>,
) {
self.inner.teardown(view_state, ctx, element);
}
fn message(
&self,
view_state: &mut Self::ViewState,
id_path: &[ViewId],
message: DynMessage,
app_state: &mut State,
) -> MessageResult<Action, DynMessage> {
self.inner.message(view_state, id_path, message, app_state)
}
}