How does JUCI work

Date: 23 May 2016
Author: Martin K. Schröder <mkschreder.uk@gmail.com>

When you first visit the homepage of your router, your browser will request the index.html file located in the www root. When JUCI is built, this file is automatically generated to include relevant plugins and styles that are part of the JUCI installation on that router. The file is generated by a script called juci-update and can be regenerated again by running the same script on the box. In fact, this script is run by any complementary plugin package that is installed after the main JUCI installation is in place in order to get the plugin loaded in the client browser.

index.html loaded..

Next, your browser will request the plugin .js files which it finds in the index.html file. These are usually loaded in the following order:

  • JUCI core (juci.js)
  • Plugins
  • Theme .js

In reality you could just have a single .js file that would contain all html templates and code, but because JUCI is modular things have been separated into several files. In fact, when JUCI project was just starting out, all files were loaded separately. That however was very inefficient so files that belonged to the same module were concatenated together into single .js files.

JUCI html code is compiled into the .js files because the templates are in fact added to the Angular.js template cache and it makes sense to just generate JavaScript code from all the templates to keep the number of files that we need to load to a minimum. This conversion process is done by a script called scripts/juci-build-tpl-cache which is called when you build juci.

Styles in JUCI are compiled by lessc from less source code. This allows you to write very compact and nested css code which then gets converted into CSS code that browsers can understand.

The JUCI Core

The first piece of code that gets loaded by the browser is the juci core. It registers several hooks for angular.js that will be called once the application is bootstrapped. Note that in JUCI, Angular.js is bootstrapped later and not when it loads. This allows you to extend angular application through plugins that are loaded after the main juci core without having to extend an already bootstrapped application (which is tricky at best). So all of your plugins will load first, then JUCI will initialize itself first (see app.js file in juci core), and only after that bootstrap angular and switch to the default view (which, unless you are logged in, would be the login page).

The angular application object is then exposed through the global JUCI object as JUCI.app field. You can access this object directly in any global JavaScript code in your plugins and in fact this is done very frequently to extend angular.js with new directives and controllers.

Note: you should never have freestanding javascript code in the global
scope. If your plugin needs to initialize something early then use either
JUCI.app.run or JUCI.app.configure. Since app here is the angular
application, these two calls are equivalent to angular configure() and
run() application hooks. 

All application configuration and initialization should either be done through the JUCI object or by registering callbacks with one of the hooks that it exposes.

Plugins

Plugins in JUCI are simply libraries of code that register new directives and new html templates. JUCI core already exposes quite a few core components such as list editors and config option groups. These are available in the juci/widgets folder in the juci source code. These generic controls can be used in any plugin.

Plugins can also register pages, which in fact work the same way as directives used for widgets (page components), however they are defined as a pair of files in the pages folder instead of widgets and they use an html template that follows a special format. The template encapsulates the contents of the page into on of the layout widgets that JUCI provides. The most common is juci-layout-with-sidebar. Then a page usually has a div with an explicitly specified controller that is defined in the .js file of the page.

Pages are available for inclusion into the menu layout by adding appropriate sections to the /etc/config/juci uci file. The menu system in juci is what forms the page layout of the gui and it is fully configurable through that uci config.

Plugins can also provide several widgets (or page components) which can be used not only by the plugin itself but also by other plugins. Widgets usually consist of an html file and a js file, just like pages, but their html template is a raw component without any layout attached to it so that it can be embedded as a control into any other page or widget in juci.

Widgets are included into other widgets by simply using the html directives that get defined by angular based on the directives defined as part of the widget code using JUCI.app.directive hook.

The fact that controller (code) and html template (view) are separated in JUCI, allows you to do interesting things. For example, in many cases you need to modify a page for a customer to look differently from what it looks in the default JUCI code. You can easily do this without modifying juci by simply defining a new page that will use the same controller (defined in JUCI) but with a different html template. Since controller does not know about the HTML template, it does not matter for it that the html template is different. This way you only need to duplicate parts of the html template without having to duplicate the code of the controller. If you need to add extra functinality to the page then you also can create a separate controller for that functionality and then include a div into your new html template that would use angular ng-controller directive to reference the new controller for that div only. Because the controller embedded in the page will also have access to the parent scope (the scope of the page controller), you can reference the same variables that are created by the page controller inside your new controller and by so doing extend the functionality of the page controller without modifying it's code.

Themes

After all plugins have been loaded, theme is loaded last. JUCI code does not contain a theme by default so if you have not compiled in a theme into your juci install you will see the default bootstrap theme and it will most probably look very ugly. The theme in juci is generally just like any other plugin, but because it defines css styles for global elements you should only ever select a single theme for your juci install to avoid getting a very weird look. Basically all code inside a theme plugin assumes that it is the only theme plugin selected.

Through the theme you can modify any aspect of the juci layout. In many cases themes override default layout widget html templates to give the gui a different look. To do that, a theme simply needs to create an html template file with the same name inside it's widgets directory. Since theme is loaded last, the html templates that are compiled into the theme will replace the templates of the previously loaded widgets within the angular.js template cache. The result of this is that they will be overridden by the theme.

JUCI RPC

Once all of the layout is loaded and you are redirected to the landing page (or the page specified in the url), juci bootstraps angular and that in turn constructs the visible layout of the page. Each controller is called and it sets up the angular $scope for that particular instance of the widget. Controllers in turn make remote RPC calls to the juci backend to retreive data that they then make available to the html view through the scope variable so that the data can be displayed to the user.

Because controllers are always completely independent of eachother (unless you create an "extension" controller where you expect certain variables to be present in the parent scope) many rpc calls are done several times. In the case of normal rpc calls that may return different data each time, juci does not cache the data so it is up to you to keep the number of calls to a minimum by clever design of your pages. For UCI data (retreived by calling $uci.$sync() function) JUCI does however cache the data and will not reload it until you completely reload the page (in which case you will lose your chages).

Any controller that does $uci.$sync does not need to care about doing it too many times, since if the data has already been loaded from the backend, the call will simply resolve the promise and no new rpc call will be made. There is a way to override this functionality but generally you should not mess with it because it will interfere with the generic Apply panel that appears at the bottom of the page when changes are pending.

Note on UCI RPC: even though juci makes uci object freely available to any
controller through $rpc.uci object, you should never use these functions
directly. You should for example never call $rpc.uci.commit directly
because that will bypass any checks done by the $uci subsystem and
generally mess up your state. Instead there is a call called $uci.$save()
that saves all uci changes, however you should not use that one either
because it will fail if there are errors. Instead, just leave the saving to
the Apply panel and let the user save his changes whenever he feels like
doing it.

User makes his changes

Once all the data is loaded the gui sits idle. There can be interval actions registered by certain widgets and pages (through JUCI.interval.repeat hook for example), which may regularly make rpc calls to reload certain data. This is the only actions that should be happening when gui is idle.

All controls and widgets usually either use raw loaded uci objects, or they break the data down into local variables that are then displayed to the user and which the user can edit. Usually a controller would need to "watch" such a value for changes and then update the uci model. The Apply button panel at the bottom of the page appears whenever there is some change to the internal cached uci objects that have been loaded by JUCI. User can then optionally save the modified state to UCI or to cancel his changes by reloading the page. When some control directly operates on the field.value of a uci section then the change is stored in the local uci object until user writes the change to the router by pressing the global Apply button.

A large portion of JUCI controls use the objects generated by the uci subsystem directly. There is a much smaller number of cases where this data needs to be split up and placed into a temporary variable. These cases usually result in a more complicated controller so generally it is recommended to bind the view directly to uci objects inside juci core.

User saves his changes

When user presses the Apply button at the bottom of the page, a $uci.$save call is made. This function will go over all sections that are loaded locally and check them for changed values. It will also validate all changed values first and bail out if it encounters any errors. The errors will be shown on the Apply button panel. If all sections validate, these values are then saved by doing multiple uci.set calls followed by a uci.commit call for that config.

JUCI local validation is only there to provide a good user experience. It is not designed to restrict user from actually setting a value that is invalid by doing it manually. Such functionality is not possible right now since it would require that there is uci validation in place for all uci sections. Such validation is at the time of this writing not there in openwrt. The security JUCI does provide is access control to individual config files making it possible to restring a user from writing or reading a certain config file.

Read more

For more juci documentation visit http://mkschreder.github.io/juci