Getting Lost in Prototype Method Calls - javascript

apologies I realise there are a number of other threads that describe similar issues however I have not been able to find one that answers my question fully (at least in a way that I understand).
I use the following code to create an Object which manages the UI interactions for the various complicated instances of an input Form across my site.
Using Prototypes, I effectively end up with an object called categoryForm with various methods contained within:
- categoryForm.addEventListeners
- categoryForm.handlers
- categoryForm.validation
The last two are objects containing a number of different Methods.
The below code is a cut down version of my finished code, however should be sufficient to get the problem across, as the rest is variations on a similar theme.
The Issue I have is that, in the below example:
- I click '.addNewItems' on my table
- This triggers my listener, which calls the 'addNewTableItem' handler method.
- The handler then attempts to Loop through the Inputs, passing them through the 'validation.checkInputVal' method to validate each input before proceeding.
However, by the time we're in this loop, the scope of this has totally changed (as expected) and I have no idea how to refer to my categoryForm Object and call the 'validation.checkInputVal' method. I just get an error saying that this is not a function.(again expected)
Here's the code:
function categoryFormFuncs(){
// VARIABLES
var _this = this;
var table = $('table');
// EVENT LISTENER FUNCTIONS
this.addEventListeners = function(){
// Listen for the AddItemButton on Tables and call the addNewTableItem function
table.on('click', '.addNewItems', function(e){
// Find new ItemGroup and Collect Inputs into an Array
var newItemGroup = $(this).parents('.newItemGroup')[0];
// Send New Item Group and Table to be updated to the Handler
_this.handlers.addNewTableItem(newItemGroup);
});
}
};
// HANDLER FUNCTIONS
categoryFormFuncs.prototype.handlers = {
// Function to Create a NewItemGroup table row
addNewTableItem: function (inputGroup){
var validationcheck;
// Get all the Inputs
var inputs = $(inputGroup).find('input');
// Check Inputs are valid and Highlight them if not
for(var i = 0; i < inputs.length; i++){
validationcheck = validation.checkInputVal(inputs[i]);
if(!validationcheck.passed){
$(inputs[i]).addClass('input-inValid')
return
} else {
$(inputs[i]).removeClass('input-inValid')
}
};
// If Valid, turn each input into a Table Cell and clear the original Input value
var rowCells = ""
for(var i = 0; i < inputs.length; i++){
rowCells += "<td>" + $(inputs[i]).val() + "</td>";
$(inputs[i]).val("");
}
// Construct the new Table row and update the DOM
var newRow = "<tr class='itemGroup'>" + rowCells + "<td><span class='float-right remove-item fa fa-minus-circle'></span></td></tr>";
$(inputGroup).before(newRow);
}
}
// VALIDATION CHECKS
categoryFormFuncs.prototype.validation = {
checkInputVal: function(input){
if($(input).val()){
return { passed: true }
} else {
return { passed: false, message: "Input with no Value"}
}
}
}
var categoryForm = new categoryFormFuncs();
categoryForm.addEventListeners();
I have found one way to make this work which is to provide the validation method as an argument to the Handler:
function categoryFormFuncs(){
// VARIABLES
var _this = this;
var table = $('table');
// EVENT LISTENER FUNCTIONS
this.addEventListeners = function(){
// Listen for the AddItemButton on Tables and call the addNewTableItem function
table.on('click', '.addNewItems', function(e){
// Find new ItemGroup and Collect Inputs into an Array
var newItemGroup = $(this).parents('.newItemGroup')[0];
// Send New Item Group and Table to be updated to the Handler
_this.handlers.addNewTableItem(newItemGroup, _this.validation.checkInputVal);
});
}
};
// handlers
categoryFormFuncs.prototype.handlers = {
// Function to Create a NewItemGroup table row
addNewTableItem: function (inputGroup, checkInputVal){
var validationcheck;
// Get all the Inputs
var inputs = $(inputGroup).find('input');
// Check Inputs are valid and Highlight them if not
for(var i = 0; i < inputs.length; i++){
validationcheck = checkInputVal(inputs[i]);
if(!validationcheck.passed){
$(inputs[i]).addClass('input-inValid')
return
} else {
$(inputs[i]).removeClass('input-inValid')
}
};
// If Valid, turn each input into a Table Cell and clear the original Input value
var rowCells = ""
for(var i = 0; i < inputs.length; i++){
rowCells += "<td>" + $(inputs[i]).val() + "</td>";
$(inputs[i]).val("");
}
// Construct the new Table row and update the DOM
var newRow = "<tr class='itemGroup'>" + rowCells + "<td><span class='float-right remove-item fa fa-minus-circle'></span></td></tr>";
$(inputGroup).before(newRow);
}
}
// VALIDATION CHECKS
categoryFormFuncs.prototype.validation = {
checkInputVal: function(input){
if($(input).val()){
return { passed: true }
} else {
return { passed: false, message: "Input with no Value"}
}
}
}
var categoryForm = new categoryFormFuncs();
categoryForm.addEventListeners();
Alternatively I could also pass a reference to _this from the listener to the handler in order to access _this.validation.
Either way these all feel like very messy and clunky solutions.
My question is:
a) Is there a way, using my original deisgn to access the validation methods?
b) Is there a better established pattern for this type of Lister / Handler / Validation scenario that I should know about?
(Disclaimer: I am very new to programming (6 months) so apologies if my description is incorrect in any way.

Related

How do I get the text() from a function to use in another function?

I'm trying to turn an option from a select list into text to then use the text in a function that returns a message topic for Firebase. I have had to use a function to get the text out of the list correctly but I don't know how to then output the text from the function in the second function to be used for the messaging. I can console.log the text, but I don't know what to use to just output the text itself inside the message function. I may be confusing myself a little. Please note: There are const being declared that are being used later on so any comment of "Why are you declaring those consts" won't be helpful. I need to know what to put in the TWO ### WHAT GOES HERE ### blocks to make this work?
I have tried just using jquery's text() to simply output the data I want but that just brings back undefined every time. The only way I've been able to actually get the option text is to place it in a function, but I have to actually DO something with it and I'm not sure what function will give me my desired results.
Here is the code
const select = $("#schoolSelect");
selectedOption = function() {
### WHAT GOES HERE? ###($("#schoolSelect option:selected").text())
}
const stage2 = $("#stage2Div");
const stage3 = $("#stage3Div");
const popupClose = $("#popupClose");
const popup = $("#popup");
const selectedValues = getSelectValues(select);
// Alert Buttons Functions
// Stage 2 Button
// Sends Soft Lockdown to all devices subscribed
stage2.click(function() {
var select = $("#schoolSelect"),
selectedValues = getSelectValues(select);
if (selectedValues.length == 0) {
popup.addClass("show");
}
else if (selectedValues.length > 0) {
for (var i = 0; i < selectedValues.length; i++) {
var message = createSLMessage(
"/topics/" + ### WHAT GOES HERE? ###
)
sendMessage(message);
}
}
});
For reference, here is the jQuery that is creating the list
populateList = function() {
$.getJSON("/assets/JSON/schools.JSON", function(result) {
$.each(result, function(i){
var schools = result.schools;
var output = "";
for (var i = 0; i < schools.length; i++) {
output += "<option>" + schools[i].topic + "</option>";
}
$("#schoolSelect").html(output);
});
})
};
Here is the html of the table (though less important) for reference
<select
size="100"
multiple
id="schoolSelect"
class="school_select"
></select>
I would like the function selectedOption to actually output the text. If I place console.log where the ### WHAT GOES HERE ### tag is in the function selectedOption itself and then call the selectedOption() later, the console indeed logs the correct value from the select list. I am losing my mind a little because I know there has to be some simple way to do this I just can't seem to figure it out.

Inner function does not assign value to element dynamically created by outer function in jQuery

I am trying to create a search result dynamically added below the form through ajax call using parameter given in the form. I can add results using a table, but in these results I have client ids where I need Name, so I again use ajax call for each client id which return success but can not assign the values to corresponding table field. Below is my code, what is wrong here?
var stopMulti = 0;
$('#salesreport-end_date').change(function(){
var endDate = $(this).val();
var startDate = $('#salesreport-start_date').val();
var param = startDate+'#'+endDate+'#date';
if(stopMulti == 0){
$.get('index.php?r=reports/sales-report/sales-report',{ id : param }, function(data){
var sReport = JSON.parse(data);
if(typeof sReport != 'undefined'){
if(sReport.length != 0){
$('<div id="sales-report-div-1" class="col-sm-12"><h2 id ="sales-report-heading-1">Sales Report</h2></div>').insertAfter('#sub-button-div');
$('<table class="table-bordered text-center col-sm-12"><tr id="sales-report-row-head"><th class="text-center">Invoice Number</th><th class="text-center">Client Name</th><th class="text-center">Company Name</th><th class="text-center">Phone</th><th class="text-center">Net Total</th><th class="text-center">VAT</th><th class="text-center">Total</th></tr></table>').insertAfter('#sales-report-heading-1');
$('<br><br>').insertAfter('#sales-report-div-1');
var count = 0;
for(var i = 0; i < sReport.length; i++){
$('<tr><td id=sales-report-col-1'+count+'></td><td id=sales-report-col-2'+count+'></td><td id=sales-report-col-3'+count+'></td><td id=sales-report-col-4'+count+'></td><td id=sales-report-col-5'+count+'><td id=sales-report-col-6'+count+'></td><td id=sales-report-col-7'+count+'></td></tr></div>').insertAfter('#sales-report-row-head');
$('#sales-report-col-1'+count).html(sReport[i].invoice_id);
$.get('index.php?r=reports/sales-report/get-client',{ id : sReport[i].client_id}, function(client){
var client = JSON.parse(client);
$('#sales-report-col-2'+count).text(client.client_name);
$('#sales-report-col-3'+count).text(client.company_name);
$('#sales-report-col-4'+count).text(client.telephon);
});
$('#sales-report-col-5'+count).html(sReport[i].sub_total);
$('#sales-report-col-6'+count).html(sReport[i].taxrate);
$('#sales-report-col-7'+count).html(sReport[i].invoice_total);
count++;
}
}
}
});
}
stopMulti = 1;
});
That is I am getting blank fields for client name, company name and telephone.
Firebug does not find any error in the code.
The $.get callback function will be called asynchronously, and therefor any value like count will be changed before the .get returns.
A simple technique to overcome this is to use anonymous function, so your function should look like this:
for(var i = 0; i < sReport.length; i++)
{
f = function(i,count)
{
... rest of your code
}
f(i,count);
count++;
}
Here is a general example: https://jsfiddle.net/BentalSW/auyLq7rp/
You can't use count inside the inner AJAX Callback because by the time those asynchronous calls complete count will equal sReport.length and the wrong divs will be updated.
Use a .forEach loop instead of the for loop:
sReport.forEach(function(report, count) {
...
});
This will give you a value of count that is correctly bound to the current iteration count.
NB: it shouldn't be necessary to call JSON.parse on data that's already in JSON format - jQuery will do that for you automatically.

Angular + DataTables: Clicking Rows Event w/JavaScript

I'm waiting until the data has been retrieved and then adding the data to the table and (trying to) add a click event to each row with the following code.
scope.$watch(attrs.aaData, function(value) {
var completeData = scope.$eval(attrs.allData);
var dataTable = element.dataTable(options);
var val = value || null;
if (val) {
dataTable.fnClearTable();
dataTable.fnAddData(scope.$eval(attrs.aaData));
var table = document.getElementsByClassName("dataTable")[1];
for (var i = 0, row; row = table.rows[i]; i++) {
console.log(row);
console.log(completeData[i]);
$(row).click(function(){
window.location.hash = '#/dashboard/patients/' + completeData[i].patient_id;
})
// javascript for click, but there should be a ng-click way
// $(row).attr('ng-click', 'changeView(/dashboard/patients/1)');
};
}
The console log confirms the row and completeData[i] are returning the correct values (and completeData[i]) has the patient_id component I want. Yet, when I click any row I get the following error:
Uncaught TypeError: Cannot read property 'patient_id' of undefined
Any ideas?
The issue is a scoping issue. You need to wrap your event handler in a closure.
for (var i = 0, row; row = table.rows[i]; i++) {
(function(i) {
console.log(row);
console.log(completeData[i]);
$(row).click(function(){
window.location.hash = '#/dashboard/patients/' + completeData[i].patient_id;
})
// javascript for click, but there should be a ng-click way
// $(row).attr('ng-click', 'changeView(/dashboard/patients/1)');
})(i);
}
Because as it stands now, this line:
$(row).click(function(){
window.location.hash = '#/dashboard/patients/' + completeData[i].patient_id;
})
will always refer i to the current value of i, not the value it had when the function was defined.
Alternatively, I recommend using jquery's $.each to clean up your loop and also build a closure at the same time:
$.each(table.rows, function(i, row) {
console.log(row);
console.log(completeData[i]);
$(row).click(function(){
window.location.hash = '#/dashboard/patients/' + completeData[i].patient_id;
})
// javascript for click, but there should be a ng-click way
// $(row).attr('ng-click', 'changeView(/dashboard/patients/1)');
});

return from JS function

basic JS question, please go easy on me I'm a newb :)
I pass 2 variables to the findRelatedRecords function which queries other related tables and assembles an Array of Objects, called data. Since findRelatedRecords has so many inner functions, I'm having a hard time getting the data Array out of the function.
As it currently is, I call showWin inside findRelatedRecords, but I'd like to change it so that I can get data Array directly out of findRelatedRecords, and not jump to showWin
function findRelatedRecords(features,evtObj){
//first relationship query to find related branches
var selFeat = features
var featObjId = selFeat[0].attributes.OBJECTID_1
var relatedBranch = new esri.tasks.RelationshipQuery();
relatedBranch.outFields = ["*"];
relatedBranch.relationshipId = 1; //fac -to- Branch
relatedBranch.objectIds = [featObjId];
facSel.queryRelatedFeatures(relatedBranch, function(relatedBranches) {
var branchFound = false;
if(relatedBranches.hasOwnProperty(featObjId) == true){
branchFound = true;
var branchSet = relatedBranches[featObjId]
var cmdBranch = dojo.map(branchSet.features, function(feature){
return feature.attributes;
})
}
//regardless of whether a branch is found or not, we have to run the cmdMain relationship query
//the parent is still fac, no advantage of the parent being branch since cmcMain query has to be run regardless
//fac - branch - cmdMain - cmdSub <--sometimes
//fac - cmdMain - cmdSub <-- sometimes
//second relationship query to find related cmdMains
var relatedQuery = new esri.tasks.RelationshipQuery();
relatedQuery.outFields = ["*"];
relatedQuery.relationshipId = 0; //fac -to- cmdMain
relatedQuery.objectIds = [featObjId];
//rather then listen for "OnSelectionComplete" we are using the queryRelatedFeatures callback function
facSel.queryRelatedFeatures(relatedQuery, function(relatedRecords) {
var data = []
//if any cmdMain records were found, relatedRecords object will have a property = to the OBJECTID of the clicked feature
//i.e. if cmdMain records are found, true will be returned; and continue with finding cmdSub records
if(relatedRecords.hasOwnProperty(featObjId) == true){
var fset = relatedRecords[featObjId]
var cmdMain = dojo.map(fset.features, function(feature) {
return feature.attributes;
})
//we need to fill an array with the objectids of the returned cmdMain records
//the length of this list == total number of mainCmd records returned for the clicked facility
objs = []
for (var k in cmdMain){
var o = cmdMain[k];
objs.push(o.OBJECTID)
}
//third relationship query to find records related to cmdMain (cmdSub)
var subQuery = new esri.tasks.RelationshipQuery();
subQuery.outFields = ["*"];
subQuery.relationshipId = 2;
subQuery.objectIds = [objs]
subTbl.queryRelatedFeatures(subQuery, function (subRecords){
//subRecords is an object where each property is the objectid of a cmdMain record
//if a cmdRecord objectid is present in subRecords property, cmdMain has sub records
//we no longer need these objectids, so we'll remove them and put the array into cmdsub
var cmdSub = []
for (id in subRecords){
dojo.forEach(subRecords[id].features, function(rec){
cmdSub.push(rec.attributes)
})
}
var j = cmdSub.length;
var p;
var sub_key;
var obj;
if (branchFound == true){
var p1 = "branch";
obj1 = {};
obj1[p1] = [cmdBranch[0].Branches]
data.push(obj1)
}
for (var i=0, iLen = cmdMain.length; i<iLen; i++) {
p = cmdMain[i].ASGMT_Name
obj = {};
obj[p] = [];
sub_key = cmdMain[i].sub_key;
for (var j=0, jLen=cmdSub.length; j<jLen; j++) {
if (cmdSub[j].sub_key == sub_key) {
obj[p].push(cmdSub[j].Long_Name);
}
}
data.push(obj);
}
showWin(data,evtObj) <---this would go away
})
}
//no returned cmdRecords; cmdData not available
else{
p = "No Data Available"
obj = {}
obj[p] = []
data.push(obj)
}
showWin(data,evtObj) <--this would go away
})
})
}
I'd like to have access to data array simply by calling
function findRelatedRecords(feature,evt){
//code pasted above
}
function newfunct(){
var newData = findRelatedRecords(feature,evt)
console.log(newData)
}
is this possible?
thanks!
Edit
Little more explanation.....
I'm connecting an Object event Listener to a Function like so:
function b (input){
dojo.connect(obj, "onQueryRelatedFeaturesComplete", getData);
obj.queryRelatedFeatures(input);
console.log(arr) //<----this doesn't work
}
function getData(relatedFeatData){
var arr = [];
//populate arr
return arr;
}
So when obj.QueryRelatedFeatures() is complete, getData fires; this part works fine, but how to I access arr from function b ?
Post Edit Update:
Due to the way that this event is being hooked up you can't simple return data from it. Returning will just let Dojo call to the next method that is hooked up to onSelectionComplete.
When init runs it is long before findRelatedRecords will ever be executed/fired by the onSelectionComplete event of the well, which is why you were seeing undefined/null values. The only way to work with this sort of system is to either 1) call off to a method like you're already doing or 2) fire off a custom event/message (technically it's still just calling off to a method).
If you want to make this method easier to work with you should refactor/extract snippets of it to make it a smaller function but contained in many functions. Also, changing it to have only one exit point at the end of the findRelatedRecords method will help. The function defined inside of subTbl.queryRelatedFeatures() would be a great place to start.
Sorry, you're kind of limited by what Dojo gives you in this case.
Pre Edit Answer:
Just return your data out of it. Everywhere where there is a showWin call just use this return.
return {
data: data,
evtObj: evtObj
}
Then your newfunct would look like this.
function newfunct(){
var newData = findRelatedRecords(feature,evt);
console.log(newData);
console.log(newData.data);
console.log(newData.evtObj);
}
If you only need that "data" object, then change your return to just return data;.
Also, start using semicolons to terminate statements.

how do you add a custom callback javascript param to a bing api callback?

The bing V2 javascript api requires a callback to work. Using jQuery to add the script block dynamically (ignoring pollution of global namespace):
function translate(text) {
var txt = "text=" + text;
var lang = "&to=fr";
var appId = "&appid=apikey"; // Add your AppId here
var func = "&oncomplete=window.translated";
$("<script><\/script>")
.attr("src", "http://api.microsofttranslator.com/V2/ajax.svc/Translate?" + txt + lang + appId + func)
.appendTo("HEAD");
}
and then using a click event on multiple elements to trigger the translation:
$(document).ready(function () {
$('a').click(function () {
var tr = $(this).parent().parent();
var txtin = tr.find('.in').text();
var out = tr.find('.out'); // would like translation inserted here
translate(txtin);
return false;
});
});
and finally the callback required by the api:
function translated(text) {
$("#translation").text(text);
}
I want to specify different elements to received the translated text, depending on what element was clicked to kick the translation of - but using the above approach I can't pass any extra params to bing, to then be returned in the callback.
How should I rewrite this to allow a click on el in row1 to put the translation in row1 and a click on an el in row2 to put the translation in row2? i.e. using the element assigned to 'out' in my click event.
The callback method does not support a state object, so you need to keep track of your objects in some global place. I've implemented a queue model to help you make it
Add the queue definition in the global variables are
var queue = new Array();
Add your 'out' object to it just before calling the service
$('a').click(function () {
var tr = $(this).parent().parent();
var txtin = tr.find('.in').text();
var out = tr.find('.out'); // would like translation inserted here
//Here it goes
queue.push(out);
////////////////
translate(txtin);
return false;
});
Append the index of your object to the text and it will be returned back to you as the service does not translate numbers. You can skip adding the index if you are not making more than one translation at a time, this is only to grant that you get the correct object in case of having some service calls slower than others.
function translate(text) {
//Here it goes
var txt = "text=" + text + " ___" + (queue.length - 1);
////////////////
var lang = "&to=fr";
//...no more changes here
}
Finally extract your object in the callback method and remove the appended index and the splitter from the translated text.
function translated(text) {
if (queue.length > 0) {
var splts = text.split(' ___')
var indx = splts[splts.length - 1];
var out = queue[indx];
//remove the out object from the queue
queue.slice(indx, indx + 1);
//remove the index number from the end of the word
text = text.substr(0, text.lastIndexOf(indx) - 4);
out.text(text);
}
}

Categories

Resources