node-red-dashboard icon indicating copy to clipboard operation
node-red-dashboard copied to clipboard

How exactly does a widget use ui_base?

Open AllanOricil opened this issue 1 year ago • 28 comments

I read the dashboard 2 docs and understood that widgets are node-red nodes, and that they "extend" the ui_base config node, but I could not understand how exactly this happens. Somehow the widget's node has some additional methods called register/deregister. Probably has other, but I did not read everything yet. So, when/how exactly does a widget receive those 2 methods?

I'm planning to recreate it as a base class of Node class called, in the nrg project.

export default class WidgetNode extends Node {

    // specific WidgetNode properties

    constructor(config){
        super(config);
        //initialize specific WidgetNode properties, such as state store
    }


    // specific WidgetNode methods, like register/ deregister
}

export default class MyWidget extends WidgetNode {

    constructor(config){
        super(config);
    } 
}

AllanOricil avatar Sep 13 '24 01:09 AllanOricil

The UI Base is the single parent object for a Dashboard.

In the constructor for each Widget, they get reference to the UI Base (normally via their assigned group/page). They call base.register, and pass themselves into that function call.

The Base then maps pages/groups/widgets/themes, and sends the relevant configurations when a Dashboard client connects

joepavitt avatar Sep 13 '24 16:09 joepavitt

@joepavitt what do you think of my idea of making ui_base into a parent class called WidgetNode, and then widgets would extends this class? I think it is possible. Could you help me to see problems I would have trying to do it?

AllanOricil avatar Sep 13 '24 18:09 AllanOricil

The UI Base has settings and configuration of its own. The widgets aren't necessarily extensions in a Class sense. They are different entities. There is only ever one Base. Having a Node an extension of Base would mean you suddenly have 100's of Dashboards/Based

If we consider Class hierarchy, there could be benefit in having a common WidgetNode class that the nodes, in order to share feature sets, etc.

The base.register function is the Widget's way of communicating to the Dashboard to say "Hey, I exist"

joepavitt avatar Sep 13 '24 18:09 joepavitt

My use of the word "extends" here is likely misleading.

From a Dashboard Hierarchy we have:

  • Base
    • Theme
    • Page
      • Group
        • Widget

That doesn't mean that there is Class Inheritance here though. Fundamentally, these are the Classes, then each node in a flow (or config node) in an instance of the respective class

joepavitt avatar Sep 13 '24 18:09 joepavitt

@joepavitt So every WidgetNode class would have to have an instance of ui_base. Is this ok? I'm still not sure if "register/deregister" is called "per instance" of a node, or per type of node. If it by type, then I need to make those 2 methods static

export default class WidgetNode extends Node {

    static #uiBase;

    constructor(config){
        super(config);
        //initialize specific WidgetNode properties, such as state store
    }

    // specific WidgetNode methods, like register/ deregister
    
    register(){
         WidgetNode.#uiBase.register(this);
    }
    
    deregister(){
         WidgetNode.#uiBase.deregister(this);
    }
}


AllanOricil avatar Sep 13 '24 18:09 AllanOricil

There are three types of reference, which depend on the "home" of the widget.

If it renders in a group (the most common, e.g Buttons, Charts), then the Widget has a group property. But in turn, can then use group.getBase().register() to register itself with the one true UI Base at the top level.

Some widgets render at the page-level, for example a UI Template might be assigned to a page to override CSS on that page. Similarly, there exists a page.getBase() in order for the Widget to register to the Base too.

Finally, the simplest in this case, is a Widget that renders at the "base" level, I.e. independent on whichever page a user is one that Widget will always be there. An example of this is "UI Notification", where the user chooses the "Base" in the Node config editor, and so we can just call base.register directly

joepavitt avatar Sep 13 '24 18:09 joepavitt

Also worth noting, and I may be remembering this wrong, the nodes themselves dont need a register/deregister function.

The base.register() is just called as part of the Node's constructor

Maybe they do need a deregister function? I can't quite remember how I handle that, as I'm not at a keyboard

joepavitt avatar Sep 13 '24 18:09 joepavitt

@joepavitt

Maybe they do need a deregister function? I can't quite remember how I handle that, as I'm not at a keyboard

I believe that if a node can call this.deregister(), maybe there are situations where the node would also call this.register() afterward. Maybe to do a "hard reset" somewhere? Do you know if there is any Widget that does it?

The base.register() is just called as part of the Node's constructor

I can ensure this is called inside the parent's constructor, or in the mixin. This way devs won't need to manually write it. The same way I did for RED.nodes.registerType or RED.nodes.createNode

https://github.com/AllanOricil/nrg/blob/main/templates/server/entrypoint.handlebars https://github.com/AllanOricil/node-red-node

AllanOricil avatar Sep 13 '24 20:09 AllanOricil

@joepavitt What do you think of this model?

import { Node, Base, Theme, Page, Group } from "@allanoricil/node-red-node";

export default class WidgetNode extends Node {

    scope: Base | Theme | Page | Group;  // each instance of a widget can have its own ui scope

    constructor(config){
        //initialize specific WidgetNode properties, such as state store
    }

    // specific WidgetNode methods, like register/ deregister
    
    register(){
         this.scope.register(this);
    }
    
    deregister(){
         this.scope.deregister(this);
    }
}

import { WidgetNode } from "@allanoricil/node-red-node";

export default class ChildWidget extends WidgetNode {

    constructor(config){
        super(config);
        this.scope = TO_BE_DEFINED; // dev configures which scope this node belongs to. The mixin will use it to call `this.register()` for his node
    }

}

AllanOricil avatar Sep 13 '24 20:09 AllanOricil

That could work, bare in mind though some widgets are permitted to be multiple, e.g. ui-template can be switched between group, page and UI

joepavitt avatar Sep 14 '24 10:09 joepavitt

That could work, bare in mind though some widgets are permitted to be multiple, e.g. ui-template can be switched between group, page and UI

How does a Widget switch between "ui scopes" (have to find a better name)?

AllanOricil avatar Sep 14 '24 11:09 AllanOricil

It is defined in template node's configuration

image

colinl avatar Sep 14 '24 20:09 colinl

It is defined in template node's configuration

image

@colinl every widget has those fields shown in that image? How can I get to that configuration form?

AllanOricil avatar Sep 15 '24 00:09 AllanOricil

I started to have a better understanding after reading the config nodes

image

I think I can start modeling it into classes. I will try to do the base ones, and a button.

AllanOricil avatar Sep 15 '24 01:09 AllanOricil

One last question.

@colinl @joepavitt

What is the schema for the "evts" object widgets pass when calling group.register(node, config, evts)? Also, why passing config to register if you are already passing node? I believe you can dereference config from the node, right?

const evts = {
            beforeSend: async function (msg)
}

group.register(node, config, evts)

After modeling into classes, this is what I'm planning for the event handlers. Therefore, I must know its schema.

import { Node } from "@allanoricil/node-red-node";
export default Widget extends Node {
    
    group;
     
     constructor(config){
          super(config);
          
          this.group = config.group;
      }
   
     /*
     * It can be implemented by a child class
     * @abstract
     */
    onBeforeSend(msg){}
     
     // all other possible event handlers a widget can have
     ...
}
import { Widget } from "@allanoricil/node-red-node";
export default MyWidget extends Widget {

      constructor(config){
          super(config);
          
          // set things that are specific for a given "instance", like initializing its own properties
          this.myCoolAttribute = config.myCoolAttribute;
      }
      
      // since this is a widget, this event handler "could" be implemented
     onBeforeSend(msg) {
            ...
      }
      
      // all other event handlers
     ...
}

Inside the mixin, I will do some sorcery js to make sure these "on" events are registered when calling group.register(node, config, evts).

AllanOricil avatar Sep 15 '24 01:09 AllanOricil

@joepavitt @colinl could you help with the last comment?

AllanOricil avatar Sep 17 '24 18:09 AllanOricil

Not me, I don't know the answer.

colinl avatar Sep 17 '24 20:09 colinl

@AllanOricil https://dashboard.flowfuse.com/contributing/guides/registration.html#evts

joepavitt avatar Sep 18 '24 15:09 joepavitt

every widget has those fields shown in that image? How can I get to that configuration form?

Drop a Dashboard "template" node into the Editor, then double click it to configure it. Only the "Template" node as the option to switch between "parent scope" like this.

joepavitt avatar Sep 18 '24 15:09 joepavitt

@AllanOricil https://dashboard.flowfuse.com/contributing/guides/registration.html#evts

@joepavitt Is the onInput from evts the same as the node's on input this.on("input", onInputHandler())?

If not, is there a widget that use both? If there is, than I would need to change the evts event handler name to avoid conflicts

import { Widget } from "@allanoricil/node-red-node";

export default MyWidget extends Widget { 

    constructor(config){ 
        super(config); 
    }
        
    // same as node-red this.on("input", ())
    onInput(msg){...}

    // dashboard ui input event
    onUIInput(msg){...}

    // dashboard ui before send
    onBeforeSend(msg){...}
}

AllanOricil avatar Sep 18 '24 15:09 AllanOricil

Not quite, the one you've quoted there is the client-side handler (Vue Widget), the onInput I linked to is the server-side handler (the Node-RED node)

joepavitt avatar Sep 18 '24 15:09 joepavitt

Not quite, the one you've quoted there is the client-side handler (Vue Widget), the onInput I linked to is the server-side handler (the Node-RED node)

So all evts handlers are sent to the client? I thought they executed on the server, because they are declared in the server js of a node.

AllanOricil avatar Sep 18 '24 15:09 AllanOricil

No, they're separate. What a node does on input server-side (defined in the .js file) will differ the the client-side behaviour of a widget when receiving a message (defined in the vue file)

joepavitt avatar Sep 18 '24 15:09 joepavitt

@joepavitt

Sorry, but I got confused a bit. Let me try to make a better question.

The doc says the evts handlers are server- side. Is the evts.onInput the same as this.on("input") inside the node's server-side js? Is there a node that make use of both? I need to see how they are used.

AllanOricil avatar Sep 18 '24 15:09 AllanOricil

Sorry, I thought this.on("input", onInputHandler()) had been taken from a vue file as it's very similar to our code there too.

Yes, if you checkout the node.register function in ui-base.js, you'll see that the on('input') handler is setup for the relevant node and the evts.onInput is passed there (if provided)

joepavitt avatar Sep 18 '24 15:09 joepavitt

Sorry, I thought this.on("input", onInputHandler()) had been taken from a vue file as it's very similar to our code there too.

Yes, if you checkout the node.register function in ui-base.js, you'll see that the on('input') handler is setup for the relevant node and the evts.onInput is passed there (if provided)

I would need a new name for that handler to put it on the class alongside the node's this.on("input")

I can't have 2 onInput methods in the class. What about onData or onDashboardInput or onUIInput for the evts.onInput? If the second one is chosen, then all other methods would also be prepended with either onDashboard or onUI

AllanOricil avatar Sep 18 '24 15:09 AllanOricil

not sure why the class needs that definition though? It needs evts, which is then an object mapped to x functions/objets/booleans

joepavitt avatar Sep 18 '24 15:09 joepavitt

not sure why the class needs that definition though? It needs evts, which is then an object mapped to x functions/objets/booleans

When one of those handlers is triggered, can't they access "instance" data? I can have multiple widget nodes, all with the same handler, but each with its own instance scope. Those events are part of the Widget, so I'm trying to encapsulate them normalized as individual class functions, instead of using an evts attribute. It looks cleaner in my opinion.

AllanOricil avatar Sep 18 '24 15:09 AllanOricil