I am using angular $http service to get/post the data. Currently all the services using common URL to retrieve the data. Now I have requirement where I want to switch between the 2 URLs based on specific condition. So what will be the best approach to implement this functionality seamlessly without making many changes into existing service code?
Note - I don't want to write identification logic at each services.
I created sample example to explain the problem. In example currently I am using TEST_FILE1 from constant service as URL but i want to switch between TEST_FILE1 and TEST_FILE2 based on specific condition say based on random number returned from randomNumberService. Sample Example - Sample Plunkar Any help appreciated!
app.constant('RESOURCES', (function() {
return {
TEST_FILE1: 'test.json',
TEST_FILE2: 'test1.json',
}
})());
app.factory('myService', function($http, RESOURCES) {
return {
async: function() {
return $http.get(RESOURCES.TEST_FILE1); //1. this returns promise
}
};
});
app.factory('randomNumberService', function() {
return {
getRandomNumber: function() {
return Math.floor((Math.random() * 6) + 1);
}
}
});
To not repeat the URL choice logic everywhere, then you have to move it in an other factory.
app.factory('identification', function (RESOURCES, randomNumberService) {
return {
getUrl: function () {
var randomNumber = randomNumberService.getRandomNumber();
var url = randomNumber > xxx ? RESOURCES.TEST_FILE1 : RESOURCES.TEST_FILE2;
return url;
}
};
});
app.factory('myService', function($http, identification) {
return {
async: function() {
return $http.get(identification.getUrl());
}
};
});
app.factory('randomNumberService', function() {
return {
getRandomNumber: function() {
return Math.floor((Math.random() * 6) + 1);
}
}
});
I am using select2 with custom data adapter. All of the data provided to select2 is generated locally in web page (so no need to use ajax). As query method can generate a lot of results (about 5k) opening select box is quite slow.
As a remedy, I wanted to use infinite scroll. Documentation for custom data adapter says that query method should receive page parameter together with term:
#param params.page The specific page that should be loaded. This is
typically provided when working with remote data sets, which rely
on pagination to determine what objects should be displayed.
But it does not: only term is present. I tried to return more: true or more: 1000, but this didn't help. I guess this is because, by default, infinite scroll is enabled iff ajax is enabled.
I am guessing that enabling infinite scroll will involve using amd.require, but I am not sure what to do exactly. I tried this code:
$.fn.select2.amd.require(
["select2/utils", "select2/dropdown/infiniteScroll"],
(Utils, InfiniteScroll) =>
input.data("select2").options.options.resultsAdapter =
Utils.Decorate(input.data("select2").options.options.resultsAdapter, InfiniteScroll)
)
This is coffee script, but I hope that it is readable for everyone. input is DOM element containing select box - I earlier did input.select2( //options )
My question is basically, how do I enable infinite scroll without ajax?
Select2 will only enable infinite scroll, if ajax is enabled. Fortunately we can enable it and still use our own adapter. So putting empty object into ajax option will do the trick.
$("select").select2({
ajax: {},
dataAdapter: CustomData
});
Next, define your own data adapter. Inside it, inn query push pagination info into callback.
CustomData.prototype.query = function (params, callback) {
if (!("page" in params)) {
params.page = 1;
}
var data = {};
# you probably want to do some filtering, basing on params.term
data.results = items.slice((params.page - 1) * pageSize, params.page * pageSize);
data.pagination = {};
data.pagination.more = params.page * pageSize < items.length;
callback(data);
};
Here is a full fiddle
Expanding on this answer to show how to retain the search functionality that comes with select2. Thanks Paperback Writer!
Also referenced this example of how to achieve infinite scrolling using a client side data source, with select2 version 3.4.5.
This example uses the oringal options in a select tag to build the list instead of item array which is what was called for in my situation.
function contains(str1, str2) {
return new RegExp(str2, "i").test(str1);
}
CustomData.prototype.query = function (params, callback) {
if (!("page" in params)) {
params.page = 1;
}
var pageSize = 50;
var results = this.$element.children().map(function(i, elem) {
if (contains(elem.innerText, params.term)) {
return {
id:[elem.innerText, i].join(""),
text:elem.innerText
};
}
});
callback({
results:results.slice((params.page - 1) * pageSize, params.page * pageSize),
pagination:{
more:results.length >= params.page * pageSize
}
});
};
Here is a jsfiddle
I found it was just easier to hijack the ajax adapter rather than creating a whole new CustomAdapter like the above answers. The above answers don't actually seem to support paging because they all start from array, which doesn't support paging. It also doesn't support delayed processing.
window.myarray = Array(10000).fill(0).map((x,i)=>'Index' + i);
let timer = null;
$('select[name=test]')
.empty()
.select2({
ajax: {
delay: 250,
transport: function(params, success, failure) {
let pageSize = 10;
let term = (params.data.term || '').toLowerCase();
let page = (params.data.page || 1);
if (timer)
clearTimeout(timer);
timer = setTimeout(function(){
timer = null;
let results = window.myarray // your base array here
.filter(function(f){
// your custom filtering here.
return f.toLowerCase().includes(term);
})
.map(function(f){
// your custom mapping here.
return { id: f, text: f};
});
let paged = results.slice((page -1) * pageSize, page * pageSize);
let options = {
results: paged,
pagination: {
more: results.length >= page * pageSize
}
};
success(options);
}, params.delay);
}
},
tags: true
});
<link href="//cdnjs.cloudflare.com/ajax/libs/select2/4.0.7/css/select2.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.7/js/select2.full.min.js"></script>
<select name='test' data-width="500px"><option>test</option></select>
I felt the answers above needed better demonstration. Select2 4.0.0 introduces the ability to do custom adapters. Using the ajax: {} trick, I created a custom dataAdapter jsonAdapter that uses local JSON directly. Also notice how Select2's 4.0.0 release has impressive performance using a big JSON string. I used an online JSON generator and created 10,000 names as test data. However, this example is very muddy. While this works, I would hope there is a better way.
See the full fiddle here: http://jsfiddle.net/a8La61rL/
$.fn.select2.amd.define('select2/data/customAdapter', ['select2/data/array', 'select2/utils'],
function (ArrayData, Utils) {
function CustomDataAdapter($element, options) {
CustomDataAdapter.__super__.constructor.call(this, $element, options);
}
Utils.Extend(CustomDataAdapter, ArrayData);
CustomDataAdapter.prototype.current = function (callback) {
var found = [],
findValue = null,
initialValue = this.options.options.initialValue,
selectedValue = this.$element.val(),
jsonData = this.options.options.jsonData,
jsonMap = this.options.options.jsonMap;
if (initialValue !== null){
findValue = initialValue;
this.options.options.initialValue = null; // <-- set null after initialized
}
else if (selectedValue !== null){
findValue = selectedValue;
}
if(!this.$element.prop('multiple')){
findValue = [findValue];
this.$element.html(); // <-- if I do this for multiple then it breaks
}
// Query value(s)
for (var v = 0; v < findValue.length; v++) {
for (var i = 0, len = jsonData.length; i < len; i++) {
if (findValue[v] == jsonData[i][jsonMap.id]){
found.push({id: jsonData[i][jsonMap.id], text: jsonData[i][jsonMap.text]});
if(this.$element.find("option[value='" + findValue[v] + "']").length == 0) {
this.$element.append(new Option(jsonData[i][jsonMap.text], jsonData[i][jsonMap.id]));
}
break;
}
}
}
// Set found matches as selected
this.$element.find("option").prop("selected", false).removeAttr("selected");
for (var v = 0; v < found.length; v++) {
this.$element.find("option[value='" + found[v].id + "']").prop("selected", true).attr("selected","selected");
}
// If nothing was found, then set to top option (for single select)
if (!found.length && !this.$element.prop('multiple')) { // default to top option
found.push({id: jsonData[0][jsonMap.id], text: jsonData[0][jsonMap.text]});
this.$element.html(new Option(jsonData[0][jsonMap.text], jsonData[0][jsonMap.id], true, true));
}
callback(found);
};
CustomDataAdapter.prototype.query = function (params, callback) {
if (!("page" in params)) {
params.page = 1;
}
var jsonData = this.options.options.jsonData,
pageSize = this.options.options.pageSize,
jsonMap = this.options.options.jsonMap;
var results = $.map(jsonData, function(obj) {
// Search
if(new RegExp(params.term, "i").test(obj[jsonMap.text])) {
return {
id:obj[jsonMap.id],
text:obj[jsonMap.text]
};
}
});
callback({
results:results.slice((params.page - 1) * pageSize, params.page * pageSize),
pagination:{
more:results.length >= params.page * pageSize
}
});
};
return CustomDataAdapter;
});
var jsonAdapter=$.fn.select2.amd.require('select2/data/customAdapter');
My solution for angular 9
this.$select2 = this.$element.select2({
width: '100%',
language: "tr",
ajax: {
transport: (params, success, failure) => {
let pageSize = 10;
let page = (params.data.page || 1);
let results = this.options
.filter(i => new RegExp(params.data.term, "i").test(i.text))
.map(i => {
return {
id: i.value,
text: i.text
}
});
let paged = results.slice((page - 1) * pageSize, page * pageSize);
let options = {
results: paged,
pagination: {
more: results.length >= page * pageSize
}
};
success(options);
}
}
});
}
Here's a shorter, searchable version for Select2 v4 that has paging. It uses lo-dash for
searching.
EDIT New fiddle: http://jsfiddle.net/nea053tw/
$(function () {
items = []
for (var i = 0; i < 1000; i++) {
items.push({ id: i, text : "item " + i})
}
pageSize = 50
jQuery.fn.select2.amd.require(["select2/data/array", "select2/utils"],
function (ArrayData, Utils) {
function CustomData($element, options) {
CustomData.__super__.constructor.call(this, $element, options);
}
Utils.Extend(CustomData, ArrayData);
CustomData.prototype.query = function (params, callback) {
var results = [];
if (params.term && params.term !== '') {
results = _.filter(items, function(e) {
return e.text.toUpperCase().indexOf(params.term.toUpperCase()) >= 0;
});
} else {
results = items;
}
if (!("page" in params)) {
params.page = 1;
}
var data = {};
data.results = results.slice((params.page - 1) * pageSize, params.page * pageSize);
data.pagination = {};
data.pagination.more = params.page * pageSize < results.length;
callback(data);
};
$(document).ready(function () {
$("select").select2({
ajax: {},
dataAdapter: CustomData
});
});
})
});
The search loop is originally from these old Select4 v3 functions: https://stackoverflow.com/a/25466453/5601169
This is not a direct answer: after struggling a lot with this for a while, I ended up switching to selectize. Select2's support for non-Ajax search, since version 4, is terribly complicated, bordering the ridiculous, and not documented well. Selectize has explicit support for non-Ajax search: you simply implement a function that returns a list.
None of this worked for me. I don't know what original question meant but in my case, I am using angular and HTTP calls and services and wanted to avoid AJAX calls.
So I simply wanted to call a service method in place of AJAX. This is not even documented on the library's site but somehow I found the way to do it using transport
ajax: {
delay : 2000,
transport: (params, success, failure) => {
this.getFilterList(params).then(res => success(res)).catch(err => failure(err));
}
}
If anyone like me came here for this then There you go!
I want to call a method within a method for clientside but I don't know how to handle it, i've tried by calling like myFunction()and this.myFunction() but it is not working... This is my code
Template.decision.rendered = function () {
if ($(".active").length == 0) {
var random = Math.floor(Math.random() * 1000);
var $items = $(".item");
$items.eq(random % $items.length).addClass("active");
}
$.each($(".item"), function (index, value) {
if (Session.get($(this).attr('id'))) {
this.showResults(value.option);
}
});
};
Template.decision.showResults = function($option) {
$('#result').html('Option ' + $option + ' is voted');
};
As you can see I want to call showResults for each item inside rendered callback...
Found it using Template.decision.showResults(); silly me.
I think that a better way depending on what you are trying to do would be either to use a Session variable or a Meteor Method:
Session Variable.
Template.decision.created = function() {
Session.setDefault('showResults', false);
}
Template.decision.rendered = function() {
// [...]
$.each($(".item"), function (index, value) {
if (Session.get($(this).attr('id'))) {
Session.set('showResults', true);
}
});
}
Template.decision.showResults = function() {
return Session.get('showResults');
}
// in your template
<template name="decision">
{{#if showResults}}
<p>Here is the results.</p>
{{/if}}
</template>
Meteor Method.
// On the client.
Template.decision.rendered = function() {
// [...]
$.each($(".item"), function (index, value) {
if (Session.get($(this).attr('id'))) {
Meteor.call('showResults', function(error, result) {
if (!error and result === true) {
$('.class').hide() // It is here that you modify the DOM element according to the server response and your needs.
}
});
}
});
}
// Server-side method
// But, if you want it to react like a stub, put it inside your lib folder.
Meteor.methods({
showResults: function() {
// do your thing
result = true; // let's say everything works as expected on server-side, we return true
return result;
}
});
I'm Trying to fetch some data from a firebase , I'm using angular firebase plugin. I double checked debugging in inspector, the url is the correct one. It responds back, that means that the url is correct but the callback's arguments is undefined.
I'm using loaded because I need it to fire once. I tried value but shame thing.
I think I exhausted all my energy on this for today so a second opinion would be great.
P.S. I really wonder why they are not using a promise instead of a callback.
fragment from angular + firebase factory
var seekMatch = function(player) {
var deferred = $q.defer();
angular.forEach(matches.$getIndex(), function(matchId) {
var matchRef = firebaseRef('matches/' + matchId); // <-- double checked, the url sends me to the correct firebase record
var matchDB = $firebase(matchRef);
matchDB.$on('loaded', function(data) {
console.log(data); // <----- return's undefined
if (matchMakingFormula(data.playerA, player)) {
if (!match) {
match = data;
deferred.resolve(match);
}
}
});
});
return deferred.promise;
};
I'm adding all the code here to give you a better idea of what I'm trying to do.
Full code of my fb.match.service
'use strict';
angular.module('angularfireApp')
.factory('FBmatchService', ['$rootScope' , '$q', '$firebase', 'firebaseRef',
function ($rootScope, $q, $firebase, firebaseRef) {
// Service logic
var matchesRef = firebaseRef( '/matches/' );
var matches = $firebase(matchesRef);
var match = null;
var matchMakingFormula = function (playerA , playerB) {
return playerA.type !== playerB.type
&& distanceFormula( playerA.location.lat , playerA.location.long, playerB.location.lat , playerB.location.long ) < 1
};
var distanceFormula = function (lat1 , lon1 , lat2, lon2) {
var R = 6371; // km
var dLat = (lat2-lat1).toRad();
var dLon = (lon2-lon1).toRad();
var lat1 = lat1.toRad();
var lat2 = lat2.toRad();
var a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.sin(dLon/2) * Math.sin(dLon/2) * Math.cos(lat1) * Math.cos(lat2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
var d = R * c;
return d;
};
var getMatch = function (matchId) {
match = matches.$getIndex(matchId);
return match;
};
var seekMatch = function ( player ) {
var deferred = $q.defer();
angular.forEach(matches.$getIndex() , function (matchId){
var matchRef = firebaseRef( 'matches/'+matchId );
var matchDB = $firebase( matchRef );
matchDB.$on('loaded',function (data) {
console.log(data);
if (matchMakingFormula(data.playerA , player)) {
if (!match) {
match = data;
deferred.resolve(match);
}
}
});
});
return deferred.promise;
};
// Public API here
return {
get: function (matchId) {
return getMatch(matchId);
},
seek: function (points) {
return seekMatch(points);
//return match.promise;
},
new: function (points) {
//return match.promise;
},
join: function (match) {
//return match;
}
};
}]);
Thanks in advance. Cheers and have fun!
OK, finally "found" the solution. Thanks to kato that remind me to check my version.
Current version 0.7.2 preview works for me. Thing is that is not on bower yet and I assumed that I had the latest version while updating from bower. Which was wrong.
collection.$child( matchId ).$on('loaded' , function(match){ //<---- match now returns the proper object but also null or {} empty object sometimes if empty.
if (match) {
if (valid(match)){ //<-- so you need to validate the output not just !match
deferred.resolve(match);
}
else
{
deferred.reject('invalid');
}
}
else
{
deferred.reject('no match');
}
});
Either way is always a good idea to validate your endpoints before consuming them for recovery and error catching reasons.
Better update from github because the project seems to advance much quicker than it's bower registry.
Cheers and have fun.
i fixed that with a small hack in angularfire.js
line 336ish in the $on handler
336. callback();
change with
336. callback(self._snapshot);
line 587ish in the end of _wrapTimeout function add
587. //hack to preserve the snapshot from the timeout wipeouts
if ( evt === "loaded" ) {
self._snapshot = param;
}
I hope this will help you for now. I will try to find a proper solution for this.
Another think to bear in mind is that this
matchDB.$on('loaded', function(data) {
console.log(matchDB); // <--- notice what is going on here
if (matchMakingFormula(data.playerA, player)) {
if (!match) {
match = data;
deferred.resolve(match);
}
}
});
return's this
matchDB: Object
$add: function (item) {
$auth: function (token) {
$bind: function (scope, name) {
$child: function (key) {
$getIndex: function () {
$off: function (type, callback) {
$on: function (type, callback) {
$remove: function (key) {
$save: function (key) {
$set: function (newValue) {
$transaction: function (updateFn, applyLocally) {
playerA: Object // <---- notice this. it was the content of the object in firebase
__proto__: Object
Which is completely crazy...
It is actually merging the matchDB which is the DB reference of match with the object that I'm expecting from the firebase.
"13131314141"{
"playerA" : {
"location" : {
"lat" : 51.6021821,
"long" : "-02582276"
},
"type" : "monster"
}
}
You can actually make a solution out of this. but how you can take the result in the callback to use it as a promise deferred.resolve?
I can understand that they did this to be able to do
$scope.match = $matchDB.$on('loaded' , function(){});
but this doesn't serve my purpose that is decoupling firebase from my controllers and I don't really think it is actually a neat solution.
Please don't accept this as a solution because it is not a real one. You can hack your way to make it one but there is probably a better way to do it or at least the project is too young and soon a proper solution will be available.
I have the following code here (with some lines removed to make it more clear). When a user clicks an edit icon the editRow() function is called and this opens a model window. After this the code tries to save data every second by calling factory.submitItem. Everything works okay except if there's a problem saving the data.
How can I make this so that if factory.submit item entityResource.update fails then the intervals are cancelled and the code stops doing a save every second.
var factory = {
gridSetup: function ($scope) {
$scope.editRow = function (row, entityType) {
// modal stuff happens here
window.setTimeout(function () {
window.setInterval(function () {
factory.submitItem($scope, $scope.modal.data);
}, 1 * 60 * 1000);
factory.submitItem($scope, $scope.modal.data);
}, 1 * 60 * 1000);
}
},
submitItem: function ($scope, formData) {
var idColumn = $scope.entityType.toLowerCase() + 'Id';
var entityId = formData[idColumn];
switch ($scope.modal.action) {
case "edit":
var entityResource = $resource('/api/:et/:id', { et: $scope.entityType }, { update: { method: 'PUT' } });
entityResource.update({ id: entityId }, formData,
function (result) {
angular.copy(result, $scope.modal.data);
}, function (result) {
// what to put here ?
})
break;
}
},
myInterval = window.setInterval(function () {
.....
}, 1 * 60 * 1000);
and when you want to cancel....
window.clearInterval(myInterval);
setInterval() returns an interval ID, which you can pass to clearInterval():
var yourInterval = setInterval(function(), time);
You can cancel it with:
clearInterval(yourInterval);
You could do something diferent to do that. The scheme could be like this:
var everythingIsOk = true;
function doSomething(){
if (everythingIsOk) {
setTimeout(doSomething(),5000);
} else {
everythingIsOk = false;
return true;
}
}