Chapter 11. Triggers

A trigger is a block of code that is attached to a variable and executed whenever the value of the variable changes, including the assignment of the initial value. You can also optionally get hold of the old values that were replaced within the trigger. Triggers are very powerful and unique in JavaFX Script, just like bind. Triggers can be attached to normal variables as well as to sequences. First let us see how triggers work for simple variables and then proceed to complicated triggers and sequence triggers.

Defining a Simple Trigger

Here is the typical syntax of a trigger defined on a variable:

Syntax

var <var name>[:data type] [= <initial value>] on replace [old value] {
           // block of code to be executed on value change
       }

or

var <var name>[:data type] [= bind expr] on replace [old value] {
           // block of code to be executed on value change
       }

Up until on replace, both syntax forms are pretty much the same as any variable declaration. The on replace clause indicates that there is a block of code that must be executed whenever the value of var name changes. This is what is called a trigger.

Note

Please note that "trigger" is just the name of the feature and not a keyword as such. It is typically represented by on replace.

Let us see a simple example of a trigger (Listing 11-1).

Example 11.1. SimpleTrigger.fx

var name="Praveen" on replace {
           println("Name has changed");
           println("New Name: {name}");
       }
       name = "Lawrence";

Output

Name has changed
New Name: Praveen
Name has changed
New Name: Lawrence

In this example, we are defining a trigger on a variable called name. Typically you would expect this trigger to be invoked when you change the value of name. But as mentioned in the introduction, assigning an initial value is also considered a change. Hence, first the trigger is called when the name variable is assigned the value praveen. That contributes to the first two lines of the output. Right after the declaration, you are changing the value of name to Lawrence. This is another change, so the same trigger gets called again, printing the last two lines of the output.

At this point you may think that the trigger is called the first time because you are specifying an initial value yourself. That is not correct. It gets called even when the compiler assigns a default value.

Note

For any variable, regardless of whether there is an initial value specified by the programmer or not, the trigger gets called during the variable initialization.

So even if you modify Listing 11-1 to the code shown in Listing 11-2, the trigger would still be called twice.

Example 11.2. SimpleTriggerWithoutInitialization.fx

var name on replace {
           println("Name has changed");
           println("New Name: {name}");
       }
       name = "Lawrence";

Output

Name has changed
New Name:
Name has changed
New Name: Lawrence

As you see in the output of Listing 11-2, the first two lines are printed when the trigger is executed while initializing the object; that's why you see no name printed on the second line of the output. The last two lines are printed because of the application-triggered change.

A Trigger with Access to the Old Value

Now if you want to access the old as well as the new value of the name variable, you can do that by changing the original Listing 11-1 code as shown in Listing 11-3.

Example 11.3. TriggerAccessOldVal.fx

var name = "Praveen" on replace oldName {
           println("Name has changed from old: {oldName} to new: {name}");
       }
       name = "Lawrence";

Output

Name has changed from old:  to new: Praveen
Name has changed from old: Praveen to new: Lawrence

In Listing 11-3, when the name is changed to a new value, the old value is assigned to the oldName variable before the trigger is called and passed to the trigger block. Note that oldName is just a variable name; it can be any name with which you want to access the old data of the variable. The oldName variable need not be declared upfront and can be implicit. However, please note that the scope of this variable is limited to the trigger block, and it cannot be accessed outside the block. Also note that this is like a final variable whose value cannot be modified within the trigger block. The sole purpose of this variable is to read the old value of the actual variable to which the trigger code is attached, and nothing beyond that. Trying to modify oldName within the trigger block would cause a compilation error.

Also note that you may have a variable already defined with the same name (oldName) outside of the trigger definition; that will not conflict with the one used in the trigger definition. These two variables will be treated separately, as you can see in Listing 11-4.

Example 11.4. TriggerVarNameSpace.fx

var oldName: String = "JavaFX";

       var name = "Praveen" on replace oldName {
           println("Name has changed from old: {oldName} to new: {name}");
       }

       name = "Lawrence";
       println("OLD NAME: {oldName}");

Output

Name has changed from old:  to new: Praveen
Name has changed from old: Praveen to new: Lawrence
OLD NAME: JavaFX

As you see in the output of Listing 11-4, the oldName you have defined outside the trigger does not get changed, and what has been defined in the trigger definition is entirely a new variable.

To summarize, the trigger functionality can be pictorially represented as shown in Figure 11-1.

Triggers in JavaFX Script

Figure 11.1. Triggers in JavaFX Script

This diagram precisely represents the on replace clause that constitutes a trigger. As you can see, it is the block between the first name clause and the last block clause. The use of sequences was explained in Chapter 10, "Sequences." The remaining clauses are pretty much self-explanatory. The first name clause is actually the name of the variable that gets the old value of the variable to which the trigger is attached. As you see in Figure 11.1, that name clause is optional, so you may choose not to get the old value.

Using Triggers with bind

There is no correlation between a trigger and bind as such, apart from the fact that the value of the variable changes through another variable or expression. Listing 11-5 shows a simple example of a trigger with bind.

Example 11.5. TriggerWithBind.fx

var w = 10;
       var h = 10;
       var d = 10;
       var isCube = true;
       var area = bind if (isCube) {w*h*d} else {w*h} on replace  oldVal {
           println("Area Changed from: {oldVal} to {area}");
       }
       w = 20;
       isCube = false;
       d = 20;

Output

Area Changed from: 0 to 1000
Area Changed from: 1000 to 2000
Area Changed from: 2000 to 200

In this example, we have a variable area that is bound to a conditional expression and has a trigger attached to it. As you learned in the binding chapter, the value of area changes whenever any of the following changes: w, h, d, or isCube. The first line of the output is generated by the variable initialization. The second line is due to the w value changing to 30. After that, we are changing the value of isCube, which again causes a value change in area from w * h * d to w * h, yielding 200 (20 × 10). Next we are changing the value of d, which does not cause the trigger to be executed, because the conditional expression is evaluating the expression in the else part, which does not include d. So this behavior is correct and expected.

Implementing Binding Using Triggers

While binding can be combined with triggers, it is also possible to implement binding without actually using the bind keyword, using triggers. This kind of implementation is needed when you want the benefit of binding a variable to an expression without losing the flexibility of being able to assign some value directly to the variable. Let us see a simple example (Listing 11-6).

Example 11.6. BindUsingTriggersError.fx

var x = 20;
       var y = 30;
       var sum = bind x + y on replace {
           println("Sum: {sum}");
       }
       x = 30;
       //sum = 100; // Runtime error

Output

Sum: 50
Sum: 60

In Listing 11-6, we are binding an expression (x + y) to sum, and whenever the x or y value changes, sum will be recalculated. However, if you want the flexibility of overriding the value of sum, it is not possible. The moment you assign some value to sum, the JavaFX Runtime will throw an error, saying you cannot assign to a bound variable. In this case, you cannot do a bidirectional binding, either, since there is an expression on the right side. You may recollect that bidirectional binding is limited to variables.

So in this situation, it would be wise to hand-wire the binding yourself, using triggers. This way, you will have the flexibility to override the value of sum at any time. Let us modify Listing 11-6 to implement hand-wired binding using triggers, as shown in Listing 11-7.

Example 11.7. BindUsingTriggers.fx

var x = 20 on replace {
               sum = x + y;
       }
       var y = 30 on replace {
           sum = x + y;
       }
       var sum = x + y on replace {
           println ("Sum: {sum}");
       }
       x = 30;
       overrideSum(100);
       function overrideSum(val: Integer) {
           sum = val;
       }

Output

Sum: 20
Sum: 50
Sum: 50
Sum: 60
Sum: 100

In Listing 11-7, we have defined a trigger on x as well as y. So whenever the value of x or y changes, we recalculate the sum and assign it to the variable sum. So you pretty much get the same effect as if you'd entered bind x + y. At the same time, you are able to override the value of sum at any time, and here we are assigning a value of 100 through the overrideSum method. Perhaps, in a real-world API library, you could expose this method as public and the library user could actually override the value of sum if needed, from the application code while leveraging on the hand-wired binding.

Similarly, you can implement bidirectional binding as well using triggers. Listing 11-8 shows simple binding code that uses bidirectional binding and demonstrates how you can implement it yourself without using bind.

Example 11.8. BidirectionalBind.fx

var name:String;
       var name1 = bind name with inverse;
       name = "JavaFX";
       println ("Name: {name}, Name1: {name1}");
       name1 = "Java";
       println ("Name: {name}, Name1: {name1}");

Output

Name: JavaFX, Name1: JavaFX
Name: Java, Name1: Java

In this example, you see a simple bidirectional binding that binds two string variables, name and name1. If either of them changes, the other one also changes, as you see in the output.

Now let us implement the same binding using triggers (Listing 11-9).

Example 11.9. BidirectionalBindUsingTrigger.fx

var name:String on replace {
           name1 = name;
       }
       var name1:String on replace {
           name = name1;
       }
       name = "JavaFX";
       println ("Name: {name}, Name1: {name1}");
       name1 = "Java";
       println ("Name: {name}, Name1: {name1}");

Output

Name: JavaFX, Name1: JavaFX
Name: Java, Name1: Java

The output of Listing 11-9 looks exactly same as that of Listing 11-8, but as you see, Listing 11-9 uses triggers to simulate the effect of bidirectional binding. When looking at the code, you may initially think that this would cause an infinite loop—after all, we are changing one variable from the other trigger, causing the triggers to be called indefinitely. However, that is not what actually happens. Please note that when name is changed to JavaFX, name's trigger is called, and it sets name1 to JavaFX as well. This in turn calls name1's trigger, which tries to set name back to JavaFX. But since name's value is already JavaFX, technically there is no change in name's value. Thus name's trigger won't be called in this case.

Note

A trigger is called only when there is a change in the value of the variable. If the variable is assigned the same value that it already holds, the trigger will not be called. In other words, if oldValue and newValue of a variable are the same, the trigger code is not executed.

The implementation of unidirectional and bidirectional binding using triggers provides a lot of flexibility, and you will find it very useful and handy in dealing with situations that demand both the power of binding and the flexibility of overriding the value of the bound variable. This technique also allows bidirectional binding of expressions that cannot be bound with the conventional with inverse clause.

Validation Within the Trigger

Triggers can be used effectively for validating the values of the variables to which they are attached, before the value is used in other bound expressions. When a variable is assigned a value, its trigger gets it first, before it is consumed by other expressions, and hence provides an opportunity to validate the values and eliminate the unwanted ones.

Listing 11-10 demonstrates a simple example of how to avoid a divide-by-zero scenario with triggers.

Example 11.10. TriggerValidation.fx

var x: Number = 10;
       var y: Number = 5 on replace oldVal {
           if (y <= 0) {
               y = oldVal;
               println("y value reset");
           }
       }

       var ratio = bind (x/y);
       println(ratio);
       y = 0;
       println(ratio);

Output

2.0
y value reset
2.0

In this example, the variable ratio is bound to an expression (x/y). If the value of y happens to be 0, it would yield an undesirable ratio value of infinity. Hence it is important to validate the y value and reset it appropriately before the ratio is recalculated. The best way to do this is to define a trigger and check the value of y from within that trigger. If the Y value is not appropriate, you can revert back to oldVal. This change will cause another trigger call, of course. Triggers can be powerful validators of data values, providing an opportunity for the programmer to veto any value change. This is evident from the output, where y is reset to its old value when it is set to 0, and the value of ratio remains unchanged, since the y value has not changed, as needed to force a recalculation of the ratio expression (x/y).

Sequence Triggers

Triggers defined on a sequence are a little more complex than the triggers defined on a normal variable that we have seen so far. Defining a trigger within a sequence has additional clauses that provide you wider access to the data changed in the sequence, from within the trigger. Let us go from a simpler trigger to more complicated examples. You may want to refer back to the diagram given in Figure 11.1 at this point.

The syntax that we have used with normal variables is applicable to sequences as well. Listing 11-11 shows an example of a trigger defined on a sequence.

Example 11.11. SequenceTrigger.fx

var seq = ['A', 'B', 'C', 'D', 'E', 'F'] on replace oldValue {
           println("Seq changed from {oldValue} to {seq}");
       }
       1.   insert 'G' into seq;
       2.   delete 'C' from seq;
       3.   insert 'Z' before seq[0];
       4.   seq[1] = 'V';
       5.   seq[3..5] = ['H', 'J'];
       6.   delete seq[1..2];

Compile and run the code to see the following result. Note that (here and in the remaining examples) the line numbers are not part of the actual output; they are added for explanation.

Output

Seq changed from  to ABCDEF
1.   Seq changed from ABCDEF to ABCDEFG
2.   Seq changed from ABCDEFG to ABDEFG
3.   Seq changed from ABDEFG to ZABDEFG
4.   Seq changed from ZABDEFG to ZVBDEFG
5.   Seq changed from ZVBDEFG to ZVBHJG
6.   Seq changed from ZVBHJG to ZHJG

In Listing 11-11, there is a simple trigger defined on the sequence, and the old value of the sequence is obtained through the oldValue variable. Now let us analyze the output line by line:

  • The first line of the output (unnumbered) is due to variable initialization, and the old value is empty. You can relate each line of numbered code to the corresponding line in the output, as explained here.

  • Code line 1: Tries to insert G at the end of the seq, and in the output, you see the seq value changed from ABCDEF to ABCDEFG.

  • Code line 2: Deletes item C from the seq and you see that the old seq ABCDEFG changes to ABDEFG since C is now removed.

  • Code line 3: Inserts an item Z at the beginning of the seq, which causes the entire seq to change. You see that Z gets inserted into the sequence.

  • Code line 4: Replaces the item at index 1 (A), changing it to V. Now you see the new value as ZVBDEFG.

  • Code line 5: Replaces items at indices [3..5], which means seq[3]. [4], and [5] with ['H', 'J']. Hence ZVBDEFG changes to ZVBHJG.

  • Code Line 6: Deletes items at indices 1,2, which changes the sequence content to ZHJG.

This output is not complicated, and this is just a simple use of triggers with sequences, where you get the entire unmodified sequence in the oldValue variable. Apart from the actual sequence modified and the old sequence, you don't get much information here about which of the indices are updated.

Triggers become more powerful when you start including the additional clauses provided exclusively for triggers. Let us see the syntax of the trigger with new clauses defined:

Syntax

var <var name>[: data type[] ] [= value] on  replace [old value [= new element(s)]] { ...
}

or

var <var name>[: data type[] ] [= value] on  replace [old value [[ firstIndex .. lastIndex ] = new element(s)]] { ...
}

For both variations the syntax up to on replace is pretty much the same as any other example you have seen so far, except that the variable is of type Sequence here. The sequence-specific clauses follow on replace; let's examine those in the first syntax in detail:

  • old value: The implicit variable to which the old sequence value would be assigned. It is similar to the example in the section "A Trigger with Access to the Old Value." The var name could be anything.

  • = new element(s): Another variable, to which the new elements added to the sequence would be assigned. The new elements would be coalesced if there is more than one new element added to the sequence. The new elements can be elements inserted newly into the sequence or can replace a set of existing elements. If you want to have this clause, then old value is also required compulsorily.

  • So the usage of this syntax form would be as follows:

    on replace oldValue {}

    or

    on replace oldValue = newElements { }

The second syntax format gives additional control over the range of values changed; let us see that clause in detail:

  • <old value>[startingIndex..endingIndex]: The starting and ending index variables are again just variable names and can be changed to any other variable name. These variables hold the starting and ending index of the sequence where the change has actually happened.

    Now let us see an example of each of these syntax formats (Listing 11-12).

Example 11.12. SequenceTriggerNewSyntax1.fx

1.  var seq = ['A', 'B', 'C', 'D', 'E', 'F'] on replace oldValue = newElements {
       2.      println("Seq changed:  {oldValue} by {newElements} to {seq}");
       3.  }
       4.  insert 'G' into seq;
       5.  delete 'C' from seq;
       6.  insert 'Z' before seq[0];
       7.  seq[1] = 'V';
       8.  seq[3..5] = ['H', 'J'];
       9.  delete seq[1..2];

Output

1.   Seq changed:   by ABCDEF to ABCDEF
2.   Seq changed:  ABCDEF by G to ABCDEFG
3.   Seq changed:  ABCDEFG by  to ABDEFG
4.   Seq changed:  ABDEFG by Z to ZABDEFG
5.   Seq changed:  ZABDEFG by V to ZVBDEFG
6.   Seq changed:  ZVBDEFG by HJ to ZVBHJG
7.   Seq changed:  ZVBHJG by  to ZHJG

In Listing 11-12, we have two variables being used after on replace : oldValue is used for getting the old value of the sequence, and newElements is used for getting the changed elements alone. The new sequence is of course, assigned to the seq variable itself.

When you look at the output, the oldValue printed is the value of the sequence before the actual change, and newElements prints out the elements that are added to the sequence—either new or replacing some of the existing elements. Note that newElements does not print anything when an element is deleted from the sequence (line 3 of the output). Another important point to notice is in line 6 of the output, which is triggered by line 8 from the code. This line tries to replace three elements in the sequence with two other elements, H and J. When you look at the output, these two new elements are coalesced to print HJ, and that's what has been assigned to newElements. Again the last line of output (line 7) does not print any newElements value, since it is triggered by a delete statement.

Now let us see the same example with the second syntax (Listing 11-13).

Example 11.13. SequenceTriggerNewSyntax2.fx

1.   var seq = ['A', 'B', 'C', 'D', 'E', 'F'] on replace oldValue[fIndex..lIndex] = newElements {
       2.       println ("Seq changed:  {oldValue} [{fIndex}..{lIndex}] by {newElements} to {seq}");
       3.   }
       4.   insert 'G' into seq;
       5.   delete 'C' from seq;
       6.   insert 'Z' before seq[0];
       7.   seq[1] = 'V';
       8.   seq[3..5] = ['H', 'J'];
       9.   delete seq[1..2];

Output

1.   Seq changed:   [0..-1] by ABCDEF to ABCDEF
2.   Seq changed:  ABCDEF [6..5] by G to ABCDEFG
3.   Seq changed:  ABCDEFG [2..2] by  to ABDEFG
4.   Seq changed:  ABDEFG [0..-1] by Z to ZABDEFG
5.   Seq changed:  ZABDEFG [1..1] by V to ZVBDEFG
6.   Seq changed:  ZVBDEFG [3..5] by HJ to ZVBHJG
7.   Seq changed:  ZVBHJG [1..2] by  to ZHJG

In Listing 11-13, the same code shown in Listing 11-12 has been modified to include the range information as well, and with these new clauses, you can precisely identify which part of the sequence is modified. Now let us analyze the output in detail:

  • Line 1: Triggered by the variable initialization.

  • Line 2: Triggered by Line 4 from the code, where a new element is inserted into the sequence. The first index changed is actually the new index created by the new element, 6,′ and it does not affect any other indices. Hence, the range is shown as [6..5], which denotes the newElements value of G.

  • Line 3: Caused by deleting the element C at index 2. It is a removal of an element and hence the newElements would not yield a valid value. The impact is just on a single element and hence the range shows [2..2]

  • Line 4: Caused by inserting Z into the seq at the first index. Pretty much the same behavior as line 2.

  • Line 5: Caused by replacing A with V at index 1. This is again a change at a single index, and so the range is [1..1]. However, since V is a new element added, newElements returns V.

  • Line 6: Caused by replacement of three values (index 3, 4, 5) with two values (H, J). The affected indices are 3,4,5, so the range is [3..5]. New elements added to the seq are coalesced and returned as newElements.

  • Line 7: Two elements have been deleted, so the affected index range shows [1..2]. Since it is a deletion, newElements is empty.

Note

The syntax [firstIndex..lastIndex] denotes a range within which the values/elements are changed within the sequence. This does not represent the index changes as such. For example, deleting an element at index-2 will change the indices of all the subsequent elements. That is not typically captured in the [firstIndex..lastIndex] range, which captures the indices of elements whose values were changed.

Now let us see another example that makes use of bind and for loops to create a sequence; we will define a trigger on that sequence. Listing 11-14 shows the code.

Example 11.14. SequenceTriggerWithBind.fx

var min = 0;
       var max = 5;

       def seq = bind for (x in [min..max]) " {x*x}" on replace oldVal[sindx..eindx] = newElm {
           println("Seq changed from {oldVal} [{sindx}..{eindx}] by {newElm} to {seq}");
       }
       min = 5;
       max = 8;

Output

1.   Seq changed from  [0..-1] by  0 1 4 9 16 25 to  0 1 4 9 16 25
2.   Seq changed from  0 1 4 9 16 25 [0..4] by  to  25
3.   Seq changed from  25 [1..0] by  36 49 64 to  25 36 49 64

In Listing 11-14, we are creating a sequence dynamically through a for expression that is bound to the seq variable. Note that the seq variable is a def, meaning that its definition is constant throughout the application life-cycle. Now we have defined a trigger on the sequence with access to the old value, start index, end index, and new element. The min and max values used in the for expression are bound, and so any automatic change to those values would cause the trigger to be executed. Now let us analyze the output in detail.

The initial sequence as shown in line 1 of the output consists of squares of [0, 1, 2, 3, 4, 5]. Now when the min value changes to 5, the sequence pretty much reduces to squares of [5..5], which is [25]. That's what has been shown in line 2 of the output. NewElm is empty because it is a removal operation. Now when you look at the difference between lines 1 and 2, the sequence size is reduced from 6 to just 1, and all items except the last one are removed. That's represented by the range [0..4] in line 2. Line 3 of the output represents the range of [5..8] when the max value is set to 8. This does not cause any change to the existing value but inserts three more values, which are represented by the newElm variable as 36 49 64. The final seq shows all the values inserted into the sequence.

Nested Triggers

A trigger can be defined on any variable regardless of where it is declared. It can be defined on a member variable of a class, on a script variable, on a local variable declared within a function, block, and so on. One can also define a trigger within another trigger since a trigger is just another block. Also, it is possible to change the value of a variable from within its own trigger.

Listing 11-15 shows a simple example of how nesting can be implemented.

Example 11.15. NestedTriggers.fx

1.   class TriggerSample {
       2.       var w = 100 on replace oldVal {
       3.           var valid = isValid(w) on replace {
       4.               if (not valid) {
       5.                   println("Invalid value {w}. Reset to {oldVal}");
       6.                   w = oldVal;
       7.               } else {
       8.                   println("Valid value {w}");
       9.               }
       10.               println(w);
       11.           }
       12.       }
       13.   }
       14.   function isValid(val: Integer) {
       15.       val > 0;
       16.   }
       17.   function run() {
       18.       var sample = TriggerSample{};
       19.       sample.w = 200;
       20.       sample.w = 0;
       21.   }

Output

1.   Valid value 100
2.   100
3.   Valid value 200
4.   200
5.   Invalid value 0. Reset to 200
6.   Valid value 200
7.   200
8 200

In Listing 11-15, we are defining a variable w as a member of a class, whose value we are validating within the trigger and resetting it if it is invalid. So there is another trigger defined within the main trigger here, and the nested trigger validates the value of w. As you see in the code, the inner trigger calls a script-level function to validate the value. Now let us analyze the output.

  • Line 1, 2: Printed when w is initialized.

  • Line 3: Setting w to 200 in the code calls the trigger, and this value has been validated to true.

  • Line 4: W is set with the value of 200 after validation.

  • Line 5: Sets a value of 0 to w. The isValid() function is called and returns false. Since the value is invalid, we are resetting the value to back to 200 (the old value).

  • Line 6: Note that 200 is not printed yet, which means line 9 from the code is not yet executed. This is because resetting the w value within the validation trigger calls the main trigger again, and this time isValid returns true for the reset value of 200.

  • Lines 7, 8: 200 is printed twice—once for the reset operation (caused by the inner trigger) and another time for the actual value set by the application (w = 0).

There are a few more things to note in this example: the script-level function isValid() is accessed from within the trigger, a nested trigger is used to validate the value, and the value of the variable is changed from within the same trigger. Also note that the variable w is accessible within the inner trigger of valid, and this is true for any block—it can access the variables of the parent block.

Note

You have to be careful when setting the value of a variable within its own trigger. If the value that is set within the trigger is incremental, that will cause the trigger to be called infinitely. For example, var w on replace { w = w + 1 } will cause the trigger to be called indefinitely and will result in a StackOverflowError.

Summary

In this chapter, you have examined in detail what triggers are. A trigger is a block of code that is attached to a variable and is executed when the value of the variable changes. You can access the old value of the variable within the trigger block and you can validate the new value assigned to the variable before it is consumed by other expressions. Triggers on sequences provide more control over the change by exposing the affected range, the old value of the sequence, and the new elements inserted into the sequence. You can define a trigger within another trigger, and the inner trigger can access the variables of the parent trigger. Triggers can be defined on any variable that is part of a class, script, block or function. You can also use triggers to implement your own binding, and you can do both unidirectional and bidirectional binding.

Thus, triggers are one of the most powerful and unique features of JavaFX Script; they can help you create an event-driven logic typically like an EventListener in Java AWT and Swing. Triggers are most widely used in animations in JavaFX, and you will see more of this in Chapter 13, "introduction to Animation."

With this chapter, you have been introduced to all the important features of JavaFX Script, and we have come to the end of our introduction to the language. In the next two chapters, we will dive deep into the JavaFX Graphics and Animation APIs, and you'll see how you can develop a full-fledged rich UI application using JavaFX Script.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset