Dynamically create widgets

Hello!

is there a way to dynamically create widgets from the custom module?

thanks

Actually it could be enough for me just to be able to edit dynamically the HTML property of a widget from custom module, is it possible?

Of course answer was the listener in the property html linked to a variable in the custom module that changes…

There’s also the possibility to perform edit actions directly using remote control commands.

Thanks, I need to look at this solution too :slight_smile:

One question,
I was able to create dynamically objects using the HTML property, but this is limited to certain type of objects
If I want instead to initiate your widgets, what should be the right way of doing it?
thanks

When I need to do that I create an empty container widget in the session and feed its widgets property from the custom module:

receive('/EDIT', 'container_id', {
    widgets: [
        {
            type: 'fader',
            colorWidget: 'red',
            range: {min:0, max: 100},
            // omitted properties will use defaults
        }, 
        // etc
    ]
})

Here a more advanced example where I build generic plugin interfaces from the informations sent by the DAW : https://github.com/jean-emmanuel/ardour-control/blob/master/ardour-plugins-module.js

1 Like

Awesome thanks :slight_smile:

Thanks,
That works perfectly for me

I want to add widgets once when the application is launched, where is the right place to put this code?
I tried in the init() but it seems is invoked even before the panel is created so nothing happens.
The unload is only when the custom module is reloaded

Is there any method I can use?
Is there a list of methods that can be overridden in the custom module?

Thanks

You can use the app object to catch the relevant events. In this case:

app.on('sessionOpened', (data, client)=>{
  receive('/EDIT', id, widget_data, {clientId: client.id})
})
2 Likes

Hello!

In my case, there is a need to draw a control interface for the supercollider project.
In general terms, this is a simple audio mixer with a focus on spatial audio.
I tried to take the simplest route and created a complete widget with all the children in code.
But quickly enough it began to not fit into one OSC message.

Then I divided content of the interface into many short OSC commands, each of which either prepares the required nesting depth of the widget or adds its content.

// noprotect
var selfAddress = '127.0.0.1:8080';

var mixesAmount = get('varMixesAmount');
var mixesOnPage = get('inpSettingsUIMixesOnPage');
var pagesInGroup = get('inpSettingsUIMixesPagesInGroup');

var mixesPagesAmount = Math.ceil(mixesAmount / mixesOnPage);
var mixesGroupsAmount = Math.ceil(mixesPagesAmount / pagesInGroup);
var primaryGroupsCount = mixesGroupsAmount;
var groupsLevels = 1;

if (mixesPagesAmount == 1) {//One page case
  //
  send(selfAddress, '/EDIT/MERGE', 'pnlMixes', {
    tabs: null,
    widgets: [{
      type: 'panel',
      id: 'pnlMixesPage1',
      //Geometry
      expand: true,
      //Panel Style
      layout: 'horizontal',
      scroll: false,
      innerPadding: false
    }]
  });
  
} else {//Several pages case
  //
  var pnlMixesTabs = [];
  
  if(mixesGroupsAmount == 1) {//One group case
    //
    for (let i = 1; i <= mixesPagesAmount; i++) {
      //
      pnlMixesTabs.push({
        type: 'tab',
        id: 'tabMixesPage' + i
      });
    }
    
    //Create Tabs
    send(selfAddress, '/EDIT/MERGE', 'pnlMixes', {
      tabsPosition: 'left',
      widgets: null,
      tabs: pnlMixesTabs
    });
    
    //Configure Tabs
    for (let i = 1; i <= mixesPagesAmount; i++) {
      //
      let tabNumFirst = (i - 1)*mixesOnPage + 1;
      let tabNumLast =  Math.min(i*mixesOnPage, mixesAmount);
      
      send(selfAddress, '/EDIT/MERGE', 'tabMixesPage' + i, {
        
        //Panel Style
        layout: 'vertical',
        scroll: false,
        innerPadding: false,
        //Tab Style
        label: tabNumFirst + '-' + tabNumLast,
        
        widgets: [{
          type: 'panel',
          id:'pnlMixesPage' + i,
          //Geometry
          expand: true,
          //Panel Style
          layout: 'horizontal',
          scroll: false,
          innerPadding: false
        }]
      });
    }
    
  } else {//Several Groups case
    //
    while (primaryGroupsCount > pagesInGroup) {
      //
      primaryGroupsCount /= pagesInGroup;
      groupsLevels++;
    }
    primaryGroupsCount = Math.ceil(primaryGroupsCount);
    
    createGroupTabs(groupsLevels, 1, mixesAmount, 'pnlMixes', 'tabMixesGroup');
  }
}

function createGroupTabs(groupsLevel, numFirst, numLast, parentTabID, tabNameDef) {
  
  let currentGroupTabs = [];
  let tabsAmount = Math.ceil((numLast - numFirst + 1)/mixesOnPage/Math.pow(pagesInGroup, groupsLevel));

  for ( let tabNum = 1; tabNum <= tabsAmount; tabNum++ ) {
    //
    currentGroupTabs.push({
      type: 'tab',
      id: tabNameDef + tabNum
    });
  }
  
  //Create Tabs
  send(selfAddress, '/EDIT/MERGE', parentTabID, {
    tabsPosition: 'left',
    widgets: null,
    tabs: currentGroupTabs
  });
  
  currentGroupTabs = null;
  
  //Configure Tabs
  for (let tabNum = 1; tabNum <= tabsAmount; tabNum++) {
    //
    let tabNumFirst = (tabNum - 1)*Math.pow(pagesInGroup, groupsLevel)*mixesOnPage + numFirst;
    let tabNumLast = Math.min(tabNum*Math.pow(pagesInGroup, groupsLevel)*mixesOnPage + numFirst - 1, numLast);
    //console.log('first=' + tabNumFirst + ' last=' + tabNumLast);
      
    send(selfAddress, '/EDIT/MERGE', tabNameDef + tabNum, {
      //Panel Style
      layout: 'vertical',
      scroll: false,
      innerPadding: false,
      //Tab Style
      tabsPosition: 'left',
      label: tabNumFirst + '-' + tabNumLast,
      
      widgets: null,
      tabs: null
    });
    
    tabNumFirst = null;
    tabNumLast = null;
  }
  
  //Recursively Create Tabs
  if (groupsLevel > 0) {
  
    for (let tabNum = 1; tabNum <= tabsAmount; tabNum++) {
      //
      let tabNumFirst = (tabNum - 1)*Math.pow(pagesInGroup, groupsLevel)*mixesOnPage + numFirst;
      let tabNumLast = Math.min(tabNum*Math.pow(pagesInGroup, groupsLevel)*mixesOnPage + numFirst - 1, numLast);

      createGroupTabs(groupsLevel - 1, tabNumFirst, tabNumLast, tabNameDef + tabNum, tabNameDef + tabNum + '-');
      //setTimeout(function(){createGroupTabs(groupsLevel - 1, tabNumFirst, tabNumLast, tabNameDef + tabNum, tabNameDef + tabNum + '-')}, tabNum*500);
      tabNumFirst = null;
      tabNumLast = null;
    }
  } else {
    
    //Create Pages
    for (let tabNum = 1; tabNum <= tabsAmount; tabNum++) {
      //
      let tabNumFirst = (tabNum - 1)*Math.pow(pagesInGroup, groupsLevel)*mixesOnPage + numFirst;
      let tabNumLast = Math.min(tabNum*Math.pow(pagesInGroup, groupsLevel)*mixesOnPage + numFirst -1, numLast);
      let pageNum = (tabNumFirst - 1)/mixesOnPage + 1;
      
      send(selfAddress, '/EDIT/MERGE', tabNameDef + tabNum, {
        
        //Panel Style
        layout: 'vertical',
        scroll: false,
        innerPadding: false,
        //Tab Style
        //label: tabNumFirst + '-' + tabNumLast,
        
        widgets: [{
          type: 'panel',
          id:'pnlMixesPage' + pageNum,
          //Geometry
          expand: true,
          //Panel Style
          scroll: false,
          innerPadding: false
        }]
      });
      
      tabNumFirst = null;
      tabNumLast = null;
      pageNum = null;
    }
  }
}

//Create Mixes
for (let pageNum = 1; pageNum <= mixesPagesAmount; pageNum++) {
  
  //Create Tabs in Mixes Page
  let pageTabs = [];
  
  pageTabs.push({
    type: 'tab',
    id: 'tabMixesPage' + pageNum + 'All'
  });
  
  for (let mixNum = (pageNum - 1)*mixesOnPage + 1; mixNum <= Math.min(pageNum*mixesOnPage, mixesAmount); mixNum++) {
    //
    pageTabs.push({
      type: 'tab',
      id: 'tabMix' + mixNum
    });
  }
  
  send(selfAddress, '/EDIT/MERGE', 'pnlMixesPage' + pageNum, {
    //
    tabs: pageTabs
  });
  pageTabs = null;
  
  send(selfAddress, '/EDIT/MERGE', 'tabMixesPage' + pageNum + 'All', {
    
    //Panel Style
    layout: 'vertical',
    scroll: false,
    innerPadding: false,
    //Tab Style
    label: 'ALL',
    
    widgets: [{
      type: 'panel',
      id: 'pnlMixesPage' + pageNum + 'All',
      //Geometry
      expand: true,
      //Style
      //padding: 8,
      //Panel Style
      layout: 'horizontal',
      //justify: 'space-around',
      scroll: false,
      //innerPadding: false
      //Panel
      traversing: 'smart'
    }]
  });
  
  //Create 'ALL' Mixes Page View
  let allViewWidgets = [];
  
  for (let mixNum = (pageNum - 1)*mixesOnPage + 1; mixNum <= Math.min(pageNum*mixesOnPage, mixesAmount); mixNum++) {
    //
    allViewWidgets.push({
      type: 'panel',
      id:'pnlAllMix' + mixNum
    });
  }
  
  send(selfAddress, '/EDIT/MERGE', 'pnlMixesPage' + pageNum + 'All', {
    
    widgets: allViewWidgets
  });
  allViewWidgets = null;
  
  //Individual Mix Panels on 'ALL' View
  for (let mixNum = (pageNum - 1)*mixesOnPage + 1; mixNum <= Math.min(pageNum*mixesOnPage, mixesAmount); mixNum++) {
    //
    let mixPanelWidgets = [];
    
    //'Select' Button
    mixPanelWidgets.push({
      //
      type: 'button',
      id: 'btnMix' + mixNum + 'Select',
      //Geometry
      width: 60,
      height: 50,
      //Style
      colorText: '#ffffff',
      //css: "font-size:125%;",
      //Button Style
      label: 'SEL',
      //Scripting
      onValue: ''
    });
    
    //Volume Panel
    mixPanelWidgets.push({
      //Widget
      type: 'panel',
      id: 'pnlMix' + mixNum + 'Volume',
      //Geometry
      height: 340,
      //Panel Style
      scroll: false,
      innerPadding: false,
      
      widgets: [{
        //Widget
        type: 'text',
        id: 'lblMix' + mixNum + 'Volume',
        //Geometry
        left: 0,
        top: 0,
        width: 60,
        height: 40,
        //Style
        colorText: '#ffffff',
        //Value
        default: 'MIX\n' + mixNum
      },{
        //Widget
        type: 'fader',
        id: 'fdrMix' + mixNum + 'Volume',
        //Geometry
        left: 0,
        top: 40,
        width: 60,
        height: 300,
        //Style
        colorText: '#ffffff',
        //Fader Style
        knobSize: 20,
        pips: true,
        dashed: true,
        //Fader
        doubleTap: true,
        range: "{\"max\": { \"+10\": 0.949481 },\"91%\": { \"+5\": 0.828787 },\"82%\": { \"0.0\": 0.716 },\"73%\": { \"-5\": 0.612454 },\"64%\": { \"-10\": 0.518539 },\"55%\": { \"-15\": 0.435375 },\"46%\": { \"-20\": 0.363271 },\"37%\": { \"-30\": 0.25009 },\"28%\": { \"-40\": 0.170984 },\"19%\": { \"-50\": 0.116622 },\"10%\": { \"-60\": 0.079482 },\"min\": { \"-inf\": 0 }}",
        sensitivity: "@{varInput1SendsfdrSensitivity}",
        //Value
        default: 0.25,
        //OSC
        decimals: 6
      },{
        //Widget
        type: 'fader',
        id: 'fdrMix' + mixNum + 'VU',
        interaction: false,
        //Geometry
        left: 25,
        top: 52,
        width: 15,
        height: 278,
        //Style
        colorText: '#ffffff',
        colorFill: '#97ff6a',
        //Fader Style
        design: 'compact',
        //Fader
        range: "{\"max\": { \"+10\": 0.949481 },\"91%\": { \"+5\": 0.828787 },\"82%\": { \"0.0\": 0.716 },\"73%\": { \"-5\": 0.612454 },\"64%\": { \"-10\": 0.518539 },\"55%\": { \"-15\": 0.435375 },\"46%\": { \"-20\": 0.363271 },\"37%\": { \"-30\": 0.25009 },\"28%\": { \"-40\": 0.170984 },\"19%\": { \"-50\": 0.116622 },\"10%\": { \"-60\": 0.079482 },\"min\": { \"-inf\": 0 }}",
        //OSC
        decimals: 14
      }]
    });
    
    //'Mute' Button
    mixPanelWidgets.push({
      //
      type: 'button',
      id: 'btnMix' + mixNum + 'Mute',
      //Geometry
      width: 60,
      height: 50,
      //Style
      colorText: '#ffffff',
      //css: "font-size:125%;",
      //Button Style
      label: 'MUTE',
      //Scripting
      onValue: ''
    });
    
    //'0 dB' Button
    mixPanelWidgets.push({
      //
      type: 'button',
      id: 'btnMix' + mixNum + '0dB',
      //Geometry
      width: 60,
      height: 40,
      //Style
      colorText: '#ffffff',
      //css: "font-size:125%;",
      //Button Style
      label: 'set\n0 dB',
      //Button
      mode: 'momentary',
      //Scripting
      onValue: "set('fdrMix" + mixNum + "Volume', 0.716);"
    });
    
    //'-inf' Button
    mixPanelWidgets.push({
      //
      type: 'button',
      id: 'btnMix' + mixNum + '-Inf',
      //Geometry
      width: 60,
      height: 40,
      //Style
      colorText: '#ffffff',
      //css: "font-size:125%;",
      //Button Style
      label: 'set\n-inf',
      //Button
      mode: 'momentary',
      //Scripting
      onValue: "set('fdrMix" + mixNum + "Volume', 0);"
    });
    
    send(selfAddress, '/EDIT/MERGE', 'pnlAllMix' + mixNum, {
      
      //Geometry
      width: 60,
      //Style
      alphaStroke: 0,
      //Panel Style
      layout: 'vertical',
      justify: 'space-around',
      scroll: false,
      innerPadding: false,
      
      widgets: mixPanelWidgets
    });
  }
  
  //Configure Individual Mix Tabs
  for (let mixNum = (pageNum - 1)*mixesOnPage + 1; mixNum <= Math.min(pageNum*mixesOnPage, mixesAmount); mixNum++) {
    //
    send(selfAddress, '/EDIT/MERGE', 'tabMix' + mixNum, {
      
      //Panel Style
      layout: 'vertical',
      scroll: false,
      innerPadding: false,
      //Tab Style
      label: mixNum,
      
      widgets: [{
          type: 'panel',
          id:'pnlMix' + mixNum,
          //Geometry
          expand: true,
          //Panel Style
          layout: 'horizontal',
          scroll: false,
          innerPadding: false
        }]
    });
    
    //Create Mix Options Tabs
    let mixPage = [];
    
    //Mix Main Tab
    mixPage.push({
      type: 'tab',
      id: 'tabMix' + mixNum + 'Main',
      //Panel Style
      layout: 'vertical',
      scroll: false,
      innerPadding: false,
      //Tab Style
      label: 'MAIN'
    });
    
    //Mix EQ Tab
    mixPage.push({
      type: 'tab',
      id: 'tabMix' + mixNum + 'EQ',
      //Panel Style
      layout: 'vertical',
      scroll: false,
      innerPadding: false,
      //Tab Style
      label: 'EQ'
    });
    
    //Mix Gate Tab
    mixPage.push({
      type: 'tab',
      id: 'tabMix' + mixNum + 'Gate',
      //Panel Style
      layout: 'vertical',
      scroll: false,
      innerPadding: false,
      //Tab Style
      label: 'GATE'
    });
    
    //Mix Compressor Tab
    mixPage.push({
      type: 'tab',
      id: 'tabMix' + mixNum + 'Comp',
      //Panel Style
      layout: 'vertical',
      scroll: false,
      innerPadding: false,
      //Tab Style
      label: 'COMPRESSOR'
    });
    
    //Mix Limiter Tab
    mixPage.push({
      type: 'tab',
      id: 'tabMix' + mixNum + 'Limit',
      //Panel Style
      layout: 'vertical',
      scroll: false,
      innerPadding: false,
      //Tab Style
      label: 'LIMITER'
    });
    
    //Mix Sends Tab
    mixPage.push({
      type: 'tab',
      id: 'tabMix' + mixNum + 'Sends',
      //Panel Style
      layout: 'vertical',
      scroll: false,
      innerPadding: false,
      //Tab Style
      label: 'SENDS'
    });
    
    send(selfAddress, '/EDIT/MERGE', 'pnlMix' + mixNum, {
      //
      tabs: mixPage
    });
    mixPage = null;
    //
  }
  
}

But another difficulty arose: all code operations in the widget proceed quickly, but working with large amount of channels (200+) to be generated OSC commands can be transmitted depending on the computer and network environment slowly up to +- once per second, despite the fact that the commands are sent through the localhost.
On Linux until the last OSC command is transmitted, the allocated RAM continues to grow (in my case, up to 30GB) until the page crashes into an error.
On Windows some packets seem to be missed and do not reach the target, thereby leaving holes in the interface.

Now I’m looking towards creating a separate widget that will deal with orbiting the sending of OSC messages accumulated on a stack. Perhaps, so that it receives, along with the next sent message, a command to itself to send the next one. Don't know.

Session File:

Widget with the code shown above:
pnlUI -> pnlUIMain -> tabMixes -> scriptGenerateMixesPage

I'm missing something.
Could it just be that I'm using "send" function instead of "receive" one?

I feel as if I need to move the sofa from one room to another and to do this I drag it through the window to the street and back.

Indeed, you should always use receive() instead of send() to transmit commands from the custom module to the clients.

Thanks for your reply!

This path didn't work either.

A short command is sent from the widget to the module.

var selfAddress = '127.0.0.1:8080';

var mixesAmount = get('varMixesAmount');
var mixesOnPage = get('inpSettingsUIMixesOnPage');
var pagesInGroup = get('inpSettingsUIMixesPagesInGroup');


  send(selfAddress, '/drawMix', mixesAmount, mixesOnPage, pagesInGroup);

The module calculates everything necessary and builds the interface.

function drawMixes( mixesAmount, mixesOnPage, pagesInGroup ) {

  console.log('drawMixes function start');

  var mixesPagesAmount = Math.ceil(mixesAmount / mixesOnPage);
  var mixesGroupsAmount = Math.ceil(mixesPagesAmount / pagesInGroup);
  var primaryGroupsCount = mixesGroupsAmount;
  var groupsLevels = 1;

  if (mixesPagesAmount == 1) {//One page case
    //
    receive('/EDIT/MERGE', 'pnlMixes', {
      tabs: null,
      widgets: [{
        type: 'panel',
        id: 'pnlMixesPage1',
        //Geometry
        expand: true,
        //Panel Style
        layout: 'horizontal',
        scroll: false,
        innerPadding: false
      }]
    });

  } else {//Several pages case
    //
    var pnlMixesTabs = [];

    if(mixesGroupsAmount == 1) {//One group case
      //
      for (let i = 1; i <= mixesPagesAmount; i++) {
        //
        pnlMixesTabs.push({
          type: 'tab',
          id: 'tabMixesPage' + i
        });
      }

      //Create Tabs
      receive('/EDIT/MERGE', 'pnlMixes', {
        tabsPosition: 'left',
        widgets: null,
        tabs: pnlMixesTabs
      });

      //Configure Tabs
      for (let i = 1; i <= mixesPagesAmount; i++) {
        //
        let tabNumFirst = (i - 1)*mixesOnPage + 1;
        let tabNumLast =  Math.min(i*mixesOnPage, mixesAmount);
        receive('/EDIT/MERGE', 'tabMixesPage' + i, {

          //Panel Style
          layout: 'vertical',
          scroll: false,
          innerPadding: false,
          //Tab Style
          label: tabNumFirst + '-' + tabNumLast,

          widgets: [{
            type: 'panel',
            id:'pnlMixesPage' + i,
            //Geometry
            expand: true,
            //Panel Style
            layout: 'horizontal',
            scroll: false,
            innerPadding: false
          }]
        });
      }

    } else {//Several Groups case
      //
      while (primaryGroupsCount > pagesInGroup) {
        //
        primaryGroupsCount /= pagesInGroup;
        groupsLevels++;
      }
      primaryGroupsCount = Math.ceil(primaryGroupsCount);

      createGroupTabs(groupsLevels, 1, mixesAmount, 'pnlMixes', 'tabMixesGroup');
    }
  }

  function createGroupTabs(groupsLevel, numFirst, numLast, parentTabID, tabNameDef) {

    let currentGroupTabs = [];
    let tabsAmount = Math.ceil((numLast - numFirst + 1)/mixesOnPage/Math.pow(pagesInGroup, groupsLevel));

    for ( let tabNum = 1; tabNum <= tabsAmount; tabNum++ ) {
      //
      currentGroupTabs.push({
        type: 'tab',
        id: tabNameDef + tabNum
      });
    }

    //Create Tabs
    receive('/EDIT/MERGE', parentTabID, {
      tabsPosition: 'left',
      widgets: null,
      tabs: currentGroupTabs
    });

    currentGroupTabs = null;

    //Configure Tabs
    for (let tabNum = 1; tabNum <= tabsAmount; tabNum++) {
      //
      let tabNumFirst = (tabNum - 1)*Math.pow(pagesInGroup, groupsLevel)*mixesOnPage + numFirst;
      let tabNumLast = Math.min(tabNum*Math.pow(pagesInGroup, groupsLevel)*mixesOnPage + numFirst - 1, numLast);
      //console.log('first=' + tabNumFirst + ' last=' + tabNumLast);

      receive('/EDIT/MERGE', tabNameDef + tabNum, {
        //Panel Style
        layout: 'vertical',
        scroll: false,
        innerPadding: false,
        //Tab Style
        tabsPosition: 'left',
        label: tabNumFirst + '-' + tabNumLast,

        widgets: null,
        tabs: null
      });

      tabNumFirst = null;
      tabNumLast = null;
    }

    //Recursively Create Tabs
    if (groupsLevel > 0) {

      for (let tabNum = 1; tabNum <= tabsAmount; tabNum++) {
        //
        let tabNumFirst = (tabNum - 1)*Math.pow(pagesInGroup, groupsLevel)*mixesOnPage + numFirst;
        let tabNumLast = Math.min(tabNum*Math.pow(pagesInGroup, groupsLevel)*mixesOnPage + numFirst - 1, numLast);

        createGroupTabs(groupsLevel - 1, tabNumFirst, tabNumLast, tabNameDef + tabNum, tabNameDef + tabNum + '-');
        //setTimeout(function(){createGroupTabs(groupsLevel - 1, tabNumFirst, tabNumLast, tabNameDef + tabNum, tabNameDef + tabNum + '-')}, tabNum*500);
        tabNumFirst = null;
        tabNumLast = null;
      }
    } else {

      //Create Pages
      for (let tabNum = 1; tabNum <= tabsAmount; tabNum++) {
        //
        let tabNumFirst = (tabNum - 1)*Math.pow(pagesInGroup, groupsLevel)*mixesOnPage + numFirst;
        let tabNumLast = Math.min(tabNum*Math.pow(pagesInGroup, groupsLevel)*mixesOnPage + numFirst -1, numLast);
        let pageNum = (tabNumFirst - 1)/mixesOnPage + 1;

        receive('/EDIT/MERGE', tabNameDef + tabNum, {

          //Panel Style
          layout: 'vertical',
          scroll: false,
          innerPadding: false,
          //Tab Style
          //label: tabNumFirst + '-' + tabNumLast,

          widgets: [{
            type: 'panel',
            id:'pnlMixesPage' + pageNum,
            //Geometry
            expand: true,
            //Panel Style
            scroll: false,
            innerPadding: false
          }]
        });

        tabNumFirst = null;
        tabNumLast = null;
        pageNum = null;
      }
    }
  }

  //Create Mixes
  for (let pageNum = 1; pageNum <= mixesPagesAmount; pageNum++) {

    //Create Tabs in Mixes Page
    let pageTabs = [];

    pageTabs.push({
      type: 'tab',
      id: 'tabMixesPage' + pageNum + 'All'
    });

    for (let mixNum = (pageNum - 1)*mixesOnPage + 1; mixNum <= Math.min(pageNum*mixesOnPage, mixesAmount); mixNum++) {
      //
      pageTabs.push({
        type: 'tab',
        id: 'tabMix' + mixNum
      });
    }

    receive('/EDIT/MERGE', 'pnlMixesPage' + pageNum, {
      //
      tabs: pageTabs
    });
    pageTabs = null;

    receive('/EDIT/MERGE', 'tabMixesPage' + pageNum + 'All', {

      //Panel Style
      layout: 'vertical',
      scroll: false,
      innerPadding: false,
      //Tab Style
      label: 'ALL',

      widgets: [{
        type: 'panel',
        id: 'pnlMixesPage' + pageNum + 'All',
        //Geometry
        expand: true,
        //Style
        //padding: 8,
        //Panel Style
        layout: 'horizontal',
        //justify: 'space-around',
        scroll: false,
        //innerPadding: false
        //Panel
        traversing: 'smart'
      }]
    });

    //Create 'ALL' Mixes Page View
    let allViewWidgets = [];

    for (let mixNum = (pageNum - 1)*mixesOnPage + 1; mixNum <= Math.min(pageNum*mixesOnPage, mixesAmount); mixNum++) {
      //
      allViewWidgets.push({
        type: 'panel',
        id:'pnlAllMix' + mixNum
      });
    }

    receive('/EDIT/MERGE', 'pnlMixesPage' + pageNum + 'All', {

      widgets: allViewWidgets
    });
    allViewWidgets = null;

    //Individual Mix Panels on 'ALL' View
    for (let mixNum = (pageNum - 1)*mixesOnPage + 1; mixNum <= Math.min(pageNum*mixesOnPage, mixesAmount); mixNum++) {
      //
      let mixPanelWidgets = [];

      //'Select' Button
      mixPanelWidgets.push({
        //
        type: 'button',
        id: 'btnMix' + mixNum + 'Select',
        //Geometry
        width: 60,
        height: 50,
        //Style
        colorText: '#ffffff',
        //css: "font-size:125%;",
        //Button Style
        label: 'SEL',
        //Scripting
        onValue: ''
      });

      //Volume Panel
      mixPanelWidgets.push({
        //Widget
        type: 'panel',
        id: 'pnlMix' + mixNum + 'Volume',
        //Geometry
        height: 340,
        //Panel Style
        scroll: false,
        innerPadding: false,

        widgets: [{
          //Widget
          type: 'text',
          id: 'lblMix' + mixNum + 'Volume',
          //Geometry
          left: 0,
          top: 0,
          width: 60,
          height: 40,
          //Style
          colorText: '#ffffff',
          //Value
          default: 'MIX\n' + mixNum
        },{
          //Widget
          type: 'fader',
          id: 'fdrMix' + mixNum + 'Volume',
          //Geometry
          left: 0,
          top: 40,
          width: 60,
          height: 300,
          //Style
          colorText: '#ffffff',
          //Fader Style
          knobSize: 20,
          pips: true,
          dashed: true,
          //Fader
          doubleTap: true,
          range: "{\"max\": { \"+10\": 0.949481 },\"91%\": { \"+5\": 0.828787 },\"82%\": { \"0.0\": 0.716 },\"73%\": { \"-5\": 0.612454 },\"64%\": { \"-10\": 0.518539 },\"55%\": { \"-15\": 0.435375 },\"46%\": { \"-20\": 0.363271 },\"37%\": { \"-30\": 0.25009 },\"28%\": { \"-40\": 0.170984 },\"19%\": { \"-50\": 0.116622 },\"10%\": { \"-60\": 0.079482 },\"min\": { \"-inf\": 0 }}",
          sensitivity: "@{varInput1SendsfdrSensitivity}",
          //Value
          default: 0.25,
          //OSC
          decimals: 6
        },{
          //Widget
          type: 'fader',
          id: 'fdrMix' + mixNum + 'VU',
          interaction: false,
          //Geometry
          left: 25,
          top: 52,
          width: 15,
          height: 278,
          //Style
          colorText: '#ffffff',
          colorFill: '#97ff6a',
          //Fader Style
          design: 'compact',
          //Fader
          range: "{\"max\": { \"+10\": 0.949481 },\"91%\": { \"+5\": 0.828787 },\"82%\": { \"0.0\": 0.716 },\"73%\": { \"-5\": 0.612454 },\"64%\": { \"-10\": 0.518539 },\"55%\": { \"-15\": 0.435375 },\"46%\": { \"-20\": 0.363271 },\"37%\": { \"-30\": 0.25009 },\"28%\": { \"-40\": 0.170984 },\"19%\": { \"-50\": 0.116622 },\"10%\": { \"-60\": 0.079482 },\"min\": { \"-inf\": 0 }}",
          //OSC
          decimals: 14
        }]
      });

      //'Mute' Button
      mixPanelWidgets.push({
        //
        type: 'button',
        id: 'btnMix' + mixNum + 'Mute',
        //Geometry
        width: 60,
        height: 50,
        //Style
        colorText: '#ffffff',
        //css: "font-size:125%;",
        //Button Style
        label: 'MUTE',
        //Scripting
        onValue: ''
      });

      //'0 dB' Button
      mixPanelWidgets.push({
        //
        type: 'button',
        id: 'btnMix' + mixNum + '0dB',
        //Geometry
        width: 60,
        height: 40,
        //Style
        colorText: '#ffffff',
        //css: "font-size:125%;",
        //Button Style
        label: 'set\n0 dB',
        //Button
        mode: 'momentary',
        //Scripting
        onValue: "set('fdrMix" + mixNum + "Volume', 0.716);"
      });

      //'-inf' Button
      mixPanelWidgets.push({
        //
        type: 'button',
        id: 'btnMix' + mixNum + '-Inf',
        //Geometry
        width: 60,
        height: 40,
        //Style
        colorText: '#ffffff',
        //css: "font-size:125%;",
        //Button Style
        label: 'set\n-inf',
        //Button
        mode: 'momentary',
        //Scripting
        onValue: "set('fdrMix" + mixNum + "Volume', 0);"
      });

      receive('/EDIT/MERGE', 'pnlAllMix' + mixNum, {

        //Geometry
        width: 60,
        //Style
        alphaStroke: 0,
        //Panel Style
        layout: 'vertical',
        justify: 'space-around',
        scroll: false,
        innerPadding: false,

        widgets: mixPanelWidgets
      });
    }

    //Configure Individual Mix Tabs
    for (let mixNum = (pageNum - 1)*mixesOnPage + 1; mixNum <= Math.min(pageNum*mixesOnPage, mixesAmount); mixNum++) {
      //
      receive('/EDIT/MERGE', 'tabMix' + mixNum, {

        //Panel Style
        layout: 'vertical',
        scroll: false,
        innerPadding: false,
        //Tab Style
        label: mixNum,

        widgets: [{
            type: 'panel',
            id:'pnlMix' + mixNum,
            //Geometry
            expand: true,
            //Panel Style
            layout: 'horizontal',
            scroll: false,
            innerPadding: false
          }]
      });

      //Create Mix Options Tabs
      let mixPage = [];

      //Mix Main Tab
      mixPage.push({
        type: 'tab',
        id: 'tabMix' + mixNum + 'Main',
        //Panel Style
        layout: 'vertical',
        scroll: false,
        innerPadding: false,
        //Tab Style
        label: 'MAIN'
      });

      //Mix EQ Tab
      mixPage.push({
        type: 'tab',
        id: 'tabMix' + mixNum + 'EQ',
        //Panel Style
        layout: 'vertical',
        scroll: false,
        innerPadding: false,
        //Tab Style
        label: 'EQ'
      });

      //Mix Gate Tab
      mixPage.push({
        type: 'tab',
        id: 'tabMix' + mixNum + 'Gate',
        //Panel Style
        layout: 'vertical',
        scroll: false,
        innerPadding: false,
        //Tab Style
        label: 'GATE'
      });

      //Mix Compressor Tab
      mixPage.push({
        type: 'tab',
        id: 'tabMix' + mixNum + 'Comp',
        //Panel Style
        layout: 'vertical',
        scroll: false,
        innerPadding: false,
        //Tab Style
        label: 'COMPRESSOR'
      });

      //Mix Limiter Tab
      mixPage.push({
        type: 'tab',
        id: 'tabMix' + mixNum + 'Limit',
        //Panel Style
        layout: 'vertical',
        scroll: false,
        innerPadding: false,
        //Tab Style
        label: 'LIMITER'
      });

      //Mix Sends Tab
      mixPage.push({
        type: 'tab',
        id: 'tabMix' + mixNum + 'Sends',
        //Panel Style
        layout: 'vertical',
        scroll: false,
        innerPadding: false,
        //Tab Style
        label: 'SENDS'
      });

      receive('/EDIT/MERGE', 'pnlMix' + mixNum, {
        //
        tabs: mixPage
      });
      mixPage = null;
      //
    }
  }
}

module.exports = {

    init: function(){

    },

    oscInFilter:function(data){
        // Filter incoming osc messages

        var {address, args, host, port} = data

        if ( address === '/drawMix' ) {

          console.log('task to draw Mixes received');
          console.log(args);

          try {

            let mixesAmount = args[0].value;
            let mixesOnPage = args[1].value;
            let pagesInGroup = args[2].value;

            console.log(mixesAmount);
            console.log(mixesOnPage);
            console.log(pagesInGroup);
            console.log('osc args correct');

            drawMixes( mixesAmount, mixesOnPage, pagesInGroup );

          } catch (e) {

          }
        }

        return {address, args, host, port}
    },

    oscOutFilter:function(data){
        // Filter outgoing osc messages

        var {address, args, host, port, clientId} = data

        return {address, args, host, port}
    },

    unload: function(){
        
    },
}

At some point the page stops responding.

After a while, a notification appears:
"the connection to the server has been restored".

After this, the rest of the messages never arrive.
This can be seen from a fragment of the unfinished interface.

https://drive.google.com/drive/folders/1bJoNYVoNhA4C6w8PNfBcd9iXaI6QiUec?usp=sharing

I'm not sure that's the only problem but I'd avoid sending /EDIT/MERGE commands in loops, it's very inefficient, it'd be much better to build the tree offline and send one big /EDIT command (or as few as possible). Each /EDIT/MERGE sent to add a child to a panel/trigger actually triggers a full rebuild of that container. Implementing something like an optimized /EDIT/ADD or /EDIT/INSERT could be a solution in cases like this one but I'm not convinced it's worth it.

Hi jean-emmanuel,
I tried to dynamically create widgets and the example code in the documentation didn't work unless I created a new project (which I didn't do -> I thought it didn't work at all).

Could you maybe change or expand this section of the example code:

app.once('open', () => {
    app.once('sessionSetPath', (data) => {
        // Session was created
        create_widgets()
    })
    // Create a new empty session
    receive('', '', '/SESSION/CREATE')
}) 

with something like this:

app.once('sessionOpened', () => {
    create_widgets()
}) 

which works when I open osc from the console using the --load option.