mirror of
https://github.com/noodlapp/noodl-docs.git
synced 2026-01-11 23:02:54 +01:00
Initial commit
Co-Authored-By: kotte <14197736+mrtamagotchi@users.noreply.github.com> Co-Authored-By: mikaeltellhed <2311083+mikaeltellhed@users.noreply.github.com> Co-Authored-By: Tore Knudsen <18231882+torekndsn@users.noreply.github.com> Co-Authored-By: Michael Cartner <32543275+michaelcartner@users.noreply.github.com>
This commit is contained in:
613
docs/guides/business-logic/client-side-biz-logic-js.mdx
Normal file
613
docs/guides/business-logic/client-side-biz-logic-js.mdx
Normal file
@@ -0,0 +1,613 @@
|
||||
---
|
||||
title: Building Business Logic Using Javascript
|
||||
hide_title: true
|
||||
---
|
||||
import CopyToClipboardButton from '/src/components/copytoclipboardbutton'
|
||||
import ImportButton from '../../../src/components/importbutton'
|
||||
|
||||
|
||||
# Client Side Business Logic Using Javascript
|
||||
|
||||
## What you will learn in this guide
|
||||
This guide shows how you use Javascript to implement business logic on the client (front-end) side of your app. While you can build business logic with a no-code approach using logic nodes (as described in [this guide](/docs/guides/business-logic/client-side-biz-logic-nodes)) its often easier to use Javascript since business logic sometimes is more readable in code form. In this guide we will make use of the [Function](/nodes/javascript/function) and [Script](/nodes/javascript/script) nodse which are the main way to mix code and no-code in Noodl.
|
||||
Another important node is the [Component Object](/nodes/component-utilities/component-object) that's the main way of storing state data so it can be accessed easily both in the code world and the node world.
|
||||
|
||||
## Overview
|
||||
The guide will implement a simple multiselect interaction on a list and a few multiselect operations, such as delete and copy. The business logic will handle the multi-select itself (selecting, deselecting) as well as the operations on the content in the list. It will also control the state of **Buttons** on the screen.
|
||||
|
||||
We will use various Noodl concepts, such as lists, Arrays and Objects, so it's probably good if you have gone through those guides before ( [List guide](/docs/guides/data/list-basics), [Array guide](/docs/guides/data/arrays) and [Object guide](/docs/guides/data/objects)). We will also use Javascript of course, and especially many Array functions, so that's worth checking out as well. A good documentation can be found for example [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array).
|
||||
|
||||
There is also a dedicated guide on the **Script** node, [here](/docs/guides/business-logic/javascript).
|
||||
|
||||
## Creating a list with multiselect UI
|
||||
|
||||
This example will revolve around a simple collection of orders, for example in a e-commerce system. The orders have an `order_nbr`, a `quantity` and a `delivery_date`. The App should list the orders and let the user select a number of them, using a standard multi select interaction. Then the selected orders can either be _deleted_, _copied_ or _merged_. We will look what the actual operations mean in more detail as we implement them. It's also possible to _select all_ or _deselect all_ items.
|
||||
|
||||
## The base data
|
||||
|
||||
We start this guide by adding in our base dataset that we want to work on. We are going to use the [Static Array](/nodes/data/array/static-array) to keep our data, but in essence this data could have come from a cloud database. So start a new project, using the "Hello World" template. Then add a **Static Array** node, set it to `JSON` format, and add in the following data:
|
||||
|
||||
```json
|
||||
[
|
||||
{"order_nbr":"A-12124", "quantity":2, "delivery_date":"2022-10-23"},
|
||||
{"order_nbr":"A-26232", "quantity":6, "delivery_date":"2022-10-25"},
|
||||
{"order_nbr":"V-23532", "quantity":3, "delivery_date":"2022-09-13"},
|
||||
{"order_nbr":"B-99243", "quantity":5, "delivery_date":"2022-08-03"},
|
||||
{"order_nbr":"V-35124", "quantity":1, "delivery_date":"2022-12-20"},
|
||||
{"order_nbr":"G-23421", "quantity":1, "delivery_date":"2022-09-09"},
|
||||
{"order_nbr":"B-86612", "quantity":8, "delivery_date":"2022-11-21"},
|
||||
{"order_nbr":"C-61633", "quantity":5, "delivery_date":"2022-05-29"},
|
||||
{"order_nbr":"V-42241", "quantity":2, "delivery_date":"2022-11-15"},
|
||||
{"order_nbr":"V-99112", "quantity":12, "delivery_date":"2022-12-20"},
|
||||
{"order_nbr":"A-51512", "quantity":1, "delivery_date":"2022-07-07"},
|
||||
{"order_nbr":"B-00914", "quantity":8, "delivery_date":"2022-09-13"},
|
||||
{"order_nbr":"C-11121", "quantity":9, "delivery_date":"2022-10-19"}
|
||||
]
|
||||
```
|
||||
|
||||
Now we have some orders in the system so ket's start building the UI. We will start with the multi select list.
|
||||
|
||||
## Building a Multi Select List
|
||||
We want to build a component that can show an array of items, as well as keeping track of which items that are selected. Let's outline the functionality of it below
|
||||
|
||||
* You should be able to feed it with items that will be shown in the list, with a checkbox in front of them
|
||||
* You should be able to feed it an initial selection, i.e. which items that should be selected
|
||||
* As you selected or deselect items, the component should trigger a signal and also provide an **Array** with the items that are currently selected
|
||||
* You should be able to select or deselect all items by triggering a signal
|
||||
|
||||
A question is how the multi select list would encode which items that are selected. One way of doing it would be that the multiselect list would provide an **Array** of items of format:
|
||||
```json
|
||||
{
|
||||
"Value":<id of selected object>
|
||||
}
|
||||
```
|
||||
|
||||
So for example, if the list has three items, with ids "111", "222" and "333". If the first and second item are selected, the output from the component would be:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"Value":"111"
|
||||
},
|
||||
{
|
||||
"Value":"222"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
So, when we are done, the component would look something like below from the outside:
|
||||
|
||||
<div className="ndl-image-with-background">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
### Selection logic
|
||||
Let's think about the logic needed by the component. Internally the component would need to keep track of which items that are selected and which are not. Partly to be able to generate the **Array** above, but also to be able to visualize it correctly.
|
||||
|
||||
You could of course store the selection state directly in the item, i.e. an order in our case, but that's actually not a great design. What would happen if the order was part of multiple Multi Select lists? Also, if we end up storing the item in the database we might also store the selection state if we are not careful, which doesn't make sense at all.
|
||||
|
||||
A better design is to "wrap" the each order in another **Object** that keeps track of the selection state. Then we display a list of those objects when we present the list instead of the original items. The "wrapper" of course needs to store the **Id** of the **Object** that it wraps so it can present the data from the item too. The format of the wrapper could be:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"Checked":<true or false depending if the item is checked or not>,
|
||||
"Value":<the id of the Object it wraps>
|
||||
}
|
||||
]
|
||||
```
|
||||
So, again in the example above with the items with id "111", "222" and "333", with the first two checked, the Checkbox array would look like the following:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"Checked":true,
|
||||
"Value":"111"
|
||||
},
|
||||
{
|
||||
"Checked":true,
|
||||
"Value":"222"
|
||||
},
|
||||
{
|
||||
"Checked":false,
|
||||
"Value":"333"
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
So in summary our Multi Select list component will need the following logic on the inside
|
||||
* When the list is fed with new items, it will create one new Checkbox Array containing an **Object** per item (the wrapper) and set `Checked` to `true` or `false` depending on initial selection.
|
||||
* The list item presenting each item will use the `Checked` property to visualize the checked status of the item. If the user clicks the checkbox the `Checked` property is toggled.
|
||||
* Whenever the checked status changes, the Multi Select List should generete a new list that only holds the items that are checked, according to the format above, i.e. `[{"Value":<id of a checked item>}, ...]`. This will be presented to the outside world.
|
||||
|
||||
Start by creating a new visual component. Call it "Multi Select List".
|
||||
|
||||
<div className="ndl-image-with-background">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
You will now have a more or less empty component. Add in a **Component Inputs** and a **Component Outputs**. We now what they should contain are already, i.e. how to interact with our component. So in the **Component Inputs** add the ports `Selection`, `Items`, `Select All` and `Clear Selection`.
|
||||
|
||||
<div className="ndl-image-with-background">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
In the **Component Outputs** we create the ports `Selection` and `Selection Changed`.
|
||||
|
||||
<div className="ndl-image-with-background">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Now we begin writing our Javascript logic. Add a **Script** node.
|
||||
|
||||
<div className="ndl-image-with-background">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
We begin by adding two inputs, one is the `Items` that the list should display. The other is the `InitialSelection` that contains any items that should be selected initially by the list. Both of these are of `Array` type.
|
||||
|
||||
<div className="ndl-image-with-background">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Ok, let's start coding! The first thing we want to do is to generate our Checkbox list, i.e. the **Array** of wrapping **Objects** that hold the checked state of each item.
|
||||
|
||||
## Setter functions - execute code when an input changes
|
||||
|
||||
We want to generate that **Array** any time the `Items` input changes. If you want to execute a piece of script any time a specific input changes you can use a _Setter_ function. These are declared using the following format:
|
||||
|
||||
```javascript
|
||||
Script.Setters.<property name> = function (value) {...}
|
||||
```
|
||||
|
||||
In our case we want to generate the Checkbox **Array** whenever **Items** is changed so we add the follwing code in our script file:
|
||||
|
||||
```javascript
|
||||
Script.Setters.Items = function (items) {
|
||||
if(!Script.Inputs.Items) return
|
||||
// Create a list of all items with checked status, based on the initial selection array
|
||||
Component.Object.Checkboxes = Script.Inputs.Items.map(o => Noodl.Object.create({
|
||||
id:Component.Object.id+'-'+o.id,
|
||||
Value:o.id,
|
||||
Checked:Script.Inputs.InitialSelection!==undefined && !!Script.Inputs.InitialSelection.find(s => s.Value === o.id)
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
This code goes through the incoming **Items** (referred to through `Script.Inputs.Items`), and for each item creates a new wrapper **Object** (using `Noodl.Object.create`). It checks the checked state and sets either `true` or `false` against the initial selection array. It also stores a pointer to the wrapped **Object** in the property `Value`.
|
||||
|
||||
### Checking for `undefined` inputs
|
||||
|
||||
If an input of a script node hasn't been set yet, for example in the case of our `Items` input or `InitialSelection` here, they will have the value `undefined`. It's often a good idea to explicitly check for this case in your code.
|
||||
|
||||
Another little trick here is that we explicitly set the **id** of the wrapper **Object**. This is actually not needed, as Noodl will generate a new **id** if no **id** is specified. But by making up an **id** of our own, that will be unique but the same for every wrapper object that wraps a specific object, we will reuse old **Objects** rather than creating new.
|
||||
|
||||
As you can see, the resulting **Array** is stored in `Component.Object.Checkboxes`. This we should pay some extra attention to.
|
||||
|
||||
## Using a Component Object in your scripts
|
||||
|
||||
Each component in Noodl have a **Component Object** that's unique. Opposite to a regular **Object** a **Component Object** _can only be accessed by the component itself, or by children of the component using **Parent Component Objects**_. This creates a scope and you does not risk other components accessing this object by mistake. For example if you would show two Multi Select lists on the same screen, if you used a regular **Object** to store your Checkboxes, you would need to make sure the **Objects** had unique ids (the Checkboxes can be different in the two lists). We avoid this problem all togheter by storing our Checkboxes in our **Component Object**.
|
||||
|
||||
All components have a Component Object, and you can access it in the Node world by adding a **Component Object** node. So close the **Script** node for now, and add in a **Component Object** node. Also add the property **Checkboxes** on the **Component Object**.
|
||||
|
||||
While we are at it we also connect the two outputs from **Component Inputs** (`Items` and `Selection`) to **Items** and **Initial Selection** inputs on the **Script** node.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Let's test what we done so far, so we add in an **Array** node and connect the **Checkboxes** property of the **Component Object** to it (through the **Items** property). Next, we add in the `Multi Select List` component into our main App. Give it some items by connecting the **Static Array** items to it. If all is set up correctly, you should see some Checkbox items flowing into the **Array**.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Making the List Items
|
||||
Now we are ready to create the List Items. Let's package up our `Multi Select List` components into a folder. So create a new folder, call it `Multi Select List`. Move our `Multi Select List` component into it. Then add another visual component. Call it `List Item`.
|
||||
|
||||
<div className="ndl-image-with-background">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
We keep the list item visually simple. A horizontal layout with a [Checkbox](/nodes/ui-controls/checkbox) and three texts. Add some padding and margins to make it a little prettier. If you want, you can copy and paste the nodes into your component.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||
<CopyToClipboardButton json={{"nodes":[{"id":"5c3f112c-604f-5c89-f500-bf8c79701ebc","type":"Group","x":-106,"y":-21,"parameters":{"flexDirection":"row","paddingTop":{"value":10,"unit":"px"},"paddingBottom":{"value":10,"unit":"px"},"paddingLeft":{"value":10,"unit":"px"},"paddingRight":{"value":10,"unit":"px"},"sizeMode":"contentHeight"},"ports":[],"children":[{"id":"8bfcaedb-94c0-e145-f973-5627c4adc2c8","type":"net.noodl.controls.checkbox","x":-86,"y":25,"parameters":{"useLabel":false,"marginRight":{"value":9,"unit":"px"}},"ports":[],"children":[]},{"id":"4abf508d-30fd-8273-0b5c-1c43ffc761fb","type":"Text","label":"Order nr","x":-86,"y":71,"parameters":{},"ports":[],"children":[]},{"id":"66a0a3b1-f67c-37fe-26ab-47df859823e7","type":"Text","label":"Quantity","x":-86,"y":131,"parameters":{},"ports":[],"children":[]},{"id":"80e557b6-964a-a9bb-87d4-13b4bf7ee1d5","type":"Text","label":"Delivery date","x":-86,"y":191,"parameters":{},"ports":[],"children":[]}]}],"connections":[],"comments":[]}} />
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Then we hook up the data. We need to keep two **Objects** in mind this time. We have the **Object** providede by the **Repeater** - the "wrapper", and the actual order. We hook it up like below. We also add a **Component Output** to we can know if a Checkbox was clicked.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||
<CopyToClipboardButton json={{"nodes":[{"id":"176161a9-4c47-55e4-411e-73235b45899a","type":"Group","x":-106,"y":-21,"parameters":{"flexDirection":"row","paddingTop":{"value":10,"unit":"px"},"paddingBottom":{"value":10,"unit":"px"},"paddingLeft":{"value":10,"unit":"px"},"paddingRight":{"value":10,"unit":"px"},"sizeMode":"contentHeight"},"ports":[],"children":[{"id":"383c1752-5771-22f5-1334-160bdc0a7d99","type":"net.noodl.controls.checkbox","x":-86,"y":25,"parameters":{"useLabel":false,"marginRight":{"value":9,"unit":"px"}},"ports":[],"children":[]},{"id":"484c1964-a75f-c9f2-cdb9-2544a4b215d7","type":"Text","label":"Order nr","x":-86,"y":127,"parameters":{},"ports":[],"children":[]},{"id":"cbe0804c-f3d2-e3bc-8773-ed005e88d4f8","type":"Text","label":"Quantity","x":-86,"y":223,"parameters":{},"ports":[],"children":[]},{"id":"68fdebf1-67ee-702f-23cb-21d5b240f3a2","type":"Text","label":"Delivery date","x":-86,"y":319,"parameters":{},"ports":[],"children":[]}]},{"id":"739ecad6-ec0f-9f2b-aeaa-4bae353e7570","type":"Model2","label":"Wrapper","x":-583.3416811729484,"y":163.33900090176894,"parameters":{"idSource":"foreach","properties":"Checked,Value"},"ports":[],"children":[]},{"id":"5e697dd5-760b-b449-dcbd-1bd2629cc786","type":"Component Outputs","x":455.8443558682088,"y":170.21729798352135,"parameters":{},"ports":[{"name":"Selection Changed","plug":"input","type":{"name":"*"},"index":1}],"children":[]},{"id":"cdd8417c-4d43-2bbe-aaa9-e23a22c8fb04","type":"SetModelProperties","x":208.84435586820882,"y":169.21729798352135,"parameters":{"properties":"Checked"},"ports":[],"children":[]},{"id":"0b1e1592-c722-9af7-9903-206b05af8cdd","type":"Model2","label":"Order","x":-321.1860370411575,"y":104.12170291824759,"parameters":{"properties":"order_nbr,quantity,delivery_date"},"ports":[],"children":[]}],"connections":[{"fromId":"739ecad6-ec0f-9f2b-aeaa-4bae353e7570","fromProperty":"id","toId":"cdd8417c-4d43-2bbe-aaa9-e23a22c8fb04","toProperty":"modelId"},{"fromId":"739ecad6-ec0f-9f2b-aeaa-4bae353e7570","fromProperty":"prop-Checked","toId":"383c1752-5771-22f5-1334-160bdc0a7d99","toProperty":"checked"},{"fromId":"383c1752-5771-22f5-1334-160bdc0a7d99","fromProperty":"checked","toId":"cdd8417c-4d43-2bbe-aaa9-e23a22c8fb04","toProperty":"prop-Checked"},{"fromId":"383c1752-5771-22f5-1334-160bdc0a7d99","fromProperty":"onChange","toId":"cdd8417c-4d43-2bbe-aaa9-e23a22c8fb04","toProperty":"store"},{"fromId":"cdd8417c-4d43-2bbe-aaa9-e23a22c8fb04","fromProperty":"stored","toId":"5e697dd5-760b-b449-dcbd-1bd2629cc786","toProperty":"Selection Changed"},{"fromId":"739ecad6-ec0f-9f2b-aeaa-4bae353e7570","fromProperty":"prop-Value","toId":"0b1e1592-c722-9af7-9903-206b05af8cdd","toProperty":"modelId"},{"fromId":"0b1e1592-c722-9af7-9903-206b05af8cdd","fromProperty":"prop-order_nbr","toId":"484c1964-a75f-c9f2-cdb9-2544a4b215d7","toProperty":"text"},{"fromId":"0b1e1592-c722-9af7-9903-206b05af8cdd","fromProperty":"prop-quantity","toId":"cbe0804c-f3d2-e3bc-8773-ed005e88d4f8","toProperty":"text"},{"fromId":"0b1e1592-c722-9af7-9903-206b05af8cdd","fromProperty":"prop-delivery_date","toId":"68fdebf1-67ee-702f-23cb-21d5b240f3a2","toProperty":"text"}],"comments":[]}} />
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
We add in a **Repeater** node in our `Multi Select List` component, pick our newly created list item as the item template. Finally we connect the **Checkboxes** output on our **Component Object** to the **Items** input of the repeater. We have a list!
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Triggering Signals in a Script
|
||||
Now it's time that we write the script that will generate our selection list, i.e. an **Array** that will contain all the items that are currently checked. As you may remember, we decided on this format:
|
||||
|
||||
```json
|
||||
{
|
||||
"Value":<id of selected object>
|
||||
}
|
||||
```
|
||||
|
||||
We go back into our Multiselect Logic Script node and start by adding in a new function. Call it `updateSelection`. It will look like below:
|
||||
```javascript
|
||||
function updateSelection () {
|
||||
Component.Object.Selection = Component.Object.Checkboxes.filter(o => o.Checked)
|
||||
.map(o => Noodl.Object.create({
|
||||
id:Component.Object.id+"_"+o.Value,
|
||||
Value:o.Value}
|
||||
));
|
||||
}
|
||||
```
|
||||
|
||||
Again, we are using our **Component Object** to store our **Array**. The **Array** is generated by walking through the Checkboxes **Array** and look for checked items. We then make a new **Object** (or recycling an old one if the **id** already exists) storing the **id** of the checked item in **Value**.
|
||||
|
||||
## Array outputs used in lists
|
||||
|
||||
:::note
|
||||
While you can re-use the **Array** that contains the old items and just modify it, this will not be recognized as a change by the **Repeater** node (or any other node for that matter) and not automatically trigger it to update. Technically the **Array** didn't change - only it's contents. That's why it's important that you actually make a new **Array** and assigning it to the output, rather than modifying the existing one, unless you want to manually trigger **Repeaters** etc, using the **Array** to update. Generally the Javascript Array functions (filter, map, find, etc) will return new **Arrays** which makes them very easy to use in these cases.
|
||||
:::
|
||||
|
||||
We actually need to call `updateSelection` in our Setter function for `Items` from before so right after we created the **Checkboxes** Array, so we immedieately get the Selection **Array** set up. So we update it like below:
|
||||
|
||||
```javascript
|
||||
Script.Setters.Items = function (items) {
|
||||
if(!Script.Inputs.Items) return
|
||||
// Create a lits of all checked items, based on the initial selection array
|
||||
Component.Object.Checkboxes = Script.Inputs.Items.map(o => Noodl.Object.create({
|
||||
id:Component.Object.id+'-'+o.id,
|
||||
Value:o.id,
|
||||
Checked:Script.Inputs.InitialSelection!==undefined && !!Script.Inputs.InitialSelection.find(s => s.Value === o.id)
|
||||
}))
|
||||
|
||||
updateSelection ();
|
||||
}
|
||||
```
|
||||
|
||||
We also need to be able to trigger the `updateSelection` whenever a user clicks a checkbox, regenerating the list. We can add __signals__ to our scripts by writing `Script.Signals.<Signal name>`. These signals will become inputs on the **Script** node that we can trigger. So our signal be named `UpdateSelection` and will look like this:
|
||||
|
||||
```javascript
|
||||
Script.Signals.UpdateSelection = function () {
|
||||
updateSelection ();
|
||||
}
|
||||
```
|
||||
|
||||
We can now connect the outgoing signal `Selection Changed` from our **Repeater** (which is triggered from the **Checkbox** in the list item) to this new signal. We also connect the `Selections` **Array** in our **Component Object** to our **Component Output** in the `Multi Select List`.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Try clicking different Checkboxes and you should see the `Selection` **Array** change accordingly.
|
||||
|
||||
## Timing of signals
|
||||
There is a timing aspect that's important to look into. Our `Multi Select List` should send out an event whenever the selection changes, together with the new selection array. If we directly connect the `Selection Changed` signal from the **Repeater** node to the **Component Output** we cannot be certain that the **Script** node has executed once the signal is triggered. It may or may not be the case. So anyone listening to the **Selection Changed** signal of the `Multi Select List` that also reads the new selection ( a very likely case) may read the previous **Array**.
|
||||
|
||||
The solution is to chain these events. The **Selection Changed** event should trigger the `UpdateSelection` signal on the **Script** node, then once the new selection **Array** is generated we need to send a signal from the **Script** that then will be used as the output signal from the component.
|
||||
|
||||
## Sending signals from a script
|
||||
|
||||
Let's add that signal. Click on the **Script** node. Then add a new output. Call it `SelectionChanged`.
|
||||
|
||||
<div className="ndl-image-with-background">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Then make sure it's set up as a **Signal**.
|
||||
|
||||
<div className="ndl-image-with-background">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Now we can send the signal from within our **Script** by calling it as a function `Script.Outputs.<signal name> ()`. In our case we add it in i our `updateSelection` function, as below:
|
||||
|
||||
```javascript
|
||||
Component.Object.Selection = Component.Object.Checkboxes.filter(o => o.Checked)
|
||||
.map(o => Noodl.Object.create({
|
||||
id:Component.Object.id+"_"+o.Value,
|
||||
Value:o.Value}
|
||||
));
|
||||
Script.Outputs.SelectionChanged ();
|
||||
```
|
||||
|
||||
We can now connect our new output `SelectionChanged` to the `Selection Changed` input on the **Component Outputs**.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Finishing off the Multi Select List
|
||||
|
||||
The only thing left now is to implement the Select all / Clear selection functionality. This will be very easily done. We add two more incoming signals in our **Scrip** node, `Script.Signals.SelectAll` and `Script.Signals.DeselectAll`.
|
||||
|
||||
Adding implementing them in our **Script** node is easy and the full code now looks like below:
|
||||
|
||||
```javascript
|
||||
function updateSelection () {
|
||||
Component.Object.Selection = Component.Object.Checkboxes.filter(o => o.Checked)
|
||||
.map(o => Noodl.Object.create({
|
||||
id:Component.Object.id+"_"+o.Value,
|
||||
Value:o.Value}
|
||||
));
|
||||
Script.Outputs.SelectionChanged ();
|
||||
}
|
||||
|
||||
Script.Setters.Items = function (items) {
|
||||
if(!Script.Inputs.Items) return
|
||||
// Create a lits of all checked items, based on the initial selection array
|
||||
Component.Object.Checkboxes = Script.Inputs.Items.map(o => Noodl.Object.create({
|
||||
id:Component.Object.id+'-'+o.id,
|
||||
Value:o.id,
|
||||
Checked:Script.Inputs.InitialSelection!==undefined && !!Script.Inputs.InitialSelection.find(s => s.Value === o.id)
|
||||
}))
|
||||
|
||||
updateSelection ();
|
||||
}
|
||||
|
||||
Script.Signals.UpdateSelection = function () {
|
||||
updateSelection ();
|
||||
}
|
||||
|
||||
Script.Signals.SelectAll = function () {
|
||||
if(!Component.Object.Checkboxes) return
|
||||
|
||||
Component.Object.Checkboxes.forEach ( o => o.Checked = true);
|
||||
updateSelection ();
|
||||
}
|
||||
|
||||
Script.Signals.DeselectAll = function () {
|
||||
if(!Component.Object.Checkboxes) return
|
||||
|
||||
Component.Object.Checkboxes.forEach ( o => o.Checked = false);
|
||||
updateSelection ();
|
||||
}
|
||||
```
|
||||
|
||||
We of course also need to trigger these functions from the **Component Inputs** ports, `Select All` and `Clear Selection`.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
We are done with the multi select list. Time to build the multi select operations we want to use it for.
|
||||
|
||||
## Adding in the multi select operations
|
||||
|
||||
As described in the beginning, we want to implement the operations _merge_, _delete_ and _copy_. We also want to be able to select all or clear all selections. So we need to build some UI. We will create a top bar that consist of a **Checkbox** and three **Buttons**. We do it as simple as possible. A horizontally laid out **Group** node with some margins and padding to make it easy to read. You can copy the nodes below if you don't want to build it yourself.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||
<CopyToClipboardButton json={{"nodes":[{"id":"d4c94e36-5463-a399-9dd0-69778d1236d8","type":"Group","label":"Top Bar","x":20,"y":46,"parameters":{"flexDirection":"row","sizeMode":"contentSize","alignX":"left","marginBottom":{"value":10,"unit":"px"},"paddingLeft":{"value":10,"unit":"px"}},"ports":[],"children":[{"id":"3aceb58c-1d9e-6511-d418-de806a995c4c","type":"net.noodl.controls.checkbox","x":40,"y":106,"parameters":{"useLabel":false,"alignY":"center","marginRight":{"value":10,"unit":"px"}},"ports":[],"children":[]},{"id":"0544f9ec-a773-5093-5b5c-296a800ee3fb","type":"net.noodl.controls.button","label":"Delete","x":40,"y":152,"parameters":{"marginRight":{"value":10,"unit":"px"},"label":"Delete"},"ports":[],"children":[]},{"id":"7d8801c3-c828-8956-0feb-f78ae9599de9","type":"net.noodl.controls.button","label":"Merge","x":40,"y":212,"parameters":{"marginRight":{"value":10,"unit":"px"},"label":"Merge"},"ports":[],"children":[]},{"id":"01ef643f-ccfb-1242-8155-47f4d1dfc9cf","type":"net.noodl.controls.button","label":"Copy","x":40,"y":272,"parameters":{"label":"Copy"},"ports":[],"children":[]}]}],"connections":[],"comments":[]}} />
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
We can now connect the Select and Clear selection logic to our multi select list. We will use a **Switch** to trigger the two signals we implemented.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
We can now test out that our select all / clear selection implementation work as it should in our multi select list.
|
||||
|
||||
## More Component Objects
|
||||
|
||||
In preparation for our upcoming script to implement the merge / copy / delete operations we need to save the selection state so we can easily access it in our Script. Again we will use a **Component Object**. For the actual storing operation of the selection we can use the node [Set Component Object Properties](/nodes/component-utilities/set-component-object-properties). Add in the node and add a property `Selection`. Set it to **Array** type.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Then connect the outgoing **Selection** property from the `Multi Select List` component to the property. We should set it everytime the selection changes, so we also connect **Selection Changed** to **Do**.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Using Function node for lightweight scripting
|
||||
|
||||
Our **Buttons** need a bit of business logic on their own. If you have no items selected the items should be disabled. And for the _merge_ operation to make sense you actually need at least two items selected.
|
||||
|
||||
If you want to write this in Javascript a **Script** node is a little heavy - we only are going to have one function. Instead we use the **Function** node.
|
||||
|
||||
So add in a **Function** node in our main app. Call it `Calculate Button States`. Also add three outputs called `DeleteEnabled`, `CopyEnabled` and `MergeEnabled`. They should be of **Boolean** type, because we want to connect them directly to the **Enabled** property of the **Buttons**.
|
||||
|
||||
<div className="ndl-image-with-background">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
The we open up the **Function** node and just write the code directly. In a **Function** node you don't have to write anything but your actual code, i.e. no function declarations etc is necessary.
|
||||
|
||||
Our code looks like below:
|
||||
|
||||
```javascript
|
||||
Outputs.CopyEnabled = Component.Object.Selection !== undefined && Component.Object.Selection.length > 0;
|
||||
Outputs.DeleteEnabled = Component.Object.Selection !== undefined && Component.Object.Selection.length > 0;
|
||||
Outputs.MergeEnabled = Component.Object.Selection !== undefined && Component.Object.Selection.length > 1;
|
||||
```
|
||||
|
||||
As you can see you access the outputs using `Outputs.<output name>` and inputs (if we had to use one) are accessed through `Inputs.<input name>`. You can also see that we make use of the **Component Object** again.
|
||||
|
||||
A **Function** node runs whenever an input changes, _unless the **Run** signal is connected in which case it only runs whtn **Run** is triggered_. In our case, we have no inputs, so we need to trigger **Run**. To make sure `Component.Object.Selection` is valid and set when we trigger it, we connect the outgoing **Done** from **Set Component Object Properties** to **Run**. We also hook up the outputs from the **Function** node to the **Buttons**.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Try out the logic and make sure the **Buttons** gets enabled/disabled as they should.
|
||||
|
||||
## Implementing the list operations
|
||||
|
||||
We are almost done. We just need to implement the actual operations in a **Script** node. Before that we need to think a little bit about how we treat the **Array** that holds the items of the list. Currently we take the items directly from our **Static Array**. This will not work once we start modifying them, we need to store the **Array** somewhere where the **Script** can retrieve it, and then update to a new **Array** once a copy / delete / merge occurs.
|
||||
|
||||
**Component Object** to the rescue! Create a **Component Object** node and add a property **Items**. Then connect the **Items** from the **Static Array** to it. Also connect from the **Component Object** to the `Mult Select List` component.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
We are now ready to write our list operation scripts. Add a new **Script** node. We implement our three functions below:
|
||||
|
||||
```javascript
|
||||
|
||||
/* Returns a new array only continain the items that's selected */
|
||||
function getSelectedItems () {
|
||||
if (Component.Object.Selection === undefined) {
|
||||
// if there is no selection, return an empty array
|
||||
return [];
|
||||
}
|
||||
else {
|
||||
return Component.Object.Selection.map ( (o) => Noodl.Object.get (o.Value));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* returns a new array containing all items
|
||||
except the ones that's selected */
|
||||
|
||||
function removeSelectedItems () {
|
||||
|
||||
return Component.Object.Items.filter ( (o) => {
|
||||
// check if this item is in the selected list
|
||||
if (Component.Object.Selection === undefined) {
|
||||
// if there is no selection, the items should be kept in the list
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
// check if this item is in the selected list. If it isn't return true (i.e. keep the item)
|
||||
return Component.Object.Selection.find (p => (p.Value === o.id)) === undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
Merge all selected items into one item.
|
||||
Use the item with the latest date as the new item
|
||||
delete the other items. */
|
||||
|
||||
Script.Signals.Merge = function () {
|
||||
if (Component.Object.Items !== undefined && Component.Object.Selection !== undefined) {
|
||||
// start with looking at the items that's selected
|
||||
let itemsToMerge = getSelectedItems();
|
||||
// sort them by latest date
|
||||
let sortedItems = itemsToMerge.sort ( (o,p) => {
|
||||
return new Date (p.delivery_date) - new Date (o.delivery_date);
|
||||
});
|
||||
// the first item in the sorted list is the one we want to keep
|
||||
let itemToKeep = sortedItems[0];
|
||||
|
||||
// calculate the sum of all quantities in the items to merge
|
||||
let sumOfQuantities = sortedItems.reduce ( (total , o) => total + o.quantity, 0);
|
||||
// and store in the item we want to keep
|
||||
itemToKeep.quantity = sumOfQuantities;
|
||||
// remove all items that's selected
|
||||
let itemsToKeep = removeSelectedItems ();
|
||||
// Add back the item to keep
|
||||
itemsToKeep.push (itemToKeep);
|
||||
// this is our new items!
|
||||
Component.Object.Items = itemsToKeep;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Script.Signals.Copy = function () {
|
||||
if (Component.Object.Items !== undefined && Component.Object.Selection !== undefined) {
|
||||
let selectedItems = getSelectedItems ();
|
||||
// Get all selected items
|
||||
// For each item in the selection, create a new object and copy the values
|
||||
// Add a "-2" to the order_nbr
|
||||
let copiedItems = selectedItems.map ( (o) => {
|
||||
return Noodl.Object.create ({
|
||||
order_nbr: o.order_nbr+"-2",
|
||||
quantity: o.quantity,
|
||||
delivery_date: o.delivery_date
|
||||
});
|
||||
});
|
||||
Component.Object.Items = Component.Object.Items.concat (copiedItems);
|
||||
}
|
||||
}
|
||||
|
||||
Script.Signals.Delete = function () {
|
||||
if (Component.Object.Items !== undefined && Component.Object.Selection !== undefined) {
|
||||
Component.Object.Items = removeSelectedItems ();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Paste the code into the **Script** node. Again note that `Component.Object.Items` is always replaced with a new **Array** - we never modify the old one. This is to ensure that the multi select list understand that it needs to update.
|
||||
|
||||
Finally we connect our **Buttons** to the respective list operation and we are done!
|
||||
|
||||
<div className="ndl-image-with-background xl">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
You can import the full project below.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
<ImportButton zip="/docs/guides/business-logic/client-side-biz-logic-js/biz-logic-js.zip" name="Multiselect list with js" thumb="/docs/guides/business-logic/client-side-biz-logic-js/final-1.png"/>
|
||||
|
||||
</div>
|
||||
376
docs/guides/business-logic/client-side-biz-logic-nodes.mdx
Normal file
376
docs/guides/business-logic/client-side-biz-logic-nodes.mdx
Normal file
File diff suppressed because one or more lines are too long
189
docs/guides/business-logic/custom-ui-components.md
Normal file
189
docs/guides/business-logic/custom-ui-components.md
Normal file
@@ -0,0 +1,189 @@
|
||||
---
|
||||
title: Custom UI Components
|
||||
hide_title: true
|
||||
---
|
||||
import CopyToClipboardButton from '/src/components/copytoclipboardbutton'
|
||||
import ImportButton from '../../../src/components/importbutton'
|
||||
|
||||
# Custom UI Components
|
||||
|
||||
## What you will learn in this guide
|
||||
A very powerful feature of Noodl is the ability to create re-usable components easily. This guide will cover some useful patterns for create re-usable UI components. This guide will involve a bit of coding so it is good if you have some basic coding skills in Javascript and have read our previous guides on business logic in Javascript.
|
||||
|
||||
## Component Inputs and Outputs
|
||||
The key to creating good re-usable components is to provide inputs and outputs that makes it useable. There are some good patterns to follow here and we will outline them here. We will start with a simple example where we create a component with a slider plus two labels. This is what it will look like:
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
And here is the content of the component. This is a simple UI component that has a slider and two text labels. One label is simple the header for the slider, and the other is formatted using the **String Format** node and the current **Value** and the **Max** value.
|
||||
|
||||
<div className="ndl-image-with-background xl">
|
||||
|
||||
<CopyToClipboardButton json={{"nodes":[{"id":"f73a5d7c-7b0e-e7f1-18a0-537f50623b8d","type":"Group","x":0,"y":0,"parameters":{},"ports":[],"children":[{"id":"5fb30353-669f-cee0-0339-e96608ad1478","type":"Text","x":20,"y":202,"parameters":{},"ports":[],"children":[]},{"id":"62063b48-b2c8-cf28-1de2-d20c7866ef42","type":"net.noodl.controls.range","x":20,"y":284,"parameters":{"marginBottom":{"value":15,"unit":"px"},"marginTop":{"value":15,"unit":"px"}},"ports":[],"children":[]},{"id":"fcee5809-211f-d59d-d566-7737e5383ceb","type":"Text","x":20,"y":406,"parameters":{"alignX":"right","sizeMode":"contentSize","color":"Light Gray"},"ports":[],"children":[]}]},{"id":"aaa82451-4b5d-874e-17c4-622b70e46249","type":"Component Inputs","x":-622.5,"y":167,"parameters":{},"ports":[{"name":"Label","plug":"output","type":{"name":"*"},"group":"Settings","index":0},{"name":"Value","plug":"output","type":{"name":"*"},"group":"Settings","index":1},{"name":"Max","plug":"output","type":{"name":"*"},"group":"Settings","index":2}],"children":[]},{"id":"1d8d66d2-b86d-6c90-a93c-8b3ffa7bfd56","type":"String Format","x":-184.5,"y":377,"parameters":{"format":"{Value} / {Max}"},"ports":[],"children":[]},{"id":"13725968-85f6-ee25-5e66-b7f208aac194","type":"Number","x":-387.5,"y":364,"parameters":{},"ports":[],"children":[]},{"id":"f4d370e6-ec69-5459-49e9-9d258172c77a","type":"Component Inputs","x":-227.5,"y":-20,"parameters":{},"ports":[{"name":"Margin Left","plug":"output","type":{"name":"*"},"index":0},{"name":"Margin Right","plug":"output","type":{"name":"*"},"index":1},{"name":"Margin Bottom","plug":"output","type":{"name":"*"},"index":3},{"name":"Margin Top","plug":"output","type":{"name":"*"},"index":2},{"name":"Align X","plug":"output","type":{"name":"*"},"index":4},{"name":"Align Y","plug":"output","type":{"name":"*"},"index":5},{"name":"Position","plug":"output","type":{"name":"*"},"index":6}],"children":[]},{"id":"51777154-9afa-4aa7-515a-6164a47ba35e","type":"Component Outputs","x":321.5,"y":287,"parameters":{},"ports":[{"name":"Value","plug":"input","type":{"name":"*"},"index":1},{"name":"Changed","plug":"input","type":{"name":"*"},"index":2}],"children":[]}],"connections":[{"fromId":"aaa82451-4b5d-874e-17c4-622b70e46249","fromProperty":"Label","toId":"5fb30353-669f-cee0-0339-e96608ad1478","toProperty":"text"},{"fromId":"aaa82451-4b5d-874e-17c4-622b70e46249","fromProperty":"Max","toId":"62063b48-b2c8-cf28-1de2-d20c7866ef42","toProperty":"max"},{"fromId":"62063b48-b2c8-cf28-1de2-d20c7866ef42","fromProperty":"value","toId":"1d8d66d2-b86d-6c90-a93c-8b3ffa7bfd56","toProperty":"Value"},{"fromId":"1d8d66d2-b86d-6c90-a93c-8b3ffa7bfd56","fromProperty":"formatted","toId":"fcee5809-211f-d59d-d566-7737e5383ceb","toProperty":"text"},{"fromId":"aaa82451-4b5d-874e-17c4-622b70e46249","fromProperty":"Max","toId":"13725968-85f6-ee25-5e66-b7f208aac194","toProperty":"value"},{"fromId":"13725968-85f6-ee25-5e66-b7f208aac194","fromProperty":"savedValue","toId":"1d8d66d2-b86d-6c90-a93c-8b3ffa7bfd56","toProperty":"Max"},{"fromId":"aaa82451-4b5d-874e-17c4-622b70e46249","fromProperty":"Value","toId":"62063b48-b2c8-cf28-1de2-d20c7866ef42","toProperty":"value"},{"fromId":"62063b48-b2c8-cf28-1de2-d20c7866ef42","fromProperty":"onChange","toId":"51777154-9afa-4aa7-515a-6164a47ba35e","toProperty":"Changed"},{"fromId":"62063b48-b2c8-cf28-1de2-d20c7866ef42","fromProperty":"value","toId":"51777154-9afa-4aa7-515a-6164a47ba35e","toProperty":"Value"},{"fromId":"f4d370e6-ec69-5459-49e9-9d258172c77a","fromProperty":"Align Y","toId":"f73a5d7c-7b0e-e7f1-18a0-537f50623b8d","toProperty":"alignY"},{"fromId":"f4d370e6-ec69-5459-49e9-9d258172c77a","fromProperty":"Align X","toId":"f73a5d7c-7b0e-e7f1-18a0-537f50623b8d","toProperty":"alignX"},{"fromId":"f4d370e6-ec69-5459-49e9-9d258172c77a","fromProperty":"Margin Top","toId":"f73a5d7c-7b0e-e7f1-18a0-537f50623b8d","toProperty":"marginTop"},{"fromId":"f4d370e6-ec69-5459-49e9-9d258172c77a","fromProperty":"Margin Bottom","toId":"f73a5d7c-7b0e-e7f1-18a0-537f50623b8d","toProperty":"marginBottom"},{"fromId":"f4d370e6-ec69-5459-49e9-9d258172c77a","fromProperty":"Margin Right","toId":"f73a5d7c-7b0e-e7f1-18a0-537f50623b8d","toProperty":"marginRight"},{"fromId":"f4d370e6-ec69-5459-49e9-9d258172c77a","fromProperty":"Margin Left","toId":"f73a5d7c-7b0e-e7f1-18a0-537f50623b8d","toProperty":"marginLeft"},{"fromId":"f4d370e6-ec69-5459-49e9-9d258172c77a","fromProperty":"Position","toId":"f73a5d7c-7b0e-e7f1-18a0-537f50623b8d","toProperty":"position"}],"comments":[]}} />
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Let's take a closer look at the **Component Inputs** of this component. First we have a couple of inputs that are the basic settings for the component, the **Label**, **Max** and **Value** inputs. There are a couple of things to note about this component inputs. If you look at the **Max** input it is first connected to a **Number** node and then to the **String Format** node. This is a common pattern to ensure that the **Max** input is represented as a number in the property panel when this component is used. The component input will get the same type in the property panel, as the node it is connected to and in this case it is connected to both the **Max** of the **Slider** (which is a number) and the **Max** input of the **String Format** node which is a string. That fact that we go via a **Number** node will make sure the property panel knows what input field to show for that input.
|
||||
|
||||
<div className="ndl-image-with-background xl">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Another thing to note is that the **Value** component input is connected to the **Value** input of the **Slider**. Most UI Components is collecting some sort of data from the user, in this case it's a range value, it is very important that the data is also exposed as an input so that it can be properly connected to a data source.
|
||||
|
||||
Moving on to the component outputs. Here you of course need the **Value** as an output as well, so that the UI component can be used to collect data from the user. It is also important to have a **Changed** signal.
|
||||
|
||||
<div className="ndl-image-with-background xl">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
:::warning
|
||||
The **Changed** signal should **ALWAYS** be on a user input, not just if the **Value** input have changed. This is to make sure that the UI component doesn't report a change if the input value is changed. That can cause unnecessary data feedback loops.
|
||||
:::
|
||||
|
||||
Finally it's a good idea to expose some minimum set of layout properties on the root node. This will make the UI component easier to use.
|
||||
|
||||
<div className="ndl-image-with-background xl">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
You can choose yourself what you want to expose as inputs but here are a few recommendations:
|
||||
|
||||
* **Margins** At least exposing margins will remove the need for extra **Group** nodes when using your component.
|
||||
* **Align** It's also common to need to re-align your component when using it, providing this as an input is helpful.
|
||||
* **Position** Maybe not as common, but could still be good to expose.
|
||||
|
||||
## Component Object
|
||||
You have learnt how to use the [Object](/nodes/data/object/object-node) node in the [working with data guides](/docs/guides/data/objects) and how to connect it to UI controls in the [Connecting UI controls to data guide](/docs/guides/data/ui-controls-and-data). There is another node which is very useful when working on re-usable UI componets and that is the [Component Object](/nodes/component-utilities/component-object) node. This node works just like the **Object** node except that it is unique to the component instance, so it will not be shared between component instances like regular objects. This is very useful when keeping the state of UI controls.
|
||||
|
||||
We will take a look at a very simple example below, the **Segment Control** UI Component.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
<ImportButton zip="/docs/guides/business-logic/custom-ui-components/segment-control-1.zip" name="Segment Control" thumb="/docs/guides/business-logic/custom-ui-components/segment-control.png"/>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
This example actually contains two components the **Segment Control** component and the **Segment Control Button** component. What it does is that it accepts an array as input containing the possible options for the control, each object in the array should have a **Label** and a **Value**. It also accepts an input that is the current selected **Value** of the control, this should correspond to one of the values in the array and that button will be shown as selected (like radio buttons).
|
||||
|
||||
<div className="ndl-image-with-background xl">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Here you can see how we use the **Component Object** node to store the currently **Selected Value** and how it is also passed through as the **Value** output. We will take a look at how it is used later. The options input array is used directly as items in the **Repeater** node. If we take a closer look at the **Segment Control Button** component (that is used as template in the repeater) we will see where the magic happends.
|
||||
|
||||
<div className="ndl-image-with-background xl">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Let's go over this one quickly:
|
||||
|
||||
* The **Object** node is used to connect the **Label** to the button. So each button that is created by the repeater will get the correct label.
|
||||
* Here we introduce a new action node, the [Set Parent Component Object Properties](/nodes/component-utilities/set-parent-component-object-properties) node that is used to set a property on the component object. But not the component object of this component instance, but instead it's closets visual parent. In this case (since this component is used as template in the repeater) it will be the **Segment Control** component. That is, each **Segment Control Button** component in the repeater will, when clicked, set it's **Value** as the **Selected Value** of the **Component Object**.
|
||||
* Now we also use the [Parent Component Object](/nodes/component-utilities/parent-component-object) to compare the currently **Selected Value** with the **Value** of this segment control button, this is done in the **Function** node that takes both the currently selected value and the value from the repeater instance object as inputs. It has the following code:
|
||||
|
||||
```javascript
|
||||
if(Inputs.SelectedValue == Inputs.MyValue)
|
||||
Outputs.Variant = "Segment Control Button Selected"
|
||||
else
|
||||
Outputs.Variant = "Segment Control Button"
|
||||
```
|
||||
|
||||
* Here comes the next little trick. The **Variant** of the **Button** is choosen by the **Function** to be either **Segment Control Button** or **Segment Control Button Selected**. We have created two different **Button** variants with those names so we can design how we want the button to look if it is selected and not. Learn more about style variants in [this guide](/docs/guides/user-interfaces/style-variants).
|
||||
* Finally we send the **Click** signal from the button as **Component Output** from this component, this will allow us to use that signal from the **Repeater** node in the parent component.
|
||||
|
||||
The [Component Object](/nodes/component-utilities/component-object) and [Parent Component Object](/nodes/component-utilities/parent-component-object) nodes, and their action nodes to set properties, [Set Component Object Properties](/nodes/component-utilities/set-component-object-properties) and [Set Parent Component Object Properties](/nodes/component-utilities/set-parent-component-object-properties) are very useful when building re-usable UI components. We recommend storing the state of your UI component in these.
|
||||
|
||||
## State management
|
||||
Some times you need to initialise your UI components when they are created. Then you can use the **Did mount** signal from the root UI element, often a **Group** node.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
You can also access the **Component Object** and **Parent Component Object** from **Function** and **Script** nodes by simply writing:
|
||||
|
||||
```javascript
|
||||
Component.Object.MyValue = "Hello"
|
||||
Component.Object["Selected Value"] = "Use this for properties with spaces"
|
||||
|
||||
Component.ParentObject.MyValue = "This works too"
|
||||
```
|
||||
|
||||
So this is a great place to initialise your **Component Object** when the UI component is created.
|
||||
|
||||
Here is another interesting example to look at. This is a **Multi Checkbox Group** example. It takes two arrays as input, one with all possible options each with their
|
||||
**Value** and **Label** and a second array which is the value, this array contains objects with just **Value**. So you can choose multiple options and not just one like the segment control. This is a little more complex so we wont go into detail here, but you can check out the example below and we will look at a few details.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
<ImportButton zip="multi-checkbox.zip"/>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
If we take a close look at the nodes in the **Multi Checkbox Group** component, this is what we find:
|
||||
|
||||
<div className="ndl-image-with-background xl">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Here is we can see that if any of the **Options** or **Selection** inputs change, we will run a **Function** node. The code of that node is as follows:
|
||||
|
||||
```javascript
|
||||
if(!Inputs.Options) return // No options, no fun
|
||||
|
||||
Component.Object.Checkboxes = Inputs.Options.map(o => Noodl.Object.create({
|
||||
id:Component.Object.id+'-'+o.Value,
|
||||
Value:o.Value,
|
||||
Label:o.Label || o.Value,
|
||||
Checked:Inputs.Selection!==undefined && !!Inputs.Selection.find(s => s.Value === o.Value)
|
||||
}))
|
||||
```
|
||||
|
||||
It creates a new array of objects in the **Component Object** called **Checkboxes**, these will get the value and label, and a **Checked** property that is true if that value is represented in the in the selection. This array is that is then used in the **Repeater** node to show all components. It is important that this function is re-run if the **Options** or **Selection** is changed so that the UI control will always show the correct state as corresponds to its inputs.
|
||||
|
||||
:::note
|
||||
We set the **id** of the object. This makes sure that the **Repeater** doesn't create new items every time the array changes. This increases performance.
|
||||
:::
|
||||
|
||||
Another important thing to notice is that the **Selection** input is passed to the **Component Object** and then directly to the corresponding output. This is also very common.
|
||||
|
||||
Finally, we have another **Function** node that is run whenever the selection changes due to user input, just like the segment control component above this is sent out from the **Repeater** node. In this component we update the current **Selection** on the **Component Object** by filtering out the objects that currently are checked, and then filtering out the **Value** property for those objects. We update the **Component Object** which in turn will update the selection output.
|
||||
|
||||
```javascript
|
||||
Component.Object.Selection = Component.Object.Checkboxes.filter(o => o.Checked).map(o => ({Value:o.Value}))
|
||||
```
|
||||
|
||||
If we look at the **Multi Checkbox Group Item** component we will see that it is very basic. It is simply a checkbox that with the corresponding **Checked** and **Label** from the object in the **Checkboxes** array that we created before. When the checkbox is updated we update the **Checked** value of the object and report the change.
|
||||
|
||||
<div className="ndl-image-with-background xl">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
There you go, that's everything need to create a multi checkbox component. This pattern can be used to create all sorts of UI components with multi selection.
|
||||
|
||||
|
||||
65
docs/guides/business-logic/events.md
Normal file
65
docs/guides/business-logic/events.md
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: Events
|
||||
hide_title: true
|
||||
---
|
||||
# Events
|
||||
|
||||
## Overview
|
||||
This guide covers an important concept in Noodl called Events. Events are used to send and recive signals with accompanying data. Signals are sent from one part of the node graph to one or many other locations in the graph. This is often useful when a user interaction occurs in one place of the app, such as the click of a button, that should trigger an action in a different place, e.g. a popup showing.
|
||||
|
||||
## What you will learn in this guide
|
||||
This guide will teach you how to use the [Send Event](/nodes/events/send-event) and [Receive Event](/nodes/events/receive-event) nodes to pass signals and data from one place in your node graph to another.
|
||||
|
||||
This concept includes two nodes, the **Send Event** node and the **Receive Event** node. As the name implies, the **Send Event** node is used when you want to send an event. Below is an example of an event being sent when a **Text** node is clicked.
|
||||
|
||||

|
||||
|
||||
## Sending and receiving events
|
||||
|
||||
In the example above, the **Click** signal of the **Text** node is connected to the **Send** input of the **Send Event** node. This will trigger the an event to be sent when the text is clicked.
|
||||
|
||||
An event is sent to a certain **Channel** which is specified in the properties of the **Send Event** node. In this case the name of the channel is **Show Popup**.
|
||||
|
||||

|
||||
|
||||
The event signal is passed to all **Receive Event** nodes that share the same **Channel**. In the example below the event that was sent above is received.
|
||||
|
||||

|
||||
|
||||
To illustrate this you can see below how when the click signal is sent via the **Send Event** node, it is passed to the **Received** output of the **Event Receiver** node.
|
||||
|
||||

|
||||
|
||||
## Passing payload data
|
||||
|
||||
So far we have seen the basic concept of the events mechanism in Noodl. Next, let's take a look at how you can pass data via payload connections to your event nodes. You start by adding ports to the **Send Event** node. You can add any number of ports for the data that you want to pass with the event.
|
||||
|
||||

|
||||
|
||||
Now you can connect data to the input ports that you created on the **Send Event** node. When the **Send** signal is received, the values on all inputs of the **Send Event** node will be captured and passed to the **Receive Event**.
|
||||
|
||||

|
||||
|
||||
When the **Receive Event** node outputs the **Received** signal it will also update all other outputs. The payload ports added on the **Send Event** node will become available on all **Receive Event** nodes that share the same channel as the **Send Event** node.
|
||||
|
||||

|
||||
|
||||
## Propagation
|
||||
|
||||
Event propagation means how an event is sent in the graph, i.e. which **Receive Event** nodes an event is sent to. The default propagation mode is **Global** which means _all_ receivers will be triggered. You can however change the propagation via the **Send To** property of the **Send Event** node.
|
||||
|
||||

|
||||
|
||||
The **Children** mode will send the events to all the children in the component where the **Send Event** node is. So in the example below, the event will first be sent to **My Child Comp** followed by any children that node may have. When all descendants of **My Child Comp** node have received the event it will pass it to all children that are dynamically created by the **Repeater** node, and their descendants.
|
||||
|
||||

|
||||
|
||||
The **Siblings** mode will pass the event to all other nodes that are on the same level as the node where the originating **Send Event** node is. So if for instance the **My Child Comp** in the graph below contains a **Send Event** node that sends an event to its siblings all other **My Child Comp** nodes will receive it, except for the one sending the event, followed by the child instances dynamically created by the **Repeater** node.
|
||||
|
||||

|
||||
|
||||
The last propagation mode is **Parent**. This mode will send events up the node graph hierarchy. The **My Other Child** in the example graph below contains a **Send Event** node that is using the **Parent** propagation mode. When an event is sent from **My Other Child**, the parent **My Child Comp** node with receive it, followed by the node we are in and then the event would be passed on to the parent of this node. The propagation follows the visual hierarchy chain.
|
||||
|
||||

|
||||
|
||||
The final thing to know about propagation is the **Consume** property of the **Receive Event** node. If that property is checked it means that when that particular node receives an event it will stop the propagation. So no other **Receive Event** nodes after that one will receive this specific event.
|
||||
969
docs/guides/business-logic/javascript.mdx
Normal file
969
docs/guides/business-logic/javascript.mdx
Normal file
@@ -0,0 +1,969 @@
|
||||
---
|
||||
id: javascript
|
||||
title: Javascript in Noodl
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
import CopyToClipboardButton from "../../../src/components/copytoclipboardbutton";
|
||||
|
||||
# Javascript in Noodl
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## What you will learn in this guide
|
||||
|
||||
This guide will introduce you to how to use Javascript in Noodl.
|
||||
While almost everything can be achieved in Noodl using Nodes,
|
||||
if you know your way around Javascript it's sometimes just more convenient to add in Javascript code directly.
|
||||
Noodl make the mix of nodes and code very easy using the [Function](/nodes/javascript/function) and [Script](/nodes/javascript/script) nodes.
|
||||
|
||||
### Overview
|
||||
|
||||
The guide will first go through the **Function** node,
|
||||
which is excellent for simple, single function, Javascript code.
|
||||
Then it will show a more extensive Javascript example using the **Script** node.
|
||||
The two nodes can also be seen used combined in the [Business Logic using Javascript Guide](/docs/guides/business-logic/client-side-biz-logic-js)
|
||||
|
||||
## Using the **Function** node
|
||||
|
||||
The easiest way to write Javascript in Noodl is using the **Function** node.
|
||||
There is literally no overhead at all - you just open the code editor and write code! Let's try it!
|
||||
|
||||
Create a new project using the "Hello World" template. Add in two [Text Input](/nodes/ui-controls/text-input) nodes before the text node.
|
||||
Give them the labels "First String" and "Second String". Then add in a **Function** node. Call the function node "Merge Strings".
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Now click the **Function** node and edit the script. Paste in the Javascript below:
|
||||
|
||||
```javascript
|
||||
if (Inputs.String1 !== undefined && Inputs.String2 !== undefined) {
|
||||
let length = Math.max(Inputs.String1.length, Inputs.String2.length);
|
||||
let outputString = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i < Inputs.String1.length) {
|
||||
outputString = outputString + Inputs.String1.charAt(i);
|
||||
}
|
||||
if (i < Inputs.String2.length) {
|
||||
outputString = outputString + Inputs.String2.charAt(i);
|
||||
}
|
||||
}
|
||||
Outputs.MergedString = outputString;
|
||||
}
|
||||
```
|
||||
|
||||
## Inputs and Outputs in the Function node
|
||||
|
||||
This little function merges two strings called `String1` and `String2`.
|
||||
They are declared as `Inputs.String1` and `Inputs.String2` and will become inputs on the **Function** node.
|
||||
You can also declare inputs manually on the function node - if you click it you will see that you can add inputs and outputs.
|
||||
By adding them manually you can be a bit more precise on which type an input is,
|
||||
but generally is not needed. One reason to explicitly state type of input is for example when you connect a **Function** node to a **Component Inputs**.
|
||||
By knowing the type Noodl can present the right control in the property of the Component.
|
||||
|
||||
There is also an output defined in the code, `Outputs.MergedString`.
|
||||
|
||||
We can now connect the **Function** node.
|
||||
Connect the **Text** property from the **Text Inputs** to `String1` and `String2` and connect the output `MergedString` to the **Text**.
|
||||
Now if you start writing in the **Text Fields** you will see the two strings merged on the screen.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Running the **Function** node on change or on a signal
|
||||
|
||||
There are two ways to make the **Function** run.
|
||||
|
||||
1. If **Run** signal is not connected, the **Function** will run as soon as any of its inputs are changed. This is the case with our current example.
|
||||
2. If **Run** signal is connected, the **Function** will only run when triggered.
|
||||
|
||||
Let's try to change to number two. We only want to merge the string once the user clicks a button.
|
||||
So add a [Button](/nodes/ui-controls/button) after the **Text Inputs**. Give it the label **Merge**.
|
||||
Then connect **Click** to **Run** on the function.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Sending Signals on Outputs
|
||||
|
||||
A **Function** node can also send signals. This is very useful for many scenarions,
|
||||
for example to trigger something that should happen once the **Function** is executed.
|
||||
For example, if we would want to store our merged string, we would want to trigger a [Set Variable](/nodes/data/variable/set-variable) node.
|
||||
Let's add in the signal, once the merging is done:
|
||||
|
||||
Now click the **Function** node and edit the script. Paste in the Javascript below:
|
||||
|
||||
```javascript
|
||||
if (Inputs.String1 !== undefined && Inputs.String2 !== undefined) {
|
||||
let length = Math.max(Inputs.String1.length, Inputs.String2.length);
|
||||
let outputString = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i < Inputs.String1.length) {
|
||||
outputString = outputString + Inputs.String1.charAt(i);
|
||||
}
|
||||
if (i < Inputs.String2.length) {
|
||||
outputString = outputString + Inputs.String2.charAt(i);
|
||||
}
|
||||
}
|
||||
Outputs.MergedString = outputString;
|
||||
Outputs.Done();
|
||||
}
|
||||
```
|
||||
|
||||
Note the additional line `Outputs.Done()`. That's all you need to send a signal.
|
||||
So add in a **Set Variable** node and save the value in an **Variable** called `Merged String`.
|
||||
You might think that connecting directly from the **Button** to the **Do** action on the **Set Variable** might have worked, but it actually doesn't.
|
||||
You cannot know if the **Function** node have executed so the **Do** signal may trigger at the wrong time.
|
||||
Instead explicitly triggering `Done` once you've set the output to the correct value takes care of this.
|
||||
|
||||
Another common way of using outgoing signals could be to trigger two different paths going forward.
|
||||
Perhaps the **Function** validates a data value and triggers a `Valid` signal if the value is ok,
|
||||
that saves then triggers a database save, while a `Invalid` signal would open an error popup.
|
||||
|
||||
Now let's look at the more powerful **Script** node.
|
||||
|
||||
## JavaScript using the **Script** node
|
||||
|
||||
This part of the guide will cover the functionality of the [Script](/nodes/javascript/script) node.
|
||||
The **Script** node is a great way to implement logic and calculations that are easier to express in code than in nodes and connections.
|
||||
It's also a great way to get access to useful JavaScript APIs in the browser, for example `Date` for date functionality or `Math` for advanced math-functionality.
|
||||
|
||||
The **Script** node is a bit more powerful than the [Function](/nodes/javascript/function) node that also can be used to write JavaScript.
|
||||
The **Script** node can have multiple methods and functions, as well as a well defined lifespan with callbacks when the node is created and destroyed.
|
||||
For simpler JavaScript for processing inputs, usually the **Function** node is a simpler choice as you have seen above.
|
||||
|
||||
A **Script** node works as any other node in Noodl, in the sense that it has inputs and outputs that can be connected to other nodes.
|
||||
All inputs and outputs are available to the developer in their JavaScript code.
|
||||
In a **Script** node you can call any JavaScript function normally available in a browser environment.
|
||||
|
||||
## The Script source file
|
||||
|
||||
You can either edit the JavaScript code directly in the built-in editor in Noodl or you can use an external file with an external editor.
|
||||
While it's easy to write code snippets in the inline editor, the external file option might be better if you are working on larger files or even multiple files and modules.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
An external file needs to be located in your project folder for Noodl to find it. You can copy a file to your project folder by dragging the file onto the Noodl window.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
The source code provided for the **Script** is executed as soon as the node is created. In order to specify inputs, outputs and receive and send signals from the node the `Node` object must be used.
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
There are a number of ways to specify the inputs and outputs of the **Script** node. One way is to use the property panel and explicitly add them there. You can also provide the type.
|
||||
|
||||
<div className="ndl-image-with-background">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Another way is to specify them programmatically in the source code. The inputs are defined in the `Script.Inputs` object. Each input also specifies what type it is. The available types are:
|
||||
|
||||
- `number`
|
||||
- `string`
|
||||
- `boolean`
|
||||
- `color`
|
||||
- `object`
|
||||
- `array`. This is for Noodl Arrays, not JavaScript arrays.
|
||||
- `reference`. A reference to a Noodl node, accessible through the _This_ output of visual nodes.
|
||||
- `cloudfile`
|
||||
|
||||
Note that there is no signal type for inputs, as the signals are handled by using the `Script.Signals` object (more on that later).
|
||||
|
||||
The outputs work in the same way as the inputs except that there's one more type you can use: `signal`. The signal type is used for triggering a pulse on an output rather than outputting a specific value. Below we have added outputs to a **Script** node.
|
||||
|
||||
Since the inputs and outputs are members of an object, they should be separated by a comma. Below is an example of a **Script** node with two inputs and one output.
|
||||
|
||||
```javascript
|
||||
Script.Inputs = {
|
||||
RangeLow: "number",
|
||||
RangeHigh: "number",
|
||||
};
|
||||
|
||||
Script.Outputs = {
|
||||
RandomNumber: "number",
|
||||
};
|
||||
```
|
||||
|
||||
Lets use the two inputs `RangeLow` and `RangeHigh` to generate a random number on the `RandomNumber` output. To execute the code, we will introduce a signal, `Generate`.
|
||||
|
||||
## Input signals
|
||||
|
||||
Input signals are mapped to functions in the `Script.Signals` object in the JavaScript node. A signal function is called when the signal with the same name is triggered. Here's the implementation of the `Generate` signal. You can copy this code and add it to the **Script** source code.
|
||||
|
||||
```javascript
|
||||
Script.Signals.Generate = function () {
|
||||
let randomNumber =
|
||||
Math.random() * (Script.Inputs.RangeHigh - Script.Inputs.RangeLow) +
|
||||
Script.Inputs.RangeLow;
|
||||
Script.Outputs.RandomNumber = randomNumber;
|
||||
};
|
||||
```
|
||||
|
||||
Let's connect the the inputs, outputs and signals to some nodes.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
<CopyToClipboardButton json={{"nodes":[{"id":"20e7a891-e750-3d60-058f-804e324722eb","type":"Javascript2","x":-588.8750006232499,"y":331.12222156165456,"parameters":{"code":"Script.Inputs = {\n RangeLow: \"number\",\n RangeHigh: \"number\",\n}\n\nScript.Outputs = {\n RandomNumber: \"number\",\n}\n\nScript.Signals.Generate = function() {\n let randomNumber = Math.random() * (Script.Inputs.RangeHigh - Script.Inputs.RangeLow) + Script.Inputs.RangeLow;\n Script.Outputs.RandomNumber = randomNumber;\n}\n","RangeLow":0},"ports":[],"children":[]},{"id":"42ca61a1-d015-69dc-48ed-a13e76818194","type":"Group","x":-365.17777825429016,"y":268.1043044317845,"parameters":{"backgroundColor":"#FFFFFF","paddingTop":{"value":20,"unit":"px"},"paddingLeft":{"value":20,"unit":"px"},"paddingRight":{"value":20,"unit":"px"}},"ports":[],"children":[{"id":"a15e91b8-5f23-47ce-446c-fea6760a8058","type":"Group","x":-345.17777825429016,"y":314.1043044317845,"parameters":{"flexDirection":"row","sizeMode":"contentHeight","marginBottom":{"value":10,"unit":"px"}},"ports":[],"children":[{"id":"c2e35d8b-c32a-8d1b-754e-6d33a90e9a8f","type":"Text","label":"Click Text","x":-325.17777825429016,"y":360.1043044317845,"parameters":{"text":"Click here to generate number","fontSize":{"value":15,"unit":"px"},"alignY":"bottom","sizeMode":"contentSize"},"ports":[],"children":[]}]},{"id":"94d50d6a-1dc6-876f-39fb-2dfddf0749ef","type":"Group","label":"Arena","x":-345.17777825429016,"y":456.1043044317845,"parameters":{"borderStyle":"solid","borderWidth":{"value":2,"unit":"px"},"marginBottom":{"value":0,"unit":"px"},"flexDirection":"column","sizeMode":"explicit","height":{"value":35,"unit":"px"}},"ports":[],"children":[{"id":"7ccbd282-8a7a-1589-edac-acacf54cea24","type":"Circle","x":-325.17777825429016,"y":552.1043044317845,"parameters":{"size":30,"position":"absolute"},"ports":[],"children":[]}]}]},{"id":"60c3158a-64a2-7664-abb1-02dae2731e9d","type":"Expression","label":"arenawidth-circlewidth","x":-590.8015248634624,"y":443.9881840161512,"parameters":{"expression":"arenawidth-circlewidth"},"ports":[],"children":[]}],"connections":[{"fromId":"94d50d6a-1dc6-876f-39fb-2dfddf0749ef","fromProperty":"boundingWidth","toId":"60c3158a-64a2-7664-abb1-02dae2731e9d","toProperty":"arenawidth"},{"fromId":"7ccbd282-8a7a-1589-edac-acacf54cea24","fromProperty":"boundingWidth","toId":"60c3158a-64a2-7664-abb1-02dae2731e9d","toProperty":"circlewidth"},{"fromId":"60c3158a-64a2-7664-abb1-02dae2731e9d","fromProperty":"result","toId":"20e7a891-e750-3d60-058f-804e324722eb","toProperty":"RangeHigh"},{"fromId":"20e7a891-e750-3d60-058f-804e324722eb","fromProperty":"RandomNumber","toId":"7ccbd282-8a7a-1589-edac-acacf54cea24","toProperty":"transformX"},{"fromId":"c2e35d8b-c32a-8d1b-754e-6d33a90e9a8f","fromProperty":"onClick","toId":"20e7a891-e750-3d60-058f-804e324722eb","toProperty":"Generate"}],"comments":[]}}/>
|
||||
</div>
|
||||
|
||||
## Reading inputs and setting outputs
|
||||
|
||||
You can read the inputs directly through the members of the `Script.Inputs` object, typically `Script.Inputs.AnInput`.
|
||||
|
||||
There are two ways to set the outputs. Set the value by setting the appropriate property of the `Script.Outputs` object:
|
||||
|
||||
```javascript
|
||||
Script.Outputs.RandomNumber = randomNumber;
|
||||
```
|
||||
|
||||
Set many outputs at the same time using the `Script.setOutputs` function:
|
||||
|
||||
```javascript
|
||||
Script.setOutputs({
|
||||
One: 1,
|
||||
Two: 2,
|
||||
});
|
||||
```
|
||||
|
||||
This is useful when you have an object that contains multiple values you want to send at once.
|
||||
|
||||
Finally if you want to send a signal on an output you need to use the output as a function, calling it when you want to send the signal.
|
||||
|
||||
```javascript
|
||||
Script.Outputs.MySignalOutput();
|
||||
```
|
||||
|
||||
Now let's add a bit more code to our JavaScript example. Instead of the `Generate` signal we will implement `Start` and `Stop` signals and have the **JavaScript** node generate new numbers continuously. We will start a timer in `Start` that will trigger after a random time, influenced by the `Lambda` input. The higher the `Lambda` the shorter the time and the higher the rate of generated numbers.
|
||||
|
||||
?> See the <a href="https://en.wikipedia.org/wiki/Poisson_point_process" target="_blank">Poisson process</a> for the math behind generating a random number using a Poisson distribution.
|
||||
|
||||
When the timer is triggered, a random number is generated based on the ranges provided to the node. Finally a signal to notify that a new number has been generated is sent and the timer is restarted with a new timeout.
|
||||
When the `Stop` signal is triggered the timer is stopped.
|
||||
|
||||
Here's the code that generates the random numbers with a Poisson distributed time in between them.
|
||||
|
||||
```javascript
|
||||
Script.Inputs = {
|
||||
Lambda: "number",
|
||||
RangeLow: "number",
|
||||
RangeHigh: "number",
|
||||
};
|
||||
|
||||
Script.Outputs = {
|
||||
Trigger: "signal",
|
||||
RandomNumber: "number",
|
||||
};
|
||||
|
||||
var timer;
|
||||
|
||||
function generateRandNum(rangeLow, rangeHigh) {
|
||||
return Math.random() * (rangeHigh - rangeLow) + rangeLow;
|
||||
}
|
||||
|
||||
function calculateIntervalMs(lambda) {
|
||||
let interval = -Math.log(1.0 - Math.random()) / lambda;
|
||||
// translate from seconds to milliseconds
|
||||
return interval * 1000;
|
||||
}
|
||||
|
||||
Script.Signals.Start = function () {
|
||||
console.log("Start");
|
||||
let timeOutFunction = () => {
|
||||
// generate the random number
|
||||
let randNum = generateRandNum(
|
||||
Script.Inputs.RangeLow,
|
||||
Script.Inputs.RangeHigh
|
||||
);
|
||||
// set it on the output "RandomNumber"
|
||||
Script.setOutputs({ RandomNumber: randNum });
|
||||
// Trigger the signal "Trigger"
|
||||
Script.Outputs.Trigger();
|
||||
// restart the timer at a new interval
|
||||
timer = setTimeout(
|
||||
timeOutFunction,
|
||||
calculateIntervalMs(Script.Inputs.Lambda)
|
||||
);
|
||||
};
|
||||
|
||||
// start the first timer
|
||||
let interval = calculateIntervalMs(Script.Inputs.Lambda);
|
||||
|
||||
timer = setTimeout(timeOutFunction, interval);
|
||||
};
|
||||
|
||||
Script.Signals.Stop = function () {
|
||||
clearTimeout(timer);
|
||||
timer = undefined;
|
||||
};
|
||||
```
|
||||
|
||||
## Changed inputs
|
||||
|
||||
You can run code whenever an input is changed. In this particular case, when the `Lambda` input of the random number generator is changed, the timer interval should be updated to avoid waiting for the next timer to time out for the change to take effect. To handle a case like this, a function with the same name as the input, `Lambda`, is added in the `Script.Setters` object. An additional state variable, `started`, is added to make sure that changing the value when the timer is stopped won't cause it to start.
|
||||
|
||||
```javascript
|
||||
var started = false;
|
||||
Script.Setters.Lambda = function (value) {
|
||||
if (started === true) {
|
||||
clearTimeout(timer);
|
||||
startTimer();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## The final code
|
||||
|
||||
After some small refactoring the final code looks as below:
|
||||
|
||||
```javascript
|
||||
Script.Inputs = {
|
||||
Lambda: "number",
|
||||
RangeLow: "number",
|
||||
RangeHigh: "number",
|
||||
};
|
||||
|
||||
Script.Outputs = {
|
||||
Trigger: "signal",
|
||||
RandomNumber: "number",
|
||||
};
|
||||
|
||||
var timer;
|
||||
var started = false;
|
||||
|
||||
function generateRandNum(rangeLow, rangeHigh) {
|
||||
return Math.random() * (rangeHigh - rangeLow) + rangeLow;
|
||||
}
|
||||
|
||||
function calculateIntervalMs(lambda) {
|
||||
let interval = -Math.log(1.0 - Math.random()) / lambda;
|
||||
// translate from seconds to milliseconds
|
||||
return interval * 1000;
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
let timeOutFunction = () => {
|
||||
// generate the random number
|
||||
let randNum = generateRandNum(
|
||||
Script.Inputs.RangeLow,
|
||||
Script.Inputs.RangeHigh
|
||||
);
|
||||
// set it on the output "RandomNumber"
|
||||
Script.setOutputs({ RandomNumber: randNum });
|
||||
// Trigger the signal "Trigger"
|
||||
Script.Outputs.Trigger();
|
||||
// restart the timer at a new interval
|
||||
timer = setTimeout(
|
||||
timeOutFunction,
|
||||
calculateIntervalMs(Script.Inputs.Lambda)
|
||||
);
|
||||
};
|
||||
|
||||
// start the first timer
|
||||
let interval = calculateIntervalMs(Script.Inputs.Lambda);
|
||||
|
||||
timer = setTimeout(timeOutFunction, interval);
|
||||
}
|
||||
|
||||
Script.Signals = {
|
||||
Start() {
|
||||
started = true;
|
||||
startTimer();
|
||||
},
|
||||
Stop() {
|
||||
clearTimeout(timer);
|
||||
started = false;
|
||||
},
|
||||
};
|
||||
|
||||
Script.Setters.Lambda = function (value) {
|
||||
if (started === true) {
|
||||
clearTimeout(timer);
|
||||
startTimer();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Using script nodes
|
||||
|
||||
Connecting to the the inputs and outputs, the **Script** nodes can be used as any other nodes in Noodl. As an example, the Random Generator **Script** node has been combined with a simple UI to control the inputs. The output of the random generator is used to move a circle on the screen and trigger state changes. We have also copy & pasted the **Script** node and use it two times. This works great, but remember that the JavaScript code is cloned if you are using an inline source so changing the code in one **Script** node does not affect the other. It's often a good idea to encapsulate a reusable **Script** node in a Noodl component.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
<CopyToClipboardButton
|
||||
json={{
|
||||
nodes: [
|
||||
{
|
||||
id: "e1b76139-42b1-f1aa-d935-8697c02446e5",
|
||||
type: "Group",
|
||||
x: 550.8894402215635,
|
||||
y: 769.5545779864274,
|
||||
parameters: {
|
||||
backgroundColor: "#FFFFFF",
|
||||
paddingTop: { value: 20, unit: "px" },
|
||||
paddingLeft: { value: 20, unit: "px" },
|
||||
paddingRight: { value: 20, unit: "px" },
|
||||
},
|
||||
ports: [],
|
||||
children: [
|
||||
{
|
||||
id: "7d179c30-3108-a71d-76bf-0f760b7f9417",
|
||||
type: "Group",
|
||||
x: 570.8894402215635,
|
||||
y: 815.5545779864274,
|
||||
parameters: {
|
||||
flexDirection: "row",
|
||||
sizeMode: "contentHeight",
|
||||
marginBottom: { value: 10, unit: "px" },
|
||||
},
|
||||
ports: [],
|
||||
children: [
|
||||
{
|
||||
id: "801c4a96-5219-b818-4564-87a704f84837",
|
||||
type: "Text",
|
||||
label: "Rate controller text",
|
||||
x: 590.8894402215635,
|
||||
y: 861.5545779864274,
|
||||
parameters: {
|
||||
text: "Random generator is",
|
||||
fontSize: { value: 15, unit: "px" },
|
||||
alignY: "bottom",
|
||||
sizeMode: "contentSize",
|
||||
},
|
||||
ports: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: "f54f6144-306d-9339-4e9c-a1fd70d0910d",
|
||||
type: "Group",
|
||||
label: "On/Off Spinner",
|
||||
x: 590.8894402215635,
|
||||
y: 921.5545779864274,
|
||||
parameters: {
|
||||
sizeMode: "explicit",
|
||||
height: { value: 20, unit: "px" },
|
||||
clip: true,
|
||||
alignY: "bottom",
|
||||
marginLeft: { value: 10, unit: "px" },
|
||||
flexDirection: "none",
|
||||
width: { value: 30, unit: "px" },
|
||||
},
|
||||
ports: [],
|
||||
children: [
|
||||
{
|
||||
id: "8d21ec9d-7d28-d1e1-6435-ed102c432236",
|
||||
type: "Text",
|
||||
label: "Off",
|
||||
x: 610.8894402215635,
|
||||
y: 1017.5545779864274,
|
||||
parameters: {
|
||||
text: "Off",
|
||||
color: "#006394",
|
||||
fontSize: { value: 15, unit: "px" },
|
||||
sizeMode: "contentSize",
|
||||
alignY: "bottom",
|
||||
},
|
||||
ports: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: "6ec39536-2ae0-13da-31a9-0c95693eb91a",
|
||||
type: "Text",
|
||||
label: "On",
|
||||
x: 610.8894402215635,
|
||||
y: 1113.5545779864274,
|
||||
parameters: {
|
||||
text: "On",
|
||||
fontSize: { value: 15, unit: "px" },
|
||||
color: "#006394",
|
||||
sizeMode: "contentSize",
|
||||
alignY: "bottom",
|
||||
},
|
||||
ports: [],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "83c96b0c-c3b3-c331-4c9c-ea903740c149",
|
||||
type: "Group",
|
||||
label: "Text and Rate controller",
|
||||
x: 570.8894402215635,
|
||||
y: 1209.5545779864274,
|
||||
parameters: {
|
||||
flexDirection: "row",
|
||||
sizeMode: "contentHeight",
|
||||
marginBottom: { value: 10, unit: "px" },
|
||||
},
|
||||
ports: [],
|
||||
children: [
|
||||
{
|
||||
id: "3e8aa8af-5940-099e-9aa4-04aafc48f785",
|
||||
type: "Text",
|
||||
label: "Set rate",
|
||||
x: 590.8894402215635,
|
||||
y: 1283.5545779864274,
|
||||
parameters: {
|
||||
text: "Set rate",
|
||||
fontSize: { value: 15, unit: "px" },
|
||||
alignY: "center",
|
||||
sizeMode: "contentSize",
|
||||
marginRight: { value: 20, unit: "px" },
|
||||
},
|
||||
ports: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: "e30dd54b-7feb-d649-f3f9-e8f89ed96f7e",
|
||||
type: "Group",
|
||||
label: "Rate Controller",
|
||||
x: 590.8894402215635,
|
||||
y: 1343.5545779864274,
|
||||
parameters: {
|
||||
sizeMode: "explicit",
|
||||
height: { value: 30, unit: "px" },
|
||||
flexDirection: "none",
|
||||
width: { value: 200, unit: "px" },
|
||||
},
|
||||
ports: [],
|
||||
children: [
|
||||
{
|
||||
id: "e6d5752f-cedd-429d-3d3d-173d747c8c85",
|
||||
type: "Group",
|
||||
label: "Rate controller Bg",
|
||||
x: 610.8894402215635,
|
||||
y: 1403.5545779864274,
|
||||
parameters: {
|
||||
height: { value: 20, unit: "px" },
|
||||
backgroundColor: "#C6C6C6",
|
||||
borderRadius: { value: 10, unit: "px" },
|
||||
alignY: "center",
|
||||
},
|
||||
ports: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: "241b0a6f-b409-7cbd-dc61-3bc39c020667",
|
||||
type: "Drag",
|
||||
x: 610.8894402215635,
|
||||
y: 1499.5545779864274,
|
||||
parameters: {},
|
||||
ports: [],
|
||||
children: [
|
||||
{
|
||||
id: "31252340-8be5-166c-4c16-f3d178135139",
|
||||
type: "Group",
|
||||
label: "Drag handle",
|
||||
x: 630.8894402215635,
|
||||
y: 1581.5545779864274,
|
||||
parameters: {
|
||||
height: { value: 20, unit: "px" },
|
||||
width: { value: 5, unit: "px" },
|
||||
backgroundColor: "#434B53",
|
||||
paddingLeft: { value: 0, unit: "px" },
|
||||
marginLeft: { value: 10, unit: "px" },
|
||||
marginRight: { value: 10, unit: "px" },
|
||||
borderRadius: { value: 2, unit: "px" },
|
||||
alignY: "center",
|
||||
},
|
||||
ports: [],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "f227417b-8cd3-3c03-5501-1b85add9566c",
|
||||
type: "Group",
|
||||
label: "Arena",
|
||||
x: 570.8894402215635,
|
||||
y: 1641.5545779864274,
|
||||
parameters: {
|
||||
borderStyle: "solid",
|
||||
borderWidth: { value: 2, unit: "px" },
|
||||
marginBottom: { value: 10, unit: "px" },
|
||||
flexDirection: "none",
|
||||
},
|
||||
ports: [],
|
||||
children: [
|
||||
{
|
||||
id: "521f5c37-4508-f725-d67d-44f0227e3aa9",
|
||||
type: "Circle",
|
||||
x: 590.8894402215635,
|
||||
y: 1737.5545779864274,
|
||||
parameters: { size: 30, position: "absolute" },
|
||||
ports: [],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "238629f6-8594-ac78-ba25-a8141cebf4f6",
|
||||
type: "States",
|
||||
x: 110.74283186907337,
|
||||
y: 946.5074351399836,
|
||||
parameters: {
|
||||
states: "Off,On",
|
||||
values: "Off y position,On y Position",
|
||||
"value-Off-Off y position": 0,
|
||||
"value-On-Off y position": -20,
|
||||
"value-Off-On y Position": 20,
|
||||
"value-On-On y Position": 0,
|
||||
},
|
||||
ports: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: "606ead21-564f-f2d3-8311-30eb63af8eab",
|
||||
type: "Javascript2",
|
||||
label: "Poisson Process",
|
||||
x: 360.9506961673602,
|
||||
y: 1177.1836206411483,
|
||||
parameters: {
|
||||
code: 'Script.Inputs = {\n Lambda: "number",\n RangeLow: "number",\n RangeHigh: "number",\n}\n\nScript.Outputs = {\n Trigger: "signal",\n RandomNumber: "number",\n}\n\nvar timer;\nvar started = false;\n\nfunction generateRandNum(rangeLow, rangeHigh) {\n return Math.random() * (rangeHigh - rangeLow) + rangeLow;\n}\n\nfunction calculateIntervalMs(lambda) {\n let interval = -Math.log(1.0 - Math.random()) / lambda;\n // translate from seconds to milliseconds\n return interval * 1000;\n}\n\nfunction startTimer() {\n let timeOutFunction = () => {\n // generate the random number\n let randNum = generateRandNum(\n Script.Inputs.RangeLow,\n Script.Inputs.RangeHigh\n );\n // set it on the output "RandomNumber"\n Script.setOutputs({ RandomNumber: randNum });\n // Trigger the signal "Trigger"\n Script.Outputs.Trigger()\n // restart the timer at a new interval\n timer = setTimeout(\n timeOutFunction,\n calculateIntervalMs(Script.Inputs.Lambda)\n );\n };\n\n // start the first timer\n let interval = calculateIntervalMs(Script.Inputs.Lambda);\n\n timer = setTimeout(timeOutFunction, interval);\n}\n\nScript.Signals = {\n Start() {\n started = true;\n startTimer();\n },\n Stop() {\n clearTimeout(timer);\n started = false;\n }\n}\n\nScript.Setters.Lambda = function (value) {\n if (started === true) {\n clearTimeout(timer);\n startTimer();\n }\n}\n',
|
||||
Lambda: 2,
|
||||
RangeLow: 0,
|
||||
},
|
||||
ports: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: "cdd08f56-26b6-c81f-729b-ef7220190933",
|
||||
type: "Expression",
|
||||
label: "Lambda value",
|
||||
x: 943.791823827788,
|
||||
y: 1223.6044302506757,
|
||||
parameters: { expression: "xpos/(DrageAreaWidth - 25)*3 + 0.1" },
|
||||
ports: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: "26b6b95e-48c9-d4a5-2255-124bef51428b",
|
||||
type: "Expression",
|
||||
x: 297.4155454673813,
|
||||
y: 1508.2775409338583,
|
||||
parameters: { expression: "ArenaWidth - CircleRadius" },
|
||||
ports: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: "99c3a02d-7b32-d31e-40d0-fc6bd485f2d3",
|
||||
type: "Transition",
|
||||
x: 272.28959152461647,
|
||||
y: 1742.5301598853775,
|
||||
parameters: {},
|
||||
ports: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: "9a29693e-97b7-e24a-c39f-95745e15289a",
|
||||
type: "Javascript2",
|
||||
label: "Poisson Process",
|
||||
x: 973.3810754787316,
|
||||
y: 1711.8056993389462,
|
||||
parameters: {
|
||||
code: 'Script.Inputs = {\n Lambda: "number",\n RangeLow: "number",\n RangeHigh: "number",\n}\n\nScript.Outputs = {\n Trigger: "signal",\n RandomNumber: "number",\n}\n\nvar timer;\nvar started = false;\n\nfunction generateRandNum(rangeLow, rangeHigh) {\n return Math.random() * (rangeHigh - rangeLow) + rangeLow;\n}\n\nfunction calculateIntervalMs(lambda) {\n let interval = -Math.log(1.0 - Math.random()) / lambda;\n // translate from seconds to milliseconds\n return interval * 1000;\n}\n\nfunction startTimer() {\n let timeOutFunction = () => {\n // generate the random number\n let randNum = generateRandNum(\n Script.Inputs.RangeLow,\n Script.Inputs.RangeHigh\n );\n // set it on the output "RandomNumber"\n Script.setOutputs({ RandomNumber: randNum });\n // Trigger the signal "Trigger"\n Script.Outputs.Trigger()\n // restart the timer at a new interval\n timer = setTimeout(\n timeOutFunction,\n calculateIntervalMs(Script.Inputs.Lambda)\n );\n };\n\n // start the first timer\n let interval = calculateIntervalMs(Script.Inputs.Lambda);\n\n timer = setTimeout(timeOutFunction, interval);\n}\n\nScript.Signals = {\n Start() {\n started = true;\n startTimer();\n },\n Stop() {\n clearTimeout(timer);\n started = false;\n }\n}\n\nScript.Setters.Lambda = function (value) {\n if (started === true) {\n clearTimeout(timer);\n startTimer();\n }\n}\n',
|
||||
Lambda: 2,
|
||||
RangeLow: 0,
|
||||
},
|
||||
ports: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: "db634033-b137-02ea-5d50-706c06432557",
|
||||
type: "Expression",
|
||||
x: 952.9005306361687,
|
||||
y: 1948.8030166078263,
|
||||
parameters: { expression: "ArenaHeight-CircleRadius" },
|
||||
ports: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: "0a89a18b-cfe3-c6f9-18bc-221a7ffb2182",
|
||||
type: "Transition",
|
||||
x: 910.411880595271,
|
||||
y: 1523.391447982002,
|
||||
parameters: {},
|
||||
ports: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: "7249be82-0072-c1ab-1e4c-c0c60903f8cf",
|
||||
type: "States",
|
||||
label: "Circle Color",
|
||||
x: 291.63095980838676,
|
||||
y: 1874.4551715183388,
|
||||
parameters: {
|
||||
states: "Color 1,Color 2,Color 3,Color 4",
|
||||
values: "Circle Color",
|
||||
"type-Circle Color": "color",
|
||||
"value-Color 1-Circle Color": "#A92952",
|
||||
"value-Color 2-Circle Color": "#F0BF56",
|
||||
"value-Color 3-Circle Color": "#006394",
|
||||
"value-Color 4-Circle Color": "#5E4275",
|
||||
},
|
||||
ports: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: "31a54762-a486-533b-6041-e2b99c000ee4",
|
||||
type: "States",
|
||||
x: 659.6426944801517,
|
||||
y: 2001.0264630214242,
|
||||
parameters: {
|
||||
states: "Small,Big",
|
||||
values: "Radius",
|
||||
"value-Small-Radius": 30,
|
||||
"value-Big-Radius": 50,
|
||||
},
|
||||
ports: [],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
connections: [
|
||||
{
|
||||
fromId: "238629f6-8594-ac78-ba25-a8141cebf4f6",
|
||||
fromProperty: "Off y position",
|
||||
toId: "8d21ec9d-7d28-d1e1-6435-ed102c432236",
|
||||
toProperty: "transformY",
|
||||
},
|
||||
{
|
||||
fromId: "238629f6-8594-ac78-ba25-a8141cebf4f6",
|
||||
fromProperty: "On y Position",
|
||||
toId: "6ec39536-2ae0-13da-31a9-0c95693eb91a",
|
||||
toProperty: "transformY",
|
||||
},
|
||||
{
|
||||
fromId: "f54f6144-306d-9339-4e9c-a1fd70d0910d",
|
||||
fromProperty: "onClick",
|
||||
toId: "238629f6-8594-ac78-ba25-a8141cebf4f6",
|
||||
toProperty: "toggle",
|
||||
},
|
||||
{
|
||||
fromId: "241b0a6f-b409-7cbd-dc61-3bc39c020667",
|
||||
fromProperty: "positionX",
|
||||
toId: "cdd08f56-26b6-c81f-729b-ef7220190933",
|
||||
toProperty: "xpos",
|
||||
},
|
||||
{
|
||||
fromId: "e6d5752f-cedd-429d-3d3d-173d747c8c85",
|
||||
fromProperty: "boundingWidth",
|
||||
toId: "cdd08f56-26b6-c81f-729b-ef7220190933",
|
||||
toProperty: "DrageAreaWidth",
|
||||
},
|
||||
{
|
||||
fromId: "238629f6-8594-ac78-ba25-a8141cebf4f6",
|
||||
fromProperty: "reached-On",
|
||||
toId: "606ead21-564f-f2d3-8311-30eb63af8eab",
|
||||
toProperty: "Start",
|
||||
},
|
||||
{
|
||||
fromId: "238629f6-8594-ac78-ba25-a8141cebf4f6",
|
||||
fromProperty: "reached-Off",
|
||||
toId: "606ead21-564f-f2d3-8311-30eb63af8eab",
|
||||
toProperty: "Stop",
|
||||
},
|
||||
{
|
||||
fromId: "cdd08f56-26b6-c81f-729b-ef7220190933",
|
||||
fromProperty: "result",
|
||||
toId: "606ead21-564f-f2d3-8311-30eb63af8eab",
|
||||
toProperty: "Lambda",
|
||||
},
|
||||
{
|
||||
fromId: "521f5c37-4508-f725-d67d-44f0227e3aa9",
|
||||
fromProperty: "boundingWidth",
|
||||
toId: "26b6b95e-48c9-d4a5-2255-124bef51428b",
|
||||
toProperty: "CircleRadius",
|
||||
},
|
||||
{
|
||||
fromId: "26b6b95e-48c9-d4a5-2255-124bef51428b",
|
||||
fromProperty: "result",
|
||||
toId: "606ead21-564f-f2d3-8311-30eb63af8eab",
|
||||
toProperty: "RangeHigh",
|
||||
},
|
||||
{
|
||||
fromId: "606ead21-564f-f2d3-8311-30eb63af8eab",
|
||||
fromProperty: "RandomNumber",
|
||||
toId: "99c3a02d-7b32-d31e-40d0-fc6bd485f2d3",
|
||||
toProperty: "targetValue",
|
||||
},
|
||||
{
|
||||
fromId: "99c3a02d-7b32-d31e-40d0-fc6bd485f2d3",
|
||||
fromProperty: "currentValue",
|
||||
toId: "521f5c37-4508-f725-d67d-44f0227e3aa9",
|
||||
toProperty: "transformX",
|
||||
},
|
||||
{
|
||||
fromId: "cdd08f56-26b6-c81f-729b-ef7220190933",
|
||||
fromProperty: "result",
|
||||
toId: "9a29693e-97b7-e24a-c39f-95745e15289a",
|
||||
toProperty: "Lambda",
|
||||
},
|
||||
{
|
||||
fromId: "f227417b-8cd3-3c03-5501-1b85add9566c",
|
||||
fromProperty: "boundingWidth",
|
||||
toId: "26b6b95e-48c9-d4a5-2255-124bef51428b",
|
||||
toProperty: "ArenaWidth",
|
||||
},
|
||||
{
|
||||
fromId: "f227417b-8cd3-3c03-5501-1b85add9566c",
|
||||
fromProperty: "boundingHeight",
|
||||
toId: "db634033-b137-02ea-5d50-706c06432557",
|
||||
toProperty: "ArenaHeight",
|
||||
},
|
||||
{
|
||||
fromId: "521f5c37-4508-f725-d67d-44f0227e3aa9",
|
||||
fromProperty: "boundingHeight",
|
||||
toId: "db634033-b137-02ea-5d50-706c06432557",
|
||||
toProperty: "CircleRadius",
|
||||
},
|
||||
{
|
||||
fromId: "db634033-b137-02ea-5d50-706c06432557",
|
||||
fromProperty: "result",
|
||||
toId: "9a29693e-97b7-e24a-c39f-95745e15289a",
|
||||
toProperty: "RangeHigh",
|
||||
},
|
||||
{
|
||||
fromId: "9a29693e-97b7-e24a-c39f-95745e15289a",
|
||||
fromProperty: "RandomNumber",
|
||||
toId: "0a89a18b-cfe3-c6f9-18bc-221a7ffb2182",
|
||||
toProperty: "targetValue",
|
||||
},
|
||||
{
|
||||
fromId: "0a89a18b-cfe3-c6f9-18bc-221a7ffb2182",
|
||||
fromProperty: "currentValue",
|
||||
toId: "521f5c37-4508-f725-d67d-44f0227e3aa9",
|
||||
toProperty: "transformY",
|
||||
},
|
||||
{
|
||||
fromId: "238629f6-8594-ac78-ba25-a8141cebf4f6",
|
||||
fromProperty: "reached-Off",
|
||||
toId: "9a29693e-97b7-e24a-c39f-95745e15289a",
|
||||
toProperty: "Stop",
|
||||
},
|
||||
{
|
||||
fromId: "238629f6-8594-ac78-ba25-a8141cebf4f6",
|
||||
fromProperty: "reached-On",
|
||||
toId: "9a29693e-97b7-e24a-c39f-95745e15289a",
|
||||
toProperty: "Start",
|
||||
},
|
||||
{
|
||||
fromId: "7249be82-0072-c1ab-1e4c-c0c60903f8cf",
|
||||
fromProperty: "Circle Color",
|
||||
toId: "521f5c37-4508-f725-d67d-44f0227e3aa9",
|
||||
toProperty: "fillColor",
|
||||
},
|
||||
{
|
||||
fromId: "606ead21-564f-f2d3-8311-30eb63af8eab",
|
||||
fromProperty: "Trigger",
|
||||
toId: "7249be82-0072-c1ab-1e4c-c0c60903f8cf",
|
||||
toProperty: "toggle",
|
||||
},
|
||||
{
|
||||
fromId: "9a29693e-97b7-e24a-c39f-95745e15289a",
|
||||
fromProperty: "Trigger",
|
||||
toId: "31a54762-a486-533b-6041-e2b99c000ee4",
|
||||
toProperty: "toggle",
|
||||
},
|
||||
{
|
||||
fromId: "31a54762-a486-533b-6041-e2b99c000ee4",
|
||||
fromProperty: "Radius",
|
||||
toId: "521f5c37-4508-f725-d67d-44f0227e3aa9",
|
||||
toProperty: "size",
|
||||
},
|
||||
],
|
||||
comments: [],
|
||||
}}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
## Debugging
|
||||
|
||||
As with any coding, you will sooner or later make a mistake in your code. Noodl will catch both syntax errors and runtime errors and highlight the **Script** node causing the error. You can also find errors in the warnings popup.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
As seen in the image above, syntax errors in the code can cause inputs and outputs of the node to becomes invalid. Fixing the syntax error will restore the connections.
|
||||
|
||||
To debug your javascript you can launch the web debugger from the viewer window by clicking the bug icon.
|
||||
|
||||
<div className="ndl-image-with-background l">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
In the web debugger you can find any external source files that your are using in your script nodes, but if you want to set a breakpoint in an internal file you can use the `debugger` command. Here's an example:
|
||||
|
||||
```javascript
|
||||
Script.Signals.Stop = function () {
|
||||
debugger; // This will cause the debugger to break here when running
|
||||
clearTimeout(timer);
|
||||
timer = undefined;
|
||||
};
|
||||
```
|
||||
|
||||
### Running code when a Script node is created or destroyed
|
||||
|
||||
A Script node is created when the Noodl component that it belongs to is created. Components are created when the app is first launched, when navigation happens, and when a [Repeater](/nodes/ui-controls/repeater) node creates items. The `Script.OnInit` function is automatically called by Noodl when the Script node is created.
|
||||
|
||||
Components can be destroyed when doing navigation or when removing items from a list used by a Repeater node. This will run the `Script.OnDestroy` function.
|
||||
|
||||
Here's an example that sets up an event listener on the `body` element and removes it when the node is destroyed to avoid memory leaks.
|
||||
|
||||
```js
|
||||
function setPosition(e) {
|
||||
Script.Outputs.PointerX = e.clientX;
|
||||
Script.Outputs.PointerY = e.clientY;
|
||||
}
|
||||
|
||||
Script.OnInit = function () {
|
||||
document.body.addEventListener("mousemove", setPosition);
|
||||
document.body.addEventListener("mousedown", setPosition);
|
||||
};
|
||||
|
||||
Script.OnDestroy = function () {
|
||||
document.body.removeEventListener("mousedown", setPosition);
|
||||
document.body.removeEventListener("mousemove", setPosition);
|
||||
};
|
||||
```
|
||||
Reference in New Issue
Block a user