Skip to content

Commit

Permalink
Add a tutorial about data flow between controllers (#112)
Browse files Browse the repository at this point in the history
* docs: Add a tutorial about data flow between controllers
* docs: Restructure data flow docs
* docs: Add tutorial main page
* docs: Link new tutorial page in docs mainpage
* docs: Add link to overview
* docs: Update link to new main page
* docs: Apply suggested changes
* docs: Add example for binding properties
* docs: Final touches to data flow tutorial
* docs: Use h1 header instead of h2
* docs: Add more information
* docs: Fix a typo

---------

Co-authored-by: Adrian Kunz <[email protected]>
  • Loading branch information
LeStegii and Clashsoft committed Jul 1, 2024
1 parent 6df4227 commit 0b9a964
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 3 deletions.
4 changes: 2 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ The [tutorial](tutorial/how-to-start.md) is a good starting point for a quick in
More detailed information can be found in the categories below.

- [Controllers and Components](controller/README.md)
- [Tutorial](tutorial/how-to-start.md)
- [Tutorials](tutorial/README.md)
- [Other Features](features/README.md)
- [Testing](testing/README.md)
- [Testing](testing/README.md)
6 changes: 6 additions & 0 deletions docs/tutorial/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Tutorials

This section includes multiple tutorials covering some topics with more examples than in the documentation.

- [How to start?](how-to-start.md)
- [Data Flow between controllers](data-flow.md)
183 changes: 183 additions & 0 deletions docs/tutorial/data-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Data Flow

There are several methods for sending data between controllers and components. This tutorial provides an overview of the different possibilities and best practices for your application.

### Depending on your usecase, different sections are relevant to you:

- Do you need bidirectional data flow or data flow between multiple components?
- Use [Properties](#properties).
- Do you want to pass data from a parent to its child?
- Do you want to update the child at some later point after initialization/initial rendering?
- Use [java methods](#java-constructs) like setters.
- Use [parameters](#parameters).
- Do you want to pass data from a child to its parent?
- Use [consumers/runnables](#callbacks).

## From parents to children

As components are usually used to split up functionality of controllers into smaller parts, e.g. for displaying certain parts of the view in a reusable way, they need to receive data.
This section covers different ways of sending data from a controller to its child components.

### Parameters

The simplest method is using the `@Param` annotation, which can be employed when displaying a controller with the `show()` or `initAndRender()` methods. This annotation can also bind various properties, making the data flow responsive to changes.

```java
@Component
public class MyComponent {

@Param("data")
Data data;

// ...
}
```

```java
@Controller
public class MyController {

@OnRender
void createSubs() {
MyComponent component = app.initAndRender(new MyComponent(), Map.of("data", myData));
// ...
}

}
```

### Java Constructs

Since controllers and components are Java objects, basic concepts like constructor parameters, setters, and getters can also be used. After creating a component instance, methods can be called on it for passing data or other effects. Unlike parameters which can only pass data during lifecycle events, the child's methods can be called at any time.

```java
@Controller
public class MyController {

MyComponent component;

@OnRender
void createSubs() {
component = new MyComponent(new Foo());
component.bar(new Data());
// ...
}

void onButtonClicked() {
component.foo();
}

}
```

## From children to parents

Data should usually only flow from parents to children. A controller can pass data directly to its sub-components using the `@Param` annotation, but a child should not directly access the parent instance, ensuring reusability without dependency on the parent.

However, sending data from a child to the parent is sometimes necessary. For example, if a component contains a button that changes the screen color when pressed, the parent needs to be updated when the button is clicked.

### Callbacks
An easy way of sending data from a child to a parent is creating a calback (e.g. a Consumer or Runnable) in the parent and passing it to the child.
The callback can then be called in the child to pass data to the parent.

```java
@Component
public class MyComponent {

@Param("colorChange")
Consumer<Color> color;

void onButtonClicked() {
Color newColor = ...;
color.accept(newColor);
}
}
```

```java
@Controller
public class MyController {

@OnRender
void createSubs() {
Consumer<Color> colorChange = (color) -> System.out.println("New color is " + color);
MyComponent component = app.initAndRender(new MyComponent(), Map.of("colorChange", colorChange));
// ...
}

}
```

The same approach can be used with Runnables instead of Consumers if only an effect needs to be triggered without passing any data.
Instead of the `@Param` annotation, callbacks can also be set using getters, setters, or constructor parameters, though that is not very advisable.

### Properties

For bidirectional data flow, Properties can be used.
Instead of defining a Runnable or Consumer directly, you can create a Property and then register listeners to it.

```java
@Component
public class MyComponent {

@Param("colorChange")
ObjectProperty<Color> color;

@Param("colorBind")
// As this field is final, bind() will be used instead
final ObjectProperty<Color> otherColor = new SimpleObjectProperty<>();

void onButtonClicked() {
Color newColor = ...;
color.set(newColor);
}
}
```

```java
@Controller
public class MyController {

Subscriber subscriber = ...;

@OnRender
void createSubs() {
ObjectProperty<Color> color = new SimpleObjectProperty(Color.WHITE);
ObjectProperty<Color> colorBind = new SimpleObjectProperty(Color.WHITE);
MyComponent component = app.initAndRender(new MyComponent(), Map.of("colorChange", color, "colorBind", colorBind));
subscriber.listen(color, (observable, oldValue, newValue) -> { // Use subscribers to prevent memory leaks
System.out.println(newValue);
});
}

}
```

The same Property could also be passed to multiple components and if one updates its value, every other component can react to it.

If the field annotated with `@Param` is final and not null, the property will be bound instead of being overwritten.
This allows to react to changes, but changes made to the property in the child will not apply to the parent.

## Bad Practices

Avoid including a direct reference to a child's parent in the code, as this reduces reusability, making the component dependent on the parent.

```java
@Component
public class MyComponent {

@Param("parent")
MyController parent; // Bad, makes MyComponent depend on MyController

void onButtonClicked() {
Color newColor = ...;
parent.setColor(newColor);
}
}
```

Additionally, avoid using JavaFX's `getParent()` method. While it might work when the child is inside another component, it does not provide access to the controller instance if the parent is a controller, but only to its view element.

---

[Overview](README.md)
2 changes: 1 addition & 1 deletion docs/tutorial/how-to-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,4 +505,4 @@ For more information about the different features of `FulibFx`, see the [documen

---

[Overview](../README.md)
[Overview](README.md)

0 comments on commit 0b9a964

Please sign in to comment.