Plugin Development Tutorial

Creating Your First Plugin

Learn to build custom plugins for WebVB Studio from scratch. We'll create a Toggle Switch control and a set of utility functions, step by step.

Before You Start

Read the Plugin System Overview to understand the architecture, type system, and plugin lifecycle. This tutorial assumes familiarity with the plugin JSON format and basic HTML/JavaScript.

I

Part 1: Functions-Only Plugin

The simplest plugin type provides custom functions without any visual controls. This is the best place to start learning.

Step 1: Create the Plugin Skeleton

Every plugin starts with the same skeleton. Open the Plugin Manager in WebVB Studio, go to the Create tab, and paste this template:

my-text-utils.vbplugin Functions-only plugin
{
  "pluginApiVersion": "1.0",
  "metadata": {
    "id": "com.myname.textutils",
    "name": "Text Utilities",
    "version": "1.0.0",
    "description": "Handy text manipulation functions",
    "author": { "name": "Your Name" },
    "icon": "📜",
    "category": "Functions",
    "tags": ["text", "string", "utility"],
    "license": "MIT"
  },
  "functions": [],
  "helpContent": "<h3>Text Utilities</h3><p>Helper functions for text.</p>"
}

Key Points

  • pluginApiVersion must be "1.0"
  • metadata.id should be a unique reverse-domain identifier
  • No controls field needed for a functions-only plugin

Step 2: Add Your Functions

Each function needs a name, params, and jsCode. The jsCode is a JavaScript function body — parameters are available as named variables.

Adding the "functions" array
"functions": [
  {
    "name": "ReverseText",
    "description": "Reverse a string",
    "params": [
      { "name": "text", "type": "String", "description": "Text to reverse" }
    ],
    "returnType": "String",
    "jsCode": "return String(text || '').split('').reverse().join('');",
    "category": "String"
  },
  {
    "name": "WordCount",
    "description": "Count words in a string",
    "params": [
      { "name": "text", "type": "String", "description": "Text to count words in" }
    ],
    "returnType": "Integer",
    "jsCode": "var s = String(text || '').trim(); return s.length === 0 ? 0 : s.split(/\\s+/).length;",
    "category": "String"
  },
  {
    "name": "RepeatText",
    "description": "Repeat text N times with optional separator",
    "params": [
      { "name": "text", "type": "String", "description": "Text to repeat" },
      { "name": "count", "type": "Integer", "description": "Number of repetitions" },
      { "name": "separator", "type": "String", "description": "Separator between repetitions", "optional": true, "defaultValue": "" }
    ],
    "returnType": "String",
    "jsCode": "var arr = []; for (var i = 0; i < (parseInt(count) || 1); i++) arr.push(String(text || '')); return arr.join(separator || '');",
    "category": "String"
  }
]

Important: jsCode Rules

  • The code is a function body, not a full function. No function keyword.
  • Parameter names from params become local variables automatically.
  • Use return to produce a value.
  • Escape backslashes in JSON: \\s not \s.
  • Always validate inputs (check for null/undefined, use parseInt/parseFloat).

Step 3: Install and Test

Click the Install Plugin button in the Create tab. If the JSON is valid, you'll see a success message and the plugin will appear in the Installed tab.

Now test your functions in a VB or Python project:

Visual Basic 6
Private Sub cmdTest_Click()
    ' Test ReverseText
    lblResult.Caption = ReverseText("Hello!")
    ' Shows: "!olleH"
    
    ' Test WordCount
    Dim count As Integer
    count = WordCount("The quick brown fox")
    MsgBox "Words: " & count   ' Shows: 4
    
    ' Test RepeatText
    lblStars.Caption = RepeatText("*", 5, " ")
    ' Shows: "* * * * *"
End Sub
Python
def cmdTest_Click():
    # Test ReverseText
    lblResult.Caption = ReverseText("Hello!")
    # Shows: "!olleH"
    
    # Test WordCount
    count = WordCount("The quick brown fox")
    MsgBox(f"Words: {count}")  # Shows: 4
    
    # Test RepeatText
    lblStars.Caption = RepeatText("*", 5, " ")
    # Shows: "* * * * *"

Step 4: Export and Share

Go to the Installed tab, find your plugin, and click Export. This downloads a .vbplugin file that anyone can import into their WebVB Studio.

Congratulations! You've created your first plugin. Functions-only plugins are great for adding reusable utilities across all your projects.

II

Part 2: Custom Control Plugin

Now let's build something more substantial: a Toggle Switch control that renders in the designer, responds to clicks at runtime, and fires events.

Step 1: Define the Plugin Metadata

toggle-switch.vbplugin - metadata
{
  "pluginApiVersion": "1.0",
  "metadata": {
    "id": "com.myname.toggleswitch",
    "name": "Toggle Switch",
    "version": "1.0.0",
    "description": "A modern toggle switch control with smooth animation",
    "author": { "name": "Your Name", "url": "https://yoursite.com" },
    "icon": "🔘",
    "category": "Input",
    "tags": ["toggle", "switch", "boolean", "on", "off"],
    "license": "MIT"
  },
  "controls": [ ... ],
  "styles": "...",
  "helpContent": "..."
}

Step 2: Define Control Properties

Properties appear in the Properties panel when the user selects your control in the designer. Think about what users need to customize.

Control properties array
"properties": [
  {
    "id": "Value",
    "label": "Value",
    "type": "boolean",
    "defaultValue": false,
    "description": "Current toggle state (true = on, false = off)",
    "category": "Data"
  },
  {
    "id": "OnColor",
    "label": "On Color",
    "type": "color",
    "defaultValue": "#22c55e",
    "description": "Color when toggle is ON",
    "category": "Appearance"
  },
  {
    "id": "OffColor",
    "label": "Off Color",
    "type": "color",
    "defaultValue": "#d1d5db",
    "description": "Color when toggle is OFF",
    "category": "Appearance"
  },
  {
    "id": "OnLabel",
    "label": "On Label",
    "type": "text",
    "defaultValue": "ON",
    "description": "Label text when ON",
    "category": "Appearance"
  },
  {
    "id": "OffLabel",
    "label": "Off Label",
    "type": "text",
    "defaultValue": "OFF",
    "description": "Label text when OFF",
    "category": "Appearance"
  },
  {
    "id": "Disabled",
    "label": "Disabled",
    "type": "boolean",
    "defaultValue": false,
    "description": "Disable user interaction",
    "category": "Behavior"
  }
]

Best Practices for Properties

  • Always provide defaultValue so the control looks good out of the box
  • Use category to group related properties (Appearance, Behavior, Data)
  • Use the color type for any color property — it shows a color picker
  • Use select type with options for enum-like choices

Step 3: Define Events

Events connect your control to user code. When an event fires, WebVB Studio looks for a matching event handler in the user's VB or Python code.

"events": [
  {
    "name": "Change",
    "description": "Fired when the toggle state changes",
    "params": ["NewValue"]
  },
  {
    "name": "Click",
    "description": "Fired when the toggle is clicked (even if disabled)"
  }
]

The user writes event handlers like Sub ToggleSwitch1_Change(NewValue) where NewValue will be True or False.

Step 4: Write the Designer Renderer

The designer renderer produces a static HTML preview for the form designer. It should reflect the current property values but doesn't need interactivity.

designerRenderHtml
"designerRenderHtml": "var isOn = props.Value === true || props.Value === 'true'; var onColor = props.OnColor || '#22c55e'; var offColor = props.OffColor || '#d1d5db'; var label = isOn ? (props.OnLabel || 'ON') : (props.OffLabel || 'OFF'); var trackColor = isOn ? onColor : offColor; var thumbLeft = isOn ? 'calc(100% - 26px)' : '2px'; return '<div style=\"display:flex;align-items:center;height:100%;padding:4px;\">' + '<div style=\"position:relative;width:50px;height:26px;background:' + trackColor + ';border-radius:13px;\">' + '<div style=\"position:absolute;top:2px;left:' + thumbLeft + ';width:22px;height:22px;background:white;border-radius:50%;box-shadow:0 1px 3px rgba(0,0,0,0.3);\">' + '</div></div>' + '<span style=\"margin-left:8px;font-size:12px;font-weight:600;color:' + trackColor + ';\">' + label + '</span></div>';"

Designer Renderer Rules

  • The code is a function body that receives props and must return an HTML string
  • Always use inline styles (no external CSS classes)
  • Always provide fallback values for every property: props.Color || '#default'
  • Handle boolean props as both true/false and 'true'/'false' (they may come as strings from JSON)

Step 5: Write the Runtime Renderer

The runtime renderer is similar to the designer renderer but produces the live, interactive HTML. It can include data attributes for the init code to hook into.

runtimeRenderHtml
"runtimeRenderHtml": "var isOn = props.Value === true || props.Value === 'true'; var onColor = props.OnColor || '#22c55e'; var offColor = props.OffColor || '#d1d5db'; var label = isOn ? (props.OnLabel || 'ON') : (props.OffLabel || 'OFF'); var disabled = props.Disabled === true || props.Disabled === 'true'; var trackColor = isOn ? onColor : offColor; var thumbLeft = isOn ? 'calc(100% - 26px)' : '2px'; var cursor = disabled ? 'not-allowed' : 'pointer'; var opacity = disabled ? '0.5' : '1'; return '<div data-toggle=\"track\" style=\"display:flex;...cursor:' + cursor + ';opacity:' + opacity + ';...\">...</div>';"

Notice the data-toggle="track" attribute — we'll use it in the init code to attach the click listener.

Step 6: Write the Runtime Init Code

The init code runs once after the control mounts. This is where you attach event listeners and set up interactivity. You have access to these special variables:

element
The DOM container element
props
Current property values
fireEvent(name, data)
Fire an event to VB/Python code
setProperty(name, value)
Update a property + re-render
runtimeInitCode
"runtimeInitCode": "element.addEventListener('click', function(e) {
  fireEvent('Click', {});
  var disabled = props.Disabled === true || props.Disabled === 'true';
  if (disabled) return;
  var isOn = props.Value === true || props.Value === 'true';
  var newVal = !isOn;
  setProperty('Value', newVal);
  fireEvent('Change', { NewValue: newVal });
});"

How Events Flow

  1. 1. User clicks the toggle
  2. 2. fireEvent('Click', ...) triggers the user's ToggleSwitch1_Click() handler
  3. 3. setProperty('Value', newVal) updates the property and re-renders the control
  4. 4. fireEvent('Change', ...) triggers ToggleSwitch1_Change(NewValue)

Step 7: Add CSS Styles (Optional)

The styles field injects global CSS into the document. Use this for animations, keyframes, or shared styles.

"styles": "[data-toggle='track']:hover { filter: brightness(1.1); } [data-toggle='track']:active { transform: scale(0.97); }"

Style Tips

  • Use data-* attribute selectors to scope styles to your control
  • Prefer inline styles in renderers; use this field for animations and pseudo-elements
  • CSS is injected when plugin activates and removed when disabled/uninstalled

Step 8: Write Help Documentation

The helpContent field accepts HTML that documents your plugin for users. Include property descriptions, event examples, and code samples.

"helpContent": "<h3>Toggle Switch Control</h3>
<p>A modern ON/OFF toggle switch.</p>
<h4>Properties</h4>
<ul>
  <li><b>Value</b> - Boolean: True (ON) or False (OFF)</li>
  <li><b>OnColor/OffColor</b> - Track colors</li>
  <li><b>OnLabel/OffLabel</b> - Text labels</li>
  <li><b>Disabled</b> - Prevent user interaction</li>
</ul>
<h4>Events</h4>
<ul>
  <li><b>Change(NewValue)</b> - Fires when toggled</li>
  <li><b>Click</b> - Fires on any click</li>
</ul>"

Step 9: The Complete Plugin

Here's the entire Toggle Switch plugin assembled into a single JSON file:

toggle-switch.vbplugin (complete) Copy & paste into Create tab
{
  "pluginApiVersion": "1.0",
  "metadata": {
    "id": "com.myname.toggleswitch",
    "name": "Toggle Switch",
    "version": "1.0.0",
    "description": "A modern toggle switch control with smooth animation",
    "author": { "name": "Your Name" },
    "icon": "🔘",
    "category": "Input",
    "tags": ["toggle", "switch", "boolean", "on", "off"],
    "license": "MIT"
  },
  "controls": [
    {
      "typeName": "ToggleSwitch",
      "displayName": "Toggle Switch",
      "icon": "🔘",
      "description": "ON/OFF toggle switch",
      "defaultWidth": 120,
      "defaultHeight": 36,
      "properties": [
        { "id": "Value", "label": "Value", "type": "boolean",
          "defaultValue": false, "description": "Toggle state",
          "category": "Data" },
        { "id": "OnColor", "label": "On Color", "type": "color",
          "defaultValue": "#22c55e", "category": "Appearance" },
        { "id": "OffColor", "label": "Off Color", "type": "color",
          "defaultValue": "#d1d5db", "category": "Appearance" },
        { "id": "OnLabel", "label": "On Label", "type": "text",
          "defaultValue": "ON", "category": "Appearance" },
        { "id": "OffLabel", "label": "Off Label", "type": "text",
          "defaultValue": "OFF", "category": "Appearance" },
        { "id": "Disabled", "label": "Disabled", "type": "boolean",
          "defaultValue": false, "category": "Behavior" }
      ],
      "events": [
        { "name": "Change", "description": "Toggle state changed",
          "params": ["NewValue"] },
        { "name": "Click", "description": "Toggle was clicked" }
      ],
      "designerRenderHtml": "var isOn = props.Value === true ...; return '<div>...</div>';",
      "runtimeRenderHtml": "var isOn = ...; return '<div data-toggle=...>...</div>';",
      "runtimeInitCode": "element.addEventListener('click', function(e) { ... });"
    }
  ],
  "styles": "[data-toggle='track']:hover { filter: brightness(1.1); }",
  "helpContent": "<h3>Toggle Switch</h3>..."
}

Step 10: Using Your Control

After installing, your Toggle Switch appears in the Toolbox under "Plugins". Drag it onto a form and write event handlers:

Visual Basic 6
Private Sub ToggleSwitch1_Change(NewValue)
    If NewValue Then
        lblStatus.Caption = "Dark mode ON"
        Form1.BackColor = "#1a1a2e"
    Else
        lblStatus.Caption = "Dark mode OFF"
        Form1.BackColor = "#ffffff"
    End If
End Sub

' Read the current value anytime:
Private Sub cmdCheck_Click()
    MsgBox "Toggle is: " & ToggleSwitch1.Value
End Sub

' Set it programmatically:
Private Sub cmdEnable_Click()
    ToggleSwitch1.Value = True
End Sub
Python
def ToggleSwitch1_Change(NewValue):
    if NewValue:
        lblStatus.Caption = "Dark mode ON"
        Form1.BackColor = "#1a1a2e"
    else:
        lblStatus.Caption = "Dark mode OFF"
        Form1.BackColor = "#ffffff"

# Read the current value anytime:
def cmdCheck_Click():
    MsgBox(f"Toggle is: {ToggleSwitch1.Value}")

# Set it programmatically:
def cmdEnable_Click():
    ToggleSwitch1.Value = True
III

Part 3: Advanced Topics

Combining Controls and Functions

A single plugin can provide both controls and functions. For example, a "Charts" plugin might include a Chart control and helper functions like FormatAxis() or ParseCSV().

{
  "pluginApiVersion": "1.0",
  "metadata": { ... },
  "controls": [
    { "typeName": "PieChart", ... },
    { "typeName": "BarChart", ... }
  ],
  "functions": [
    { "name": "ParseCSV", ... },
    { "name": "FormatAxis", ... }
  ],
  "styles": "@keyframes chart-fadein { ... }",
  "helpContent": "..."
}

Cleanup and Resource Management

If your control sets up intervals, timeouts, or external event listeners, use runtimeCleanupCode to clean up when the control is unmounted.

// runtimeInitCode:
"var intervalId = setInterval(function() {
  /* update clock */
}, 1000);
element.__myInterval = intervalId;"

// runtimeCleanupCode:
"if (element.__myInterval) clearInterval(element.__myInterval);"

Memory Leak Prevention

  • Always clear setInterval and setTimeout
  • Remove any listeners attached to window or document
  • Store references on element (e.g. element.__myRef) so cleanup code can access them

Handling Property Changes

When properties are changed programmatically (e.g. MyControl.Value = 50 in VB code), the control re-renders. If you need custom logic beyond re-rendering, use runtimePropertyChangeCode:

// runtimePropertyChangeCode receives:
// (element, propName, newValue, oldValue, props)

"if (propName === 'Value' && parseInt(newValue) >= parseInt(props.Max)) {
  fireEvent('Complete', {});
}"

Async Functions

Plugin functions can be asynchronous by setting isAsync: true. The jsCode can then use await:

{
  "name": "FetchJSON",
  "description": "Fetch JSON from a URL",
  "params": [
    { "name": "url", "type": "String", "description": "URL to fetch" }
  ],
  "returnType": "Object",
  "jsCode": "var response = await fetch(url); return await response.json();",
  "isAsync": true,
  "category": "Network"
}

Naming Conventions

Plugin IDs

Use reverse-domain notation to ensure uniqueness:

Good: com.yourname.pluginname
Good: io.github.youruser.pluginname
Avoid: my-plugin (too generic, may conflict)

Control Type Names

Use PascalCase. The name is prefixed with the plugin ID automatically:

Good: ToggleSwitch, StarRating, ColorPicker
Avoid: toggle_switch, mycontrol1

Function Names

Use PascalCase. Functions are registered globally (case-insensitive):

Good: Slugify, FormatPhoneNumber, IsValidEmail
Avoid: MsgBox, Val (conflicts with built-in functions)

Troubleshooting

"Plugin render error" in the designer

Your designerRenderHtml has a JavaScript error. Common causes:

  • • Unescaped quotes inside the HTML string
  • • Missing return statement
  • • Syntax error in string concatenation

"Invalid plugin JSON" on import

The JSON is malformed. Common issues:

  • • Trailing commas (not allowed in JSON)
  • • Single quotes instead of double quotes
  • • Unescaped special characters in code strings
  • • Missing pluginApiVersion: "1.0"

Events not firing

Check that:

  • • The event name in fireEvent('Change', ...) matches the event name in the events array exactly (case-sensitive)
  • • The user's event handler follows the naming pattern: ControlName_EventName
  • • Your runtimeInitCode is correctly attaching the event listener

Function returns undefined

Make sure your jsCode includes a return statement. The code is a function body, so without return, it implicitly returns undefined.

Control doesn't re-render after setProperty

The setProperty call should trigger a re-render automatically. If it doesn't:

  • • Ensure you're calling setProperty (not directly modifying props)
  • • Check the browser console for errors
  • • Verify the property name matches exactly (case-sensitive)

Best Practices Checklist

Use a unique reverse-domain ID for your plugin
Provide default values for all properties
Always use fallback values in renderers: props.X || 'default'
Handle boolean properties as both true and "true"
Validate all inputs in function code (null checks, parseInt, parseFloat)
Clean up intervals and global listeners in runtimeCleanupCode
Use inline styles in renderers; reserve the styles field for animations
Include helpContent with property descriptions and code examples
Test your plugin in both VB and Python modes before distributing
Don't name functions the same as built-in VB/Python functions

Distributing Your Plugin

Plugins are simple JSON files, making distribution straightforward. Here are several ways to share your plugins:

📤

Export & Send

Use the Export button in the Plugin Manager to download a .vbplugin file. Send it via email, chat, or file sharing. The recipient imports it via the Import tab.

📁

GitHub / Hosting

Host the .vbplugin file on GitHub, your website, or a CDN. Users can download it and import into WebVB Studio.

📋

Paste JSON

Share the raw JSON in documentation, forum posts, or tutorials. Users paste it into the Import tab's text area or the Create tab's editor.

🎉

You're a Plugin Developer!

You now know everything needed to create, test, and distribute custom plugins. Check the built-in examples for more inspiration, and happy building!