Learning plugin development - problem with creating Knockout functions

Hi, I want to learn plugin development, and I am following the knockout tutorial to create a button that can change text to upper case: Knockout tutorial - Introduction lesson 5 of 5

I have to variables:
some_text and max_diff

I want to set both variables in setting view, then display them in tab view.
I also created a button to change the text value to all cap.

So far, I can only display the value of max_diff in the tab view, but it doesn't work for "some_text".
What did I do wrong?

I created a test plugin, with following structure:

OctoPrint-TestPlugin
   OctoPrint-TestPlugin
      static
         css
            TestPlugin.css
         js
           TestPlugin.js
         less
      templates
         TestPlugin_settings.jinja2
         TestPlugin_tab.jinja2
      __init__.py

Content of "_ _ init _ _.py"

# coding=utf-8
from __future__ import absolute_import

### (Don't forget to remove me)
# This is a basic skeleton for your plugin's __init__.py. You probably want to adjust the class name of your plugin
# as well as the plugin mixins it's subclassing from. This is really just a basic skeleton to get you started,
# defining your plugin as a template plugin, settings and asset plugin. Feel free to add or remove mixins
# as necessary.
#
# Take a look at the documentation on what other plugin mixins are available.

import octoprint.plugin
import logging


class TestPlugin(octoprint.plugin.StartupPlugin,
                                octoprint.plugin.TemplatePlugin,
                                octoprint.plugin.SettingsPlugin,
                                octoprint.plugin.AssetPlugin,
                                octoprint.plugin.SimpleApiPlugin,
                                ):

    def __init__(self):
        self._logger = logging.getLogger("octoprint.plugins.Test")
        self._logger.info("this is Test init func")

    def on_after_startup(self):
        self._logger.info("### Test Plugin ###")
        self._logger.info("### Test Plugin max_diff: %s" % self._settings.get(["max_diff"]))

    ##~~ SettingsPlugin mixin

    def get_settings_defaults(self):
        return dict(
            # put your plugin's default settings here/
            max_diff=1,  # some variable for playing around
            some_text="abcdefg", # some text variable for playing around
        )

    def get_template_configs(self):
        return [
            dict(type="settings", custom_bindings=False),
            dict(type="tab", custom_bindings=False)
        ]

    ##~~ AssetPlugin mixin

    def get_assets(self):
        # Define your plugin's asset files to automatically include in the
        # core UI here.
        return dict(
            js=["js/Test.js"],
            css=["css/Test.css"],
            less=["less/Test.less"]
        )

    def gcode_processor(self, comm, line, *args, **kwargs):
        if "MACHINE_TYPE" not in line:
            return line

        from octoprint.util.comm import parse_firmware_line

        # Create a dict with all the keys/values returned by the M115 request
        printer_data = parse_firmware_line(line)

        logging.getLogger("octoprint.plugin." + __name__).info(
            "Machine type detected: {machine}.".format(machine=printer_data["firmware_name"]))

        return line


# If you want your plugin to be registered within OctoPrint under a different name than what you defined in setup.py
# ("OctoPrint-PluginSkeleton"), you may define that here. Same goes for the other metadata derived from setup.py that
# can be overwritten via __plugin_xyz__ control properties. See the documentation for that.
__plugin_name__ = "Test"

# Starting with OctoPrint 1.4.0 OctoPrint will also support to run under Python 3 in addition to the deprecated
# Python 2. New plugins should make sure to run under both versions for now. Uncomment one of the following
# compatibility flags according to what Python versions your plugin supports!
# __plugin_pythoncompat__ = ">=2.7,<3" # only python 2
# __plugin_pythoncompat__ = ">=3,<4" # only python 3
__plugin_pythoncompat__ = ">=2.7,<4"  # python 2 and 3


def __plugin_load__():
    global __plugin_implementation__
    __plugin_implementation__ = TestPlugin()

    global __plugin_hooks__
    __plugin_hooks__ = {
        "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information,
        # "octoprint.comm.protocol.scripts": __plugin_implementation__.message_on_connect,
        "octoprint.comm.protocol.gcode.received": __plugin_implementation__.gcode_processor,
    }

Content of "TestPlugin.js"

/*
 * View model for OctoPrint-TestPlugin
 *
 * Author: tester
 * License: AGPLv3
 */
$(function() {
    function TestPluginViewModel(parameters) {
        var self = this;

        // assign the injected parameters, e.g.:
        // self.loginStateViewModel = parameters[0];
        // self.settingsViewModel = parameters[1];

        // TODO: Implement your plugin's view model here.
        self.settings = parameters[0];

        self.currentMaxDiff = ko.observable();
        self.newMaxDiff = ko.observable();
        self.someText = ko.observable();

        self.showMaxDiff = function() {
            self.currentMaxDiff(self.newMaxDiff());
        };

        self.capitalizeSomeText = function() {
            var currentText = self.someText();        // Read the current value
            self.someText(currentText.toUpperCase()); // Write back a modified value
        };

         self.onBeforeBinding = function() {
            self.someText(self.settings.settings.plugins.TestPlugin.some_text());
            self.newMaxDiff(self.settings.settings.plugins.TestPlugin.max_diff());
            //self.showMaxDiff();
        };
    };

    /* view model class, parameters for constructor, container to bind to
     * Please see http://docs.octoprint.org/en/master/plugins/viewmodels.html#registering-custom-viewmodels for more details
     * and a full list of the available options.
     */
    OCTOPRINT_VIEWMODELS.push({
        // This is the constructor to call for instantiating the plugin
        construct: TestPluginViewModel,
        // ViewModels your plugin depends on, e.g. loginStateViewModel, settingsViewModel, ...
        //dependencies: [ /* "loginStateViewModel", "settingsViewModel" */ ],
        // Elements to bind to, e.g. #settings_plugin_TestPlugin, #tab_plugin_TestPlugin, ...
        //elements: [ /* ... */ ]
        // instantiation via the parameters argument
        dependencies: ["settingsViewModel"],

        // Finally, this is the list of selectors for all elements we want this view model to be bound to.
        elements: ["#tab_plugin_TestPlugin"]
    });
});

Content of "TestPlugin_settings.jinja2"

<form class="form-horizontal">
    <div class="control-group">
        <!-- <label class="control-label">{{ _('Maximum Difference') }}</label> -->
        <div class="controls">
            <!-- <label for="inputMaxDiff"></label> -->
            <!-- class="input-block-levels" -->
            <p> Enter Max Diff: <input type="text" data-bind="value: settings.plugins.TestPlugin.max_diff"></p>
            <p> Enter any text: <input type="text" data-bind="value: settings.plugins.TestPlugin.some_text"></p>
        </div>
    </div>
</form>

Content of "TestPlug_tab.jinja2"

<!--
<div class="input-append">
    <input type="text" class="input-block-level" data-bind="value: max_diff">

</div>
-->

<div>
    <h1>Hello!</h1>
    <p> newMaxDiff: <strong data-bind="text: newMaxDiff"></strong></p>
    <p> currentMaxDiff: <strong data-bind="text: currentMaxDiff"></strong></p>
    <p> max_diff: <strong data-bind="text: max_diff"></strong></p>
    <p> settings.plugins.TestPlugin.max_diff: <strong data-bind="text: settings.plugins.TestPlugin.max_diff"></strong></p>
    <p> someText: <strong data-bind="text: someText"></strong></p>
    <button class="btn btn-primary" data-bind="click: showMaxDiff">Refresh</button>
    <button class="btn btn-primary" data-bind="click: capitalizeSomeText">Go Cap!</button>
</div>

custom_bindings=False needs to be custom_bindings=True if you are calling functions within your own viewmodel.

1 Like

after I made custom_bindings=True, either variable worked.

1 Like

If that worked, don't forget to mark the solution.

what I meant was, after I made custom_bindings=True, both variables stopped displaying on the tab view.

if you use custom_bindings=True you also need to change your references in the jinja template. So the above would become.

settingsViewModel.settings.plugins.TestPlugin.max_diff

correction, your parameter is not getting assigned to settingsViewModel so should be

settings.settings.plugins.TestPlugin.max_diff

Hi jneilliii

I ended up trying to start afresh with the basics:
I created a new project, let's call it sensor_plotter, again, following the tutorial I have created:

  • _ _ init _ _.py
  • sensor_plotter.js
  • sensor_plotter_settings.jinja2
  • sensor_plotter_navbar.jinja2
  • sensor_plotter_tab.jinja2

General description:
In __ init __.py, there are two variables created by get_settings_defaults() : url, and some_var
The get_template_config sets settings, navbar, and tab to custom_bindings=True

In sensor_plotter.js, self.some_settings = parameters[0];

and in settings, tab, and navbar, I called the variables by some_settings.settings.plugins..sensor_plotter.url

The results:
in setting, I am able to assign a new value to url or some_var with input: <input type="text" data-bind="value: some_settings.settings.plugins.sensor_plotter.url">
<input type="text" data-bind="value: some_settings.settings.plugins.sensor_plotter.some_var">

in tab, I am able to bind data with some_settings.settings.plugins..sensor_plotter.url and some_settings.settings.plugins..sensor_plotter.some_var, and each tie I update with input from setting, the tab will update accordingly.

in navbar, however it will not bind data with <a href="#" data-bind="attr: {href: some_settings.settings.plugins.sensor_plotter.url}">Sensor Plotter navbar

What went wrong?

Actual code content:

_ init _.py

# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import octoprint.plugin
import logging
class SensorPlotterPlugin(octoprint.plugin.StartupPlugin,
                          octoprint.plugin.TemplatePlugin,
                          octoprint.plugin.SettingsPlugin,
                          octoprint.plugin.AssetPlugin):

    def get_settings_defaults(self):
        return dict(url="https://google.com",
                    some_var="this is some variable")

    def get_template_configs(self):
        return [
            dict(type="settings", custom_bindings=True),
            dict(type="navbar", custom_bindings=True),
            dict(type="tab", custom_bindings=True),
        ]

    def get_assets(self):
        return dict(
            js=["js/sensor_plotter.js"],
            css=["css/sensor_plotter.css"],
            less=["less/sensor_plotter.less"]
        )


__plugin_name__ = "Sensor Plotter"
__plugin_pythoncompat__ = ">=2.7,<4"
__plugin_implementation__ = SensorPlotterPlugin()

sensor_plotter.js

$(function() {
    function sensor_plotterViewModel(parameters) {
        var self = this;
        self.some_settings= parameters[0];
    }

    OCTOPRINT_VIEWMODELS.push({
        construct: sensor_plotterViewModel,
        dependencies: ["settingsViewModel"],
        elements: ["#tab_plugin_sensor_plotter", "#settings_plugin_sensor_plotter", "#navbar_plugin_sensor_plotter"]
    });
});

sensor_plotter_settings.jinja2

<form>
    <div>
        <label>{{ _('URL') }}</label>
        <div>
            <input type="text"  data-bind="value: some_settings.settings.plugins.sensor_plotter.url">
            <input type="text"  data-bind="value: some_settings.settings.plugins.sensor_plotter.some_var">
        </div>
    </div>
</form>

sensor_plotter_tab.jinja2

<p>URL: <strong data-bind="text: some_settings.settings.plugins.sensor_plotter.url"></strong></p>
<p>URL: <strong data-bind="text: some_settings.settings.plugins.sensor_plotter.some_var"></strong></p>
<a href="#" data-bind="attr: {href: some_settings.settings.plugins.sensor_plotter.url}">Sensor Plotter tab</a>

sensor_plotter_navbar.jinja2

<a href="#" data-bind="attr: {href: some_settings.plugins.sensor_plotter.url}">Sensor Plotter navbar</a>

ah, i figured it out.

  • In the javascript, I need to specific self.pluginName = "sensor_plotter";

  • in the javascript, the parameters index need to match the dependences self.some_settings = parameters[0];, and dependencies: ["settingsViewModel"],. Index 0 matches index 0;

  • in the javascript, the elements need to be included, and the plugin names need to match: elements: ["#tab_plugin_sensor_plotter", "#settings_plugin_sensor_plotter", "#navbar_plugin_sensor_plotter"]

  • in jinja2, the variables are referenced by variables declared in settings' parameter (point #2 above). e.g. some_settings.settings.plugins.sensor_plotter.url

2 Likes