mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 07:34:35 -05:00
Fixes #4248 During SSR, multiple `class` attributes were incorrectly concatenating instead of overwriting like they do in browsers. This inconsistency caused code that appeared to work in SSR to fail in CSR/hydration. The fix distinguishes between two types of class attributes: - `class="..."` attributes should overwrite (clear previous values) - `class:name=value` directives should merge (append to existing classes) Implementation: - Added `should_overwrite()` method to `IntoClass` trait (defaults to `false`) - Modified `Class::to_html()` to clear buffer before rendering if `should_overwrite()` returns `true` - Implemented `should_overwrite() -> true` for string types (`&str`, `String`, `Cow<'_, str>`, `Arc<str>`) - Tuple type `(&'static str, bool)` keeps default `false` for merge behavior Added comprehensive tests to verify: - `class="foo" class:bar=true` produces `"foo bar"` (merge) - `class:foo=true` works standalone - Correct behavior with macro attribute sorting - Global class application 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -103,6 +103,76 @@ fn test_classes() {
|
||||
assert_eq!(rendered.to_html(), "<div class=\"my big red car\"></div>");
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn test_class_with_class_directive_merge() {
|
||||
use leptos::prelude::*;
|
||||
|
||||
// class= followed by class: should merge
|
||||
let rendered: View<HtmlElement<_, _, _>> = view! {
|
||||
<div class="foo" class:bar=true></div>
|
||||
};
|
||||
|
||||
assert_eq!(rendered.to_html(), "<div class=\"foo bar\"></div>");
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn test_solo_class_directive() {
|
||||
use leptos::prelude::*;
|
||||
|
||||
// Solo class: directive should work without class attribute
|
||||
let rendered: View<HtmlElement<_, _, _>> = view! {
|
||||
<div class:foo=true></div>
|
||||
};
|
||||
|
||||
assert_eq!(rendered.to_html(), "<div class=\"foo\"></div>");
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn test_class_directive_with_static_class() {
|
||||
use leptos::prelude::*;
|
||||
|
||||
// class:foo comes after class= due to macro sorting
|
||||
// The class= clears buffer, then class:foo appends
|
||||
let rendered: View<HtmlElement<_, _, _>> = view! {
|
||||
<div class:foo=true class="bar"></div>
|
||||
};
|
||||
|
||||
// After macro sorting: class="bar" class:foo=true
|
||||
// Expected: "bar foo"
|
||||
assert_eq!(rendered.to_html(), "<div class=\"bar foo\"></div>");
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn test_global_class_applied() {
|
||||
use leptos::prelude::*;
|
||||
|
||||
// Test that a global class is properly applied
|
||||
let rendered: View<HtmlElement<_, _, _>> = view! { class="global",
|
||||
<div></div>
|
||||
};
|
||||
|
||||
assert_eq!(rendered.to_html(), "<div class=\"global\"></div>");
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn test_multiple_class_attributes_overwrite() {
|
||||
use leptos::prelude::*;
|
||||
|
||||
// When multiple class attributes are applied, the last one should win (browser behavior)
|
||||
// This simulates what happens when attributes are combined programmatically
|
||||
let el = leptos::html::div().class("first").class("second");
|
||||
|
||||
let html = el.to_html();
|
||||
|
||||
// The second class attribute should overwrite the first
|
||||
assert_eq!(html, "<div class=\"second\"></div>");
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn ssr_with_styles() {
|
||||
|
||||
@@ -57,6 +57,10 @@ where
|
||||
_style: &mut String,
|
||||
_inner_html: &mut String,
|
||||
) {
|
||||
// If this is a class="..." attribute (not class:name=value), clear previous value
|
||||
if self.class.should_overwrite() {
|
||||
class.clear();
|
||||
}
|
||||
class.push(' ');
|
||||
self.class.to_html(class);
|
||||
}
|
||||
@@ -156,6 +160,12 @@ pub trait IntoClass: Send {
|
||||
/// Renders the class to HTML.
|
||||
fn to_html(self, class: &mut String);
|
||||
|
||||
/// Whether this class attribute should overwrite previous class values.
|
||||
/// Returns `true` for `class="..."` attributes, `false` for `class:name=value` directives.
|
||||
fn should_overwrite(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Renders the class to HTML for a `<template>`.
|
||||
#[allow(unused)] // it's used with `nightly` feature
|
||||
fn to_template(class: &mut String) {}
|
||||
@@ -289,6 +299,10 @@ impl IntoClass for &str {
|
||||
class.push_str(self);
|
||||
}
|
||||
|
||||
fn should_overwrite(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
el: &crate::renderer::types::Element,
|
||||
@@ -346,6 +360,10 @@ impl IntoClass for Cow<'_, str> {
|
||||
IntoClass::to_html(&*self, class);
|
||||
}
|
||||
|
||||
fn should_overwrite(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
el: &crate::renderer::types::Element,
|
||||
@@ -403,6 +421,10 @@ impl IntoClass for String {
|
||||
IntoClass::to_html(self.as_str(), class);
|
||||
}
|
||||
|
||||
fn should_overwrite(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
el: &crate::renderer::types::Element,
|
||||
@@ -460,6 +482,10 @@ impl IntoClass for Arc<str> {
|
||||
IntoClass::to_html(self.as_ref(), class);
|
||||
}
|
||||
|
||||
fn should_overwrite(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
el: &crate::renderer::types::Element,
|
||||
|
||||
Reference in New Issue
Block a user