2222
2323import java .lang .reflect .InvocationTargetException ;
2424import java .lang .reflect .Modifier ;
25+ import java .util .ArrayList ;
26+ import java .util .List ;
2527import java .util .Objects ;
2628import java .util .concurrent .ConcurrentHashMap ;
2729import java .util .concurrent .CopyOnWriteArrayList ;
@@ -61,6 +63,11 @@ public class Component<THIS extends Component<THIS, VALUE>, VALUE> {
6163 * </pre>
6264 */
6365 public final int id = idCounter .getAndIncrement ();
66+ /**
67+ * List of children. Normally it's read-only. <br>
68+ * Thus do not modfify directly and use methods like {@link #add(Component[])} or {@link #remove(Component[])}
69+ * instead, to ensure the changes are also visible in the browser.
70+ */
6471 public final CopyOnWriteArrayList <Component > children = new CopyOnWriteArrayList <>();
6572 /**
6673 * Executed when a child was added on the Java side. <br>
@@ -149,31 +156,30 @@ public boolean isAttached(){
149156 child .element .remove ();
150157 child .update ();
151158
152- // Update UI
153- if (isAttached && !ui .isLoading ()){
154- ui .executeJavaScriptSafely (ui .jsGetComp ("comp" , id ) +
155- ui .jsGetComp ("childComp" , child .id ) +
156- "comp.removeChild(childComp);\n " ,
157- "internal" , 0 );
158- }
159-
160- child .isAttached = false ;
161- onChildRemove .execute (child );
162159 }
160+ UI .PendingAppend .removeFromPendingAppends (ui , child );
161+
162+ // Update UI always
163+ // Child might have already been removed in Java but not in JS
164+ executeJS (ui .jsGetComp ("childComp" , child .id ) +
165+ "comp.removeChild(childComp);\n " +
166+ (App .isInDepthDebugging ? "console.log('parent comp:', comp); console.log('➡️❌ removed childComp:', childComp); \n " : "" ));
167+
168+ child .isAttached = false ;
169+ onChildRemove .execute (child );
163170 };
164171 public Consumer <Component > _removeSelf = self -> {
165172 UI ui = UI .get (); // Necessary for updating the actual UI via JavaScript
166173 if (self .element .parent () != null ){
167174 self .element .remove ();
168175 }
169176 self .update ();
177+ UI .PendingAppend .removeFromPendingAppends (ui , self );
170178
171179 // Update UI
172- if (isAttached && !ui .isLoading ()){
173- ui .executeJavaScriptSafely (ui .jsGetComp ("comp" , self .id ) +
174- "comp.parentNode.removeChild(comp);\n " ,
175- "internal" , 0 );
176- }
180+ executeJS (ui .jsGetComp ("comp" , self .id ) +
181+ "comp.parentNode.removeChild(comp);\n " +
182+ (App .isInDepthDebugging ? "console.log('parent comp:', comp.parentNode); console.log('➡️❌ removed self:', comp); \n " : "" ));
177183
178184 self .isAttached = false ;
179185 //onChildRemove.execute(self);
@@ -191,6 +197,8 @@ public boolean isAttached(){
191197 e .childComp .update ();
192198 element .insertChildren (iOtherComp , e .childComp .element );
193199 } else if (e .isReplace ) {
200+ // childComp is the new component to be added
201+ // and otherChildComp is the one that gets removed/replaced
194202 int iOtherComp = children .indexOf (e .otherChildComp );
195203 children .set (iOtherComp , e .childComp );
196204 e .childComp .update ();
@@ -263,7 +271,9 @@ public boolean isAttached(){
263271 executeJS ("comp.setAttribute(`" + key
264272 + "`, `" + value + "`);\n " +
265273 "comp[`" +key +"`] = `" +value +"`\n " ); // Change UI representation
266- //System.out.println(key+" = "+ value);
274+ if (App .isInDepthDebugging ) AL .debug (this .getClass (), this .toPrintString ()+" _attributeChange javascript -> " +key +" = " + value );
275+ } else {
276+ if (App .isInDepthDebugging ) AL .debug (this .getClass (), this .toPrintString ()+" _attributeChange Java -> " +key +" = " + value );
267277 }
268278
269279 } else {// Remove attribute
@@ -334,11 +344,15 @@ public Component(@UnknownNullability VALUE value, @NotNull Class<VALUE> valueCla
334344 * that was set in the constructor.
335345 */
336346 public THIS getValue (Consumer <@ NotNull VALUE > v ) {
347+
337348 UI ui = UI .get ();
338- if (!isAttached || ui == null || ui .isLoading ()) // Since never attached once, user didn't have a chance to change the value, thus return internal directly
349+ if (!isAttached || ui == null || ui .isLoading ()) { // Since never attached once, user didn't have a chance to change the value, thus return internal directly
339350 v .accept (internalValue );
351+ if (App .isInDepthDebugging ) AL .debug (this .getClass (), this .toPrintString ()+" getValue() returns internalValue = " + internalValue );
352+ }
340353 else
341354 gatr ("value" , valueAsString -> {
355+ if (App .isInDepthDebugging ) AL .debug (this .getClass (), this .toPrintString ()+" getValue() returns from javascript value attribute = " +valueAsString );
342356 VALUE value = Value .stringToVal (valueAsString , this );
343357 v .accept (value );
344358 });
@@ -359,6 +373,8 @@ public THIS setValue(@Nullable VALUE v) {
359373 else newValJsonSafe = "\" " +Value .escapeForJSON (newVal )+"\" " ; // json object or other primitive
360374
361375 String message = "{\" newValue\" : " +newValJsonSafe +"}" ;
376+ if (App .isInDepthDebugging ) AL .debug (this .getClass (), this .toPrintString ()+" setValue() message -> " +message );
377+
362378 JsonObject jsonEl = JsonFile .parser .fromJson (message , JsonObject .class );
363379 ValueChangeEvent <THIS , VALUE > event = new ValueChangeEvent <>(message , jsonEl , _this , v , this .internalValue , true );
364380 this .internalValue = v ;
@@ -411,8 +427,16 @@ public boolean isValuesEqual(VALUE val1, VALUE val2){
411427 /**
412428 * Executes the provided JavaScript code now, or later
413429 * if this component is not attached yet. <br>
430+ * <br>
431+ * Does nothing and directly returns if the UI is null or still loading, since
432+ * we assume that your provided JS code is strongly related to this component and that
433+ * it does a manipulation that was done in Java code before (like for example changing its HTML, a style or attribute)
434+ * to prevent duplicate operations. <br>
435+ * If that is not the case use {@link UI#executeJavaScriptSafely(String, String, int)} instead. <br>
436+ * <br>
414437 * Your code will be encapsulated in a try/catch block and errors logged to
415438 * the clients JavaScript console. <br>
439+ * <br>
416440 * A reference of this component will be added before your code, thus you can access
417441 * this component via the "comp" variable in your provided JavaScript code.
418442 */
@@ -424,6 +448,7 @@ public THIS executeJS(String code){
424448 * @see #executeJS(String)
425449 */
426450 public THIS executeJS (UI ui , String code ){
451+ if (ui == null || ui .isLoading ()) return _this ;
427452 if (isAttached ){
428453 ui .executeJavaScriptSafely (
429454 "try{" +
@@ -475,14 +500,17 @@ public THIS now(Consumer<THIS> code) {
475500 }
476501
477502 /**
478- * Executes the provided code asynchronously in a new thread. <br>
503+ * Executes the provided code asynchronously in a thread from {@link App#executor} and returns directly. <br>
479504 * This function needs to be run inside UI context
480505 * since it executes {@link UI#get()}, otherwise {@link NullPointerException} is thrown. <br>
481506 * <br>
482507 * Note that your code-block will have access to the current UI,
483508 * which means that you can add/remove/change UI components without issues.
484509 * This also means that you will have to handle Thread-safety yourself
485- * when doing things to the same component from multiple threads at the same time.
510+ * when doing things to the same component from multiple threads at the same time. <br>
511+ * <br>
512+ * Also note that your code is not allowed to run forever/block, since
513+ * sometimes added components get added (in the browser) only after your code was run, see {@link UI#pendingAppends} and {@link UI#access(Runnable)}.
486514 *
487515 * @param code the code to be executed asynchronously, contains this component as parameter.
488516 */
@@ -837,6 +865,26 @@ public THIS visible(boolean b) {
837865 return _this ;
838866 }
839867
868+ /**
869+ * Requires that the children have absolute width/height. <br>
870+ * If this component has width/height set, those are used, otherwise 100% as width/height is used.
871+ */
872+ public THIS scrollable (boolean b ) {
873+ String width = style .get ("width" ).isEmpty () ? "100%" : style .get ("width" );
874+ String height = style .get ("height" ).isEmpty () ? "100%" : style .get ("height" );
875+ scrollable (b , width , height , "" , "" );
876+ return _this ;
877+ }
878+
879+ /**
880+ * Requires that the children have absolute width/height.
881+ */
882+ public THIS scrollable (boolean b , String width , String height ) {
883+ scrollable (b , width , height , "" , "" );
884+ return _this ;
885+ }
886+
887+
840888 /**
841889 * Makes this component scrollable. <br>
842890 * Note that you must also set the width and height for this to work, <br>
0 commit comments