NativeScript - how can I filter an observable array with SearchBar? - filtering

Hi I'm trying to filter an observable array of data fetched via a HTTP request on keypress of the SearchBar.
I managed to get the SearchBar property change to work but I can't seem to figure out what I'm doing wrong in the filtering logic.
Ideally I want to update the list as I type in the search term in the SearchBar. I've searched the API on the Telerik site, there wasn't really any examples I could find.
XML
<Page loaded="pageLoaded">
<ActivityIndicator busy="{{ isLoading }}" />
<ActionBar title="People">
</ActionBar>
<GridLayout>
<StackLayout>
<SearchBar id="searchBar" hint="Search for someone"></SearchBar>
<ListView items="{{ peopleList }}" itemTap="showDetail">
<ListView.itemTemplate>
<StackLayout>
<Label text="{{ fullName }}" horiztonalAlignment="left" verticalAlignment="center"></Label>
<Label text="{{ company }}" class="info"></Label>
</StackLayout>
</ListView.itemTemplate>
</ListView>
</StackLayout>
</GridLayout>
</Page>
JS
var frames = require("ui/frame");
var Observable = require("data/observable").Observable;
var PeopleListViewModel = require("../../shared/people-viewModel");
var activityIndicatorModule = require("ui/activity-indicator");
var page;
var userkey;
var peopleList = new PeopleListViewModel([]);
var pageData = new Observable({ peopleList: peopleList });
exports.pageLoaded = function(args) {
page = args.object;
page.bindingContext = pageData;
userkey = userkey || page.navigationContext.userkey;
peopleList.load(userkey); // fetch data from the backend
var searchBar = page.getViewById("searchBar");
searchBar.on("propertyChange", function (args) {
var searchText = args.object.text;
if (searchText === "") {
// NOT SURE WHAT TO DO HERE.
} else {
peopleList.filter(function (element, index, array) {
// DOESN"T WORK PROPERLY
console.log("element: ", JSON.stringify(element));
return element.fullName == searchText;
});
console.log("Text types: ", searchText);
}
});
};
exports.showDetail = function(args) {
var person = peopleList.getItem(args.index);
var navigateEntry = {
moduleName: "views/people/people-detail",
context: { person: person },
animated: false
};
frames.topmost().navigate(navigateEntry);
};
PeopleListViewModel.js
var config = require("./config");
var fetchModule = require("fetch");
var ObservableArray = require("data/observable-array").ObservableArray;
function PeopleListViewModel(people) {
var viewModel = new ObservableArray(people);
viewModel.load = function (userKey) {
return fetchModule.fetch(config.baseUrl + "/api/people/all/" + userKey)
.then(function (response) {
return response.json();
})
.then(function (data) {
data.forEach(function (person) {
viewModel.push(person);
});
}, function (error) {
console.log("Error: ", error);
});
};
viewModel.empty = function () {
while (viewModel.length) {
viewModel.pop();
}
};
return viewModel;
}
function handleErrors(response) {
if (!response.ok) {
console.log("Error occurred");
}
}
module.exports = PeopleListViewModel;
Updated people-list
var frames = require("ui/frame");
var Observable = require("data/observable").Observable;
var ObservableArray = require("data/observable-array").ObservableArray;
var PeopleListViewModel = require("../../shared/people-viewModel");
var activityIndicatorModule = require("ui/activity-indicator");
var page;
var userkey;
var peopleList = new PeopleListViewModel([]);
var pageData = new Observable({ peopleList: peopleList });
var resultList = new ObservableArray([]);
exports.pageLoaded = function(args) {
page = args.object;
page.bindingContext = pageData;
userkey = userkey || page.navigationContext.userkey;
peopleList.load(userkey);
var searchBar = page.getViewById("searchBar");
searchBar.on("propertyChange", function (args) {
var searchText = args.object.text;
if (searchText === "") {
} else {
while (resultList.length > 0) {
resultList.pop();
}
peopleList.forEach(function (element) {
if (element.fullName === searchText) {
resultList.push(element);
}
});
}
});
};

I had the same issue. If you want to filter your data after every character has changed in search-bar you can try my solution.
Definitions
My playerList is your peopleList. This is the data from view-model.
resultList is an array where the data will be pushed.
var observableArrayModule = require("data/observable-array").ObservableArray;
var playerList = new PlayerListViewModel([]);
var resultList = new observableArrayModule([]);
var pageData = new observableModule.Observable({
resultList: resultList,
player: ""
});
Inside expors.loaded()
page = args.object;
searchBar = page.getViewById("search-bar");
page.bindingContext = pageData;
Load Initial Data - inside expors.loaded()
We are loading initial data when user navigates to the screen for the first time. We are also pushing the same data to resultList since we are using {{resultList}} in xml. You can add loadingIndicator while the list is populated.
playerList
.load()
.then(function() {
setTimeout(function() {
playerList.forEach(function (element) {
pageData.resultList.push(element);
});
}, 1000);
})
.catch(function(error) {
dialogsModule.alert({
message: "An error occurred while loading players.",
okButtonText: "OK"
});
});
Clear autofocus - inside expors.loaded()
This is to prevent keyboard from opening on initial screen navigation.
if (searchBar.ios) {
searchBar.ios.endEditing(true);
} else if (searchBar.android) {
searchBar.android.clearFocus();
}
Search data when character has changed - inside expors.loaded()
I am calling filter functionality. Lodash _.debounce function is used to delay looping through resultList array. Without it, the app would loop every time letter is typed. Now we are waiting for user to stop typing to start looping.
searchBar.on('propertyChange', _.debounce(searchList, 500));
searchList Function
This is the actual loop. You can change element.name for your needs.
function searchList(args) {
var searchText = args.object.text;
while(resultList.length > 0) {
resultList.pop();
}
playerList.forEach(function (element) {
if (element.name.toLowerCase().indexOf(searchText) >= 0) {
resultList.push(element);
}
});
}
Hide keyboard if search-bar is cleared - inside exports.loaded()
And finally we want to hide the keyboard if user clears the search-bar.
searchBar.on(searchBarModule.SearchBar.clearEvent, function (args) {
setTimeout(function() {
searchBar.dismissSoftInput();
}, 10);
});
PS
You probably solved your issue, but this could help someone else in the future.

Okay so your problem is a Javascript problem than a NativeScript problem. For the sake of this problem, think of observable arrays as just your ordinary arrays.
In your JS you're creating a new PeopleListViewModel which you're then attaching to the bindingContext via the pageData object. So far so good. Then you're calling the load method on the PeopleListViewModel (It returns a promise which you're not really doing anything with but for this specific problem it doesn't matter).
However, when text is inputed you're not really doing anything. This is your code:
peopleList.filter(function (element, index, array) {
// DOESN"T WORK PROPERLY
console.log("element: ", JSON.stringify(element));
return element.fullName == searchText;
});
peopleList is an instance of PeopleListViewModel which returns an ObservableArray. The ObservableArray does indeed have a method called filter (which works just like filter of a regular array. Check out the NativeScript documentation and Javascript documentation of filter).
What you need to understand here is that filter returns a new array with the filtered results. Doing peopleList.filter() will send that new array into empty space. You want to var yourNewFilteredArray = peopleList.filter(). But you don't really want to redefine the array bound to the binding context, you want to modify the content of it.
Here's an example of how you could do that:
/*
* Attach a new obsersable array to the binding context.
* you can prepopulate it with the data from the
* PeopleListViewModel if you want to
*/
var resultList = new ObservableArray([]);
var pageData = new Observable({ resultList: resultList });
/*
* Then on search/filter you want to modify this new
* array. Here I first remove every item in it and then
* push matching items to it.
*/
searchBar.on("propertyChange", function (args) {
var searchText = args.object.text;
// ...
while(resultList.length > 0) {
resultList.pop();
}
peopleList.forEach(function (element) {
if (element.fullName === searchText) {
resultList.push(element);
}
});
});

Related

Salesforce lightning input's element.set not working anymore after summer 18 release

I am using below snippet to check for valid phone number format and then trying to set the formatted value to the current input element. But after summer 18 release I'm unable to set input with new formatted value.
TestApp
<aura:application extends="force:slds">
<lightning:input type="text" label="Num" aura:id="ele" onchange="
{!c.changeNum}" />
</aura:application>
Controller:
({
changeNum : function(component, event, helper) {
helper.changeNum(component, event);
}
})
Helper:
({
changeNum : function(component, event) {
var element = event.getSource();
var phonenumber = element.get("v.value");
if(phonenumber){
var updatedValue = phonenumber.replace(/-/g, "");
if(/^\d{10}$/.test(updatedValue)){
phonenumber = updatedValue.match(new RegExp('\\d{4}$|\\d{3}', 'g')).join("-");
}
else{
var x = phonenumber.replace(/[^0-9._-]/g, "").replace(/ +/, " ");
phonenumber = x;
if(!/^[0-9-]+$/.test(phonenumber.slice(-1))){
phonenumber = phonenumber.slice(0, -1);
}
}
}
console.log(phonenumber);
element.set('v.value', phonenumber);
}
})
element.set is not able to update the formatted value. The lightning input element is still able to accept alphabets.
We can solve the issue by using Promise
({
handleInputChange : function(component, event) {
try {
var element = event.getSource();
var inputValue = element.get("v.value");
var formattedValue;
var chkPattern = new Promise(
function (resolve, reject) {
if (inputValue) {
formattedValue = inputValue.replace(/[^0-9-]/g, "").replace(/ +/, " ");
resolve(formattedValue); // fulfilled
} else {
var reason = new Error('kitten is not happy');
reject(reason); // reject
}
}
);
chkPattern.then(function (fulfilled) {
element.set('v.value', fulfilled);
}).catch(function (error) {
console.log(error.message);
});
} catch(e) {
this.consoleLog(e.stack, true)
}
}
})

Custome Formatter within a Controller - SAPUI5

I'm trying to format gender field (in SAP table field: CHAR1 to 0(F) and 1(M) to fit the selectedIndex property of RadioButtonGroup.
This is my view : (DetailDialog.fragment.xml )
<RadioButtonGroup width="100%" columns="2" selectedIndex="{path: 'Gendr', formatter:'.formatter' }" id="__group1">
The above XML Fragment is called by the main view controller:
ItemPress: function(oEvent) {
var detailDialog = this.getView().byId("DetailDialog");
var that = this;
var view = this.getView();
var path = oEvent.getParameter("listItem").getBindingContext().getPath();
var oDummyController = {
formatter: function(gendr) {
switch (gendr) {
case "M":
return 0;
case "F":
return 1;
}
},
closeDialog: function() {
detailDialog.close();
}
};
if (!detailDialog) {
detailDialog = sap.ui.xmlfragment(view.getId(), "Demo1.view.DetailDialog", oDummyController);
}
var jSonModel = new sap.ui.model.json.JSONModel();
function fnSuccess(oData, oResponse) {
jSonModel.setData(oData);
}
var oModel = view.getModel();
oModel.read(path, {
success: fnSuccess
})
//Set data for dialog
this.getView().byId("__formDetail").setModel(jSonModel);
detailDialog.open();
}
My problem is that the formatter is not working at all.
Any suggestion?
Option one: (not sure if it works for fragments too)
Change formatter:'.formatter' to formatter:'Demo1.view.DetailDialog.formatter'.
Option two: Format the data since anyway you are binding data from controller. (And surely will work.)
function fnSuccess(oData, oResponse) {
oData.GendrValue = oData.Gendr == "M"?1:0;
jSonModel.setData(oData);
}
and also change the binding: selectedIndex="{path: 'GendrValue'}"

mongodb showing array of null when printing the outside of query

i am trying to push the resultant of the count to an array in mogodb query, while pushing it showing the array after that if print it outside of query it is showing empty array.
collection1 in db is like below
[{title:Home,
date:24-10-2016},
{title:Accesories,
date:13-02-2016}
]
my code
exports.listOfCategories=function(req,res){
collection1.find().exec(function (err, categories) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
var categoryList = categories;
var catTitle;
var allCat = [];
// console.log(categoryList);
for (var i = 0; i < categoryList.length; i++) {
catTitle = categoryList[i].title;
contentCounts(catTitle);
function contentCounts(content, callback) {
var catName = new RegExp(content, 'i');
var mongoQuery = {
"ProCategory.title": catName
}
collection2.find(mongoQuery).count(function (err, count) {
generateContentArr(content, count)
});
}
function generateContentArr(content, count) {
allCat.push({
name: content,
count: count
});
console.log(JSON.stringify(allCat));
// Here it is showing the array what i pushed
}
}
console.log(JSON.stringify(allCat));
// Here it not showing the total array, it showing an empty array
res.json(allCat);
}
});
}
Thanks in advance
You are not waiting for the result of an async operation, in your case in the for loop you need to wait for the result of mongo operation, but as for loop is synchronous, you are just making calls to mongo but don't wait for the results, and print the empty array right after the loop.
I would suggest you to use promises instead of callbacks, I don't know which version of mongoose you are using but the last version have promise support for mongo methods like find and count. Here is an example for your case:
var Promise = require("bluebird");
function countByTitle(catTitle){
var mongoQuery = {"ProCategory.title": new RegExp(catTitle, 'i')}
return collection2.count(mongoQuery).then(function(count) {
return {
name: catTitle,
count: count
};
});
}
collection1.find().then(function (categories) {
var categoryList = categories;
var promises = [];
for (var i = 0; i < categoryList.length; i++) {
promises.push(countByTitle(categoryList[i].title));
}
return Promise.all(promises).then(results => {
console.log(JSON.stringify(results));
})
}).catch(function (err) {
//if there is any error while resolving the promises, this block will be called
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
});

How can a range be used across different Word.run contexts?

I have created a taskpane addin for word that runs a search and displays information about the results as a list to the user.
When the user clicks on an item in the list I want to select the range in word to show the user the location of the item.
The addin will then allow the user to perform additional tasks on the range, for example change the font colour.
I am able to run the search and get ranges for display using the function below:
function runSearch(textToFind) {
var items = [];
return Word.run(function(context) {
var options = Word.SearchOptions.newObject(context);
options.matchWildCards = false;
var rangesFind = context.document.body.search(textToFind, options);
context.load(rangesFind, 'text, font, style');
return context.sync().then(function() {
for (var i = 0; i < rangesFind.items.length; i++) {
items.push(rangesFind.items[i]);
context.trackedObjects.add(rangesFind.items[i]);
}
return context.sync();
});
})
.then(function() {
return items;
});
};
However I am having difficulty selecting the range on user click.
I have tried using the ranges context:
function selectRange(range){
range.select();
return range.context.sync();
}
Or using the range in a new Word.run context:
function selectRange(range){
return Word.run(function(context) {
context.load(range);
return context.sync().then(function(){
range.select();
return context.sync();
});
});
}
I have come across a potential method that involves creating a content control for each search result and then reloading all the content controls in the selectRangefunction in the new context and finding the matching control, but that seems very inefficient when I have the range already.
What is the best method for reusing a range across different Word.run contexts?
You cannot use an object across Word.run invocations. Word.run creates a new context every time that it's invoked, whereas the original object is tied to its own context, creating a mismatch.
That being said, you absolutely can, from within a Word.run, add the objects you desire to context.trackedObjects.add(obj), and they will remain as working objects even after Word.run finishes executing. By "working objects" I mean that their path will not get invalidated (think something similar to garbage collection, but for remote objects).
Once you have such object (and it looks above like you do), you should be able to call
range.select();
range.context.sync().catch(...);
If it's not working for you, can you provide an example of the error you're getting?
For completeness sake, I should note that once you add objects to the trackedObjects collection, you're effectively taking memory management of those objects into your own hands. This means that if you don't properly release the memory, you will be slowing down Word by bogging down its memory / range-adjustment chain. So once you're done using the tracked object(s), you should call obj.context.trackedObjects.remove(obj), followed by obj.context.sync(). Don't forget the last part - if you don't do a sync, your request to remove the tracked objects will not be dispatched, and you'll continue to use up the memory.
======= Update 1 =======
Tom, thanks for providing the error message. It looks like this might be a bug in the Word implementation of the APIs -- I'll follow up on that, and someone might reach out to you if there's more questions.
From a conceptual standpoint, you are absolutely on the right path -- and the following does work in Excel, for example:
var range;
Excel.run(function (ctx) {
var sheet = ctx.workbook.worksheets.getActiveWorksheet();
range = sheet.getRange("A5");
range.values = [[5]];
ctx.trackedObjects.add(range);
return ctx.sync();
})
.then(function(){
setTimeout(function() {
range.select();
range.context.trackedObjects.remove(range);
range.context.sync();
}, 2000);
})
.catch(function (error) {
showMessage("Error: " + error);
});
======= Update 2 =======
It turns out there is indeed a bug in the product. However, the good news is that it's easy to fix with a JavaScript-only fix, and in fact we'll do so in the next couple of weeks, updating the CDN.
With the fix, the following code works:
var paragraph;
Word.run(function (ctx) {
var p = ctx.document.body.paragraphs.first;
paragraph = p.next;
ctx.trackedObjects.add(paragraph);
return ctx.sync();
})
.then(function(){
setTimeout(function() {
paragraph.select();
paragraph.context.trackedObjects.remove(paragraph);
paragraph.context.sync()
.then(function() {
console.log("Done");
})
.catch(handleError);
}, 2000);
})
.catch(handleError);
function handleError (error) {
console.log('Error: ' + JSON.stringify(error));
if (error instanceof OfficeExtension.Error) {
console.log('Debug info: ' + JSON.stringify(error.debugInfo));
}
}
Want even better news? Until the CDN is updated, you can use the code below to "patch" the JavaScript library and make the code above run. You should run this code some time after Office.js has already loaded (i.e., within your Office.initialize function), and before you do a Word.run.
var TrackedObjects = (function () {
function TrackedObjects(context) {
this._autoCleanupList = {};
this.m_context = context;
}
TrackedObjects.prototype.add = function (param) {
var _this = this;
if (Array.isArray(param)) {
param.forEach(function (item) { return _this._addCommon(item, true); });
}
else {
this._addCommon(param, true);
}
};
TrackedObjects.prototype._autoAdd = function (object) {
this._addCommon(object, false);
this._autoCleanupList[object._objectPath.objectPathInfo.Id] = object;
};
TrackedObjects.prototype._addCommon = function (object, isExplicitlyAdded) {
if (object[OfficeExtension.Constants.isTracked]) {
if (isExplicitlyAdded && this.m_context._autoCleanup) {
delete this._autoCleanupList[object._objectPath.objectPathInfo.Id];
}
return;
}
var referenceId = object[OfficeExtension.Constants.referenceId];
if (OfficeExtension.Utility.isNullOrEmptyString(referenceId) && object._KeepReference) {
object._KeepReference();
OfficeExtension.ActionFactory.createInstantiateAction(this.m_context, object);
if (isExplicitlyAdded && this.m_context._autoCleanup) {
delete this._autoCleanupList[object._objectPath.objectPathInfo.Id];
}
object[OfficeExtension.Constants.isTracked] = true;
}
};
TrackedObjects.prototype.remove = function (param) {
var _this = this;
if (Array.isArray(param)) {
param.forEach(function (item) { return _this._removeCommon(item); });
}
else {
this._removeCommon(param);
}
};
TrackedObjects.prototype._removeCommon = function (object) {
var referenceId = object[OfficeExtension.Constants.referenceId];
if (!OfficeExtension.Utility.isNullOrEmptyString(referenceId)) {
var rootObject = this.m_context._rootObject;
if (rootObject._RemoveReference) {
rootObject._RemoveReference(referenceId);
}
delete object[OfficeExtension.Constants.isTracked];
}
};
TrackedObjects.prototype._retrieveAndClearAutoCleanupList = function () {
var list = this._autoCleanupList;
this._autoCleanupList = {};
return list;
};
return TrackedObjects;
}());
OfficeExtension.TrackedObjects = TrackedObjects;
Hope this helps!
~ Michael Zlatkovsky, developer on Office Extensibility team, MSFT
In addition to the TrackedObjects fix the runSearch method needed updating to get the range of the searchResult rather than using the searchResult directly.
function runSearch(textToFind) {
var items = [];
return Word.run(function(context) {
var options = Word.SearchOptions.newObject(context);
options.matchWildCards = false;
var rangesFind = context.document.body.search(textToFind, options);
context.load(rangesFind);
return context.sync().then(function() {
for (var i = 0; i < rangesFind.items.length; i++) {
var range = rangesFind.items[i].getRange();
context.load(range, 'text');
items.push(range);
context.trackedObjects.add(items[items.length-1]);
}
return context.sync();
});
})
.then(function() {
return items;
});
};

Cannot call method 'push' of undefined

I had this working in a fiddle just fine (which I can't find now), but when I moved it to VS2012 I'm getting the error "Cannot call method 'push' of undefined'.
In the code below, the createItemDiv() function works great and creates the UI elements. It's the line before that, showItem() which calls this.visibleItem.push() that is throwing the error. I had a problem with this in the fiddle originally and added the "this" to fix it. If I remove "this" as it is now, I get "visibleItem is not defined".
viewmodel.js
var dummyResults = [
{ //sample data is here }
]
var dummyItems = [
]
function VisibleItem(data) {
var self = this;
this.name = ko.observable(data.name);
this.type = ko.observable(data.type);
this.description = ko.observable("");
}
function SearchResult(data) {
var self = this;
this.name = ko.observable(data.name);
this.type = ko.observable(data.type);
}
var viewModel = {
searchResult: ko.observableArray(ko.utils.arrayMap(dummyResults, function (item) {
return new SearchResult(item);
})),
visibleItem: ko.observableArray(ko.utils.arrayMap(dummyItems, function (item) {
return new VisibleItem(item);
})),
showItem: function (item) {
this.visibleItem.push(item);
}
};
ko.applyBindings(viewModel);
site.js
$(document).on('click', '.result', function () {
var item = ko.dataFor(this);
viewModel.showItem(item); //add item to "visibleItems" viewmodel for management
createItemDiv(item); //ui function to show item on screen
});
try this :
var viewModel = function() {
var self = this;
self.searchResult = ko.observableArray(ko.utils.arrayMap(dummyResults, function (item) {
return new SearchResult(item);
})),
self.visibleItem = ko.observableArray(ko.utils.arrayMap(dummyItems, function (item) {
return new VisibleItem(item);
})),
self.showItem = function (item) {
self.visibleItem.push(item);
}
};
I just realized I had an old binding in my html still... will vote to close.