07. Tutorial: Creating a Kanban Board
The general Reactor guide and FRToolbox Reference document outline the suite of functionality that Reactor can provide. However, they don’t really achieve anything that can’t already be done in FileMaker.
The real power of Reactor comes when you use Reactor’s features with a fully-interactive JavaScript user interface, employing a dynamic library.
This article takes you through how to create a fully-interactive Kanban board using Reactor. What’s a Kanban board? It’s a visual project management tool used to show how tasks progress through different stages. It can be post-it notes on the wall, or be achieved through software.
The first step is to find a library that gets us as close to the finish line as possible. Why write something that someone has already written? It just so happens that the jQuery library has exactly what we need.
The Sortable
interaction has a ‘Connect Lists’ example that allows us to reorder items within a grid. You can move them up or down vertically, or across horizontally.
https://jqueryui.com/sortable/#connect-lists
Ok, so let’s put together what we want to build. We’re planning three space exhibitions, in which we’re visiting various planets (presumably in a futuristic super-fast spaceship). The trips will be represented horizontally, and we’ll want to drag a planet into one of these ‘trips’ and even change the order in which we visit the planets.
So let’s start building our BlackBox.
From the Reactor main screen, click the + button to get started.
We’re starting from scratch.
Give your BlackBox a name and a description:
We’re using the JQuery library, so add the following as source files:
https://www.dropbox.com/s/i0bw4rpuokdxa3p/jquery-3.4.1.min.js?dl=0
https://www.dropbox.com/s/jpajabb9a9iee88/jquery-ui.min.js?dl=0
Now switch over to the Functions tab, and add your function, as well as the following parameters:
The first five are all FileMaker fields:
PlanetIDField: A unique ID for a planet record
PlanetNameField: The field containing the name of a planet
PlanetDescriptionField: The field containing the planet’s description
PlanetTripNoField: This represents what ‘trip’ we’ve selected for a planet
PlanetSortNoField: This represents the order of planets within a trip
And the last is a text parameter:
Columns: A comma separated list of columns to show
We’ve built the basics of our function, so now let’s go ahead and build a template file.
What’s a template file? You can read more about that in the General Reactor Tutorial, but basically, it does its best to get you started.
If you have added a JavaScript library (such as JQuery) it will ensure this is imported. If you have five parameters that are field names, it will store these as variables. It will also get you started with methods to read, update, create or delete records based on these field names, but we’ll be writing our own methods for this.
So go ahead and delete all those functions, and ensure your new HTML source file is as follows:
<html>
<head>
<style>
<!–– CSS Goes here ––>
</style>
</head>
<script type='text/javascript' src='FRToolBox.js'></script>
<script src="jquery-3.4.1.min.js"></script>
<script src="jquery-ui.min.js"></script>
<!–– Import other sources here ––>
<script>
FMLink = 'FM.link';
// This is where variables are set from the given parameters
var RecordsObj = new Object;
FMTableOccurrenceName = '<?Reactor bbdev_TO( $PlanetIDField ) Reactor?>';
FunctionWhereClause = '<?Reactor bbdev_Relationship( bbdev_TO( $PlanetIDField ) ) Reactor?>';
CreationValues = '<?reactor bbdev_relationshipKeyBuilder ( $FMTableOccurrenceName ) reactor?>';
PlanetIDField = '<?Reactor bbdev_Field( $PlanetIDField ) Reactor?>';
PlanetNameField = '<?Reactor bbdev_Field( $PlanetNameField ) Reactor?>';
PlanetDescriptionField = '<?Reactor bbdev_Field( $PlanetDescriptionField ) Reactor?>';
PlanetTripNoField = '<?Reactor bbdev_Field( $PlanetTripNoField ) Reactor?>';
PlanetSortNoField = '<?Reactor bbdev_Field( $PlanetSortNoField ) Reactor?>';
Columns = '<?Reactor $Columns Reactor?>';
</script>
<body>
<div>
<!–– This is the main HTML for the BlackBox ––>
</div>
</body>
</html>
Great, so the first step with any BlackBox is to get your UI up and running - then when that’s done you hook your FileMaker data into it. The reason we do the UI first, is so we have somewhere to put our FileMaker data. If we started with that, we’d just be supplying our data to a dark indifferent void.
Ok, so let’s switch back to that JQuery example, and click view source
:
Unfortunately it’s not a simple copy and paste - instead it’s 3 simple copy and pastes. Copies and paste?
Copy all of contents between the
<style>
tags, and paste it between your BB<style>
tags:
#sortable1, #sortable2 {
border: 1px solid #eee;
width: 142px;
min-height: 20px;
list-style-type: none;
margin: 0;
padding: 5px 0 0 0;
float: left;
margin-right: 10px;
}
#sortable1 li, #sortable2 li {
margin: 0 5px 5px 5px;
padding: 5px;
font-size: 1.2em;
width: 120px;
}
Copy the
$( function()
block, and paste it it at the end of your BB<script>
block:
$( function() {
$( "#sortable1, #sortable2" ).sortable({
connectWith: ".connectedSortable"
}).disableSelection();
} );
Copy the HTML near the bottom, and paste it between your BB
<body>
tags:
<ul id="sortable1" class="connectedSortable">
<li class="ui-state-default">Item 1</li>
<li class="ui-state-default">Item 2</li>
<li class="ui-state-default">Item 3</li>
<li class="ui-state-default">Item 4</li>
<li class="ui-state-default">Item 5</li>
</ul>
<ul id="sortable2" class="connectedSortable">
<li class="ui-state-highlight">Item 1</li>
<li class="ui-state-highlight">Item 2</li>
<li class="ui-state-highlight">Item 3</li>
<li class="ui-state-highlight">Item 4</li>
<li class="ui-state-highlight">Item 5</li>
</ul>
The full contents of your HTML file should now be as follows:
<html>
<head>
<style>
#sortable1, #sortable2 {
border: 1px solid #eee;
width: 142px;
min-height: 20px;
list-style-type: none;
margin: 0;
padding: 5px 0 0 0;
float: left;
margin-right: 10px;
}
#sortable1 li, #sortable2 li {
margin: 0 5px 5px 5px;
padding: 5px;
font-size: 1.2em;
width: 120px;
}
</style>
</head>
<script type='text/javascript' src='FRToolBox.js'></script>
<script src="jquery-3.4.1.min.js"></script>
<script src="jquery-ui.min.js"></script>
<script>
FMLink = 'FM.link';
FMTableOccurrenceName = '<?Reactor bbdev_TO( $PlanetIDField ) Reactor?>';
FunctionWhereClause = '<?Reactor bbdev_Relationship( bbdev_TO( $PlanetIDField ) ) Reactor?>';
CreationValues = '<?reactor bbdev_relationshipKeyBuilder ( $FMTableOccurrenceName ) reactor?>';
PlanetIDField = '<?Reactor bbdev_Field( $PlanetIDField ) Reactor?>';
PlanetNameField = '<?Reactor bbdev_Field( $PlanetNameField ) Reactor?>';
PlanetDescriptionField = '<?Reactor bbdev_Field( $PlanetDescriptionField ) Reactor?>';
PlanetTripNoField = '<?Reactor bbdev_Field( $PlanetTripNoField ) Reactor?>';
PlanetSortNoField = '<?Reactor bbdev_Field( $PlanetSortNoField ) Reactor?>';
Columns = '<?Reactor $Columns Reactor?>';
$( function() {
$( "#sortable1, #sortable2" ).sortable({
connectWith: ".connectedSortable"
}).disableSelection();
} );
</script>
<body>
<ul id="sortable1" class="connectedSortable">
<li class="ui-state-default">Item 1</li>
<li class="ui-state-default">Item 2</li>
<li class="ui-state-default">Item 3</li>
<li class="ui-state-default">Item 4</li>
<li class="ui-state-default">Item 5</li>
</ul>
<ul id="sortable2" class="connectedSortable">
<li class="ui-state-highlight">Item 1</li>
<li class="ui-state-highlight">Item 2</li>
<li class="ui-state-highlight">Item 3</li>
<li class="ui-state-highlight">Item 4</li>
<li class="ui-state-highlight">Item 5</li>
</ul>
</body>
</html>
Now switch to the ‘Preview’ screen and click the Compile button. You should see your new BlackBox appear before your eyes:
Next, in our Kanban board we’re going to want to show a planet name, and a planet description. So change the HTML so that each <li>
element contains a second line of text:
<ul id="sortable1" class="connectedSortable">
<li><div class=name>Item 1</div><div class=desc>The first item</div></li>
<li><div class=name>Item 2</div><div class=desc>The second item</div></li>
<li><div class=name>Item 3</div><div class=desc>The third item</div></li>
<li><div class=name>Item 4</div><div class=desc>The fourth item</div></li>
<li><div class=name>Item 5</div><div class=desc>The fifth item</div></li>
</ul>
<ul id="sortable2" class="connectedSortable">
<li><div class=name>Item 1</div><div class=desc>The first item</div></li>
<li><div class=name>Item 2</div><div class=desc>The second item</div></li>
<li><div class=name>Item 3</div><div class=desc>The third item</div></li>
<li><div class=name>Item 4</div><div class=desc>The fourth item</div></li>
<li><div class=name>Item 5</div><div class=desc>The fifth item</div></li>
</ul>
Great. Doesn’t quite ‘pop’ though does it? Replace the CSS (contents of your <style>
block) with this:
#sortable1, #sortable2 {
width: 142px;
min-height: 20px;
list-style-type: none;
margin: 0;
padding: 5px 0 0 0;
float: left;
margin-right: 10px;
}
#sortable1 li, #sortable2 li {
margin: 0 5px 5px 5px;
padding: 5px;
width: 120px;
border:black solid 1px;
background-color:#123a53;
color:white;
font-size:16px;
}
Recompile and take a look:
There, that’s looking a bit better. Feel free to make further CSS adjustments.
Ok, we have our base UI sorted, let’s start putting some data into it.
Accessing Planet Data
First, replace the contents inside our <body>
tags with this:
<div id=Kanban></div>
We need to dynamically generate the HTML content from our Planet
records, so this just provides us with a place to put it.
Now, for getting the data in. We need to:
Query Reactor for the Planet records
Create an ‘Unassigned’ column for records that don’t have a
Trip No
valueAdd an HTML element for each of these records to the ‘Unassigned’ column
For each column provided in our parameter:
Check our query response for records that belong in this column (based on the
Trip No
field, and add an HTML element for each of these records to the column
So, replace the contents inside our <script>
tags with the following:
FMLink = 'FM.link';
// This is where variables are set from the given parameters
FMTableOccurrenceName = '<?Reactor bbdev_TO( $PlanetIDField ) Reactor?>';
FunctionWhereClause = '<?Reactor bbdev_Relationship( bbdev_TO( $PlanetIDField ) ) Reactor?>';
CreationValues = '<?reactor bbdev_relationshipKeyBuilder ( $FMTableOccurrenceName ) reactor?>';
PlanetIDField = '<?Reactor bbdev_Field( $PlanetIDField ) Reactor?>';
PlanetNameField = '<?Reactor bbdev_Field( $PlanetNameField ) Reactor?>';
PlanetDescriptionField = '<?Reactor bbdev_Field( $PlanetDescriptionField ) Reactor?>';
PlanetTripNoField = '<?Reactor bbdev_Field( $PlanetTripNoField ) Reactor?>';
PlanetSortNoField = '<?Reactor bbdev_Field( $PlanetSortNoField ) Reactor?>';
Columns = '<?Reactor $Columns Reactor?>';
var PlanetsArr = new Array();
// Populate columns
function findPlanets() {
FRTB.find(
FMTableOccurrenceName + "::" + PlanetIDField,
FMTableOccurrenceName + "::" + PlanetNameField,
FMTableOccurrenceName + "::" + PlanetDescriptionField,
FMTableOccurrenceName + "::" + PlanetTripNoField,
FMTableOccurrenceName + "::" + PlanetSortNoField
).where(FunctionWhereClause).send(function(response) {
for ( var i=0; i < response.data.length; i++) {
ThisPlanetObj = new Object();
ThisPlanetObj.id = response.data[i][FMTableOccurrenceName + "::" + PlanetIDField];
ThisPlanetObj.name = response.data[i][FMTableOccurrenceName + "::" + PlanetNameField];
ThisPlanetObj.description = response.data[i][FMTableOccurrenceName + "::" + PlanetDescriptionField];
ThisPlanetObj.trip_no = response.data[i][FMTableOccurrenceName + "::" + PlanetTripNoField];
ThisPlanetObj.sort_no = response.data[i][FMTableOccurrenceName + "::" + PlanetSortNoField];
PlanetsArr.push(ThisPlanetObj);
}
buildHtml();
PlanetsArr.sort(function(a, b){
return a.sort_no > b.sort_no;
});
});
}
function buildHtml() {
ColumnsArr = Columns.split(",");
htmlString = "";
columnsIDsArray = Array();
htmlString += "<ul id='sortable_unassigned' class='connectedSortable'>";
htmlString += "<div class=Column>Unassigned</div>";
for ( var i=0; i < PlanetsArr.length; i++) {
if (PlanetsArr[i].trip_no == "") {
htmlString += "<li class=name id='" + PlanetsArr[i].id + "'><div>";
htmlString += PlanetsArr[i].name;
htmlString += "</div><div class=desc>";
htmlString += PlanetsArr[i].description;
htmlString += "</div></li>";
}
}
htmlString += "</ul>";
columnsIDsArray[0] = "#sortable_unassigned";
for ( var i=0; i < ColumnsArr.length; i++) {
htmlString += "<ul id='sortable_" + i + "' class='connectedSortable'>";
htmlString += "<div class=Column>" + ColumnsArr[i] + "</div>";
for ( var j=0; j < PlanetsArr.length; j++) {
if (PlanetsArr[j].trip_no == (i+1)) {
htmlString += "<li class=name id='" + PlanetsArr[j].id + "'><div>";
htmlString += PlanetsArr[j].name;
htmlString += "</div><div class=desc>";
htmlString += PlanetsArr[j].description;
htmlString += "</div></li>";
}
}
htmlString += "</ul>";
columnsIDsArray[i+1] = "#sortable_" + i;
}
$('#Kanban').html(htmlString);
$( columnsIDsArray.join() ).sortable({
connectWith: ".connectedSortable"
}).disableSelection();
}
$( function() {
findPlanets();
});
Quite a lot of code isn’t it? Let’s go through it bit-by-bit.
Query Planet Data
function findPlanets() {
FRTB.find(
FMTableOccurrenceName + "::" + PlanetIDField,
FMTableOccurrenceName + "::" + PlanetNameField,
FMTableOccurrenceName + "::" + PlanetDescriptionField,
FMTableOccurrenceName + "::" + PlanetTripNoField,
FMTableOccurrenceName + "::" + PlanetSortNoField
).where(FunctionWhereClause).send(function(response) { $('#Kanban').html("");
PlanetsArr = new Array();
for ( var i=0; i < response.data.length; i++) {
ThisPlanetObj = new Object();
ThisPlanetObj.id = response.data[i][FMTableOccurrenceName + "::" + PlanetIDField];
ThisPlanetObj.name = response.data[i][FMTableOccurrenceName + "::" + PlanetNameField];
ThisPlanetObj.description = response.data[i][FMTableOccurrenceName + "::" + PlanetDescriptionField];
ThisPlanetObj.trip_no = response.data[i][FMTableOccurrenceName + "::" + PlanetTripNoField];
ThisPlanetObj.sort_no = response.data[i][FMTableOccurrenceName + "::" + PlanetSortNoField];
PlanetsArr.push(ThisPlanetObj);
}
PlanetsArr.sort(function(a, b){
return a.sort_no > b.sort_no;
});
buildHtml();
});
}
First we set up our find request, accessing the following fields:
Planet ID
Planet Name
Planet Description
Trip No
Sort No
When a response comes back, we loop through the different records in the response. We create an object for each record, assign each object the above properties, and push the object onto an array of Planet objects.
We then sort the array of Planet objects based on the Sort No
value.
Then we run the buildHtml()
function.
Add column for records without a Trip No and assign planets to it
ColumnsArr = Columns.split(",");
htmlString = "";
columnsIDsArray = Array();
htmlString += "<ul id='sortable_unassigned' class='connectedSortable'>";
htmlString += "<div class=Column>Unassigned</div>";
for ( var i=0; i < PlanetsArr.length; i++) {
if (PlanetsArr[i].trip_no == "") {
htmlString += "<li class=name id='" + PlanetsArr[i].id + "'><div>";
htmlString += PlanetsArr[i].name;
htmlString += "</div><div class=desc>";
htmlString += PlanetsArr[i].description;
htmlString += "</div></li>";
}
}
htmlString += "</ul>";
columnsIDsArray[0] = "#sortable_unassigned";
Now we take our comma separated list of columns and turn them into an array - this allows us to easily iterate through it later. We also start to generate our HTML for our Unassigned
column.
Then we loop through our array of Planet records. For each, each check to see if there isn’t a Trip No
set, and if not we add it to our Unassigned
column by appending the HTML for each of these Planets. We also create a columnsIDArray
array and pop our new Unassigned
column into it as its first value. We’ll use this later to instruct the JavaScript library to treat it as a draggable box.
Go through each column, and categorise our Planet records
for ( var i=0; i < ColumnsArr.length; i++) {
htmlString += "<ul id='sortable_" + i + "' class='connectedSortable'>";
htmlString += "<div class=Column>" + ColumnsArr[i] + "</div>";
for ( var j=0; j < PlanetsArr.length; j++) {
if (PlanetsArr[j].trip_no == (i+1)) {
htmlString += "<li class=name id='" + PlanetsArr[j].id + "'><div>";
htmlString += PlanetsArr[j].name;
htmlString += "</div><div class=desc>";
htmlString += PlanetsArr[j].description;
htmlString += "</div></li>";
}
}
htmlString += "</ul>";
columnsIDsArray[i+1] = "#sortable_" + i;
}
First we loop through each Column
and start generating the HTML for it, similar to how we did for the Unassigned
column.
Then for each Column
we loop through our array of Planet records, and if a Planet record belongs in this Column
we add it to that column by appending the HTML for each of these planets. Before we loop to the next Column
we pop it onto our columnsIDArray
.
Instantiate our draggable list
$('#Kanban').html(htmlString);
$( columnsIDsArray.join() ).sortable({
connectWith: ".connectedSortable"
}).disableSelection();
Now we tell the JavaScript to place the variable containing our generated HTML into our <div id="Kanban">
element.
(Just a quick note on the technique we’re using to populate the HTML - this is quite primitive. There are much more elegant ways using the JQuery library to change or append to HTML elements, this technique we’re employing - of building a single string bit-by-bit - makes it clear what we’re actually doing).
Then we tell the JQuery library to instantiate the .sortable()
method on our array of Column IDs (this is the IDs of our HTML elements)- although first we have to use the join()
method to transform our array of ColumnIDs into a comma separated list of Column IDs - as this is the form that the method expects. This is what ‘activates’ our planet boxes - makes them draggable.
Now, ensure your JavaScript matches the JavaScript as above, recompile your BlackBox, and set the following parameters over on the Preview tab:
Fantastic!
The Planets might appear under different columns depending on whether they have Trip No
values set. If there are none set, you should see them all under Unassigned.
If you’re seeing something completely different, or nothing at all, make sure you’ve entered the JavaScript exactly as above. Ensure you’ve imported all required JavaScript libraries - the order matters too. You must import the core JQuery library before importing the JQuery-UI library.
Failing that, enter debugging mode by popping the following into your FM data viewer:
And click the Reactor icon in the bottom-right of your web-viewer, and click Browser
.
This will open the BlackBox in your default web browser, where you can use the Developers Tools to debug it.
Changing sort order
So now we’ve got our Kanban board pulling in our Planet records and sorting them into the right columns. We can move these Planets around - but these changes are not recorded. The next time you load the web-viewer, your changes will be lost (unless you also change the data back in FileMaker Pro).
The first thing we need is a way to intercept when a Planet has been moved. We can do this with an event handler.
An Event is something a user, or your browser, does. Handlers can be attached to particular events to capture when they happen, and the details of what’s happened.
To proceed, we need to consult the documentation.
https://api.jqueryui.com/sortable/#event-receive
https://api.jqueryui.com/sortable/#event-stop
These two events are useful to us. The recieve
event fires when a Planet is moved into another list, and we need to update the Trip No
and Sort No
. The stop
event fires when a Planet is moved within its own list, and we need to update the Sort No
.
To capture an event, change this:
$( columnsIDsArray.join() ).sortable({
connectWith: ".connectedSortable"
}).disableSelection();
To this:
$( columnsIDsArray.join() ).sortable({
connectWith: ".connectedSortable",
receive: function (event,ui) {
// Do something
}
}).disableSelection();
So let’s have a think about what we need to tell FileMaker when dragging a Planet to a different location. First off, we’re changing what column it’s assigned to, so we need to write that back. But just as importantly, we have effectively changed the sort order in the target column, as the Planets that sit below where we’re dragging to are now pushed down the sort order.
So we need to write the sort order of all records in the target column, including the record we’ve just moved.
To do this, we need to know the contents of our target column. We can do this by looking at the child elements that belong to the column element:
$("#" + event.target.id)[0].childNodes[3].id;
This would return the record ID of the third planet below the target. Not sure how this works?
When we generated our HTML after querying our Planet records, we stored the ID against the element for the Planet:
htmlString += "<li class=name id='" + PlanetsArr[j].id + "'><div>";
So we look at the HTML element of our column, look at the child elements, and inspect each for our PlanetID.
So now we just need to loop through each of these IDs, and update the sort order. We can start at 1, and increase it by 1 every time.
So replace our .sortable()
method call with this:
$( columnsIDsArray.join() ).sortable({
connectWith: ".connectedSortable",
stop: saveLocation,
receive: saveLocation,
}).disableSelection();
This will call the same function for both stop
and receive
events.
Write the function somewhere in our JavaScript prior to this - the best place would be before the findPlanets()
function.
function saveLocation(event,ui) {
NewTripNo = PlanetID = $("#" + event.target.id)[0].childNodes[0].innerText.replace("Trip ","");
for (var i=1; i <= $("#" + event.target.id)[0].childNodes.length; i++) {
PlanetID = $("#" + event.target.id)[0].childNodes[i].id;
FRTB.update(
[FMTableOccurrenceName + "::" + PlanetSortNoField, i],
[FMTableOccurrenceName + "::" + PlanetTripNoField, NewTripNo]
).filter(
FMTableOccurrenceName + "::" + PlanetIDField + "=" + PlanetID
).send();
}
}
Now for our obligatory step through what it does. First we the extract trip number of the number we’ve moved our Planet to - to do this we pull out the text name of the Column HTML element (the column text that actually displays), and parse out the actual trip number. If it’s our Unassigned
column we ensure that a blank value will be set.
Then we loop through every Planet within this column, grab its ID, and update the sort number, and trip number, for each record based on each ID.
For the sort number, we just send through the current “for loop” value, this will ensure the order they appear under the column will be the relative order numbers for each record.
So if you recompile, switch to the preview tab, any changes you make should be persistent. Move a planet to a different space in the list, and it will remember the new order. Move a planet to a different column, it should remember that it’s now under the column, and its order within that column - you should be able to switch to a different tab, then switch back, and it will remain the same.
If your BlackBox doesn’t remember any of this, ensure your function is exactly as written above, and you set it against the events as in the snippet above that.
We could stop right here. It brings our Planets from FileMaker into our BlackBox, and allows us to move them around and have the changes write back to FileMaker. So next time it loads it will remember where you left everything.
However, let’s ponder for a moment - let’s say we’re building this for a database hosted on FileMaker Server. We’re using the Kanban board to plan our various exhibitions, and another one of our Space Trip Planners moves Venus into Trip 2. If we want your Kanban board to update in real-time we’ll need to employ polling.
Polling for changes
Now we’re ensuring that our interactions with our BlackBox are persistent, it’s not just for show - it’s actually doing something.
What if one day we discover a new planet? What if something terrible happens and one of our planets disappears? What if the name/description of a planet changes? Then our BlackBox would be showing out-of-date information.
To pick up on record changes, apply the .poll()
method to our original query that brings in all the Planet data, and replace this line:
).where(FunctionWhereClause).send(function(response) {
With this:
).where(FunctionWhereClause).poll(pollChanges).send(function(response) {
This instructs Reactor so that once the query is complete, it will continue to poll for changes (default is every 5 seconds).
When a new planet is discovered we need to populate it into the relevant column. But where in the column do we put it? The Sort No
could have been set by another user, which could affect the position of all other Planets in that column. So to play it safe, we need to re-query all Planets that belong to that column, and re-list them based on their order.
When an existing planet gets a new name/description, we we need to show those new details. But what if the Trip No
changes? Then we need to update the original column to remove it, and update the target column to add it. What if the Sort No
changes, then similar to new planets, we need to re-list both columns.
What if an existing planet disappears, and is deleted? We’d need to update the column to remove it.
We could really tailor these polling scenarios to update the relevant UI elements. For example:
New Planet
Add it to the relevant column. If it has no
Sort No
set, put it at the bottom of the column, and write back theSort No
values of all Planets in that column. If it does have aSort No
set, we’d need to place it in the appropriate place in the column.
Updated Planet
When an existing planet has details changed, it’s possible that the
Sort No
could have changed, so to ensure the order of the Planets in that column are correct we’d need to check itsSort No
verses theSort No
of the planets above/below it. If it should be above/below an adjacent Planet and it’s not, then they should be switched, and then check the Planet above/below it again - and keep doing this comparison until its reached its correct location.
Deleted Planet
This one’s much simpler. If a planet disappears, we simply need to remove it from its relevant column
Where did we lose you? We assume we lost you somewhere in that confusion.
If we were to enforce all these interactions in our polling, this guide would get insufferably long - and frankly it’s not necessary. Instead, we’ll simply redraw the Kanban board any time Reactor reports that there are new records, updated records, or deleted records, from our original found set.
So write a polling function as so:
function redrawBoard(results) {
if (OverridePoll) return;
if (results.remove.length > 0 || results.create.length > 0 || results.update.length > 0) {
findPlanets();
}
}
And tell our initial query to run this function when polling detects a new/changed/removed record:
).where(FunctionWhereClause).poll(redrawBoard).send(function(response) {
So what does our function do? It’s quite simple. It automatically gets passed through the results of the polling. This is an object containing three arrays of row ID’s. One array for new records, one array for changed records, and one array for deleted records. What is a row ID? It’s a number that FileMaker uses internally to identify records, any Reactor query automatically includes the row ID in its response, regardless of whether or not you asked for it.
However in this example we don’t need to know what records have been changed/added/deleted, all we need to know is that there are records that have been changed/added/deleted. So simply checking the lengths of the three arrays to see if any have values will do, for our purposes.
If there are records changed/added/deleted we simply run our findPlanets()
function, this re-queries the data and displays it - the same as when the BlackBox first loads.
However it’s not ready yet - we need to ignore any polling changes when we’re the ones writing the data back. Why? Think about this sequence of events:
We move a planet to somewhere else in the Kanban board
It writes the change back to FileMaker
This triggers polling, triggering the reloading of the content
The content reloads in the middle of the ‘dragging’ action, halting the action
This is why we check the OverridePoll
variable at the start of the function.
In our saveLocation()
function, we need to set the OverridePoll
variable to true
:
function saveLocation(event,ui) {
var OverridePoll = true;
...}
And set it to false when the FRTB.update()
function is completed:
.send(function(response) {
var OverridePoll = false;
});
And since the variable is used in multiple functions we need to define it globally. So if you add it to our global variables before our functions, any function will have access to it:
...
Columns = '<?Reactor $Columns Reactor?>';
var PlanetsArr = new Array();
var OverridePoll = false;
There, now we have an option to Override polling. This means while this Override is activated, any polling will be ignored, so we activate it at the start of our action that writes to FileMaker, and we deactivate it when it has been written.
So now if you pop into the data layout for Planets, you’ll be able to edit the raw data and should see the Kanban board changing before your eyes.
You’ve successfully taken a dynamic JavaScript library, and implemented it using Reactor to communicate with FileMaker. Any visual interface you wish to create in Reactor involves these core principles:
Read data into your BlackBox
Intercept events that should change data, and write changes back to FileMaker (adding a record, updating a record, editing record fields, or even running a script - though that isn’t part of this guide)
Show data changes in real time
Any BlackBox comes down to these things. Some only read data, same also write data back, others also use polling.
Were we being a bit presumptuous in arrogantly declaring your BlackBox is working? Luckily we prepared one earlier. Check out our Kanban BlackBox that comes included in our Reactor Core, or just download our completed BlackBox file and see where you went wrong:
https://www.dropbox.com/s/lz3p751qezikjsu/PlanetKanban.rbb?dl=0
Import it into your Reactor, by clicking ‘Import a new BlackBox’ when creating a new BlackBox. Just be sure to rename the BlackBox and function if they’re the same name as yours - otherwise Moogie will get confused.