1515 */
1616package com .vaadin .flow .internal .nodefeature ;
1717
18+ import java .io .Serializable ;
19+ import java .util .HashMap ;
20+ import java .util .Map ;
21+
1822import com .vaadin .flow .dom .ClassList ;
23+ import com .vaadin .flow .dom .Element ;
24+ import com .vaadin .flow .dom .ElementEffect ;
1925import com .vaadin .flow .internal .StateNode ;
26+ import com .vaadin .flow .shared .Registration ;
27+ import com .vaadin .signals .BindingActiveException ;
28+ import com .vaadin .signals .Signal ;
2029
2130/**
2231 * Handles CSS class names for an element.
2837 */
2938public class ElementClassList extends SerializableNodeList <String > {
3039
40+ private Map <String , SignalBinding > bindingsByName ;
41+
3142 private static class ClassListView extends NodeList .SetView <String >
3243 implements ClassList {
3344
45+ private final ElementClassList elementClassList ;
46+
3447 private ClassListView (ElementClassList elementClassList ) {
3548 super (elementClassList );
49+ this .elementClassList = elementClassList ;
3650 }
3751
3852 @ Override
@@ -41,7 +55,7 @@ protected void validate(String className) {
4155 throw new IllegalArgumentException ("Class name cannot be null" );
4256 }
4357
44- if ("" . equals ( className )) {
58+ if (className . isEmpty ( )) {
4559 throw new IllegalArgumentException (
4660 "Class name cannot be empty" );
4761 }
@@ -50,6 +64,103 @@ protected void validate(String className) {
5064 "Class name cannot contain spaces" );
5165 }
5266 }
67+
68+ private void internalSetPresence (String name , boolean set ) {
69+ // Directly mutate the underlying NodeList to bypass SetView
70+ // add/remove overrides which enforce BindingActiveException for
71+ // manual updates.
72+ ElementClassList list = this .elementClassList ;
73+ int index = list .indexOf (name );
74+ if (set ) {
75+ if (index == -1 ) {
76+ // append at the end
77+ list .add (list .size (), name );
78+ }
79+ } else {
80+ if (index != -1 ) {
81+ list .remove (index );
82+ }
83+ }
84+ }
85+
86+ private Map <String , SignalBinding > getBindings () {
87+ return elementClassList .getBindings ();
88+ }
89+
90+ private boolean isBound (String name ) {
91+ return elementClassList .isBound (name );
92+ }
93+
94+ private StateNode getNode () {
95+ return elementClassList .getNode ();
96+ }
97+
98+ @ Override
99+ public void bind (String name , Signal <Boolean > signal ) {
100+ validate (name );
101+ if (signal == null ) {
102+ // Unbind: remove existing binding and leave the current class
103+ // presence as-is
104+ if (isBound (name )) {
105+ SignalBinding old = getBindings ().remove (name );
106+ if (old != null && old .registration != null ) {
107+ old .registration .remove ();
108+ }
109+ }
110+ return ;
111+ }
112+
113+ if (isBound (name )) {
114+ throw new BindingActiveException ("Class name '" + name
115+ + "' is already bound to a signal" );
116+ }
117+ Element owner = Element .get (getNode ());
118+ Registration registration = ElementEffect .bind (owner , signal ,
119+ (element , value ) -> internalSetPresence (name ,
120+ Boolean .TRUE .equals (value )));
121+ SignalBinding binding = new SignalBinding (signal , registration ,
122+ name );
123+ getBindings ().put (name , binding );
124+ }
125+
126+ @ Override
127+ public boolean add (String className ) {
128+ if (isBound (className )) {
129+ throw new BindingActiveException ("Class name '" + className
130+ + "' is bound and cannot be modified manually" );
131+ }
132+ return super .add (className );
133+ }
134+
135+ @ Override
136+ public boolean remove (Object className ) {
137+ if (className instanceof String name ) {
138+ if (isBound (name )) {
139+ throw new BindingActiveException ("Class name '" + name
140+ + "' is bound and cannot be modified manually" );
141+ }
142+ }
143+ return super .remove (className );
144+ }
145+
146+ @ Override
147+ public void clear () {
148+ clearBindings ();
149+ super .clear ();
150+ }
151+
152+ // Bulk operations in AbstractCollection ultimately delegate to
153+ // add/remove
154+ // which are guarded above. No need to override
155+ // addAll/removeAll/retainAll
156+ // unless optimization is required.
157+
158+ /**
159+ * Clears all signal bindings.
160+ */
161+ public void clearBindings () {
162+ elementClassList .clearBindings ();
163+ }
53164 }
54165
55166 /**
@@ -70,4 +181,31 @@ public ElementClassList(StateNode node) {
70181 public ClassList getClassList () {
71182 return new ClassListView (this );
72183 }
184+
185+ private Map <String , SignalBinding > getBindings () {
186+ if (bindingsByName == null ) {
187+ bindingsByName = new HashMap <>();
188+ }
189+ return bindingsByName ;
190+ }
191+
192+ private boolean isBound (String name ) {
193+ return bindingsByName != null && bindingsByName .containsKey (name );
194+ }
195+
196+ private void clearBindings () {
197+ if (bindingsByName == null || bindingsByName .isEmpty ()) {
198+ return ;
199+ }
200+ for (SignalBinding binding : bindingsByName .values ()) {
201+ if (binding .registration != null ) {
202+ binding .registration .remove ();
203+ }
204+ }
205+ bindingsByName .clear ();
206+ }
207+
208+ private record SignalBinding (Signal <Boolean > signal ,
209+ Registration registration , String name ) implements Serializable {
210+ }
73211}
0 commit comments