Modal settings dialog?

I've been searching around for an example of opening a modal dialog from a plugins settings but haven't found anything. What I want to achieve is to have a button on a plugin settings page to add a new setting. That button opens a modal dialog (with Save/Cancel options) where the users can fill out a form with additional settings.

Is this possible? Do we have any examples of that?

Another paradigm for doing this is what's done in Themeify, for instance. There's an existing collection of settings, you press the New button and it adds another blank set of four fields and the controls on the right.

If you think about it, the entire Settings behavior in OctoPrint is what you've asked for so you may want to go to school on that.

OctoPrint JavaScript search for "self.show("

Hi @Stefan_Cohen,
if you just want to know how to create a modal-dialog, you can find a code-snippet in my Knowledge-Base Wiki: https://github.com/OllisGit/OctoPrint-KnowledgeBase/wiki/UI#create-modal-dialog

BR
Olli

1 Like

The way @OllisGit linked in his knowledge-base is the approach I've taken in the past.

Looking through that UI stuff though @OllisGit, in the section here item #2, what is textInput data-bind? I'm used to using data-bind="value: knockout_observable" for inputs.

value is also an option. I think, as I documented that hint, I was looking for a mechanism that updates the viewmodel on keystrokes and not only on focus-lost. So, that you can implement an autosuggest-feature.

You can find more informations about textInput here: https://knockoutjs.com/documentation/textinput-binding.html

@OutsourcedGuru I have considered the approach in Themeify, but I have too many properties to fit in a table and still be usable. I think I will use a hybrid with a table for the overview (read only) and an edit button for each row to launch a settings editor.

@OllisGit Thanks! That is exactly what I was looking for.

Your welcome! I forgot to mention, that you can also look into my Autostart-Plugin (https://github.com/OllisGit/Octoprint-AutostartPrint).
The "countdown"-dialog also prevents closing by "X" and "sideclicks".

These options were hard to find and not documented in my wiki:

            countdownDialog.modal({
                keyboard: false,
                clickClose: false,
                showClose: false,
                backdrop: "static"
```

Brilliant!

That's why I used the modal dialog in the smart plug plugins, as seen on the screenshots over here.

Thanks @OllisGit, I wonder if that will help me resolve an issue with a fontawesome icon picker attached to an input. I haven't had the time to dig deeper but for some reason the value gets set with the iconpicker, but then on save it reverts back to the stored setting. I may end up having to program my own knockout binding handler I think.

Thanks. That example is almost exactly what I'm trying to achieve.

I've been struggling a bit with adding an observable Array to your example (to hold http header key-value pairs). Reading headers from the settings works flawlessly but I haven't been able to add or remove headers.

Given the following (simplified):

config.yaml settings:

  dash:
    dataSources:
    - enabled: true
      headers:
      - name: X-Api-Key
        value: My API Key
      method: get
      name: OctoPrint

Viewmodel:

     self.selectedDataSource = ko.observable();
   
     self.addDataSource = function() {
          self.selectedDataSource({
                    'name':ko.observable(''),
                    'enabled':ko.observable(false),
                    'type':ko.observable(''),
                    'pollingInterval':ko.observable(2),
                    'url':ko.observable(''),
                    'method':ko.observable('GET'),
                    'headers':ko.observableArray([{'name' : '', 'value' : ''}])
          });
          self.settingsViewModel.settings.plugins.dash.dataSources.push(self.selectedDataSource());
          $("#dataSourceEditor").modal("show");
        }

        self.editDataSource = function(data) {
          self.selectedDataSource(data);
          $("#dataSourceEditor").modal("show");
        }

        self.addHeader = function(data) {
          //TODO: Add an empty header
        }

Template:

!-- dataSourceEditor Modal-Dialog -->
<div id="dataSourceEditor" class="modal hide fade" data-bind="with: selectedDataSource">
    <div class="modal-header">
        <a href="#" class="close" data-dismiss="modal" aria-hidden="true">&times;</a>
        <h3 class="modal-title">{{ _('Data Source') }}</h3>
    </div>
    <div class="modal-body">
        <table class="table table-condensed">
            <tr>
                <td><div class="controls"><label class="control-label">{{ _('Name') }}</label><input type="text" class="input-block-level" data-bind="value: name" /></div></td>
            </tr>
            <tr>
                <td><button class="btn" data-bind="click: $root.addHeader">{{ _('Add Header') }}</button></td>
            </tr>
            <table class="table table-condensed" data-bind="if: type() == 'json' && headers()">
                <thead>
                    <tr>
                        <td>{{ _('Header') }}</td>
                        <td>{{ _('Value') }}</td>
                        <td>{{ _('Action') }}</td>
                    </tr>
                </thead>
                <tbody data-bind="foreach: headers">
                    <tr>
                        <td><div class="controls"><input type="text" class="input" data-bind="value: name" /></div></td>
                        <td><div class="controls"><input type="text" class="input" data-bind="value: value" /></div></td>
                        <td><a href="#" class="btn btn-mini icon-trash"></a></td>
                    </tr>
                </tbody>
            </table>
        </table>
    </div>
    <div class="modal-footer">
        <button class="btn btn-cancel" data-dismiss="modal" aria-hidden="true">{{ _('Close') }}</button>
    </div>
</div>

How should I approach this? addHeader is called as it should but I haven't been able to add an element to the headers array. I've tried a bunch of things but I always end up with an "TypeError: undefined is not an object (evaluating 'self.selectedDataSource.headers.push')" error. I guess I have totally misunderstood how the context bindings work or that I struggle with how to deal with an array in an array.

The full source including my current attempt state is here: https://github.com/StefanCohen/Dash

Ugh. Why do I always find the solution right after I've posted a question? And why is it always so much easier than I thought:

        self.addHeader = function() {
          console.log("clicked");
          self.newHeader = {
            name : ko.observable(""),
            value : ko.observable("")
          };
          self.selectedDataSource().headers.push(self.newHeader);
        }

:smiley:

1 Like

Yep, knockout observable arrays doesn't inherently make it's objects observable, you have to wrap them the way you did.