I think I have finaly reached a final design for the framework. You can check it out in the following link
Write Node-RED nodes using Vue and Typescript
The following is an example of an IONode. IONodes are the ones we can drag and drop in the canvas.
src/nodes/your-node/server/index.ts
import { Static } from "@sinclair/typebox";
import {
CloseDoneFunction,
InputDoneFunction,
IONode,
IONodeValidations,
SendFunction,
} from "../../../core/server/nodes";
import RemoteServerConfigNode from "../../remote-server/server";
import {
ConfigsSchema,
CredentialsSchema,
InputMessageSchema,
OutputMessageSchema,
} from "../schemas";
export type YourNodeConfigs = Static<typeof ConfigsSchema>;
export type YourNodeCredentials = Static<typeof CredentialsSchema>;
export type YourNodeInputMessage = Static<typeof InputMessageSchema>;
export type YourNodeOutputMessage = Static<typeof OutputMessageSchema>;
export default class YourNode extends IONode<
YourNodeConfigs,
YourNodeCredentials,
YourNodeInputMessage,
YourNodeOutputMessage
> {
static override validations: IONodeValidations = {
configs: ConfigsSchema,
credentials: CredentialsSchema,
input: InputMessageSchema,
outputs: OutputMessageSchema,
};
static override async init() {
console.log("testing your node init");
}
async onInput(
msg: {
payload?: string | undefined;
topic?: string | undefined;
_msgid?: string | undefined;
} & { myVariable?: string | undefined },
send: SendFunction<
{
payload?: string | undefined;
topic?: string | undefined;
_msgid?: string | undefined;
} & { originalType: "string" | "number"; processedTime: number }
>,
done: InputDoneFunction
): Promise<void> {
console.log(this);
console.log(msg);
const server = IONode.getNode<RemoteServerConfigNode>(
this.configs.remoteServer
);
console.log(server?.users);
}
async onClose(removed: boolean, done: CloseDoneFunction): Promise<void> {
console.log("removing node");
done();
}
}
src/nodes/your-node/client/index.ts
import { defineNode } from "../../../core/client";
import component from "./Form.vue";
export default defineNode({
category: "function",
color: "#FFFFFF",
inputs: 1,
outputs: 1,
icon: "vue.png",
form: {
component,
disableSaveButtonOnError: true,
}
});
src/nodes/your-node/client/Form.vue
<template>
<div>
<div class="form-row">
<label><i class="fa fa-tag"></i> Username</label>
<NodeRedInput
v-model:value="node.credentials.username"
:error="errors['node.credentials.username']"
/>
</div>
<div class="form-row">
<label><i class="fa fa-tag"></i> Password</label>
<NodeRedInput
v-model:value="node.credentials.password"
type="password"
:error="errors['node.credentials.password']"
/>
</div>
<div class="form-row">
<label><i class="fa fa-tag"></i> Password 2</label>
<NodeRedInput
v-model:value="node.credentials.password2"
type="password"
:error="errors['node.credentials.password2']"
/>
</div>
<div class="form-row">
<label>Typed Input</label>
<NodeRedTypedInput
v-model:value="node.myProperty"
:types="types"
:error="errors['node.myProperty']"
/>
</div>
<div class="form-row">
<label>Typed Input 2</label>
<NodeRedTypedInput
v-model:value="node.myProperty2"
:error="errors['node.myProperty2']"
/>
</div>
<div class="form-row">
<label>Config Input</label>
<NodeRedConfigInput
v-model:value="node.remoteServer"
type="remote-server"
:error="errors['node.remoteServer']"
/>
</div>
<div class="form-row">
<label>Config Input</label>
<NodeRedConfigInput
v-model:value="node.anotherRemoteServer"
type="remote-server"
:error="errors['node.anotherRemoteServer']"
/>
</div>
<div class="form-row">
<label>Select Input</label>
<NodeRedSelectInput
v-model:value="node.country"
:options="countries"
:error="errors['node.country']"
/>
</div>
<div class="form-row">
<label>MultiSelect Input</label>
<NodeRedSelectInput
v-model:value="node.fruit"
:options="fruits"
multiple
:error="errors['node.fruit']"
/>
</div>
<div class="form-row">
<label>Select Input</label>
<NodeRedSelectInput
v-model:value="node.number"
:options="numbers"
:error="errors['node.number']"
/>
</div>
<div class="form-row">
<label>Select Input</label>
<NodeRedSelectInput
v-model:value="node.object"
:options="objects"
multiple
:error="errors['node.object']"
/>
</div>
<div class="form-row">
<label>Select Input</label>
<NodeRedSelectInput
v-model:value="node.array"
:options="arrays"
:error="errors['node.array']"
/>
</div>
<div class="form-row">
<label>Editor with default height 200px and JSON</label>
<NodeRedEditorInput
v-model:value="node.jsontest"
:error="errors['node.jsontest']"
/>
</div>
<div class="form-row">
<label>Editor with custom height and CSS</label>
<NodeRedEditorInput
v-model:value="node.csstest"
language="css"
style="height: 100px"
:error="errors['node.csstest']"
/>
</div>
<!-- NOTE: this is loaded from the resources/{pkg.name} folder -->
<img src="/vue.png" />
<!-- NOTE: this is added to the bundled .js -->
<img :src="logo" alt="Logo" />
</div>
</template>
<script>
import logo from "../../../assets/vue.png";
export default {
name: "NodeRedNodeForm",
props: {
node: {
type: Object,
required: true,
},
errors: {
type: Object,
default: () => ({}),
},
},
data() {
return {
logo,
types: ["str", "msg", "node"],
countries: [
{ value: "usa", label: "usa" },
{ value: "argentina", label: "argentina" },
{ value: "brasil", label: "brasil" },
],
fruits: [
{ value: "apple", label: "apple" },
{ value: "melon", label: "melon" },
{ value: "raspberry", label: "raspberry" },
],
numbers: [
{ value: "1", label: "1" },
{ value: "2", label: "2" },
{ value: "3", label: "3" },
],
objects: [
{ value: JSON.stringify({ test: "a" }), label: "a" },
{ value: JSON.stringify({ test: "b" }), label: "b" },
{ value: JSON.stringify({ test: "c" }), label: "c" },
],
arrays: [
{ value: JSON.stringify(["a"]), label: "a" },
{ value: JSON.stringify(["b"]), label: "b" },
{ value: JSON.stringify(["c"]), label: "c" },
],
};
},
mounted() {
console.log("MOUNTED");
console.log(this.logo);
},
};
</script>
src/nodes/your-node/schemas.ts
import { Type, Static } from "@sinclair/typebox";
import {
IONodeConfigsSchema,
TypedInputSchema,
MessageSchema,
} from "../../core/schemas";
const ConfigsSchema = Type.Object(
{
...IONodeConfigsSchema.properties,
name: Type.String({ default: "your-node" }),
myProperty: TypedInputSchema,
myProperty2: TypedInputSchema,
remoteServer: Type.String({ nodeType: "remote-server" }),
anotherRemoteServer: Type.Optional(
Type.String({ nodeType: "remote-server" })
),
country: Type.String({ default: "brazil" }),
fruit: Type.Array(Type.String(), { default: ["apple", "melon"] }),
number: Type.String({ default: "1" }),
object: Type.Array(Type.String(), {
default: [JSON.stringify({ test: "a" }), JSON.stringify({ test: "b" })],
}),
array: Type.String({
default: '["a"]',
}),
jsontest: Type.String({ default: "" }),
csstest: Type.String({ default: "" }),
},
{
$id: "YourNodeConfigsSchema",
}
);
const CredentialsSchema = Type.Object(
{
password: Type.Optional(
Type.String({
default: "",
minLength: 8,
maxLength: 20,
pattern: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]+$/.source,
format: "password",
})
),
password2: Type.Optional(
Type.String({
default: "",
pattern:
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/
.source,
format: "password",
})
),
username: Type.Optional(
Type.String({ default: "", maxLength: 10, minLength: 5 })
),
},
{
$id: "YourNodeCredentialsSchema",
}
);
const InputMessageSchema = Type.Intersect(
[
MessageSchema,
Type.Object({
myVariable: Type.Optional(Type.String()),
}),
],
{
$id: "YourNodeInputMessageSchema",
}
);
const OutputMessageSchema = Type.Intersect(
[
MessageSchema,
Type.Object({
originalType: Type.Union([
Type.Literal("string"),
Type.Literal("number"),
]),
processedTime: Type.Number(),
}),
Type.Unknown(),
],
{ $id: "YourNodeOutputMessageSchema" }
);
export {
ConfigsSchema,
CredentialsSchema,
InputMessageSchema,
OutputMessageSchema,
};
The only part that is a little bit complicated is defining JSON Schemas using TypeBox. However, if you don't like it, you can define the JSON Schema by hand, using an object.