use crate::Classes;
use leptos::reactive::effect::RenderEffect;
use leptos::tachys::{
html::class::IntoClass,
renderer::{Rndr, dom::Element},
};
use leptos::web_sys;
const CLASS_ATTRIBUTE: &str = "class";
#[doc(hidden)]
#[derive(Clone)]
pub struct Elem(web_sys::Element);
impl Elem {
fn read_class_attribute(&self) -> String {
self.0.get_attribute(CLASS_ATTRIBUTE).unwrap_or_default()
}
fn set_class_attribute(&self, value: &str) {
if value.is_empty() {
self.remove_class_attribute();
} else {
Rndr::set_attribute(&self.0, CLASS_ATTRIBUTE, value);
}
}
fn remove_class_attribute(&self) {
Rndr::remove_attribute(&self.0, CLASS_ATTRIBUTE);
}
}
#[doc(hidden)]
#[derive(Default)]
pub struct ClassBuffers {
current: String,
scratch: String,
}
impl ClassBuffers {
fn sync_class_attribute(&mut self, classes: &Classes, el: &Elem) {
self.scratch.clear();
classes.write_active_classes(&mut self.scratch);
if self.scratch != self.current {
el.set_class_attribute(&self.scratch);
}
std::mem::swap(&mut self.current, &mut self.scratch);
}
}
#[doc(hidden)]
pub struct ClassesState {
el: Elem,
kind: ClassesKind,
}
enum ClassesKind {
Static {
buffers: ClassBuffers,
},
Reactive {
render_effect: RenderEffect<ClassBuffers>,
},
}
impl Default for ClassesKind {
fn default() -> Self {
Self::Static {
buffers: ClassBuffers::default(),
}
}
}
impl ClassesKind {
fn build(classes: Classes, el: &Elem, mut buffers: ClassBuffers) -> Self {
if classes.is_reactive() {
let closure_el = el.clone();
Self::Reactive {
render_effect: RenderEffect::new_with_value(
move |prev| {
let mut buffers = prev.unwrap_or_default();
buffers.sync_class_attribute(&classes, &closure_el);
buffers
},
Some(buffers),
),
}
} else {
buffers.sync_class_attribute(&classes, el);
Self::Static { buffers }
}
}
}
impl ClassesState {
fn new(classes: Classes, el: Elem, buffers: ClassBuffers) -> Self {
let kind = ClassesKind::build(classes, &el, buffers);
Self { el, kind }
}
fn take_buffers(&mut self) -> ClassBuffers {
match std::mem::take(&mut self.kind) {
ClassesKind::Static { buffers } => buffers,
ClassesKind::Reactive { render_effect } => {
render_effect.take_value().unwrap_or_default()
}
}
}
}
impl IntoClass for Classes {
type AsyncOutput = Self;
type State = ClassesState;
type Cloneable = Self;
type CloneableOwned = Self;
fn html_len(&self) -> usize {
self.estimated_class_len()
}
fn to_html(self, class: &mut String) {
self.write_active_classes(class);
}
fn should_overwrite(&self) -> bool {
true
}
fn hydrate<const FROM_SERVER: bool>(self, el: &Element) -> Self::State {
let el = Elem(el.clone());
let mut buffers = ClassBuffers::default();
if FROM_SERVER {
buffers.current = el.read_class_attribute();
}
ClassesState::new(self, el, buffers)
}
fn build(self, el: &Element) -> Self::State {
let el = Elem(el.clone());
ClassesState::new(self, el, ClassBuffers::default())
}
fn rebuild(self, state: &mut Self::State) {
let buffers = state.take_buffers();
state.kind = ClassesKind::build(self, &state.el, buffers);
}
fn into_cloneable(self) -> Self::Cloneable {
self
}
fn into_cloneable_owned(self) -> Self::CloneableOwned {
self
}
fn dry_resolve(&mut self) {
self.touch_reactive_dependencies();
}
async fn resolve(self) -> Self::AsyncOutput {
self
}
fn reset(state: &mut Self::State) {
let mut buffers = state.take_buffers();
buffers.current.clear();
state.el.remove_class_attribute();
state.kind = ClassesKind::Static { buffers };
}
}
#[cfg(test)]
mod tests {
use assertr::prelude::*;
use leptos::tachys::html::class::IntoClass;
use crate::Classes;
#[test]
fn to_html_writes_active_tokens() {
let classes = Classes::builder().with("foo").with("bar").build();
let mut html = String::new();
classes.to_html(&mut html);
assert_that!(html).is_equal_to("foo bar".to_string());
}
#[test]
fn to_html_writes_nothing_when_empty() {
let classes = Classes::new();
let mut html = String::new();
classes.to_html(&mut html);
assert_that!(html).is_equal_to(String::new());
}
#[test]
fn to_html_skips_inactive_entries() {
let classes = Classes::builder()
.with_reactive("active", true)
.with_reactive("disabled", false)
.with_reactive("visible", true)
.build();
let mut html = String::new();
classes.to_html(&mut html);
assert_that!(html).is_equal_to("active visible".to_string());
}
#[test]
fn to_html_appends_to_nonempty_buffer() {
let classes = Classes::builder().with("new-class").build();
let mut html = String::from("existing");
classes.to_html(&mut html);
assert_that!(html).is_equal_to("existing new-class".to_string());
}
#[test]
fn should_overwrite_is_true() {
let classes = Classes::new();
assert_that!(classes.should_overwrite()).is_true();
}
#[test]
fn html_len_is_exact_for_all_single_entries() {
let classes = Classes::builder().with("foo").with("bar").build();
let rendered = classes.clone().to_class_string();
assert_that!(classes.html_len()).is_equal_to(rendered.len());
}
#[test]
fn html_len_overshoots_toggle_by_inactive_branch_diff() {
let classes = Classes::builder()
.with("base")
.with_toggle(false, "active-state", "off") .build();
let rendered = classes.clone().to_class_string();
let longer_branch = "active-state".len();
let active_branch = "off".len();
let expected_overshoot = longer_branch - active_branch;
assert_that!(classes.html_len()).is_equal_to(rendered.len() + expected_overshoot);
}
}