Update knockout viewmodel when uploading documents via ajax - ajax

I'm trying to use knockout for a view where I'm uploading documents and showing a list. For this I'm using jquery.form.js in order to upload them using ajax. I've changed that to use knockout and my viewmodel looks like this
var ViewModel = function (groups) {
var self = this;
self.groups = ko.observableArray(ko.utils.arrayMap(groups, function (group) {
return {
planName: ko.observable(group.Key),
documentList: ko.observableArray(ko.utils.arrayMap(group.Values, function (value) {
return {
document: ko.observable(new Document(value))
};
}))
};
}));
var options = {
dataType: 'json',
success: submissionSuccess
};
self.add = function () {
$('#addForm').ajaxSubmit(options);
return false;
};
function submissionSuccess(result) {
alert('success');
}
};
Having one Document function for doing the mapping. I'm stuck when receiving the Json data from the controller. The result is correct, a list of objects in the same format I'm receiving on first load but I don't know how to "refresh" the viewmodel to use this new list.
Don't know if using the ko mapping plugin would make it easier as I have never used it and don't even know if it's applicable for this.
The controller method, in case is relevant, is this (if something else neede let me know althoug won't have access to the code in the next hours)
[HttpPost]
public ActionResult AddDocument(AddDocumentViewModel viewModel)
{
var partyId = Utils.GetSessionPartyId();
if (viewModel.File.ContentLength > Utils.GetKBMaxFileSize * 1024)
ModelState.AddModelError("File", String.Format("The file exceeds the limit of {0} KB", Utils.GetKBMaxFileSize));
if (ModelState.IsValid)
{
_documentsManager.AddDocument(viewModel, partyId);
if (Request.IsAjaxRequest())
{
var vm = _displayBuilder.Build(partyId);
return Json(vm.Documents);
}
return RedirectToAction("Index");
}
var newViewModel = _createBuilder.Rebuild(viewModel, partyId);
return PartialView("_AddDocument", newViewModel);
}
Thanks
EDIT: I came up with this code which seems to work (this function is inside the ViewModel one
function submissionSuccess(result) {
self.groups(ko.utils.arrayMap(result, function (group) {
return {
planName: ko.observable(group.Key),
documentList: ko.utils.arrayMap(group.Values, function (value) {
return {
document: new Document(value)
};
})
};
}));
};

Are you sure the documentList and document need to be observables themselves ?
To update the list you can push to it as you'd do on a regular array.
You could try something like this:
function submissionSuccess(result) {
self.groups.removeAll();
$.each(result, function(index, value) {
var documentList = [];
$.each(value.Values, function(index, value) {
documentList.push(new Document(value));
});
var group = {
planName:value.Key,
documentList: documentList
};
self.groups.push(group);
});
};

Related

how to integrate google-recaptcha in oracle-jet

I am trying to integrate google-recaptcha but no success.
Getting error
feedback.js:39 Uncaught TypeError: grecaptcha.render is not a function
main.js
'googlerecaptcha':'https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit',
define(['ojs/ojcore', 'knockout', 'jquery', 'appController', 'ckeditor', 'googlerecaptcha', 'ojs/ojlabel',
'ojs/ojknockout', 'ojs/ojinputtext', 'ojs/ojformlayout'],
function (oj, ko, $, app, ckeditor, grecaptcha) {
/**
* The view model for the main content view template
*/
function feedbackViewModel() {
var self = this;
// For small screens: labels on top
// For medium screens and up: labels inline
this.labelEdge = ko.computed(function () {
return app.smScreen ? "top" : "start";
}, this);
onloadCallback = function (a) {
grecaptcha.render('submit', {
'sitekey': 'YOUR_API_KEY',
'callback': self.onSubmit
}, true);
};
this.handleActivated = function (info) {
};
self.onSubmit = function (token) {
console.info("google recatpcha onSubmit", token)
//do validation/application code using token
var data = {secret: grecaptcha, response: recaptchaToken};
$.post({
url: "https://www.google.com/recaptcha/api/siteverify",
form: data
}).then(function (e) {
//recaptcha service called...check result
var resp = JSON.parse(e);
if (resp.success == false) {
console.info("recaptcha token outcome is false")
} else {
console.info("recaptcha token validated")
}
});
};
}
return feedbackViewModel;
});
Do you have a mapping for 'googlerecaptcha' in src/js/path_mapping.json? If I go to https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit, I do not see that it is returning any valid object. So most likely 'grecaptcha' variable is undefined.
reCaptcha + RequireJS
Looks like reCaptcha is a function that has to be executed vs an object that can be interacted with directly. So you may need a different approach, something mentioned in this thread.

ngTagsInput Autocomplete does not Open

I am unable to get the AutoComplete list to display. My service returns json model: TagID: 1, text:MyText
but the AutoComplete list never displays. My HTML:
<tags-input ng-model="tags" tag-class="{even: $index % 2 == 0, odd: $index % 2 != 0}" on-tag-added="addTag(tags)"> <auto-complete source="loadTags($query)"></auto-complete> </tags-input>
My Controller code:
$scope.loadTags = function ($query) {
var tags;
contractorService.gettags()
.success(function (data) {
tags = data;
return tags.filter(function(tag) {
return tag.text.toLowerCase().indexOf($query.toLowerCase()) != -1
UPDATE
I have discovered that it just does not like the Json returned from Ajax call to MVC Controller.
public async Task<ActionResult> GetMajorTags()
{
majorId = UserInfo.intMajorID;
var tags = await CompanyClient.GetAvailableTags(majorId);
return Json(tags, JsonRequestBehavior.AllowGet);
}
Even bypassing the service and calling the MVC Controller method directly like below:
$scope.loadTags = function (query) {
return $http.get('/SSQV4/SSQV5/Contractor/GetMajorTags');
};
If I make the source static like below:
var auto = [
{ TagID: 4,text: 'Tag4' },
{ TagID: 5, text: 'Tag5' },
{ TagID: 6, text: 'Tag6' }
];
It works, but it will not show what is returned from the MVC Controller even though the data returned is in the EXACT same format.
Any assistance is greatly appreciated!
This code is not correct:
$scope.loadTags = function ($query) {
var tags;
contractorService.gettags()
.success(function (data) {
tags = data;
// return where?
return tags.filter(function(tag) {
return tag.text.toLowerCase().indexOf($query.toLowerCase()) != -1
});
});
}
There is no reason to have a return statement within your success callback. Where would that return to? If you did something like this:
var tags = $scope.loadTags();
console.log(tags); // undefined
... tags would be undefined. The reason is because the return statement is NOT returning from the call to loadTags. It is instead returning from within a promise callback.
This is actually how it's done:
var tags = [];
$scope.loadTags = function () {
contractorService
.gettags()
.success(function (data) {
tags = data;
tags = tags.filter(function(tag) {
return tag.text.toLowerCase().indexOf($query.toLowerCase()) != -1;
});
});
};
Notice how there are no return statements (except for your filter).
This just does not work at all:
$scope.loadTags = function (query) {
return $http.get('/SSQV4/SSQV5/Contractor/GetMajorTags');
};
If you were to do something like this:
var tags = $scope.loadTags();
console.log(tags); // promise object. NO DATA
tags would contain a promise object NOT your data. You would need to do the following to get the actual data:
var tags = [];
$scope.loadTags().success(function(data) {
tags = data;
});

Ember: Fetching data for objects that depend on each other

I'm trying to build a front-end for a metrics tool with Ember. The code that I've written so far has been very much influenced by Eviltrout's emberreddit application
https://github.com/eviltrout/emberreddit
The goal is to have two classes that depend on each other: metrics and filters.
1) Once the application initializes, the filters, which are instances of the Filter-class, are loaded from the server. Once the filters have loaded, they are displayed as checkboxes on the screen. After that, the metrics objects should take the filters as parameters and query the server for data.
2) Once the user changes the checkboxes and thus updates the filter objects, the application should take the filters as parameters again and fetch new metrics data from the server.
My problem is that I don't know how to handle the dependencies between these two sets of objects with asynchronous ajax calls. At it's current state, my application doesn't finish loading the filters when it already starts loading the metrics. Therefore, the filters don't get passed as parameters for the metrics ajax-call.
My question is: What's the best way to do this ember? There surely has to be a way to handle the order of ajax calls. My intuition is that manually adding observers isn't the way to go.
Here are the models of my application:
//FILTER MODELS
var defaultFilters = ['dates', 'devices'];
//set Filter class. The Filter object will be multiplied for each filter.
App.Filter = Ember.Object.extend({
//capitalize first letter to get title
filterTitle: function() {
return this.get('id').charAt(0).toUpperCase() + this.get('id').slice(1);
}.property('id'),
//set attribute to see if filter has loaded
loadedFilter: false,
//create method to load filter values from server
loadValues: function() {
var filter = this;
return Ember.Deferred.promise(function (p) {
if (filter.get('loadedFilter')) {
p.resolve(filter.get('values'));
} else {
p.resolve($.getJSON("http://127.0.0.1:13373/options/" + filter.get('id')).then(function(response) {
var values = Ember.A();
response[filter.get('id')].forEach(function(value) {
values.push(value);
});
filter.setProperties({values: values, loadedFilter: true});
return values;
}))
}})}
}
);
//reopen class to create "all" method which returns all instances of Filter class
App.Filter.reopenClass({
all: function() {
if (this._all) {return this._all; }
var all = Ember.A();
defaultFilters.forEach(function(id) {
all.pushObject(App.Filter.create({id: id}));
});
this._all = all;
return all;
}});
//Create a Filters array to store all the filters.
App.Filters = App.Filter.all();
//METRIC MODELS
App.Metric = Ember.Object.extend({
metricTitle: function() {
return this.get('id').charAt(0).toUpperCase() + this.get('id').slice(1);
}.property('id'),
loadedMetric: false,
filtersBinding: 'App.Filters',
loadValues: function() {
var metric = this;
var filters = metric.get('filters');
if (filters.get('loadedFilters'))
console.log('loading metrics');
return Ember.Deferred.promise(function (p) {
if (metric.get('loadedMetric')) {
p.resolve(metric.get('values'));
} else {
p.resolve(
console.log('sending ajax'),
$.ajax({
url: "http://127.0.0.1:13373/" + metric.get('id') + "/",
data: JSON.stringify(metric.get('filters')),
}).then(function(response) {
var values = Ember.A();
response[metric.get('id')].forEach(function(value) {
values.push(value);
});
metric.setProperties({"values": values, "loadedMetric": true});
return values;
}))
}})}
});
App.Metric.reopenClass({
findByView: function(searchView) {
if (this._metrics) {return this._metrics; }
var metrics = Ember.A();
defaultMetricsSettings.forEach(function(metric) {
if (metric.view == searchView)
metrics.pushObject(App.Metric.create({id: metric.id},{view: metric.view}, {calculation: metric.calculation}, {format: metric.format}, {width: metric.width}));
});
this._metrics = metrics;
return metrics;
}
});
And here are the routes:
App.ApplicationRoute = Ember.Route.extend({
//set application routes model to all filters
model: function() {
return App.Filter.all();
},
//after filter has loaded, let's load its values
afterModel: function(model) {
return model.forEach(function(item) {
item.loadValues();
});
},
//create a controller called ApplicationController and pass the filter as its model
setupController: function(controller, filter) {
controller.set('model', filter);
}
});
App.DashboardRoute = Ember.Route.extend({
model: function() {
return App.Metric.findByView('Dashboard');
},
afterModel: function(model) {
return model.forEach(function(item) {
item.loadValues();
});
},
setupController: function(controller, metric) {
controller.set('model', metric);
}
});
Controllers:
App.ApplicationController = Ember.ArrayController.extend({
//ApplicationController controls all the filters. Let's create a controller to handle each instance of a filter
itemController: 'filter'
});
App.FilterController = Ember.ObjectController.extend({
//this sets the titleId property that is used only for binding html attributes in template. Stupid way to do this.
titleId: function() {
return "#" + this.get('filterTitle');}.property('filterTitle')
});
Your afterModel hook could do this in a sequence of dependent promises. The current implementation is returning immediately, instead you chain the promise and finally return the last promise as the result of the hook. The router will wait for the whole set of calls to complete before continuing to setupController.
afterModel: function(model) {
var promise;
model.forEach(function(item)) {
if (promise) {
promise = promise.then(function() {
item.loadValues();
});
} else {
promise = item.loadValues();
}
}
return promise;
}
I'm not sure how many of the calls you have, but you may want to batch some of these together to reduce the number of HTTP requests.

Bootstrap typeahead suggestions replaced when navigation

I'm using Bootstrap Typeahead to suggest som search results. The results are returned from a ajax ressource, and since this resource creates a delay, I'm experiencing a unfortunate effect.
Example:
If typing a 4 letter word, the suggestions will appear after 2 letters, I can then go through the results with the keys up/down, but suddenly the suggestions will reload because the last request has finished.
Is there any way to "cancel" any remaining, if user is currently using the keys up/down to go through the suggestions?
('#query').typeahead({
items: 4,
source: function (query,process) {
map = {};
$.getJSON('/app_dev.php/ajax/autosuggest/'+query, function (data) {
vehicles = [];
$.each(data, function(i,vehicle){
map[vehicle.full] = vehicle;
vehicles.push(vehicle.full);
});
process(vehicles);
});
},
updater: function (item) {
// do something here when item is selected
},
highlighter: function (item) {
return item;
},
matcher: function (item) {
return true;
}
});
I think the following will satisfy your needs (its hard to reproduce exactly) :
There is no easy way to abort a delayed response, but you could extend typeahead as I figured out here (without modifying bootstrap.js)
The concept is to catch keydown, detect if the event is KEY_UP or KEY_DOWN, set a flag is_browsing, and then abort process if is_browsing is true (that is, if the user has hitted KEY_UP or KEY_DOWN and no other keys afterwards).
Extending typeahead :
// save the original function object
var _superTypeahead = $.fn.typeahead;
// add is_browsing as a new flag
$.extend( _superTypeahead.defaults, {
is_browsing: false
});
// create a new constructor
var Typeahead = function(element, options) {
_superTypeahead.Constructor.apply( this, arguments )
}
// extend prototype and add a _super function
Typeahead.prototype = $.extend({}, _superTypeahead.Constructor.prototype, {
constructor: Typeahead
, _super: function() {
var args = $.makeArray(arguments)
// call bootstrap core
_superTypeahead.Constructor.prototype[args.shift()].apply(this, args)
}
//override typeahead original keydown
, keydown: function (e) {
this._super('keydown', e)
this.options.is_browsing = ($.inArray(e.keyCode, [40,38])>-1)
}
//override process, abort if user is browsing
, process: function (items) {
if (this.options.is_browsing) return
this._super('process', items)
}
});
// override the old initialization with the new constructor
$.fn.typeahead = $.extend(function(option) {
var args = $.makeArray(arguments),
option = args.shift()
// this is executed everytime element.modal() is called
return this.each(function() {
var $this = $(this)
var data = $this.data('typeahead'),
options = $.extend({}, _superTypeahead.defaults, $this.data(), typeof option == 'object' && option)
if (!data) {
$this.data('typeahead', (data = new Typeahead(this, options)))
}
if (typeof option == 'string') {
data[option].apply( data, args )
}
});
}, $.fn.typeahead);
This typeahead-extension could be placed anywhere, eg in a <script type="text/javascript"> -section
Testing the extension :
<input type="text" id="test" name="test" placeholder="type some text" data-provide="typeahead">
<script type="text/javascript">
$(document).ready(function() {
var url='typeahead.php';
$("#test").typeahead({
items : 10,
source: function (query, process) {
return $.get(url, { query: query }, function (data) {
return process(data.options);
});
}
});
});
</script>
A "serverside" PHP script that returns a lot of randomized options with forced delay, typeahead.php :
<?
header('Content-type: application/json');
$JSON='';
sleep(3); //delay execution in 3 secs
for ($count=0;$count<30000;$count++) {
if ($JSON!='') $JSON.=',';
//create random strings
$s=str_shuffle("abcdefghijklmnopq");
$JSON.='"'.$s.'"';
}
$JSON='{ "options": ['.$JSON.'] }';
echo $JSON;
?>
It really seems to work for me. But I cannot be sure that it will work in your case. Let me now if you have success or not.

value of stateid is not passing to controller

In the below script, the value of stateId is not passing to the controller. Can any one help me with this?
$("#ddlState").bind("change",function() {
var stateId = $(this).val();
$("#div_city_wrapper").load("page/new_city", {"stateId": stateId}, function(r) {
alert('done loading');
});
});
Add the extra alert with the comment next to it to your JavaScript:
$("#ddlState").bind("change",function()
{
var stateId = $(this).val();
alert(stateId); // if this isn't null or undefined, then the problem is in your controller
$("#div_city_wrapper").load("page/new_city", {"stateId": stateId}, function(r) {
alert('done loading');
});
});
If the problem is in your controller, then you need to add it as a parameter, e.g.
function new_city($stateId = NULL)
{
if ($stateId === NULL)
{
// nothing was passed to the controller, so redirect somewhere or display an error
}
// the rest of your function...
}

Resources