diff --git a/docs/plugins/viewmodels.rst b/docs/plugins/viewmodels.rst index ab2e4e28..f389ce5c 100644 --- a/docs/plugins/viewmodels.rst +++ b/docs/plugins/viewmodels.rst @@ -6,7 +6,7 @@ Viewmodels .. contents:: :local: -When implementing frontend components, you'll sooner or later want to define your own `KnockoutJS viewmodels `_ +When implementing frontend components, you'll sooner or later want to define your own `KnockoutJS view models `_ in order to provide custom functionality. .. _sec-plugins-viewmodels-registering: @@ -14,112 +14,171 @@ in order to provide custom functionality. Registering custom viewmodels ----------------------------- -Register your viewmodel with OctoPrint's web app by pushing a 3-tuple consisting of your viewmodel's class, a list -of all required dependencies to be injected into the constructor and a list of all elements to bind the viewmodel to. +Register your view model with OctoPrint's web app by pushing a config object unto the global array ``OCTOPRINT_VIEWMODELS``. + +Possible properties in this config object are: + +construct + Function to use for constructing the view model instance. Usually that will be the view model class, which acts as + a constructor. This property is mandatory. +name + Name to register the view model under. If not provided, the name of the ``construct`` function will + be used, turning the first letter lower case. If a view model under the same name already exists at time + of construction, an error will be logged and the view model will not be instantiated. +additionalNames + A list of additional names to also register the view model under. Only those that do not already exist will be + registered. +dependencies + List of dependencies the view model needs injected. If any of the view models in this list cannot be found, + initialization of the view model will fail. The parameters injected on instantiation will consist of first + the ``dependencies``, then the ``optional`` list concatenated. +optional + A list of optional dependencies the view model needs injected. If any of the view models in this list cannot be found, + they will be ``null`` in the parameter list injected to the constructor on instantiation. The parameters injected on + instantiation will consist of first the ``dependencies``, then the ``optional`` list concatenated. +elements + A list of UI elements to bind to. Each binding target can be either a string which will then be passed to jQuery's + ``$(...)`` method to resolve the target, or alternatively directly a jQuery element Example: .. code-block:: javascript + :emphasize-lines: 12-22 + + $(function() { + function MyCustomViewModel(parameters) { + var self = this; + + self.loginState = parameters[0]; // requested as first dependency below + self.settings = parameters[1]; // requested as second dependency below + self.someOtherViewModel = parameters[2]; // requested as first optional dependency below + + // more of your view model's implementation + } + + // we don't explicitely declare a name property here + // our view model will be registered under "myCustomViewModel" (implicit + // name derived from contructor name) and "yourCustomViewModel" (explicitely + // provided as additional name) + OCTOPRINT_VIEWMODELS.push({ + construct: MyCustomViewModel, + additionalNames: ["yourCustomViewModel"], + dependencies: ["loginStateViewModel", "settingsViewModel"], + optional: ["someOtherViewModel"], + elements: ["#some_div", "#some_other_div"] + }); + }) + +You might also come across a different approach to view model declaration, providing not a config object but instead +a 3-tuple of constructor, dependencies and elements to bind to. Additional names, different names than the default name +and optional dependencies cannot be specified with this format. It should be considered deprecated. Still, an example +of how that would look in practice is provided here as well: + +.. code-block:: javascript + :caption: Old tuple-based configuration format, consider this deprecated + :emphasize-lines: 11-16 $(function() { function MyCustomViewModel(parameters) { var self = this; self.loginState = parameters[0]; // requested as first constructor parameter below + self.settingsViewModel = parameters[1] // requested as second constructor parameter below - // more of your viewmodel's implementation + // more of your view model's implementation } + // construct, dependencies, elements OCTOPRINT_VIEWMODELS.push([ MyCustomViewModel, - ["loginStateViewModel"], + ["loginStateViewModel", "settingsViewModel"], ["#some_div", "#some_other_div"] ]); }) -.. note:: - - Each provided binding target may be either a string which will then be passed to jQuery's ``$(...)`` method to resolve - the target, or alternatively directly a jQuery element. - .. _sec-plugins-viewmodels-dependencies: Dependencies ------------ -OctoPrint will try to inject all viewmodel dependencies requested by your viewmodel. In order to do this it will -perform multiple passes iterating over all registered viewmodels and collecting the necessary dependencies prior to +OctoPrint will try to inject all view model dependencies requested by your view model. In order to do this it will +perform multiple passes iterating over all registered view models and collecting the necessary dependencies prior to construction. Circular dependencies (A depends on B, B on C, C on A) naturally cannot be resolved and will cause an error to be logged to the JavaScript console. -OctoPrint's core currently comes with the following viewmodels that your plugin can request for injection: +OctoPrint's core currently comes with the following view models that your plugin can request for injection: appearanceViewModel - Viewmodel that holds the appearance settings (name, color and transparency flag). + View model that holds the appearance settings (name, color and transparency flag). connectionViewModel - Viewmodel for the connection sidebar entry. + View model for the connection sidebar entry. controlViewModel - Viewmodel for the control tab. -gcodeFilesViewModel - Viewmodel for the files sidebar entry. + View model for the control tab. +filesViewModel + View model for the files sidebar entry. Also available under the deprecated name ``gcodeFilesViewModel``. firstRunViewModel - Viewmodel for the first run dialog. + View model for the first run dialog. gcodeViewModel - Viewmodel for the gcode viewer tab. + View model for the gcode viewer tab. +gcodeFilesViewModel + Deprecated in favor of ``filesViewModel``. logViewModel - Viewmodel for the logfile settings dialog. + View model for the logfile settings dialog. loginStateViewModel - Viewmodel for the current loginstate of the user, very interesting for plugins that need to + View model for the current loginstate of the user, very interesting for plugins that need to evaluate the current login state or information about the current user, e.g. associated roles. navigationViewModel - Viewmodel for the navigation bar. + View model for the navigation bar. printerProfilesViewModel - Viewmodel for the printer profiles settings dialog. + View model for the printer profiles settings dialog. printerStateViewModel - Viewmodel for the current printer state, very interesting for plugins that need + View model for the current printer state, very interesting for plugins that need to know information about the current print job, if the printer is connected, operational etc. settingsViewModel - Viewmodel for the settings dialog, also holds all settings to be used by other viewmodels, hence + View model for the settings dialog, also holds all settings to be used by other view models, hence very interesting for plugins as well. slicingViewModel - Viewmodel for the slicing dialog. + View model for the slicing dialog. temperatureViewModel - Viewmodel for the temperature tab, also holds current temperature information which + View model for the temperature tab, also holds current temperature information which might be interesting for plugins. terminalViewModel - Viewmodel for the terminal tab, also holds terminal log entries. + View model for the terminal tab, also holds terminal log entries. timelapseViewModel - Viewmodel for the timelapse tab. + View model for the timelapse tab. usersViewModel - Viewmodel for the user management in the settings dialog. + View model for the user management in the settings dialog. userSettingsViewModel - Viewmodel for settings associated with the currently logged in user, used for + View model for settings associated with the currently logged in user, used for the user settings dialog. +wizardViewModel + View model for the wizard dialog. -Additionally each plugin's viewmodel will be added to the viewmodel map used for resolving dependencies as well, using -the viewmodel's class name with a lower case first character as identifier (so "MyCustomViewModel" will be registered -for dependency injection as "myCustomViewModel"). +Each plugin's view model will be added to the view model map used for resolving dependencies as well, using +the view model's class name with a lower case first character as identifier (so "MyCustomViewModel" will be registered +for dependency injection as "myCustomViewModel") or an alternative name provided in the ``name`` property of the +config object, plus any configured ``additionalNames``. .. _sec-plugins-viewmodels-callbacks: Callbacks --------- -OctoPrint's web application will call several callbacks on all registered viewmodels, provided they implement them. +OctoPrint's web application will call several callbacks on all registered view models, provided they implement them. Those are listed below: onStartup() - Called when the first initialization has been done: All viewmodels are constructed and hence their dependencies + Called when the first initialization has been done: All view models are constructed and hence their dependencies resolved, no bindings have been done yet. onBeforeBinding() - Called per viewmodel before attempting to bind it to its binding targets. + Called per view model before attempting to bind it to its binding targets. onAfterBinding() - Called per viewmodel after binding it to its binding targets. + Called per view model after binding it to its binding targets. onAllBound(allViewModels) - Called after all viewmodels have been bound, with the list of all viewmodels as the single parameter. + Called after all view models have been bound, with the list of all view models as the single parameter. onStartupComplete() Called after the startup of the web app has been completed. @@ -161,60 +220,64 @@ onUserLoggedIn(user) onUserLoggedOut() Called when a user gets logged out of the web app. -onTabChange(current, previous) +onTabChange(next, current) Called before the main tab view switches to a new tab, so `before` the new tab becomes visible. Called with the - current and previous tab's hash (e.g. ``#control``). + next (changed to) and current (still visible) tab's hash (e.g. ``#control``). Note that ``current`` might be undefined + on the very first call. -onAfterTabChange +onAfterTabChange(current, previous) Called after the main tab view switches to a new tab, so `after` the new tab becomes visible. Called with the current and previous tab's hash (e.g. ``#control``). -getAdditionalControls - Your viewmodel may return additional custom control definitions for inclusion on the "Control" tab of OctoPrint's +getAdditionalControls() + Your view model may return additional custom control definitions for inclusion on the "Control" tab of OctoPrint's interface. See :ref:`the custom control feature`. -onSettingsShown +onSettingsShown() Called when the settings dialog is shown. -onSettingsHidden +onSettingsHidden() Called when the settings dialog is hidden. -onSettingsBeforeSave - Called just before the settings viewmodel is sent to the server. This is useful, for example, if your plugin - needs to compute persisted settings from a custom viewmodel. +onSettingsBeforeSave() + Called just before the settings view model is sent to the server. This is useful, for example, if your plugin + needs to compute persisted settings from a custom view model. -onUserSettingsShown +onUserSettingsShown() Called when the user settings dialog is shown. -onUserSettingsHidden +onUserSettingsHidden() Called when the user settings dialog is hidden. -onWizardDetails +onWizardDetails(response) Called with the response from the wizard detail API call initiated before opening the wizard dialog. Will contain the data from all :class:`~octoprint.plugin.WizardPlugin` implementations returned by their :meth:`~octoprint.plugin.WizardPlugin.get_wizard_details` method, mapped by the plugin identifier. -onWizardTabChange - Called before the wizard tab/step is changed, with the ids of the current and the next tab as parameters. Return false - in order to prevent the tab change, e.g. if the wizard step is mandatory and not yet completed by the user. Take a look at - the "Core Wizard" plugin bundled with OctoPrint and the ACL wizard step in particular for an example on how to use this. +onBeforeWizardTabChange(next, current) + Called before the wizard tab/step is changed, with the ids of the next (changed to) and the current (still visible) tab + as parameters. Return false in order to prevent the tab change, e.g. if the wizard step is mandatory and not yet + completed by the user. Take a look at the "Core Wizard" plugin bundled with OctoPrint and the ACL wizard step in + particular for an example on how to use this. -onAfterWizardTabChange - Called after the wizard tab/step is changed, with the id of the current tab as parameter. +onAfterWizardTabChange(current) + Called after the wizard tab/step is changed, with the id of the current tab as parameter. The id of the previous + tab is sadly not available currently. -onBeforeWizardFinish +onBeforeWizardFinish() Called before executing the finishing of the wizard. Return false here to stop the actual finish, e.g. if some step is still incomplete. -onWizardFinish +onWizardFinish() Called after executing the finishing of the wizard and before closing the dialog. Return ``reload`` here in order to instruct OctoPrint to reload the UI after the wizard closes. -In order to hook into any of those callbacks, just have your viewmodel define a function named accordingly, e.g. -to get called after all viewmodels have been bound during application startup, implement a function ``onAllBound`` -on your viewmodel, taking a list of all bound viewmodels: +In order to hook into any of those callbacks, just have your view model define a function named accordingly, e.g. +to get called after all view models have been bound during application startup, implement a function ``onAllBound`` +on your view model, taking a list of all bound view models: .. code-block:: javascript + :emphasize-lines: 7-8 $(function() { function MyCustomViewModel(parameters) { @@ -229,17 +292,17 @@ on your viewmodel, taking a list of all bound viewmodels: // ... } - OCTOPRINT_VIEWMODELS.push([ - MyCustomViewModel, - ["loginStateViewModel"], - ["#some_div", "#some_other_div"] - ]); + OCTOPRINT_VIEWMODELS.push({ + construct: MyCustomViewModel, + dependencies: ["loginStateViewModel"], + elements: ["#some_div", "#some_other_div"] + }); }) .. seealso:: `OctoPrint's core viewmodels `_ - OctoPrint's own viewmodels use the same mechanisms for interacting with each other and the web application as + OctoPrint's own view models use the same mechanisms for interacting with each other and the web application as plugins. Their source code is therefore a good point of reference on how to achieve certain things. `KnockoutJS documentation `_ OctoPrint makes heavy use of KnockoutJS for building up its web app. diff --git a/src/octoprint/static/js/app/helpers.js b/src/octoprint/static/js/app/helpers.js index 421ff4c7..d7a186b9 100644 --- a/src/octoprint/static/js/app/helpers.js +++ b/src/octoprint/static/js/app/helpers.js @@ -842,7 +842,7 @@ function callViewModelsIf(allViewModels, method, condition, callback) { } } else { // provide the method to the callback - callback(viewModel[method]); + callback(viewModel[method], viewModel); } } }); diff --git a/src/octoprint/static/js/app/main.js b/src/octoprint/static/js/app/main.js index e3d9e8b8..a5677a75 100644 --- a/src/octoprint/static/js/app/main.js +++ b/src/octoprint/static/js/app/main.js @@ -271,10 +271,14 @@ $(function() { // if name is not set, get name from constructor, if it's an anonymous function generate one viewModel.name = viewModel.name || _getViewModelId(viewModel.construct.name) || _.uniqueId("unnamedViewModel"); + // no alternative names? empty array + viewModel.additionalNames = viewModel.additionalNames || []; + // make sure all value's are in an array viewModel.dependencies = (_.isArray(viewModel.dependencies)) ? viewModel.dependencies : [viewModel.dependencies]; viewModel.elements = (_.isArray(viewModel.elements)) ? viewModel.elements : [viewModel.elements]; viewModel.optional = (_.isArray(viewModel.optional)) ? viewModel.optional : [viewModel.optional]; + viewModel.additionalNames = (_.isArray(viewModel.additionalNames)) ? viewModel.additionalNames : [viewModel.additionalNames]; // make sure that we don't have two view models going by the same name if (_.has(viewModelMap, viewModel.name)) { @@ -300,6 +304,20 @@ $(function() { allViewModelData.push([viewModelInstance, viewModelBindTargets]); allViewModels.push(viewModelInstance); viewModelMap[viewModel.name] = viewModelInstance; + + if (viewModel.additionalNames.length) { + var registeredAdditionalNames = []; + _.each(viewModel.additionalNames, function(additionalName) { + if (!_.has(viewModelMap, additionalName)) { + viewModelMap[additionalName] = viewModelInstance; + registeredAdditionalNames.push(additionalName); + } + }); + + if (registeredAdditionalNames.length) { + log.debug("Registered", viewModel.name, "under these additional names:", registeredAdditionalNames); + } + } } // anything that's now in the postponed list has to be readded to the unprocessedViewModels diff --git a/src/octoprint/static/js/app/viewmodels/files.js b/src/octoprint/static/js/app/viewmodels/files.js index 750c7dec..c6481410 100644 --- a/src/octoprint/static/js/app/viewmodels/files.js +++ b/src/octoprint/static/js/app/viewmodels/files.js @@ -1,5 +1,5 @@ $(function() { - function GcodeFilesViewModel(parameters) { + function FilesViewModel(parameters) { var self = this; self.settingsViewModel = parameters[0]; @@ -1058,9 +1058,11 @@ $(function() { } } - OCTOPRINT_VIEWMODELS.push([ - GcodeFilesViewModel, - ["settingsViewModel", "loginStateViewModel", "printerStateViewModel", "slicingViewModel","printerProfilesViewModel"], - ["#files_wrapper", "#add_folder_dialog"] - ]); + OCTOPRINT_VIEWMODELS.push({ + construct: FilesViewModel, + name: "filesViewModel", + additionalNames: ["gcodeFilesViewModel"], + dependencies: ["settingsViewModel", "loginStateViewModel", "printerStateViewModel", "slicingViewModel", "printerProfilesViewModel"], + elements: ["#files_wrapper", "#add_folder_dialog"], + }); }); diff --git a/src/octoprint/static/js/app/viewmodels/wizard.js b/src/octoprint/static/js/app/viewmodels/wizard.js index adf7e1bf..4f460989 100644 --- a/src/octoprint/static/js/app/viewmodels/wizard.js +++ b/src/octoprint/static/js/app/viewmodels/wizard.js @@ -97,7 +97,17 @@ $(function() { if (current != undefined && next != undefined) { var result = true; - callViewModels(allViewModels, "onWizardTabChange", function(method) { + callViewModels(allViewModels, "onBeforeWizardTabChange", function(method) { + // we want to continue evaluating even if result becomes false + result = (method(next, current) !== false) && result; + }); + + // also trigger the onWizardTabChange event here which we misnamed and + // on which we misordered the parameters on during development but which might + // already be used somewhere - log a deprecation warning to console though + callViewModels(allViewModels, "onWizardTabChange", function(method, viewModel) { + log.warn("View model", viewModel, "is using deprecated callback \"onWizardTabChange\", please change to \"onBeforeWizardTabChange\""); + // we want to continue evaluating even if result becomes false result = (method(current, next) !== false) && result; });