diff --git a/fx/src/main/java/de/neitzel/fx/view/BindingList.java b/fx/src/main/java/de/neitzel/fx/view/BindingList.java
index 97fc280..a3f7bb1 100644
--- a/fx/src/main/java/de/neitzel/fx/view/BindingList.java
+++ b/fx/src/main/java/de/neitzel/fx/view/BindingList.java
@@ -11,8 +11,8 @@ import java.util.Map;
/**
* The list of binding elements declared inside {@code fx:define}.
*
- * The list is not a scene-graph child. FXML adds each {@link DirectBinding} or
- * {@link ComputedBinding} child to {@link #getElements()}.
+ * The list is not a scene-graph child. FXML adds each {@link DirectBinding},
+ * {@link ComputedBinding}, or {@link Repeat} child to {@link #getElements()}.
*/
@DefaultProperty("elements")
public class BindingList {
@@ -29,8 +29,8 @@ public class BindingList {
/**
* Returns the binding elements in this list.
*
- * FXML appends child elements to the returned list. A child is a {@link DirectBinding} or a
- * {@link ComputedBinding}.
+ * FXML appends child elements to the returned list. A child is a {@link DirectBinding}, a
+ * {@link ComputedBinding}, or a {@link Repeat}.
*
* @return the mutable list of binding elements
*/
@@ -54,6 +54,8 @@ public class BindingList {
DirectBindingApplier.apply(namespace, viewModel, directBinding, context);
} else if (element instanceof ComputedBinding computedBinding) {
ComputedBindingApplier.apply(namespace, viewModel, controller, root, computedBinding);
+ } else if (element instanceof Repeat repeat) {
+ RepeatApplier.apply(namespace, viewModel, repeat, context);
} else {
throw new IllegalArgumentException("Not a binding element: " + element);
}
diff --git a/fx/src/main/java/de/neitzel/fx/view/Components.java b/fx/src/main/java/de/neitzel/fx/view/Components.java
index 780ecc0..be7c6de 100644
--- a/fx/src/main/java/de/neitzel/fx/view/Components.java
+++ b/fx/src/main/java/de/neitzel/fx/view/Components.java
@@ -31,7 +31,9 @@ final class Components {
*
* An unknown type, such as a JavaFX control, makes {@link ApplicationContext#getComponent}
* throw {@link IllegalArgumentException}. This method returns null so FXML constructs that
- * type itself. A failure while creating a known Component is not caught.
+ * type itself. A failure while creating a known Component is not caught. The first Component
+ * of a class in one load is the cached singleton. A second Component of that class is a new
+ * injected instance, so the document can show two of them.
*
* @param type the type FXML is about to construct
* @param context the container that creates Components
@@ -44,7 +46,38 @@ final class Components {
} catch (IllegalArgumentException notAComponent) {
return null;
}
- return () -> load(component, context);
+ if (!LoadSession.claimFirst(type)) {
+ component = context.createComponent(type);
+ }
+ Object created = component;
+ return () -> load(created, context);
+ }
+
+ /**
+ * Creates a new Component and loads its document.
+ *
+ * The instance is not the cached singleton. Its dependencies still come from
+ * {@link ApplicationContext#getComponent}. Bindings wait until {@link #setModel} runs.
+ *
+ * @param type the Component class
+ * @param context the container that creates the instance and its dependencies
+ * @return the loaded Component
+ */
+ static Object fresh(Class> type, ApplicationContext context) {
+ Object component = context.createComponent(type.asSubclass(Object.class));
+ return load(component, context);
+ }
+
+ /**
+ * Sets the Component's model property.
+ *
+ * A non-null value selects the ViewModel and then runs the Component's bindings.
+ *
+ * @param component the Component
+ * @param model the Model instance
+ */
+ static void setModel(Object component, Object model) {
+ modelProperty(component).setValue(model);
}
private static Object load(Object component, ApplicationContext context) {
diff --git a/fx/src/main/java/de/neitzel/fx/view/LoadSession.java b/fx/src/main/java/de/neitzel/fx/view/LoadSession.java
index 3e14e81..e2f91cb 100644
--- a/fx/src/main/java/de/neitzel/fx/view/LoadSession.java
+++ b/fx/src/main/java/de/neitzel/fx/view/LoadSession.java
@@ -3,7 +3,9 @@ package de.neitzel.fx.view;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
/**
* Holds the binding lists instantiated while one View is loading.
@@ -18,6 +20,7 @@ final class LoadSession {
private static final ThreadLocal> STACK = new ThreadLocal<>();
private final List bindingLists = new ArrayList<>();
+ private final Set> built = new HashSet<>();
private LoadSession() {
}
@@ -39,20 +42,33 @@ final class LoadSession {
* Opens a nested session for a Component document.
*
* Binding lists created until {@link #unnest()} register here, not on the parent session.
+ * A Component created after the parent load has finished, such as a repeated child added
+ * later, still gets a session so its own binding list can be captured.
*
* @return the nested session
*/
static LoadSession nest() {
+ Deque stack = STACK.get();
+ if (stack == null) {
+ stack = new ArrayDeque<>();
+ STACK.set(stack);
+ }
LoadSession session = new LoadSession();
- STACK.get().push(session);
+ stack.push(session);
return session;
}
/**
* Restores the session that was current before {@link #nest()}.
+ *
+ * When the nested session was the only one, the thread-local stack is dropped.
*/
static void unnest() {
- STACK.get().pop();
+ Deque stack = STACK.get();
+ stack.pop();
+ if (stack.isEmpty()) {
+ STACK.remove();
+ }
}
/**
@@ -75,6 +91,23 @@ final class LoadSession {
stack.peek().bindingLists.add(bindingList);
}
+ /**
+ * Records the first Component of {@code type} built during this load.
+ *
+ * Nested documents share the outermost session, so a second Component of the same class
+ * anywhere in the load is not the first.
+ *
+ * @param type the Component class
+ * @return {@code true} when this is the first time that class is built during the load
+ */
+ static boolean claimFirst(Class> type) {
+ Deque stack = STACK.get();
+ if (stack == null || stack.isEmpty()) {
+ return true;
+ }
+ return stack.peekLast().built.add(type);
+ }
+
/**
* Returns the lists registered during this session, in document order.
*
diff --git a/fx/src/main/java/de/neitzel/fx/view/Repeat.java b/fx/src/main/java/de/neitzel/fx/view/Repeat.java
new file mode 100644
index 0000000..2ca04e1
--- /dev/null
+++ b/fx/src/main/java/de/neitzel/fx/view/Repeat.java
@@ -0,0 +1,69 @@
+package de.neitzel.fx.view;
+
+/**
+ * A repeat element declared in a {@link BindingList}.
+ *
+ * The element names the pane by {@code fx:id}, the Component class, and the collection
+ * Attribute. The library keeps one Component child in that pane for each Model instance, in
+ * list order.
+ */
+public class Repeat {
+
+ private String pane;
+ private String component;
+ private String collection;
+
+ /**
+ * Sets the {@code fx:id} of the pane that holds the Component children.
+ *
+ * @param pane the pane id
+ */
+ public void setPane(String pane) {
+ this.pane = pane;
+ }
+
+ /**
+ * Sets the Component class name. Each element gets a new instance of that class.
+ *
+ * @param component the class name
+ */
+ public void setComponent(String component) {
+ this.component = component;
+ }
+
+ /**
+ * Sets the collection Attribute name. The list holds the Model instances.
+ *
+ * @param collection the attribute name
+ */
+ public void setCollection(String collection) {
+ this.collection = collection;
+ }
+
+ /**
+ * Returns the {@code fx:id} of the pane.
+ *
+ * @return the pane id
+ */
+ public String getPane() {
+ return pane;
+ }
+
+ /**
+ * Returns the Component class name.
+ *
+ * @return the class name
+ */
+ public String getComponent() {
+ return component;
+ }
+
+ /**
+ * Returns the collection Attribute name.
+ *
+ * @return the attribute name
+ */
+ public String getCollection() {
+ return collection;
+ }
+}
diff --git a/fx/src/main/java/de/neitzel/fx/view/RepeatApplier.java b/fx/src/main/java/de/neitzel/fx/view/RepeatApplier.java
new file mode 100644
index 0000000..529b5dc
--- /dev/null
+++ b/fx/src/main/java/de/neitzel/fx/view/RepeatApplier.java
@@ -0,0 +1,83 @@
+package de.neitzel.fx.view;
+
+import de.neitzel.injection.ApplicationContext;
+import javafx.beans.property.Property;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.scene.Node;
+import javafx.scene.layout.Pane;
+
+import java.util.Map;
+
+/**
+ * Keeps one Component child in a pane for each Model instance in a collection.
+ */
+final class RepeatApplier {
+
+ private RepeatApplier() {
+ }
+
+ /**
+ * Fills the named pane with one new Component for each element, in list order.
+ *
+ * Each Component is a new injected instance. The cached singleton of that class is left
+ * in place. The child's model property is the element. Adding or removing an element
+ * adds or removes the matching child. The list is the one the entity holds, so a change
+ * on either side is visible in the pane.
+ *
+ * @param namespace objects addressed by {@code fx:id}
+ * @param viewModel the ViewModel for this View
+ * @param repeat the declaration
+ * @param context the container that creates each Component
+ */
+ static void apply(Map namespace, ViewModel viewModel, Repeat repeat,
+ ApplicationContext context) {
+ Object named = namespace.get(repeat.getPane());
+ if (!(named instanceof Pane pane)) {
+ throw new IllegalArgumentException("No pane \"" + repeat.getPane() + "\"");
+ }
+ Property> property = viewModel.property(repeat.getCollection());
+ if (property == null || !(property.getValue() instanceof ObservableList> values)) {
+ throw new IllegalArgumentException("No collection \"" + repeat.getCollection() + "\"");
+ }
+ Class> type = componentType(repeat.getComponent());
+ for (Object element : values) {
+ pane.getChildren().add(child(type, context, element));
+ }
+ listen(pane, type, context, values);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static void listen(Pane pane, Class> type, ApplicationContext context, ObservableList> values) {
+ ObservableList