Hmm, let me think - do I know anyone using editableList? Maybe I do ... wait one ...
Here is a simple section taken from the uibuilder v2 admin panel:
<!-- ---- Hideable NPM Section ---- -->
<div id="npm-props">
<h3>Manage Front-End Libraries</h3>
<p>
Install, remove or update npm packages that provide front-end libraries such as
VueJS, jQuery, MoonJS, etc.
</p>
<p>
You can search for packages on the
<a href="https://www.npmjs.com/" target="_blank">official npm site</a>
or on <a href="https://npms.io/" target="_blank">npms.io</a>.
Take the name and paste it below then use one of the buttons.
</p>
<!-- Package List -->
<div class="form-row">
<ol id="node-input-packageList"></ol>
</div>
</div>
And here is some code to go with it:
RED.nodes.registerType('uibuilder', {
....
/** AddItem function for package list
* @param {JQuery<HTMLElement>} element the jQuery DOM element to which any row content should be added
* @param {number} index the index of the row
* @param {string|*} data data object for the row. {} if add button pressed, else data passed to addItem method
*/
function addPackageRow(element,index,data) {
var hRow = ''
if (Object.entries(data).length === 0) {
// Add button was pressed so we have no packageName, create an input form instead
hRow='<input type="text" id="packageList-input-' + index + '"> <button id="packageList-button-' + index + '">Install</button>'
} else {
// addItem method was called with a packageName passed
hRow = data
}
// Build the output row
var myRow = $('<div id="packageList-row-' + index + '" class="packageList-row-data">'+hRow+'</div>').appendTo(element)
// Create a button click listener for the install button for this row
$('#packageList-button-' + index).click(function(){
//console.log('.packageList-row-data button::click', $('#packageList-input-' + index).val() )
// show activity spinner
$('i.spinner').show()
const packageName = '' + $('#packageList-input-' + index).val()
if ( packageName.length !== 0 ) {
// Call the npm installPackage API (it updates the package list)
$.get( 'uibnpmmanage?cmd=install&package=' + packageName, function(data){
//console.log('.packageList-row-data get::uibnpm', data )
if ( data.success === true) {
console.log('PACKAGE INSTALLED')
// Replace the input field with the normal package name display
myRow.html(packageName)
} else {
console.log('ERROR ON INSTALLATION ' )
console.dir( data.result )
}
// Hide the progress spinner
$('i.spinner').hide()
}).fail(function(jqXHR, textStatus, errorThrown) {
console.error( '[uibuilder:addPackageRow:get] Error ' + textStatus, errorThrown )
$('i.spinner').hide()
return 'addPackageRow failed'
// TODO otherwise highlight input
})
} // else Do nothing
}) // -- end of button click -- //
} // --- End of addPackageRow() ---- //
/** RemoveItem function for package list */
function removePackageRow(packageName) {
console.log('PACKAGE NAME: ', packageName)
// If package name is an empty object - user removed an add row so ignore
if ( (packageName === '') || (typeof packageName !== 'string') ) {
return false
}
// show activity spinner
$('i.spinner').show()
// Call the npm installPackage API (it updates the package list)
$.get( 'uibnpmmanage?cmd=remove&package=' + packageName, function(data){
console.log('removePackageRow get::uibnpm', data )
if ( data.success === true) console.log('PACKAGE REMOVED')
else {
console.log('ERROR ON REMOVAL ', data.result )
// Put the entry back again
$('#node-input-packageList').editableList('addItem',packageName)
}
$('i.spinner').hide()
}).fail(function(jqXHR, textStatus, errorThrown) {
console.error( '[uibuilder:removePackageRow:get] Error ' + textStatus, errorThrown )
// Put the entry back again
$('#node-input-packageList').editableList('addItem',packageName)
$('i.spinner').hide()
return 'removePackageRow failed'
// TODO otherwise highlight input
})
} // ---- End of removePackageRow ---- //
/** Get full package list via API and show in admin ui
* param {string} url
* param {boolean} rebuild - Rebuild the vendorPaths list
*/
function packageList() {
$.getJSON('uibvendorpackages', function(vendorPaths) {
console.log('uibuilder:packageList:uibvendorpackages', vendorPaths)
$('#node-input-packageList').editableList('empty');
const pkgList = Object.keys(vendorPaths)
pkgList.forEach(function(packageName,index){
if ( packageName !== 'socket.io' )
$('#node-input-packageList').editableList('addItem',packageName)
})
})
} // --- End of packageList --- //
....
oneditprepare: function () {
....
//#region ---- npm ---- //
// NB: Assuming that the edit section is closed
// Show the npm section, hide the main & adv sections
$('#show-npm-props').click(function(e) {
e.preventDefault() // don't trigger normal click event
$('#main-props').hide()
$('#adv-props').hide()
$('#show-adv-props').html('<i class="fa fa-caret-right"></i> Advanced Settings')
$('#npm-props').show()
// TODO Improve feedback
//#region Setup the package list
$('#node-input-packageList').editableList({
addItem: addPackageRow, // function
removeItem: removePackageRow, // function(data){},
resizeItem: function(row,index) {},
header: $('<div>').append('<h4 style="display: inline-grid">Installed Packages</h4>'),
height: 'auto',
addButton: true,
removable: true,
scrollOnAdd: true,
sortable: false,
})
/** Initialise default values for package list
* NOTE: This is build dynamically each time the edit panel is opened
* we are not saving this since external changes would result in
* users having being prompted to deploy even when they've made
* no changes themselves to a node instance.
*/
packageList()
// spinner
$('.red-ui-editableList-addButton').after(' <i class="spinner"></i>')
$('i.spinner').hide()
//#endregion --- package list ---- //
})
// Hide the npm section, show the main section
$('#npm-close').click(function(e) {
e.preventDefault() // don't trigger normal click event
$('#main-props').show()
$('#npm-props').hide()
})
//#endregion ---- npm ---- //
...
},
....
}
Sorry, that's a bit of a dump, hopefully you can see how it is done.
While I'm not using columns as such, those are just some html and some CSS to control the width.
You can easily break up rows either wrapping elements with <div>
or a simple <br>
if you don't want paragraph spacing.