diff --git a/README.md b/README.md
index 641a032..638e4a0 100644
--- a/README.md
+++ b/README.md
@@ -43,11 +43,11 @@ Some encryption classes that use format-preserving-encryption.
### fx
-Some JavaFX additions.
+JavaFX library that binds an FXML View to an ordinary Java Model. See [fx/README.md](fx/README.md).
### fx-example
-Some example programs that use the fx component
+Person desk that shows how to use `fx`. Run `mvn -pl fx-example javafx:run`.
### gson
diff --git a/fx-example/README.md b/fx-example/README.md
new file mode 100644
index 0000000..9056906
--- /dev/null
+++ b/fx-example/README.md
@@ -0,0 +1,11 @@
+# fx-example
+
+A person desk that uses the [`fx`](../fx/README.md) library: a Direct binding, a Converter, a Computed binding, a nested Component, a Repeat, a list of simple values, and Messages.
+
+From the NeitzelLib root, with Java 21:
+
+```bash
+mvn -pl fx-example javafx:run
+```
+
+The launch class is `de.neitzel.fx.example.Main`. The Model is an ordinary `Person`. The View is `Desk.fxml`.
diff --git a/fx-example/pom.xml b/fx-example/pom.xml
index 7dca1fb..b8cc2d9 100644
--- a/fx-example/pom.xml
+++ b/fx-example/pom.xml
@@ -16,27 +16,46 @@
${project.artifactId}${project.artifactId}${project.artifactId}
- de.neitzel.fx.injectfx.example.Main
+ de.neitzel.fx.example.Main${project.artifactId}-${project.version}
-
de.neitzel.libfx${project.version}
+
+ org.slf4j
+ slf4j-simple
+ runtime
+ ${jar.filename}
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ none
+
+
+
+ org.openjfx
+ javafx-maven-plugin
+ ${javafx.maven.plugin}
+
+ ${main.class}
+
+ com.github.spotbugsspotbugs-maven-plugin
-
org.apache.maven.pluginsmaven-pmd-plugin
diff --git a/fx-example/src/main/java/de/neitzel/fx/component/example/Address.java b/fx-example/src/main/java/de/neitzel/fx/component/example/Address.java
deleted file mode 100644
index e1a13cb..0000000
--- a/fx-example/src/main/java/de/neitzel/fx/component/example/Address.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package de.neitzel.fx.component.example;
-
-import lombok.Getter;
-import lombok.Setter;
-
-/**
- * Simple address model used in the FX example component.
- */
-@Getter
-@Setter
-public class Address {
-
- /**
- * Street name and number.
- */
- private String street;
-
- /**
- * City name.
- */
- private String city;
-
- /**
- * Default constructor only
- */
- public Address() {
- // default constructor only
- }
-}
\ No newline at end of file
diff --git a/fx-example/src/main/java/de/neitzel/fx/component/example/ExampleApp.java b/fx-example/src/main/java/de/neitzel/fx/component/example/ExampleApp.java
deleted file mode 100644
index 8eed94d..0000000
--- a/fx-example/src/main/java/de/neitzel/fx/component/example/ExampleApp.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package de.neitzel.fx.component.example;
-
-import de.neitzel.fx.component.ComponentLoader;
-import javafx.application.Application;
-import javafx.scene.Parent;
-import javafx.scene.Scene;
-import javafx.stage.Stage;
-
-/**
- * Example JavaFX application demonstrating the {@link de.neitzel.fx.component.ComponentLoader} usage.
- */
-public class ExampleApp extends Application {
-
- /**
- * Default constructor only
- */
- public ExampleApp() {
- // default constructor only
- }
-
- /**
- * Launches the JavaFX application.
- *
- * @param args command line arguments
- */
- public static void main(String[] args) {
- launch(args);
- }
-
- /**
- * Initializes sample model data, loads the FX component and shows the primary stage.
- *
- * @param primaryStage the primary stage for this application
- */
- @Override
- public void start(Stage primaryStage) throws Exception {
- // Beispielmodel initialisieren
- Address address = new Address();
- address.setStreet("Sample Street 1");
- address.setCity("Sample City");
-
- Person person = new Person();
- person.setName("Max Mustermann");
- person.setAddress(address);
-
- // ComponentLoader verwenden
- ComponentLoader loader = new ComponentLoader();
- Parent root = loader.load(person, "/person.fxml");
-
- // Scene erstellen und anzeigen
- Scene scene = new Scene(root);
- primaryStage.setTitle("ComponentLoader Example");
- primaryStage.setScene(scene);
- primaryStage.show();
- }
-}
\ No newline at end of file
diff --git a/fx-example/src/main/java/de/neitzel/fx/component/example/Main.java b/fx-example/src/main/java/de/neitzel/fx/component/example/Main.java
deleted file mode 100644
index 4ac3c9b..0000000
--- a/fx-example/src/main/java/de/neitzel/fx/component/example/Main.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package de.neitzel.fx.component.example;
-
-/**
- * Another Main class as workaround when the JavaFX Application ist started without
- * taking care os Classloader Requirements of JavaFX. (Important when starting from inside NetBeans!)
- */
-public class Main {
-
- /**
- * Default constructor only
- */
- public Main() {
- // default constructor only
- }
-
- /**
- * Additional main methode to start Application.
- *
- * @param args Commandline Arguments.
- */
- public static void main(String[] args) {
- ExampleApp.main(args);
- }
-}
diff --git a/fx-example/src/main/java/de/neitzel/fx/component/example/Person.java b/fx-example/src/main/java/de/neitzel/fx/component/example/Person.java
deleted file mode 100644
index e77a086..0000000
--- a/fx-example/src/main/java/de/neitzel/fx/component/example/Person.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package de.neitzel.fx.component.example;
-
-import lombok.Getter;
-import lombok.Setter;
-
-/**
- * Simple person model used in the FX example component.
- */
-@Getter
-@Setter
-public class Person {
-
- /**
- * The person's full name.
- */
- private String name;
-
- /**
- * The address of the person.
- */
- private Address address;
-
- /**
- * Default constructor only
- */
- public Person() {
- // default constructor only
- }
-}
diff --git a/fx-example/src/main/java/de/neitzel/fx/example/DeskApp.java b/fx-example/src/main/java/de/neitzel/fx/example/DeskApp.java
new file mode 100644
index 0000000..1ba8d82
--- /dev/null
+++ b/fx-example/src/main/java/de/neitzel/fx/example/DeskApp.java
@@ -0,0 +1,70 @@
+package de.neitzel.fx.example;
+
+import de.neitzel.fx.example.model.Address;
+import de.neitzel.fx.example.model.Person;
+import de.neitzel.fx.example.view.DeskController;
+import de.neitzel.fx.view.LoadedView;
+import de.neitzel.fx.view.MessagePopup;
+import de.neitzel.fx.view.ViewLoader;
+import de.neitzel.injection.ApplicationContext;
+import javafx.application.Application;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Opens the person desk for one ordinary {@link Person}.
+ */
+public class DeskApp extends Application {
+
+ /**
+ * Creates the JavaFX application.
+ */
+ public DeskApp() {
+ }
+
+ /**
+ * Builds the person the desk edits.
+ *
+ * @return a person with a home address, one visit, and two tags
+ */
+ public static Person sample() {
+ Address home = new Address("Sample Street 1", "Sample City");
+ Address visit = new Address("Work Road 2", "Office City");
+ Person person = new Person();
+ person.setName("Ada Lovelace");
+ person.setAge(36);
+ person.setHome(home);
+ person.setVisits(new ArrayList<>(List.of(visit)));
+ person.setSelectedVisit(visit);
+ person.setTags(new ArrayList<>(List.of("dns", "math")));
+ return person;
+ }
+
+ /**
+ * Launches the desk.
+ *
+ * @param args unused
+ */
+ public static void main(String[] args) {
+ launch(args);
+ }
+
+ /**
+ * Loads the desk for {@link #sample()} and shows it.
+ *
+ * @param stage the primary stage
+ */
+ @Override
+ public void start(Stage stage) {
+ ApplicationContext context = new ApplicationContext(DeskApp.class);
+ Person person = sample();
+ LoadedView loaded = ViewLoader.load(DeskController.class.getResource("Desk.fxml"), person, context);
+ MessagePopup.showNewMessages(loaded.getViewModel());
+ stage.setTitle("fx example");
+ stage.setScene(new Scene(loaded.getRoot()));
+ stage.show();
+ }
+}
diff --git a/fx-example/src/main/java/de/neitzel/fx/example/Main.java b/fx-example/src/main/java/de/neitzel/fx/example/Main.java
new file mode 100644
index 0000000..717567b
--- /dev/null
+++ b/fx-example/src/main/java/de/neitzel/fx/example/Main.java
@@ -0,0 +1,23 @@
+package de.neitzel.fx.example;
+
+import javafx.application.Application;
+
+/**
+ * Launches the example without extending {@link Application}.
+ *
+ * JavaFX on the classpath is started from a class that is not itself an {@link Application}.
+ */
+public final class Main {
+
+ private Main() {
+ }
+
+ /**
+ * Starts the person desk.
+ *
+ * @param args unused
+ */
+ public static void main(String[] args) {
+ Application.launch(DeskApp.class, args);
+ }
+}
diff --git a/fx-example/src/main/java/de/neitzel/fx/example/model/Address.java b/fx-example/src/main/java/de/neitzel/fx/example/model/Address.java
new file mode 100644
index 0000000..1621577
--- /dev/null
+++ b/fx-example/src/main/java/de/neitzel/fx/example/model/Address.java
@@ -0,0 +1,73 @@
+package de.neitzel.fx.example.model;
+
+/**
+ * An ordinary address the desk edits.
+ */
+public class Address {
+
+ private String street = "";
+ private String city = "";
+
+ /**
+ * Creates an empty address.
+ */
+ public Address() {
+ }
+
+ /**
+ * Creates an address with the given street and city.
+ *
+ * @param street the street
+ * @param city the city
+ */
+ public Address(String street, String city) {
+ this.street = street;
+ this.city = city;
+ }
+
+ /**
+ * Returns the street.
+ *
+ * @return the street
+ */
+ public String getStreet() {
+ return street;
+ }
+
+ /**
+ * Sets the street.
+ *
+ * @param street the street
+ */
+ public void setStreet(String street) {
+ this.street = street;
+ }
+
+ /**
+ * Returns the city.
+ *
+ * @return the city
+ */
+ public String getCity() {
+ return city;
+ }
+
+ /**
+ * Sets the city.
+ *
+ * @param city the city
+ */
+ public void setCity(String city) {
+ this.city = city;
+ }
+
+ /**
+ * Returns the street and city, for a combo box row.
+ *
+ * @return the street and city
+ */
+ @Override
+ public String toString() {
+ return street + ", " + city;
+ }
+}
diff --git a/fx-example/src/main/java/de/neitzel/fx/example/model/Person.java b/fx-example/src/main/java/de/neitzel/fx/example/model/Person.java
new file mode 100644
index 0000000..e3de57e
--- /dev/null
+++ b/fx-example/src/main/java/de/neitzel/fx/example/model/Person.java
@@ -0,0 +1,137 @@
+package de.neitzel.fx.example.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An ordinary person the desk edits.
+ *
+ * Read-write attributes are what the View binds to. The class carries no JavaFX types.
+ */
+public class Person {
+
+ private String name = "";
+ private Integer age;
+ private Address home;
+ private List
visits = new ArrayList<>();
+ private Address selectedVisit;
+ private List tags = new ArrayList<>();
+
+ /**
+ * Creates an empty person.
+ */
+ public Person() {
+ }
+
+ /**
+ * Returns the name.
+ *
+ * @return the name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the name.
+ *
+ * @param name the name
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns the age in years.
+ *
+ * @return the age, or {@code null} when none is set
+ */
+ public Integer getAge() {
+ return age;
+ }
+
+ /**
+ * Sets the age in years.
+ *
+ * @param age the age
+ */
+ public void setAge(Integer age) {
+ this.age = age;
+ }
+
+ /**
+ * Returns the home address.
+ *
+ * @return the home address, or {@code null} when none is set
+ */
+ public Address getHome() {
+ return home;
+ }
+
+ /**
+ * Sets the home address.
+ *
+ * @param home the home address
+ */
+ public void setHome(Address home) {
+ this.home = home;
+ }
+
+ /**
+ * Returns the visits.
+ *
+ * After the View has loaded, this is the list the View is watching.
+ *
+ * @return the visits
+ */
+ public List
getVisits() {
+ return visits;
+ }
+
+ /**
+ * Sets the visits.
+ *
+ * @param visits the visits
+ */
+ public void setVisits(List visits) {
+ this.visits = visits;
+ }
+
+ /**
+ * Returns the visit chosen in the combo box.
+ *
+ * @return the selected visit, or {@code null} when none is selected
+ */
+ public Address getSelectedVisit() {
+ return selectedVisit;
+ }
+
+ /**
+ * Sets the visit chosen in the combo box.
+ *
+ * @param selectedVisit the selected visit
+ */
+ public void setSelectedVisit(Address selectedVisit) {
+ this.selectedVisit = selectedVisit;
+ }
+
+ /**
+ * Returns the tags.
+ *
+ * After the View has loaded, this is the list the View is watching.
+ *
+ * @return the tags
+ */
+ public List getTags() {
+ return tags;
+ }
+
+ /**
+ * Sets the tags.
+ *
+ * @param tags the tags
+ */
+ public void setTags(List tags) {
+ this.tags = tags;
+ }
+}
diff --git a/fx-example/src/main/java/de/neitzel/fx/example/view/AddressForm.java b/fx-example/src/main/java/de/neitzel/fx/example/view/AddressForm.java
new file mode 100644
index 0000000..e2d01c6
--- /dev/null
+++ b/fx-example/src/main/java/de/neitzel/fx/example/view/AddressForm.java
@@ -0,0 +1,51 @@
+package de.neitzel.fx.example.view;
+
+import jakarta.inject.Singleton;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.scene.layout.VBox;
+
+/**
+ * Component that edits one address.
+ *
+ * The class is the root and the Controller of {@code AddressForm.fxml}. A parent View sets
+ * {@link #modelProperty()} from a Direct binding. A repeated row gets its own instance.
+ */
+@Singleton
+public class AddressForm extends VBox {
+
+ private final ObjectProperty