NoodlのScript(Javascript)ノード変更点(2.1.0以降)

## はじめに
Noodlのバージョンが上がりScriptノード(以前はJavascriptノードと呼ばれていました)で使うJavascriptの書き方が変更されています。
ここではバージョン2.5.xを対象として、Scriptノードの使い方について[公式ドキュメント](http://docs.noodl.net/#/guides/javascript)を元に解説していきます。

![script-inline-code.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192442/8346cbba-46c4-0564-b750-c699fa2c12b1.png)

## 概要：Scriptノードとは
NoodlではScriptノードを使うことでJavascriptコードを直接記述することが可能です。Scriptノードから他のJavaScript APIにアクセスすることもできます。
Scriptノードは他のノードに接続することができる入力・出力があるので、通常のNoodlノードと同じように動作します。

---
## Javascriptの書き方
Noodl2.1.xよりScriptノードの書き方が若干変わっています。以前のNoodlを知っている方からすると下記変更がありました。

- ReactベースとなりJavascriptのバージョンがUpしています。アロー関数なども使えるようになっています。
- `document.body.addEventListener`なども使えるようになりました
- scriptやdefineで囲まなくなりました
- inputs,outputsはNodeのメンバに代入する形となりました
- 入力信号に名前を付けられるようになりました

```js:Scriptノードの記述例
//入力定義
Node.Inputs = {
    Lambda: 'number',
    RangeLow: 'number',
    RangeHigh: 'number',
}

//出力定義
Node.Outputs = {
    Trigger: 'signal',
    RandomNumber: 'number',
}

//Start信号関数
Node.Signals.Start = function () {
}

//Stop信号関数
Node.Signals.Stop = function () {
    //信号を出力する場合
    Node.Outputs.Trigger();

}
```

## 入力と出力の定義
　スクリプトノードの入力と出力を定義する方法はそれぞれ2つあります。

1. プロパティパネルを使用して明示的に追加する方法です。型を指定することもできます。
2. もう１つはプログラム内で入出力を指定する方法です。

![function-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192442/70af420f-ff60-8955-f3f9-09351e73e54d.png)
図　Scriptノードのプロパティパネル

入力は`Node.Inputs`オブジェクトで定義します。各入力では型も指定します。利用可能な型は下記表にまとめました。
信号は`Node.Signals`オブジェクトを使用して処理されるため、入力に信号型はないことに注意してください。

### 表　Inputs,Outputsで設定可能な型

|型|解説|
|---|---|
|number | Javascriptの数字
|string | Javascriptストリング
|boolean | Javascriptブーリアン
|color | カラーピッカーを有効にするHEXカラーコード
|reference | ビジュアルノードのThis出力からアクセスできるNoodlノードへの参照
|object|オブジェクト|
|array|これはNoodl配列用であり、JavaScript配列用ではありません|
|cloudfile|クラウド上のファイル|
|*|すべてのタイプを受け入れる|
|image | イメージピッカーを有効にする画像までのパス
|font | フォントピッカーを有効にするフォントファイルまでのパス
|component | コンポネントピッカーを有効にするコンポネントの名前

出力は入力と同じように機能し、使用できるタイプがもう1つあります。signal型です。信号は特定の値を出力するのではなく、出力パルスで他のノードをトリガーするために使用されます。

## 入力信号
入力信号は`Node.Signals`としてScriptノードの関数にマップされます。同じ名前の入力信号がトリガーされると関数が呼び出されます。

```javascript
Node.Signals.Start = function () {
  //入力信号(ここではStart)を受けた時の処理を記述する
}
```

## 入力の読取りと出力設定
Node.Inputsオブジェクトのメンバーを介して入力を直接読み取ることができます

```js
HeaderTitlle = Node.Inputs.titlle
```

出力を設定する方法は2つあります。Node.Outputsオブジェクトのプロパティで値を設定します。

```js
Node.Outputs.RandomNumber = randomNumber
```

複数の出力を同時に変更させたい場合はSetOutputsメソッドを利用します。Node.setOutputsは一度に複数の値を含むオブジェクトを送信する場合に役立ちます。

```js
Node.setOutputs({
    One: 1,
    Two: 2,
})
```

出力で信号を送信する場合は出力を関数として使用します。

```js
Node.Outputs.MySignalOutput()
```
---
## 具体的な例
JavaScriptの例としてJavaScriptノードに新しい数値を継続的に生成させます。
Start入力信号を受けて、ランダムな時間の後にトリガーされるタイマーを開始します。
Lambdaの値が高いほど、時間は短くなり生成される数の割合は高くなります。
タイマーがトリガーされると乱数が生成されます。
最後に新しい番号が生成されたことを通知する信号が送信され、タイマーが新しいタイムアウトで再開されます。
Stop信号がトリガーされるとタイマーが停止します。

![random1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192442/7926890f-1437-e1c8-f409-fd4eafaa4ff9.gif)


```js:ポアソン分布の時間で乱数を生成するコード
Node.Inputs = {
    Lambda: 'number',
    RangeLow: 'number',
    RangeHigh: 'number',
}

Node.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(
            Node.Inputs.RangeLow,
            Node.Inputs.RangeHigh
        )
        // set it on the output "RandomNumber"
        Node.setOutputs({ RandomNumber: randNum })
        // Trigger the signal "Trigger"
        Node.Outputs.Trigger()
        // restart the timer at a new interval
        timer = setTimeout(
            timeOutFunction,
            calculateIntervalMs(Node.Inputs.Lambda)
        )
    }

    // start the first timer
    let interval = calculateIntervalMs(Node.Inputs.Lambda)

    timer = setTimeout(timeOutFunction, interval)
}

Node.Signals = {
    Start() {
        started = true
        startTimer()
    },
    Stop() {
        clearTimeout(timer)
        started = false
    },
}

Node.Setters.Lambda = function (value) {
    if (started === true) {
        clearTimeout(timer)
        startTimer()
    }
}
```

## 入力値が変更され場合に関数を実行する
場合によっては入力が変更されたときにJavascriptで反応する必要があります。
例えばLambdaは乱数ジェネレーターの入力が変更された場合、次のタイマーがタイムアウトするのを待たずにタイマー間隔をすぐに更新する必要があります。
このようなケースを処理するためにNode.Settersを利用します。下記の例では入力Lambdaが変更されると関数が実行されます。

```js:入力値が変化した場合に関数を実行する例
var started = false
Node.Setters.Lambda = function (value) {
    if (started === true) {
        clearTimeout(timer)
        startTimer()
    }
}
```

## Scriptノードが作成または破棄されたときにコードを実行する
Scriptノードは属するNoodlコンポーネントが作成されるときに作成されます。
具体的には
1. アプリが最初に起動されたとき、ナビゲーションが発生したとき
1. ForEachノードがアイテムを作成したとき
に作成されます。

また、Scriptノードは属するNoodlコンポーネントが破棄されるときに破棄されます。
具体的には
1. ナビゲーションを実行するとき
1. ForEachノードで使用されるリストからアイテムを削除するとき
に破棄される可能性があります。

`Node.OnInit`関数はScriptノードが作成されるときに呼び出されます。
`Node.OnDestroy`関数はコンポーネントが破棄されるときに実行されます。

下記は要素にイベントリスナーを設定し
bodyノードが破棄されたときにそれを削除してメモリリークを回避する例です。

```js:イベントリスナーを設定/破棄する例
function setPosition(e) {
    Node.Outputs.PointerX = e.clientX
    Node.Outputs.PointerY = e.clientY
}

Node.OnInit = function () {
    document.body.addEventListener('mousemove', setPosition)
    document.body.addEventListener('mousedown', setPosition)
}

Node.OnDestroy = function () {
    document.body.removeEventListener('mousedown', setPosition)
    document.body.removeEventListener('mousemove', setPosition)
}
```

---
## Noodl Javascript API
関数ノードとスクリプトノード内で使用できるJavaScriptAPIのリファレンスドキュメントを紹介します。

- プログラムによるオブジェクトへのアクセス
- プログラムによるアレイへのアクセス
- APIを介したイベントの送受信

-------------------------------------------------
### Noodl.Object：オブジェクトへのアクセス
JavaScriptからオブジェクト機能へのアクセスを許可します。

### Noodl.Object.get(id)
指定されたIDのオブジェクトを返します。オブジェクトが存在するかどうかを確認したい場合はNoodl.Object.exists関数を使用してください。

### Noodl.Object.create(data)
新しいオブジェクトを作成して返します。たとえば下のコードは2つのプロパティとIDが「A」に設定されたオブジェクトを作成します。

```js
Noodl.Object.create({
    id: 'A',
    myProp1: 10,
    myProp2: 'Hello',
    myProp3: Noodl.Object.create({ anotherProp: 15 }),
})
```
::: note warn
IDが指定されていない場合、新しく作成されたオブジェクトには一意のIDが割り当てられます。上記の例に示されているように、オブジェクトのプロパティには他のオブジェクトを含めることができます。
:::

### Noodl.Object.exists(id)
指定されたIDを持つオブジェクトがNoodl.Object.getまたはNoodl.Object.createの呼び出しで作成された場合、trueを返します。

### object.on(event,listener) / object.off(event,listener)
オブジェクトにイベントリスナーを追加および削除します。

#### サポートされているイベント：
- change-オブジェクトのプロパティが変更されます
- add-オブジェクトが配列に追加されます
- remove-オブジェクトが配列から削除されます。

使用例：
```js
myObject.on('change', function (args) {
    // property with name args.name was changed
    // new value in args.value
    // old value in args.old
})

myObject.on('add', function (args) {
    // object was added to the array args.array
})

myObject.on('remove', function (args) {
    // object was removed from the array args.array
})
```

### object.getId()
オブジェクトのIDを返します。

### object.set(name,value,options)
オブジェクトのプロパティを設定します。
> myObject.set('myProp1',44)

### object.setAll(data)
指定されたオブジェクトのすべてのプロパティに対してセットを実行します。
これは、データオブジェクトのすべてのプロパティのsetを呼び出すことと同じです。
プロパティの更新はidサポートされておらず、無視されます。

### object.get(name,options)
指定された名前のプロパティの値を返します。set関数と同様に、オブジェクトにプロパティとして別のオブジェクトがあり、オプション{resolve：true}が指定されている場合は、ドット表記を使用できます。

> myObject.get('myProp3.anotherProp',{resolve:true})

-------------------------------------------------------
## Noodl.Array：Noodl配列へのアクセス

### Noodl.Array.get(id)
指定されたIDの配列を返します。Idが以前に使用されていない場合は、新しく作成された空の配列が返されます。IDが指定されていない場合、一意のIDを持つ新しい配列が生成されます。

## Noodl.Array.exists(id)
true指定されたIDの配列が存在するかどうか、つまり。で作成されているかどうかを返しますNoodl.Array.get。

## array.on(event,listener) / array.off(event,listener)
配列にイベントリスナーを追加または削除します。

サポートされているイベント：
- add-オブジェクトが追加されました
- remove-オブジェクトが削除されます
- change-追加と削除の両方でトリガーされます。

使用例：
```js
myArray.on("change", () => {
  // The items of myArray has changed, e.g. an Noodl.Object has been added or removed.
  // will NOT be triggered if the properties of the objects have changed
});

myArray.on("add", (args) => {
  // The object args.item have been added to this array
});

myArray.on("remove", (args) => {
  // The object args.item have been removed from this array
  // The object was removed from index args.index
});
```

### array.set(items)
配列のすべてのアイテムが置き換えられます。
引数は、items通常のJavaScriptオブジェクト、Noodl.Objectアイテム、またはそれらを組み合わせた配列にすることができます。

オブジェクトのID（オブジェクトのid属性）は、オブジェクトがこの配列の項目の一部であるかどうかを判別するために使用されます。配列にすでに含まれているオブジェクトが更新され、追加されていないオブジェクトが追加され（適切なイベントが発生し）、配列に含まれなくなったオブジェクトが削除されます。

### array.contains(item)
Noodl.Objectにitem配列が存在するかどうかを返します。

### array.add(item)
Noodl.Objectの最後にitem配列のを追加します。

### array.addAtIndex(item, index)
Noodl.Objectにitemで指定されたの配列を追加します。

array.remove(item)
Noodl.Objectからitem配列を削除します。

### array.removeAtIndex(index)
Noodl.Objectから指定されたでindexを削除します。

### array.size()
配列のサイズ、配列内のオブジェクトの数を返します。

### array.get(index)
Noodl.Object item指定されたインデックスで、またはundefined範囲外の場合はを返します。

### array.each(callback)
配列内のすべてのオブジェクトを反復処理し、引数を使用してobject、およびindex引数として各オブジェクトのコールバック関数を呼び出します。

### array.find(callback)
配列内のすべてのオブジェクトを反復処理し、コールバックが返す最初のオブジェクト`true`を返します。
一致するものが見つからない場合は`undefined`を返します。

使用例：
```js
const item = myArray.find((item) => item.label === "Noodl");
if (item) {
  //item was found (item is a Noodl.Object)
} else {
  //no item found
}
```

### array.findIndex(callback)
配列内のすべてのオブジェクトを反復処理し、コールバックが返す最初のインデックス`true`を返します。
一致するものが見つからない場合は`-1`を返します。

使用例：
```js
const index = myArray.findIndex((item) => item.label === "Noodl");
if (index === -1) {
  //no match was found
} else {
  //match was found, and the index is now valid
}
```

----------------------------------------------
## イベントAPI：イベントの送信

### Noodl.eventEmitter.emit(eventName, data)
イベントを送信します。イベントレシーバーと一緒に機能します。
```js
Noodl.eventEmitter.emit('event name', {
    value: 'hello',
    someOtherValue: 100,
})
```

### Noodl.eventEmitter.on(eventName, callback(data)) / Noodl.eventEmitter.once(eventName, callback(data))
イベントを受信します。イベント送信者と連携します
```js
Noodl.eventEmitter.on('event name', function (eventData) {
    console.log(eventData.value)
})
```
---------------------------------------------------
## 外部ライブラリの読み込み
Project Settings内のCustom CodeでHead Codeを記述することができます。
外部ライブラリの読み込みはここで行います。

:::note warn
以前あったScriptDownloaderノードは廃止になっています。
:::

```html:Head Code
<script type="text/javascript" src="vanilla-tilt.js"></script>
```
:::note warn
HeaderCodeを使って外部ライブラリを読み込むので、NoodlのScriptノードで外部ライブラリを使用するときには常にライブラリが読み込まれていると考えてOKです。
なので、以前のNoodlでは下記のようなチェックが必要でしたが、チェックは不要になりました。

機能：Javascriptライブラリが読込まれるのを待って実行する
接続：ScriptDownload：->Javascript:scriptLoaded

```Javascript:

スクリプトの読込待ち

define({
    inputs:{
        mySignal: 'signal',
        scriptLoaded: 'boolean'
    },
    mySignal:function(inputs,outputs) {
        if (!inputs.scriptLoaded) {
                return;
        }
        // ...
    }
})

```
:::

---
## [ポインタ位置の取得](https://docs.noodl.net/2.5/#/snippets/javascript/pointer-position)

![pointer-position.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192442/72d0ad7d-f8c7-dff8-ec25-6c7a0e1e64ef.png)

最後に、これまで説明した内容を利用した例を紹介します。
イベントリスナーをWebページのbody要素にアタッチし、mousemoveをリッスンします。
```js
Node.OnInit = function () {
    document.body.addEventListener('mousemove', (e) => {
        Node.Outputs.PointerX = e.clientX
        Node.Outputs.PointerY = e.clientY
    })
}
```

次に、これを拡張してmousedownイベントも含めるようにします。これにより、ポインタイベントが移動したときだけでなく、開始したときに直接更新されます。
また、JavaScriptノードが破棄されたときにイベントリスナーを削除することで、これをさらに拡張できます。これは、ユーザーがこのコードを実行しているページから移動したとき、またはこのコードを含むコンポーネントがForEachノードによって生成されたときに発生する可能性があります。
```js:ポインタ一取得の例
function setPosition(e) {
    Node.Outputs.PointerX = e.clientX
    Node.Outputs.PointerY = e.clientY
}

Node.OnInit = function () {
    document.body.addEventListener('mousemove', setPosition)
    document.body.addEventListener('mousedown', setPosition)
}

Node.OnDestroy = function () {
    document.body.removeEventListener('mousedown', setPosition)
    document.body.removeEventListener('mousemove', setPosition)
}
```

---
## [DOM要素を取得する](https://docs.noodl.net/2.5/#/snippets/javascript/get-dom-element)

![get-dom-element.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192442/c8055693-5d20-296a-3501-c385f3c6eadf.png)


場合によってはDOM要素にアクセスして、ブラウザーAPIを直接使用する必要があります。
ビジュアルノードからDOM要素に移動するには、ビジュアルノードをScriptノードに接続します。
入力タイプは`reference`である必要があります。
この例ではGroupノードを使用していますが、どのビジュアルノードでも機能します。

```js
Node.Inputs = {
    group: 'reference',
}
```

Noodlノードへの参照を取得したら、NoodlノードでDOM要素参照のため`getDOMElement()`します。
ビジュアルノードはマウントされていないか、まだレンダリングする機会がない可能性があります。DOM要素があることを確認するために、NoodlノードからのDidMount出力を使用します。これにより、最新のDOM参照を取得し、ビジュアルノードが実際にレンダリングされるまでコードが実行されないようにします。

```js:DOM要素を取得する例
Node.Inputs = {
    group: 'reference',
}

Node.Signals = {
    didMount() {
        const domElement = Node.Inputs.group.getDOMElement()
        //domElement.addEventListener(...)
    },
    willUnmount() {
        // const domElement = Node.Inputs.group.getDOMElement();
        // domElement.removeEventListener(...)
    },
}
```

## まとめ
Noodlが2.xになり、ReactベースになってからJavascriptの書き方が複数回変化してきましたが、ある程度落ち着いてきたので。
今回、Noodl2.5.1をベースとして、Scriptノード・Javascriptの記述方法について整理してみました。
これまでもNoodlは非営利利用では無料(Starterプラン)でしたが、2022年中はNoodlで作成したコンテンツを無料でホスティングできるProプランも無料で使えます。ぜひこの機会に[Noodl](https://www.noodl.net/)に触れてみてください。