Variable Dependency with knockoutJS - javascript

I'm building an application with KnockoutJS with a component that essentially acts as a sequential spreadsheet. On different lines users may define variables or use them to represent a value.
So for example
x =2
x //2
x = 4
x //4
I have this working in the straightforward case of continuing adding new lines. The output function for each line checks and iterates backwards to see if the variable was ever defined previously. If it was it uses the first example it finds and sets that as the value. This works when initially defining the lines, and also works when you edit a line after a previous line has changed.
However, I would like variables to update if a previous definition of that variable has changed, been removed, or been added. That behavior does not exist right now. I have tried adding my own custom dependency handling code using a map to track the variables, but it badly impacted performance. I would like to tap into Knockouts dependency management to solve this, but I'm not sure of the best way to do so. Here is a brief summary of my code structure, I would be happy to add more detail if needed.
calcFramework is the view-model object I bind to the map. It consists of an observable list of Lines, a varMap, and other unrelated properties and functions
Line is a custom object. The relevant code is below
var Line = function (linenum,currline) {
var self = this;
self.varMap = {};
self.input = ko.observable("");
self.linenum = ko.observable(linenum);
self.lnOutput = ko.computed({
read:function(){
return outputFunction(self,self.input());
},
write:function(){},
owner:self
});
};
function outputFunction(self,input) {
try{
var out = EQParser.parse(input,10,self);
return out.toString();
}
catch(ex){
//error handling
}
}
Line.prototype.getVar = function (varName, notCurrentLine) {
if(typeof varName === "undefined"){
return null;
}
//Actually don't want ones set in the current varMap, only past lines
if(varName in this.varMap && notCurrentLine){
return this.varMap[varName];
}
if (this.linenum() > 0) {
var nextLine = calcFramework.lines()[this.linenum() - 1];
return nextLine.getVar(varName,true);
} else {
//eventually go to global
return calcFramework.varMap[varName];
}
};
Line.prototype.setVar = function(varName,value){
this.varMap[varName] = value;
};
SetVar and getVar are passed to eqParser, which gets the value of the expression, calling those functions as needed if a variable is referenced. So the variable value is not explicitly passed to the function and thus knockout does not view it as a dependency. But I'm not sure how I would pass the variable as a parameter without traversing the list every time.
So my question is, given this setup, what is the best way to track changes to a variable assignment (and/or new assignments) and update the lines that reference that variable, while maintaining good performance.
I recognize my question is lengthy and I have attempted to trim out all unnecessary detail. Thanks for your patience in reading.

I would be tempted to use a publish/subscribe model, using something like Peter Higgins' PubSub jquery plugin
Your overall app would subscribe/listen out for lines publishing an event that they have a variable definition. This would store any variable names in a standard javascript hashtable, along with the value. When a variable found event is published by a line, the app would check through all the known variables, and if it finds that it is a change to an existing variable value, it would publish a variable changed event. All the lines would subscribe to that event. They can then check whether they have a variable matching that name, and update the value accordingly.
Here's some untested code to give you an idea of what I mean:
var app = function()
{
var self = this;
self.variables = {};
$.subscribe('/variableAssigned', function (key, value)
{
// I think that this is the best way of checking that there is a variable
// in the object
if(self.variables.hasOwnProperty(key))
{
if(self.variables[key] !== value)
{
$.publish('/variableChanged', [ key, value ]);
}
}
});
}
In your Line object:
$.subscribe('/variableChanged', function (key, value)
{
// loop through varMap and see if any of them need updating.
});

Related

Remove JS object reference

Currently I'm solving https://www.hackerrank.com/challenges/the-trigram in JS. When I run the solution against the test case input - it "passes". But when submitting it - it gets Runtime error.
The main idea is I have a class (Trigram), where using the input.split(" ").forEach( ... ) (after normalising the input and stuff) I load all the possible trigrams, compare to a candid and if it occurs more times - save to a var outside the forEach.
In the loop, the objects are initialised within let scope (I'm not sure in the terminology).
After googling around, I've found out that the objects are being referenced for ever (despite let), so garbage collector does not get rid of them. That is why I get Runtime error.
How can I get rid of the unnecessary references?
// not the exact code
function processData(input) {
var candid = new Trigram();
input.split(" ").forEach(function(element, index, array) {
let obj = new Trigram(array[index], array[index+1], array[index+2]); // I guess, by using array[n] I'm using some kind of ultimate referencing
if (magic) {
candid = obj; // with the test case's input it runs twice
}
});
}

Use window to execute a formula instead of using eval

My code need to execute a forumla (like Math.pow(1.05, mainObj.smallObj.count)).
My path is :
var path = mainObj.smallObj.count;
as you can see.
If needed, my code can split all variable names from this path and put it in an array to have something like :
var path = ["mainObj", "smallObj", "count"];
Since I don't want to use eval (this will cause memory leaks as it will be called many times every seconds), how can I access it from window?
Tried things like window["path"] or window.path.
If it is always unclear, let me know.
Thanks in advance for any help.
EDIT: forget to tell that some config are written in JSON, so when we take the formula, it's interpreted as "Math.pow(1.05, mainObj.smallObj.count)" so as a string.
I would say there are better solutions then eval, but it depends how the forumla can be structured. It could be precompiled using new Function (this is also some kind of eval) but allowing it to be called multiple times without the need to recompile for each invocation. If it is done right it should perform better then an eval.
You could do something like that:
var formula = {
code : 'Math.pow(1.05, mainObj.smallObj.count)',
params : ['mainObj']
}
var params = formula.params.slice(0);
params.push('return '+formula.code);
var compiledFormula = Function.apply(window, params);
//now the formula can be called multiple times
var result = compiledFormula({
smallObj: {
count: 2
}
});
You can get the path part reconciled by recursively using the bracket notation:
window.mainObj = { smallObj: { count: 2 } };
var path = ["mainObj", "smallObj", "count"];
var parse = function (obj, parts) {
var part = parts.splice(0, 1);
if (part.length === 0) return obj;
obj = obj[part[0]];
return parse(obj, parts);
};
var value = parse(window, path);
alert(value);
Basically, parse just pulls the first element off the array, uses the bracket notation to get that object, then runs it again with the newly shortened array. Once it's done, it just returns whatever the result of the last run is.
That answers the bulk of your question regarding paths. If you're trying to interpret the rest of the string, #t.niese's answer is as good as any other. The real problem is that you're trusting code from an external source to run in the context of your app, which can be a security risk.

Modifying scope object values in checkbox using ngchange

Forgive if this is a newbie error but I think this must be really simple and I am obviously missing something..
I have the following ngrepeat:
<div class="panel-body" data-ng-repeat="participant in activity.Participants" ng-show="showp" ng-init="participant.CheckInTime ='not set'"> <--The 'init' is for debug
{{getparticipantName(participant.ParticipantID)}}
Check in = {{participant.CheckInTime}}
<input type="checkbox" ng-model="participant.CheckedIn"
ng-change="setToNowOrNull(participant.CheckedIn, 'participant.CheckInTime')">
<br />
Check in = {{participant.CheckInTime}}
</div>
Which is nested within another ng-repeat which defines the controller etc. and that works fine. But when I click the checkbox, the settoNowOrNull function gets called, changes the value as it should, but this isn't returned to the participant.CheckInTime .. here is the function:
$scope.setToNowOrNull = function (deciderbool, thingtoset) {
if (deciderbool) //its been set to true.
{
$scope[thingtoset] = Date.now();
}
else //its been cleared to false
{
$scope[thingtoset] == null;
}
}
I added the $scope[thingtoset] after reading another question on here but to no avail.. the same with the single quotes around 'participant.CheckInTime' in the ng-change line. (This is supposed to pass the object not the value?)..
I'm obviously not getting something, and I'd have thought I could have done it in the html angular anyway rather than needing to call the controller for something so trivial - I just need to record the Date.Now() into the participant.CheckInTime. Thoughts anyone?
You can try this:
<input type="checkbox" ng-model="participant.CheckedIn"
ng-change="setToNowOrNull(participant)">
$scope.setToNowOrNull = function (participant) {
if (participant.CheckedIn) //its been set to true.
{
participant.CheckInTime = Date.now();
}
else //its been cleared to false
{
participant.CheckInTime == null;
}
}
There are several problems with your approach, particularly with
$scope[thingtoset] = Date.now();
Using value being passed into function this would be
$scope['participant.CheckInTime'] = Date.now();
This isn't the same as
$scope.participant.CheckInTime;
The whole string represents one object key and would have to be parsed into parts to get 2 levels out of it. Essentially it's invalid syntax for what you had hoped to accomplish
Even if it was valid there is no such object on your scope since participant is an alias for an object in the view. That object is within the array activity.Participants.
In conclusion, pass the actual object into your function and work with the whole object
If you want a generic method for multiple properties it would need to be more like:
$scope.setToNowOrNull = function (object, key){
switch(key){
case 'CheckInTime':
// code for this key
break;
}
}
Then in markup would use:
<input ng-change="setToNowOrNull(participant, 'CheckInTime')">
I suspect that you likely don't need such a generic method and can simply pass a single argument, the participant object, for that specific change handler
Within the ng-repeat, the object has its own scope, therefore when you try to write into $scope[thingtoset], instead of overriding the parent scope's corresponding object, you just create another object with the same name in the lower scope. Instead you can create an object in the upper scope, say $scope.thingHolder, and when this function is called, it can update $scope.thingHolder[thingtoset]. Then you will be able to observe the changes correctly in the upper scope too.

Javascript - Array of prototype functions

I'm a javascript newbie so I'm writing ugly code so far sometimes due to my lack of experience and how different it is to the languages I'm used to, so the code I'll post below works, but I'm wondering if I'm doing it the right way or perhaps it works but it's a horrible practice or there is a better way.
Basically, I have a little dude that moves within a grid, he receives from the server an action, he can move in 8 directions (int): 0:up, 1: up-right, 2: right... 7: up-left.
the server will send him this 0 <= action <= 7 value, and he has to take the correct action... now, instead of using a switch-case structure. I created a function goUp(), goLeft(), etc, and loaded them in an array, so I have a method like this:
var getActionFunction = actions[action];
actionFunction();
However, what to set all this up is this:
1) create a constructor function:
function LittleDude(container) {
this.element = container; //I will move a div around, i just save it in field here.
}
LittleDude.prototype.goUp() {
//do go up
this.element.animate(etc...);
}
LittleDude.prototype.actions = [LittleDude.prototype.goUp, LittleDude.prototype.goUpLeft, ...];
//In this array I can't use "this.goUp", because this points to the window object, as expected
LittleDude.prototype.doAction = function(action) {
var actionFunction = this.actions[action];
actionFunction(); //LOOK AT THIS LINE
}
Now if you pay attention, the last line won't work.. because: when i use the index to access the array, it returns a LittleDude.prototype.goUp for instance... so the "this" keyword is undefined..
goUp has a statement "this.element"... but "this" is not defined, so I have to write it like this:
actionFunction.call(this);
so my doAction will look like this:
LittleDude.prototype.doAction = function(action) {
var actionFunction = this.actions[action];
actionFunction.call(this); //NOW IT WORKS
}
I need to know if this is hackish or if I'm violating some sort of "DO NOT DO THIS" rule. or perhaps it can be written in a better way. Since it seems to me kind of weird to add it to the prototype but then treating it like a function that stands on its own.
What you are trying to do is one of the possible ways, but it is possible to make it more simple. Since object property names are not necessary strings, you can use action index directly on prototype. You even don't need doAction function.
LittleDude = function LittleDude(container) {
this.container = container;
}
LittleDude.prototype[0] = LittleDude.prototype.goUp = function goUp() {
console.log('goUp', this.container);
}
LittleDude.prototype[1] = LittleDude.prototype.goUpRight = function goUpRight() {
console.log('goUpRight', this.container);
}
var littleDude = new LittleDude(123),
action = 1;
littleDude[action](); // --> goUpRight 123
littleDude.goUp(); // --> goUp 123
actionFunction.call(this); //NOW IT WORKS
I need to know if this is hackish or if I'm violating some sort of "DO NOT DO THIS" rule. or perhaps it can be written in a better way.
No, using .call() is perfectly fine for binding the this keyword - that's what it's made for.
Since it seems to me kind of weird to add it to the prototype but then treating it like a function that stands on its own.
You don't have to define them on the prototype if you don't use them directly :-) Yet, if you do you might not store the functions themselves in the array, but the method names and then call them with bracket notation:
// or make that a local variable somewhere?
LittleDude.prototype.actions = ["goUp", "goUpLeft", …];
LittleDude.prototype.doAction = function(action) {
var methodName = this.actions[action];
this[methodName](); // calls the function in expected context as well
}

How to retrieve a conditionally set variable from inside a function?

What can I do to retrieve a conditional variable from inside a function in the webpage being parsed by greasemonkey?
Something like this:
var myglobal = 5;
function myfunc() {
myglobal = myglobal -1;
if (myglobal == -1) {
var this1 = 'Test';
document.getElementById("mybutton").href = this1;
}
}
In this case I would like to read what is in 'this1', either directly or modifying 'myglobal', calling 'myfunc' and somehow getting the value of href from 'mybutton'... any ideas?
You usually can't get a variable value from outside a scope like that, but you may not have to in this case.
"myfunc is a counter, and I want to skip it, otherwise mybutton.href won't be set yet."
Based on the sample code, you might be able to cheat that timer just by using:
unsafeWindow.myglobal = 0;
(In fact, I use this exact technique on one very misguided training site.)
To answer the stated question further, you cannot get dynamic values from within such a scope but the initial state may be enough, as it appears to be for this question.
So you could get myfunc()s code and parse it with regex to obtain the desired value:
var theFunc = unsafeWindow.myfunc.toString ();
var desiredHref = theFunc.match (/var\s+this1\s*=\s*["']([^"']+)["']/);
if (desiredHref.length > 1) {
// Found!
desiredHref = desiredHref[1];
}

Categories

Resources