LuCI JavaScript-API renders views on the client side, resulting in accelerated performance of the Web interface and offering developers more convenient tools for web interface creation. This tutorial will demonstrate how to create a simple LuCI form view using the JavaScript API. Throughout this tutorial, it is recommended referring to the API Reference for comprehensive details on the mentioned functions and classes.
LuCI apps are typically developed for embedded Linux systems like routers, so you’ll need access to such a system for testing and deployment. Here are the prerequisites and steps to get started:
Let’s assume you want to create a new application example. To set up a new LuCI application you need to create 3 files:
example.js file in /www/luci-static/resources/view/
directory
luci-app-example.json file in /usr/share/luci/menu.d/
directory, which registers the app in LuCI dispatching tree and defines ACL file
{
"admin/example": {
"title": "Example LuCI-App",
"order": 10,
"action": {
"type": "firstchild",
"recursive": true
},
"depends": {
"acl": [ "luci-app-example" ],
"uci": { "rpcd": true }
}
},
"admin/example/example": {
"title": "Example LuCI-App",
"order": 10,
"action": {
"type": "view",
"path": "example"
},
"depends": {
"acl": [ "luci-app-example" ],
"uci": { "rpcd": true }
}
}
}
/usr/share/rpcd/acl.d/
directory, which defines app’s permissions{
"luci-app-example": {
"description": "Grant access to Example config",
"read": {
"uci": ["example", "example_helper"],
"ubus": {
"system": ["*"]
},
"file": {
"/tmp/example.txt": ["read", "write", "exec"]
}
},
"write": {
"uci": ["example", "example_helper"]
}
}
}
As shown in ACL file example app will use two UCI files and example.txt file with some text
1. example
config first_section
option some_bool '1'
option some_address '172.19.100.43'
option some_file_dir '/tmp/example.txt'
2. example_helper
config some_choice
option id '1234'
option choice 'Red'
config some_choice
option id '4231'
option descr 'Green'
config some_choice
option id '4321'
option descr 'Blue'
The following code maps the example configuration file
'use strict';
'require form';
return L.view.extend({
render: function () {
var m, s, o;
m = new form.Map('example', 'Example form');
s = m.section(form.TypedSection, 'first_section', 'The first section',
'This sections maps "config example first_section" of /etc/config/example');
//s.anonymous = true;
o = s.option(form.Flag, 'some_bool', 'A checkbox option');
o = s.option(form.ListValue, 'some_choice', 'A select element');
o.value('choice1', 'The first choice');
o.value('choice2', 'The second choice');
return m.render()
}
});
This code is essentially defining a configuration form with two options: a checkbox option (‘some_bool’) and a select element (‘some_choice’) with two choices (‘The first choice’ and ‘The second choice’). The form is organized into sections, and the entire form is rendered when the ‘render’ function is called. The configuration data collected through this form is typically used to configure settings in the ‘/etc/config/example’ configuration file.
Let’s break down the code step by step:
To access the LuCI web interface enter the IP address of your OpenWRT in a web browser. Example: http://192.168.1.1
Let’s read values for ListValue from example_helper .
You can load UCI configuration data using the uci module before render function:
//..
return L.view.extend({
load: function () {
return Promise.all([
uci.load('example_helper')
]);
},
render: function () {
//..
o = s.option(form.ListValue, 'some_choice', 'A select element');
var choiceList = uci.sections('example_helper', 'some_choice')
choiceList.forEach(choice => o.value(choice['id'], choice['name']));
return m.render()
}
});
It is possible to extend and override methods inherited from the AbstractValue class. Let’s define custom write function for MultiValue class.
Add following option to the section
o = s.option(form.MultiValue, "multi_choice", "A select multiple elements")
choiceList.forEach(choice => o.value(choice['name']));
o.display_size = 4;
Multivalue saves values by default in the following form:
list multichoice 'White'
list multichoice 'Red'
list multichoice 'Green'
To save multi_choice option like:
option multichoice 'White Red Green'
you can override the option’s write function like this:
o.write = function (section_id, value) {
uci.set('example', section_id, 'multi_choice', value.join(' '));
}
Let’s reate a custom option value node that pings IP Address. To create new DOM Elements LuCI uses E() function which is alias for LuCI.dom.create() First import ui and dom modules.
The following code defines a custom form widget named CBIPingAddress by extending the form.Value class. This custom widget is designed to render an input field along with a “Ping” button that allows you to test the connectivity of a network device using a given IP address or hostname:
var CBIPingAddress = form.Value.extend({
renderWidget: function (section_id, option_index, cfgvalue) {
var node = this.super('renderWidget', [section_id, option_index, cfgvalue]);
dom.append(node,
E('button', {
'class': 'btn cbi-button-edit',
'id': 'custom-ping-button',
'style': 'vertical-align: bottom; margin-left: 1em;',
'click': ui.createHandlerFn(this, function () {
L.resolveDefault(ui.pingDevice('http', cfgvalue), 'error').then(
result => {
if (result === 'error') alert('ERROR: Device ' + cfgvalue + ' is not reachable');
else if (result === 'null') alert('The connectivity check timed out');
else alert('Device ' + cfgvalue + ' is reachable');
})
.catch((error) => {
alert(error)
});
})
}, 'Ping'))
return node
}
});
To render the custom widget just pass CBIPingAddress as a first parameter to s.option method:
o = s.option(CBIPingAddress, 'some_address', 'IP-Address');
LuCI API offers some modules to interact with backend to enable RPC (Remote Procedure Call) communication with the router and its services like LuCI.rpc, LuCI.fs and LuCI.uci. Defining permissions for ubus methods, files, and uci configurations in a corresponding ACL (Access Control List) file is a crucial step. Note: All RPC related methods return a Promise.
This widget is uses fs module to read the content of a file specified by cfgvalue and displays it in a modal dialog.
var CBIReadFile = form.Value.extend({
renderWidget: function (section_id, option_index, cfgvalue) {
var node = this.super('renderWidget', [section_id, option_index, cfgvalue]);
dom.append(node,
E('button', {
'class': 'btn cbi-button-edit',
'style': 'vertical-align: bottom; margin-left: 1em;',
'click': ui.createHandlerFn(this, function () {
L.resolveDefault(fs.read(cfgvalue), 'Error: Could not read file').then(
result => {
ui.showModal(_('File Content'), [
E('p', _(result)),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'btn cbi-button-edit',
'click': ui.createHandlerFn(this, function () {
ui.hideModal();
})
}, [_('Close modal')]),
])
]);
})
}),
}, 'Read'))
return node
}
});
The CBIBoardInfo widget is used to display information about the router’s hardware board, such as its hostname, model, and board name.
var callBoardInfo = rpc.declare({
object: 'system',
method: 'board',
params: []
});
var CBIBoardInfo = form.TextValue.extend({
renderWidget: function (section_id, option_index, cfgvalue) {
var node = this.super('renderWidget', [section_id, option_index, cfgvalue]);
L.resolveDefault(callBoardInfo(), 'unknown').then(function (result) {
console.log(result)
var contentNode = [
E('p', {}, 'Hostname : ' + result['hostname']),
E('p', {}, 'Model : ' + result['model']),
E('p', {}, 'Board name : ' + result['board_name'])
]
dom.content(node, contentNode)
})
return node
}
});
var boardInfo = rpc.declare({ … }); declares a function named boardInfo. It uses the rpc.declare function to wrap the following ubus call:
root@vfc_x64:~# ubus call system board
{
"kernel": "5.10.134",
"hostname": "vfc_x64.test.lan",
"system": "Common KVM processor",
"model": "QEMU Standard PC (i440FX + PIIX, 1996)",
"board_name": "qemu-standard-pc-i440fx-piix-1996",
"rootfs_type": "squashfs",
"release": {
"distribution": "OpenMonitoring",
"version": "0.4.0",
"revision": "r19590-042d558536",
"target": "x86/64",
"description": "OpenMonitoring 0.4.0 release"
}
}
‘params’: [] indicates that the ‘board’ method does not require any parameters. To call an ubus method with parameters specify their names in params Array as string.
To save changes made to a form you need click on Save&Apply button. It is possible to trigger Save&Apply button with Vanilla JavaScript or Jquery, but it’s not considered an elegant approach. Save&Apply, Save and Reset buttons are rendered by default. To remove them override handleSaveApply, handleSave and handleReset functions of view module by setting them to null:
'use strict';
'require form';
'require uci';
return L.view.extend({
load: function () {
//..
},
render: function () {
//..
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});
Let’s extend the MultiValue widget by appending a button that will set multi_choice to ‘White’, then save and apply changes:
var CBIMultiValue = form.MultiValue.extend({
renderWidget: function (section_id, option_index, cfgvalue) {
var node = this.super('renderWidget', [section_id, option_index, cfgvalue]);
dom.append(node,
E('button', {
'class': 'btn cbi-button-default',
'style': '',
'click': ui.createHandlerFn(this, function () {
uci.set('example', section_id, 'multi_choice', 'White');
uci.save()
.then(L.bind(ui.changes.init, ui.changes))
.then(L.bind(ui.changes.apply, ui.changes))
.then(ui.hideModal());
})
}, 'Set default'))
return node
}
});
The button handler function sets a configuration value, saves the changes, initializes changes using ui.changes.init, applies the changes using ui.changes.apply, and hides a modal dialog.
The polling loop, powered by the LuCI.poll class, is often used in LuCI applications to periodically check for changes in configuration settings, monitor system status, or update the user interface with real-time information.
To create a simple example of displaying real-time updates using poll first define a function, which shows a current time.
function showCurrentTime() {
var date = new Date().toLocaleString();
var datetimeNode = E('p', {'class' : '', 'style': 'color: #004280;'}, date)
$('h3').html(datetimeNode)
}
And before calling m.render bind the showCurrentTime function to the current context and add it to poll, which will run every second.
var pollfunction = L.bind(showCurrentTime, this);
poll.add(pollfunction, 1);
For more LuCI Framework Documentation: References and HowTos