Repeat control with checkbox? - xpages

I have a repeat control which displays a view. I now want to include a checkbox, so that I can perform actions on the selected documents. I have no problem adding the check box, but how can I check if the checkbox is checked then get to the associated document for that row?
Thoughts I had were:
Have the check box change event add or remove the document's UNID from a array scope variable. Then just perform actions on documents in that array.
Forget the checkbox and just popup a list box, to allow the user to select from that.
But is there an easier way?

For maximum flexibility, it's best to not bind our user interface components directly to data; rather, if we introduce a middle "data model" layer (typically, one that describes the real-world objects / people / processes the data is representative of, rather than thinking in "documents", which are ultimately just evidence that these real-world things exist), our UI code becomes very clean, easy to understand, and easy to maintain. It also makes it much easier to introduce features that are otherwise frustrating to implement when we continue to think in documents.
Suppose, for instance, that we use the Object Data Source from the Extension Library to create an arbitrary object (for example, let's call it pendingRequests) that we can later bind our repeat control to (instead of binding it directly to the view):
// Create an empty array to return at the end:
var results = [];
// Create a view navigator instance for iterating the view contents:
var pendingView = database.getView("pendingRequests");
var entryNavigator = pendingView.createViewNav();
var eachEntry = entryNavigator.getFirst();
while (eachEntry != null) {
// Add metadata about each entry to result array:
var metaData = eachEntry.getColumnValues();
results.push({
startDate: metaData.get(0).getDateOnly(),
endDate: metaData.get(1).getDateOnly(),
employeeName: metaData.get(2),
status: metaData.get(3),
unid: eachEntry.getUniversalID(),
selected: "0"
});
// In case any column values were Domino objects:
recycleAll(metaData);
// Cruise on to the next:
eachEntry = navigateToNext(entryNavigator, eachEntry);
}
// Final Domino handle cleanup:
recycleAll(entryNavigator, pendingView);
// Return our now populated array:
return results;
Before proceeding, I should point out that the above example includes two pieces of syntactical candy that aren't native to the platform: recycleAll() and navigateToNext(). Both of these are just utility functions for making the stupid recycle stuff easier to handle:
recycleAll
* More convenient recycling
*/
function recycleAll() {
for(var i = 0; i < arguments.length; i++) {
var eachObject = arguments[i];
// assume this is a collection
try {
var iterator = eachObject.iterator();
while (iterator.hasNext()) {
recycleAll(iterator.next());
}
} catch (collectionException) {
try {
eachObject.recycle();
} catch (recycleException) {
}
}
}
}
navigateToNext
/*
* Safe way to navigate view entries
*/
function navigateToNext(navigator, currentEntry) {
var nextEntry = null;
try {
nextEntry = navigator.getNext(currentEntry);
} catch (e) {
} finally {
recycleAll(currentEntry);
}
return nextEntry;
}
Okay, now back to the data model... specifically, this block:
var metaData = eachEntry.getColumnValues();
results.push({
startDate: metaData.get(0).getDateOnly(),
endDate: metaData.get(1).getDateOnly(),
employeeName: metaData.get(2),
status: metaData.get(3),
unid: eachEntry.getUniversalID(),
selected: "0"
});
So for each view entry, we create a very simple object that has all the pertinent info we want to allow the user to interact with, as well as two extra properties that are there for our own code's convenience: unid, which allows to get back to the document if we need to, and selected, which gives us a way to bind a checkbox to a property of this metadata object... which means the user can toggle its value via the checkbox.
So here's a basic example of how we might represent this data to the user:
<ul style="list-style-type: none;">
<xp:repeat var="vacationRequest" value="#{pendingRequests}">
<li style="margin-bottom:10px;">
<strong>
<xp:checkBox value="#{vacationRequest.selected}" text="#{vacationRequest.startDate} - #{vacationRequest.endDate}"
checkedValue="1" uncheckedValue="0" />
</strong>
<xp:text value="#{vacationRequest.employeeName} (#{vacationRequest.status})" tagName="div" />
</li>
</xp:repeat>
</ul>
Each checkbox in the repeat control is now bound directly to the selected property of the metadata object each "row" represents... which also has a unid property, so acting on the actual documents that correspond to this data model is simple:
for (var i = 0; i < pendingRequests.length; i++) {
var eachRequest = pendingRequests[i];
if (eachRequest.selected == "1") {
var requestDataSource = database.getDocumentByUNID(eachRequest.unid);
requestDataSource.replaceItemValue("status", "Approved");
if (requestDataSource.save()) {
// update in-memory metadata:
eachRequest.status = "Approved";
}
}
}
Since our data source is just an array of these metadata objects, we can just loop through each, ask whether the user has toggled the selected property of each, and, if so, get a handle on its corresponding document, modify one or more items, and save it. NOTE: because we're using a data source in this example, it won't reload the back end view data on every event. For performance reasons, this is a Very Good Thing (tm). But it does mean that we have to update the in-memory metadata object to match what we changed on the document (i.e. eachRequest.status = "Approved")... but it also means we can update only that, instead of having to scrap our entire data source and have it read everything back in from the view.
As a bonus, adding something like a "Select All" button is even simpler:
for (var i = 0; i < pendingRequests.length; i++) {
pendingRequests[i].selected = "1";
}
So, in summary, we have an in-memory data model where, in many cases, equivalent operations will execute more rapidly, but also allows us to write less code -- and more readable code -- to do fancier things.
If you want to play with this pattern live (and / or download all of the above source code in context), I've immortalized it here.

Bruce,
I did a Video on selecting documents from a repeat control. I did not use check boxes but used an Add and Remove button and then did some CSS to highlight the selected documents. I'm sure a checkbox could be used with basically the same code as in the add/remove buttons.
Basically I created a java.util.ArrayList in memory to hold the unids and then populated that array when a repeat row is clicked on. I computed the CSS for each row and if that UNID exists in the Array I change the background color to show it's "selected". I don't actually show any processing on the selected unids but since that array is in scoped memory you can pretty much do anything you want with it. Anyway the video demo is here:
http://notesin9.com/index.php/2011/04/01/notesin9-025-selecting-documents-from-a-repeat-control/

As always, Tim's answer is your best bet for long-term health, sanity, and code-maintainability.
There's also another route I've taken in the past, before I started using Java for all of my back-end logic. You can create a page-load-bound dataContext containing a HashMap and then bind each checkbox to that - it will fill in true or false for each key, so you can then loop through the map entries and find the ones that are true, which are the checked values.
I put together a quick example that pulls in the list from the names database to show what I mean:
<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
<xp:this.data>
<xp:dominoView var="names" databaseName="names.nsf" viewName="$NamesFieldLookup"/>
</xp:this.data>
<xp:this.dataContexts>
<xp:dataContext var="checkedNames" value="${javascript: new java.util.HashMap() }"/>
</xp:this.dataContexts>
<xp:div id="refresher">
<xp:repeat value="#{names}" var="name" rows="3">
<xp:this.facets>
<xp:pager xp:key="header" id="pager1" layout="Previous Group Next" />
</xp:this.facets>
<div>
<xp:checkBox value="#{checkedNames[name.$9]}">
<xp:eventHandler event="onclick" submit="true" refreshMode="partial" refreshId="refresher"/>
</xp:checkBox>
<xp:text value="#{name.$9}"/>
</div>
</xp:repeat>
<p><xp:text value="#{checkedNames}"/></p>
</xp:div>
</xp:view>

Related

Select2 in Xpages Cannot add new value: Error is "Validation Error: value is not valid"

I am using Select2 (4.0) in an XPage. I have a ComboBox whose value is tied to a property in a bean, and the select items are populated (for now) by a viewScope variable I populate in before page load.
The user is able to type in a new value, but when I go to save the data I am getting a validation error:
This is not a validation error that I wrote but from Xpages. Although I am able to enter a new value ("Apple") when I save the page, it displays this error and removes my new value.
I believe this has something to do with the fact that the new value is not in the list of possible values, but I am not sure.
I found other posts that suggested that I have to add the new value to the list of values before submitting the page. I tried this but it didn't work either.
I really like select2 and want to use it for all comboBoxes and ListBoxes, but there are many times where the use needs to enter new values.
Code for the comboBox:
<xp:comboBox id="model"
value="#{javascript:PCModel.model}" xp:key="field">
<xp:selectItems>
<xp:this.value><![CDATA[#{javascript:viewScope.models}]]></xp:this.value>
</xp:selectItems>
</xp:comboBox>
And my script block:
<xp:scriptBlock id="scriptBlock1">
<xp:this.value><![CDATA[$(document).ready(
function()
{
x$("#{id:model}").select2({
tags: true,
createTag: function (params) {
return {
id: params.term,
text: params.term,
newOption: true
}
;},
templateResult: function (data) {
var $result = $("<span></span>");
$result.text(data.text);
if (data.newOption) {
$result.append(" <em>(new)</em>");
}
return $result;
}
}
)
}
)]]></xp:this.value>
</xp:scriptBlock>

How can you control visibility of datasource in Cesiumjs?

I want to display multiple datasources in a cesiumjs viewer but need to allow the user to select which ones they want to see at any given time. For example, if I load a kml and a czml file, how do I hide one and show the other? I can't find the cesiumjs way to do this with its API.
Update Feb 2016: A show flag has been proposed and may be added to a future version of Cesium.
Original answer:
Currently there is no show flag on the dataSource, however it is easy to add and remove the dataSource from the list of available dataSources, and this is used to get the show/hide functionality.
Here's a working demo: Load the Cesium Sandcastle Hello World example, and paste the following code into the left side, then hit Run (F8). It should display a checkbox in the upper-left with show/hide functionality.
var viewer = new Cesium.Viewer('cesiumContainer');
// Create a typical CzmlDataSource.
var dataSource1 = new Cesium.CzmlDataSource();
dataSource1.load('../../SampleData/simple.czml');
// Add a checkbox at the top.
document.getElementById('toolbar').innerHTML =
'<label><input type="checkbox" id="showCheckbox" /> Show CZML</label>';
var checkbox = document.getElementById('showCheckbox');
checkbox.addEventListener('change', function() {
// Checkbox state changed.
if (checkbox.checked) {
// Show if not shown.
if (!viewer.dataSources.contains(dataSource1)) {
viewer.dataSources.add(dataSource1);
}
} else {
// Hide if currently shown.
if (viewer.dataSources.contains(dataSource1)) {
viewer.dataSources.remove(dataSource1);
}
}
}, false);
This code could be improved, for example it could be a "lazy load" where the dataSource.load does not get called until the first time it's shown. Also if a dataSource has been hidden a while, you have to consider at what point should you be saving memory by destroying the dataSource rather than continuing to hold onto it (triggering a new lazy load if it is later shown again).
as of now, show is a property of the data source, you can control it by accessing the property in dot or bracket notation:
https://cesiumjs.org/Cesium/Build/Documentation/CzmlDataSource.html#show
const src = new Cesium.CzmlDataSource();
src.show = false;

xpages custom deleting doc. from view panel

I'm trying to delete selected doc from viewPanel1. The view is categorized ( can be > 1 category ) and is listing documents from 2 different datasources, let say: Cdoc and Pdoc. These docs. are linked by a common field.
my scenario: If the users select a Cdoc => the delete action will take place to the respective Cdoc but also for the all Pdoc being in the same category. If the user selects a Pdoc => delete just the Pdoc. Also, I would like to add some confirmation text with some information ( Value fields ) from the selected documents.
I tried the following
var viewPanel=getComponent("viewPanel1");
var docIDArray=viewPanel.getSelectedIds();
for(i=0;i < docIDArray.length;i++){
var docId=docIDArray[i];
var doc=database.getDocumentByID(docId);
var formName = (doc == null)? null : doc.getItemValueString("Form");
if( formName =="fmPersContact" ){
.....
} // in this case, it works OK.
else if ( formName =="fmCompanie" ){ // here if I selected > 1 Cdoc, it deletes just one Cdoc + the respective PDocs.
var doc:NotesDocument = null;
doc=database.getDocumentByID(docId);
var ky:java.util.Vector = new java.util.Vector();
ky.add(doc.getItemValueString("txt_NumeCompanie"));
... // delete method
}
Could you tell me what I did wrong and what am I missing in the above code? thanks for your time!
The first thing you want to do is confirm. Unlike Lotusscript, you cannot use a function in the middle of a script to open the confirm dialog and get the answer. To do this, I recommend using the confirm simple action before going into an execute script simple action.
<xp:button
value="delete"
id="button1"
>
<xp:eventHandler event="onclick" submit="true" refreshMode="complete">
<xp:this.action>
<xp:actionGroup>
<xp:confirm message="Are you certain?"></xp:confirm>
<xp:executeScript
script="#{javascript:doSomething();}"
>
</xp:executeScript>
</xp:actionGroup>
</xp:this.action></xp:eventHandler></xp:button>
EDIT
In the past, I have also used built my own dialogs with the extension library, filled the text in with SSJS and then called the doWhatever() or close() from the dialog itself. This is not the best solution as it requires an update from the server to get the string. The best solution would be, as Paul Withers says, to use CSJS to perform the confirmation. I have yet to do this though.
/EDIT
For your delete function, I recommend getting the document you want to delete, then tell whether it is a P- or C- doc, either by the form name or whatever mechanism you use, and then either delete the single document or by getting a documentcollection from the view by using getAllDocumentsBykey(), then iterating through them all, deleting them one by one.
var ky:java.util.Vector = new java.util.Vector();
ky.add("MainCat");
ky.add("subCat");
ky.add("subCat2");
var vw:NotesView = database.getView("vw_myView");
var docs:NotesDocumentCollection = vw.getAllDocumentsByKey(ky);
//... delete stuff...
//dont forget to recycle
Post Question Edit
I recommend the following to get the form name:
var getSelectedDoc = function(){
var vwPnl = getComponent("viewpanel");
var ids = vwPnl.getSelectedIds();
var id = null;
var doc:NotesDocument = null;
if(ids.length > 0){ //could use for loop var i = 0; i < ids.length;i++
id = ids[0]; //could pack all ids into java.util.ArrayList and return that list to work on further
//be warned that if the user selects a parent doc and those automatically deleted by it that you need a mechanism to check if the document was already deleted!
}
if(id != null){
doc = database.getDocumentByID(id);
}
return doc;
}
var doc = getSelectedDoc();
var formName = (doc == null)? null : doc.getItemValueString("form");
if(PDOC_FORM_NAME.equalsIgnoreCase(formName)){
deleteFunctionComplete(doc);
} else if (CDOC_FORM_NAME.equalsIgnoreCase(formName)){
deleteFunctionTwo(doc);
} else {
// uh-oh
}
this also allows you to have the document in case you want to delete it right away.
Edits for comments
If Cdoc should delete more than one document, then yes. You should be using the getAllDocumentsBykey keeping in mind that the view needs to be built for it. By that I mean if you have a view with one single category, there is no issue, just plug in the string and you are fine. If you have a view with three categories, you cannot feed a vector into the getalldocs function with only two values, it must be all three. So, you want to delete all for "mycomp" with the underlying pdocs "Greg" "Sally "Bob", just use alldocsbykey("mycomp"), if the view looks like:
mycomp
---Greg
---Sally
---Bob
but if the view looks like
Poland
---mycomp
------Greg
------Sally
------Bob
then the a vector with poland and mycomp must be used. "poland" does not get the correct documents. --just an fyi and pitfall that is sometimes had.
Edit after further question clarification
I prefer this loop style to remove docs
var doc_temp:NotesDocument = null;
var doc_toDelete:NotesDocument = null;
var coll_docs:NotesDocumentCollection = ...; //get document collection
var doc_nextDoc = coll_docs.getFirstDocument();
while(doc_nextDoc != null){
doc_temp = doc_nextDoc; //set document to delete
doc_nextDoc = coll_docs.getNextDocument(doc_nextDoc); // set next document before deletion
try{
doc_temp.remove(true);//lots of errors can happen here, such as ACL settings
} catch(e) {
//handle, or just break
} finally{
if(doc_temp != null) try{doc_temp.recycle()} catch(e){}// try to recycle, could also cause errors
doc_temp = null;// for the sense of completeness
}
}
Even further edit based on the question edits
of course you are only deleting one Pdoc, the way you have that set up, you are only ever returning one document. You could expand the getSelectedDoc() to put all selected documents into an java.util.ArrayList or something, and then use that arraylist to delete more than one at a time, but that could be dangerous depending on what you do because NotesDocuments are not serialisable. In that case, I recommend using the same code that you use for getSelected doc, use a for loop to get the document IDs, get the document, if the document is not null, then delete.
apropos getAllDocumentsByKey(with a vector)
The way this is currently set up, no Vector is necessary.
If you have a view with a category and sub category and you want to get all the documents in that sub category, then you must use a vector to get at it. If you include a simple string or a vector with only one value, then the documents in the sub category will not be returned. The vector can be thought of as "cat1", "subcat", "furtherSubCat"
Furthermore, there is no check here to see if the string returned from the document is empty. This should be done. There is also no check to see if the DocumentCollection is empty. This should also be done. My expectation is that there is an issue retrieving the collection based on above mentioned reasons.

Specialized type-ahead

I am trying to provide type-ahead functionality for a job number field. The pattern of the field is 8 followed by as many zeros as necessary to make the string they type a total of 10 digits. In other words, 8000001234 or 8001234567. In these examples, the users only want to type 1234 or 1234567 and have the type-ahead return the corresponding documents. Is this possible?
This can be done by using the parameter valueMarkup in xp:typeAhead.
In the suggestion response you add the value you wish to add to the field in the display:none section, the span of class informal is the part display in the suggestion list. You can modify/design the informal section with HTML code (f.e. include multiline informations, add images, etc.)
Here is a simple example:
<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
<xp:inputText id="inputText1" value="#{requestScope.TypeAhead}">
<xp:typeAhead mode="partial" minChars="1"
var="searchValue" valueMarkup="true">
<xp:this.valueList>
<![CDATA[#{javascript:
var directoryTypeahead = function (searchValue:string) {
/*** generate your matches ***/
var matches = {};
for( var i=10;i<20;i++){
matches[i] = { display: "80000" + i };
}
/*** return typeahead data ***/
var returnList = "<ul>";
for (var matchEntry in matches) {
var match = matches[matchEntry];
var matchDetails:string = [
"<li><div style=\"display:none;\">",
matchEntry,
"</div><span class=\"informal\"><strong>",
match.display,
"</span></li>"
].join("");
returnList += matchDetails;
}
returnList += "</ul>";
return returnList;
}
directoryTypeahead(searchValue)
}]]>
</xp:this.valueList>
</xp:typeAhead>
</xp:inputText>
You have to change the part between generate your matches to fit your requirements.
Roy - another options would be to roll your own typeAhead and not use the out of the box version
http://xomino.com/2012/05/01/jquery-in-xpages-8-tokeninput-autocomplete/
Using the Token Autocomplete you can control the search input and the display output - in this way you can display the whole 80000123 string and the 123 would be highlighted as the text that the user has input.
Possibly a partial answer - but I stumbled across a blog post by Rasmus Bauck a while back that explained a technique for handling the type-ahead calls with code of your own.
I didn't get around to trying it, but I saw your question and it jogged my memory.
http://devxpages.blogspot.com.au/2010/04/extending-xpages-type-ahead.html
Hope it helps,
Brendan

dijit.Tree search and refresh

I can't seem to figure out how to search in a dijit.Tree, using a ItemFileWriteStore and a TreeStoreModel. Everything is declarative, I am using Dojo 1.7.1, here is what I have so far :
<input type="text" dojoType="dijit.form.TextBox" name="search_fruit" id="search_fruit" onclick="search_fruit();">
<!-- store -->
<div data-dojo-id="fruitsStore" data-dojo-type="dojo.data.ItemFileWriteStore" clearOnClose="true" urlPreventCache="true" data-dojo-props='url:"fruits_store.php"'></div>
<!-- model -->
<div data-dojo-id="fruitsModel" data-dojo-type="dijit.tree.TreeStoreModel" data-dojo-props="store:fruitsStore, query:{}"></div>
<!-- tree -->
<div id="fruitsTree" data-dojo-type="dijit.Tree"
data-dojo-props='"class":"container",
model:fruitsModel,
dndController:"dijit.tree.dndSource",
betweenThreshold:5,
persist:true'>
</div>
The json returned by fruits_store.php is like this :
{"identifier":"id",
"label":"name",
"items":[{"id":"OYAHQIBVbeORMfBNZXFGOHPdaRMNUdWEDRPASHSVDBSKALKIcBZQ","name":"Fruits","children":[{"id":"bSKSVDdRMRfEFNccfTZbWHSACWbLJZMTNHDVVcYGcTBDcIdKIfYQ","name":"Banana"},{"id":"JYDeLNIGPDBRMcfSTMeERZZEUUIOMNEYYcNCaCQbCMIWOMQdMEZA","name":"Citrus","children":[{"id":"KdDUfEDaKOQMFNJaYbSbAcAPFBBdLALFMIPTFaYSeCaDOFaEPbJQ","name":"Orange"},{"id":"SDWbXWbTWKNJDIfdAdJbbbRWcLZFJHdEWASYDCeFOZYdcZUXJEUQ","name":"Lemon"}]},{"id":"fUdQTEZaIeBIWCHMeBZbPdEWWIQBFbVDbNFfJXNILYeBLbWUFYeQ","name":"Common ","children":[{"id":"MBeIUKReBHbFWPDFACFGWPePcNANPVdQLBBXYaTPRXXcTYRTJLDQ","name":"Apple"}]}]}]}
Using a grid instead of a tree, my search_fruit() function would look like this :
function search_fruit() {
var grid = dijit.byId('grid_fruits');
grid.query.search_txt = dijit.byId('search_fruit').get('value');
grid.selection.clear();
grid.store.close();
grid._refresh();
}
How to achieve the same using the tree ? Thanks !
The refreshing of a dijit.Tree becomes a little more complicated, since there is a model involved (which in grid afaik is inbuilt, the grid component implements query functionality)
Performing search via store
But how to search, thats incredibly easy whilst using the ItemFileReadStore. Syntax is as such:
myTree.model.store.fetch({
query: {
name: 'Oranges'
},
onComplete: function(items) {
dojo.forEach(items, function(item) {
console.log(myTree.model.store.getValue(item, "ID"));
});
}
});
Displaying search results only
As shown above, the store will fetch, the full payload is put into its _allItemsArray and the store queryengine then filters out what its told by query argument to the fetch method. At any time, we could call fetch on store, even without sending an XHR for json contents - fetch with query argument can be considered as a simple filter.
It becomes slightly more interesting to let the Model know about this query.. If you do so, it will only create treeNodes to fill the tree, based on the returned results from store.fetch({query:model.query});
So, instead of sending store.fetch with a callback, lets _try to set model query and update the tree.
// seing as we are working with a multi-parent tree model (ForestTree), the query Must match a toplevel item or else nothing is shown
myTree.model.query = { name:'Fruits' };
// below method must be implemented to do so runtime
// and note, that the DnD might become invalid
myTree.update();
Refreshing tree with new xhr-request from store
You need to do exactly as you do with regards to the store. Close it but then rebuild the model. Model contains all the TreeNodes (beneath its root-node) and the Tree itself maps an itemarray which needs to be cleared to avoid memory leakage.
So, performing following steps will rebuild the tree - however this sample does not take in account, if you have DnD activated, the dndSource/dndContainer will still reference the old DOM and thereby 'keep-alive' the previous DOMNode hierachy (hidden ofc).
By telling the model that its rootNode is UNCHECKED, the children of it will be checked for changes. This in turn will produce the subhierachy once the tree has done its _load()
Close the store (So that the store will do a new fetch()).
this.model.store.clearOnClose = true;
this.model.store.close();
Completely delete every node from the dijit.Tree
delete this._itemNodesMap;
this._itemNodesMap = {};
this.rootNode.state = "UNCHECKED";
delete this.model.root.children;
this.model.root.children = null;
Destroy the widget
this.rootNode.destroyRecursive();
Recreate the model, (with the model again)
this.model.constructor(this.model)
Rebuild the tree
this.postMixInProperties();
this._load();
Creds; All together as such, scoped onto the dijit.Tree:
new dijit.Tree({
// arguments
...
// And additional functionality
update : function() {
this.model.store.clearOnClose = true;
this.model.store.close();
delete this._itemNodesMap;
this._itemNodesMap = {};
this.rootNode.state = "UNCHECKED";
delete this.model.root.children;
this.model.root.children = null;
this.rootNode.destroyRecursive();
this.model.constructor(this.model)
this.postMixInProperties();
this._load();
}
});

Resources