Referencing properties of object constructor outside view model in knockout - javascript

So, I'm not entirely sure how to phrase this question as it's sort of two in one. I'm having a weird issue where I have an object constructor to create new 'projects' from an HTML form which are then pushed into an observableArray when the form is submitted. Everything works fine but to reference the related observables I have to use 'value: Project.title' or 'value: Project.whatever'. I haven't seen 'value: NameOfConstructor.property' used in any of the examples I've seen. I assume this works this way because the constructor is outside of my view model.
My question is this: Is there a better way to assign the value of a property in a constructor that is not in my view model? In other words, is there a better or more correct way than 'Project.title', ect?
I ask partially because one thing in my code doesn't work currently; the knockout enable property doesn't work on my "New Project" button, it stays disabled even if there is something written in the 'title' input box. I have the feeling it's because it's written as data-bind='enable: Project.title' but I can't figure how else to write it.
I've included a jsfiddle for reference though it obviously isn't working because of external dependencies.
https://jsfiddle.net/bmLh0vf1/1/
My HTML:
<form id='addBox' action='#' method='post'>
<label for='pTitle'> Title: </label>
<input id='pTitle' data-bind='value: Project.title' />
<br/>
<label for='pPriority'> Priority </label>
<select id='pPriority' data-bind='options: priorityOptions, value: Project.priority'></select>
<br/>
<button data-bind='enable: Project.title, click: newProject'>New Project</button>
</form>
And my Javascript:
function Project(title, priority) {
this.title = ko.observable(title);
this.priority = ko.observable(priority);
};
function ProjectViewModel() {
var self = this;
this.priorityOptions = ko.observableArray(['High', 'Medium', 'Low'])
this.projectList = ko.observableArray([
new Project('Create App', 'High')
]);
this.newProject = function() {
var np = new Project(Project.title, Project.priority);
self.projectList.push(new Project(Project.title, Project.priority));
console.log(self.projectList().length);
if (self.projectList().length > 1) {
console.log(self.projectList()[1].title());
};
}
};
var viewModel = new ProjectViewModel();
$(document).ready(function() {
ko.applyBindings(viewModel);
});
Lastly, I apologize if I've missed any posting conventions or if my code is especially bad. I'm very new and still teaching myself.

Your code is setting title and priority properties on the object created by new Project, but then later you're expecting to see those properties on Project itself. It doesn't have them; Project is the function, not the object created by new Project. So Project.title and Project.priority will give you undefined (and not an observable, and so not useful targets for the value binding).
Instead, have an "editing" Project instance that you use, binding the value of the inputs to the editing' instances title and priority, and then in newProject grab that instance and replace it with a new, fresh one.
Roughly speaking, in ProjectViewModel's constructor:
this.editing = ko.observable(new Project());
Update Project to default title and priority:
function Project(title, priority) {
this.title = ko.observable(title || "");
this.priority = ko.observable(priority || "Medium");
}
And in the bindings:
<input id='pTitle' data-bind='value: editing().title' />
<select id='pPriority' data-bind='options: priorityOptions, value: editing().priority'></select>
And in newProject:
var np = this.editing();
this.editing(new Project());
Then use np (instead of another new Project) when adding to the array.
Here's a simplified example:
function Project(title, priority) {
this.title = ko.observable(title || "");
this.priority = ko.observable(priority || "Medium");
}
function ProjectViewModel() {
var self = this;
this.priorityOptions = ko.observableArray(["High", "Medium", "Low"]);
this.projects = ko.observableArray();
this.editing = ko.observable(new Project());
this.addProject = function() {
this.projects.push(this.editing());
this.editing(new Project());
};
}
ko.applyBindings(new ProjectViewModel(), document.body);
<div>
<div>
<label>
Title:
<input type="text" data-bind="value: editing().title, valueUpdate: 'input'">
</label>
</div>
<div>
<label>
Priority:
<select data-bind='options: priorityOptions, value: editing().priority'></select>
</label>
</div>
<div>
<button type="button" data-bind="click: addProject, enable: editing().title">Add Project</button>
</div>
<hr>
<div>Projects:</div>
<div data-bind="foreach: projects">
<div>
<span data-bind="text: title"></span>
(<span data-bind="text: priority"></span>)
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

Related

knockout observableArray remove causes errors and fails to remove

I have an issue that I have spent two days on trying to figure out, I will try to put everything here to explain the background without unnecessary information but please ask and I shall provide the info.
The problem
The process is, that the user selects the team they want to add to the competition, clicks add, the team selected will then be removed from the main teams list, and added to the competition.teams list. To remove a team, the user selects the team from the options box, and clicks remove. This will remove the team from the competition.teams array, and re-added to the teams array.
I have a list of "teams" in a drop down box, with a button to "add team". When clicked, it will add the team selected from the drop-down box to the select options box. When I remove the Team from the box, it will fail to remove it from the parent list that is data bound to the options box.
When the team is then readded, both entries in the select are the same team name.
The desired outcome
I want the code to work as described above, its possible I have over-engineered the solution due to my limited knowledge of knockout/javascript. I am open to other solutions, I havent got to the stage of submitting this back to the server yet, i predict this will not be as easy as a normal form submit!
The exception
The error in the chrome console is:
Uncaught TypeError: Cannot read property 'name' of undefined
at eval (eval at parseBindingsString (knockout-min.3.4.2.js:68), :3:151)
at f (knockout-min.3.4.2.js:94)
at knockout-min.3.4.2.js:96
at a.B.i (knockout-min.3.4.2.js:118)
at Function.Uc (knockout-min.3.4.2.js:52)
at Function.Vc (knockout-min.3.4.2.js:51)
at Function.U (knockout-min.3.4.2.js:51)
at Function.ec (knockout-min.3.4.2.js:50)
at Function.notifySubscribers (knockout-min.3.4.2.js:37)
at Function.ha (knockout-min.3.4.2.js:41)
The Code
The HTML for the screenshot:
<div class="form-group">
<div class="col-md-3">
<label for="selectedTeams" class="col-md-12">Select your Teams</label>
<button type="button" data-bind="enable:$root.teams().length>0,click:$root.addTeam.bind($root)"
class="btn btn-default col-md-12">Add Team</button>
<button type="button" data-bind="enable:competition().teams().length>0,click:$root.removeTeam.bind($root)"
class="btn btn-default col-md-12">Remove Team</button>
<a data-bind="attr:{href:'/teams/create?returnUrl='+window.location.pathname+'/'+competition().id()}"class="btn btn-default">Create a new Team</a>
</div>
<div class="col-md-9">
<select id="teamSelectDropDown" data-bind="options:$root.teams(),optionsText:'name',value:teamToAdd,optionsCaption:'Select a Team to Add..'"
class="dropdown form-control"></select>
<select id="selectedTeams" name="Teams" class="form-control" size="5"
data-bind="options:competition().teams(),optionsText:function(item){return item().name;},value:teamToRemove">
</select>
</div>
</div>
The addTeam button click code:
self.addTeam = function () {
if ((self.teamToAdd() !== null) && (self.competition().teams().indexOf(self.teamToAdd()) < 0)){// Prevent blanks and duplicates
self.competition().teams().push(self.teamToAdd);
self.competition().teams.valueHasMutated();
}
self.teams.remove(self.teamToAdd());
self.teamToAdd(null);
};
the removeTeam button click code:
self.removeTeam = function () {
self.teams.push(self.teamToRemove());
self.competition().teams.remove(self.teamToRemove());
self.competition().teams.valueHasMutated();
self.teamToRemove(null);
};
the Competition object (some properties removed for brevity):
function Competition(data) {
var self = this;
self.id = ko.observable(data.id);
self.name = ko.observable(data.name);
self.teams = ko.observableArray(
ko.utils.arrayMap(data.teams, function (team) {
return ko.observable(new Team(team));
}));
};
the team object:
function Team(data) {
var self = this;
self.id = ko.observable(data.id);
self.name = ko.observable(data.name);
}
Anything missing or unclear? Please ask and I will add to the materials on the question.
The Solution
As suggested by #user3297291
The problem was that the objects being added to competition.teams were observable in some places and not observable in others. This was causing a binding error in some places where it would try to access the observable property inside the observable object.
Changed Competition Object
function Competition(data) {
var self = this;
self.id = ko.observable(data.id);
self.name = ko.observable(data.name);
self.teams = ko.observableArray(
ko.utils.arrayMap(data.teams, function (team) {
return new Team(team);
}));
};
Revised HTML binding (only simplified the optionsText binding)
<div class="form-group">
<div class="col-md-3">
<label for="selectedTeams" class="col-md-12">Select your Teams</label>
<button type="button" data-bind="enable:$root.teams().length>0,click:$root.addTeam.bind($root)"
class="btn btn-default col-md-12">Add Team</button>
<button type="button" data-bind="enable:competition().teams().length>0,click:$root.removeTeam.bind($root)"
class="btn btn-default col-md-12">Remove Team</button>
<a data-bind="attr:{href:'/teams/create?returnUrl='+window.location.pathname+'/'+competition().id()}"class="btn btn-default">Create a new Team</a>
</div>
<div class="col-md-9">
<select id="teamSelectDropDown" data-bind="options:$root.teams(),optionsText:'name',value:teamToAdd,optionsCaption:'Select a Team to Add..'"
class="dropdown form-control"></select>
<select id="selectedTeams" name="Teams" class="form-control" size="5"
data-bind="options:competition().teams(),optionsText:'name',value:teamToRemove">
</select>
</div>
</div>
Revised Add Team function
self.addTeam = function () {
if ((self.teamToAdd() !== null) && (self.competition().teams().indexOf(self.teamToAdd()) < 0)){
self.competition().teams().push(self.teamToAdd());
self.competition().teams.valueHasMutated();
}
self.teams.remove(self.teamToAdd());
self.teamToAdd(null);
};
Revised Remove Team Function
pretty sure I don't need the valueHasMutated() call anymore but at least it works..
self.removeTeam = function () {
self.teams.push(self.teamToRemove());
self.competition().teams.remove(self.teamToRemove());
self.competition().teams.valueHasMutated();
self.teamToRemove(null);
};
You're filling an observableArray with observable instances. This is something you generally should not do:
// Don't do this:
self.teams = ko.observableArray(
ko.utils.arrayMap(data.teams, function(team) {
return ko.observable(new Team(team));
})
);
Instead, include the Team instances without wrapping them:
// Do this instead:
self.teams = ko.observableArray(
ko.utils.arrayMap(data.teams, function(team) {
return new Team(team);
})
);
Now, you can use the "simple" optionsText binding, like you did earlier:
data-bind="optionsText: 'name', /* ... */"
Personal preference: you don't need the utils.arrayMap helper when we have .map in every browser. I'd personally write:
Team.fromData = data => new Team(data);
// ...
self.teams = ko.observableArray(data.teams.map(Team.fromData));

How to add an Knockout custom function in Typescript

I want to bind an event on my observable array so that when the input (eg, description has changed it will trigger the function. I've been reading different solutions online but none of them seem to work using Typescript. Here is my code below.
HTML:
<tbody data-bind="foreach: income">
<tr>
<td><input data-bind="value: description, event: {change: save} "/></td>
<td><input data-bind="value: amount"/></td>
</tr>
</tbody>
Typescript:
ko.observableArray.fn['save'] = function () {
// does something
}
income = ko.observableArray([{ description: 'Description', amount: '0'}]);
this.addIncome.save();
It is telling me that Property 'saveExtendedBalance' does not exist on type 'KnockoutObservableArray'. How do I rewrite this?
I have seen you have a problem declaring your knockout observable array, if you want initialize your array at least with one object, you have to change this line:
income = ko.observableArray({ description: 'Description', amount: '0'});
to this:
income = ko.observableArray([{ description: 'Description', amount: '0'}]);
This is because you have to initialize and always treat this variable income like an array an you are initializing like object.
Here is a complete and functionnal example of what you want:
I also made you a function for removing element from you array.
There is the typescript code:
class TestAdd {
dummyArray: KnockoutObservableArray<string> = ko.observableArray([]);
dummyToAdd: KnockoutObservable<string> = ko.observable("");
save = ():void => this.dummyArray.push(this.dummyToAdd());
deleteElement = (elementToDelete: string):string[] => this.dummyArray.remove(elementToDelete);
}
ko.applyBindings(new TestAdd(), document.getElementById("Test"));
And the HTML code:
<section id="test">
<input type="text" data-bind="value: dummyToAdd, event: {change: save}">
<h3 data-bind="visible: dummyArray().length > 0">Your array:</h3>
<ul data-bind="foreach: dummyArray">
<li><span data-bind="text: $data"></span> - <button data-bind="click: $parent.deleteElement">Delete element</button></li>
</ul>
</section>
In my dependencies I'm using "#types/knockout": "^3.4.38"

KnockoutJS : Validate model's property only if the bound control is visible

I have model in a page that is bound to several controls. Based on some condition some of these controls will be visible or invisible. And on the final submit I should only validate those which are visible.
The following is a sample code to explain my requirement
<script src="knockout-3.4.0.js" type="text/javascript"></script>
<input type="checkbox" data-bind="checked:requireAge" >Age Required</input><br />
Name : <input data-bind="value:Name" /><br />
<div data-bind="visible:requireAge">
Age: <input data-bind="value:Age,visible:requireAge" />
</div>
<button type="button" onclick="validateModel();">Validate</button>
<script type="text/javascript">
var viewModel = { Name: ko.observable(), Age: ko.observable(),requireAge:ko.observable(false) };
ko.applyBindings(viewModel);
function validateModel() {
//validate visible properties and throw a common message that all visible fields should be filled
}
</script>
My suggestion is to use the knockout-validation library (you made no mention of it in your question so I assume you're not using it already) It ties in seamlessly with knockout and makes validation far more convenient. I've used it extensively over the past year and its make my life a whole lot easier. No need to create computeds to keep track of whether an observable contains a valid value or not. You can find the knockout-validation library on github.
In your case you can simply do the following:
var viewModel = function(){
var self = this;
self.name = ko.observable();
self.requireAge = ko.observable(false);
self.age = ko.observable().extend({
required: {
onlyIf: function() { return self.requireAge(); }
}
});
};
Validation error messages are inserted automatically below the element the observable is bound to. You can also create your own validation rules but there are many that work straight out the box including the one demonstrated above. You can even use some data attributes for some rules. This is probably the best way to go about validation in conjunction with knockout.
Based on some condition some of these controls will be visible or invisible.
It would be better if these conditions are contained in the model. And validation method too.
See snippet:
var viewModel = function() {
this.Name = ko.observable("");
this.Age = ko.observable("");
this.requireAge = ko.observable(false);
this.isValid = ko.computed(function() {
if (ko.unwrap(this.Name).length === 0) return false;
if (ko.unwrap(this.requireAge) &&
ko.unwrap(this.Age).length === 0) return false;
return true;
}, this);
};
window.onload = function() {
ko.applyBindings(new viewModel());
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<input type="checkbox" data-bind="checked:requireAge" >Age Required</input><br />
Name : <input data-bind="value:Name" /><br />
<div data-bind="visible:requireAge">
Age: <input data-bind="value:Age,visible:requireAge" />
</div>
<div>is valid: <span data-bind="text: isValid"></span></div>

Knockout options with forms not binding to model

I am having problems with KnockoutJS and the options binding.
What I'm trying to achieve is functionality to display a form based on selected option with the options binding.
The odd thing is that it works with the first form-field but the model is not value binded for the others.
Here is my markup:
<div id="page">
<form data-bind="submit: executeTask">
<select data-bind="options: availableTasks, value: selectedTask, optionsText: 'description', optionsCaption: 'Select...',"></select>
<div data-bind="visible: selectedTask">
<input type="text" data-bind="value:selectedTask().assignee">
<input type="text" data-bind="value:selectedTask().estimatedTime">
</div>
<button class="btn" type="submit">Submit</button>
</form>
</div>
<script>
$(function() {
var taskController = new TaskController(document.getElementById("page"));
});
</script>
And here is my model:
(function () {
function Task(id, description) {
var model = this;
model.id = ko.observable(id);
model.description = ko.observable(description);
model.assignee = ko.observable();
model.estimatedTime = ko.observable();
};
window.TaskController = function (element) {
var model = this;
model.availableTasks = [
new Task("0", "Laundry"),
new Task("1", "Dinner")];
model.selectedTask = ko.observable();
model.executeTask = function (form) {
console.log(ko.toJSON(model.selectedTask()));
};
ko.applyBindings(this, element);
};
})();
The full code can be found at the following fiddle:
http://jsfiddle.net/LkqTU/17057/
As you can see in the console, only the "assignee" property is getting binded, but not the "estimatedTime" property.
What am I doing wrong?
Thanks,
Bj Blazkowicz
Solution 1:
Replace the visible binding with an if binding:
<div data-bind="visible: selectedTask">
Should be
<div data-bind="if: selectedTask">
Explanation
You were getting an error because it could not find selectedTask().assignee. The problem was that selectedTask() returned undefined at page load. Even though the inputs were not visible due to visible: selectedTask, the binding was interpreted.
With the if binding, what is inside the tag is ignored when selectedTask() returns undefined.
Solution 2:
As nemesv pointed out, you can use the with binding (see his answer):
<div data-bind="with: selectedTask">
<input type="text" data-bind="value: assignee">
<input type="text" data-bind="value: estimatedTime">
</div>

Knockout is generating two bindings for single foreach

I'm facing headache issue that led me to spend two days looking for a solution for it. I hope anyone would help me with it. I'm using knockout to generate bindings with json data for HTML markups. However, I'm not able to change the css of the element because I realized the element is generated twice and assigned the same id. Here's snippet of my code
<div id = 'divBinder' data-bind="foreach: Results" >
<div id='rowStyle' class='eligibilitydivContentTableRow' >
<div class='eligibilitydivContentLeftCell' style="float:left;" data-bind=" text: RequirementDescription"></div>
<div class='eligibilitydivContentMiddleCell' style="float:left;">
<span class='spanClass'></span>
<input class='inputRadio' type='radio' value:'true' data-bind="attr: { checked: IsChecked,'name': $index() }" />
<span class='spanClass'></span>
</div>
<div class='eligibilitydivContentRightCell' style="float:left;"><span class='spanClass'></span>
<input class='inputRadio2' type='radio' value:'false' data-bind="attr: { checked: IsChecked, 'name': $index(), onclick:'testFunction.apply(this);return true;'}" />
<span class='spanClass'></span>
</div>
</div>
<div data-bind=" attr: {'id': getSuffixRowID($index())}" style="display:none;" >
<div style="float:left;">
<textarea > </textarea>
</div>
<div>
<input type='text' id='dateField' onfocus='showDate()' /></div>
</div>
</div>
Here are the javascript function I'm using to generate ids
function getSuffixRowID(suffix) {
// alert(suffix);
return 'hiddenRows' + suffix;
}
Here's my binding
viewModel = [];
viewModel.Results = ko.mapping.fromJS(globalizedData.Results);
ko.cleanNode(document.getElementById("parentDivElement"));
ko.applyBindings(viewModel, document.getElementById("parentDivElement"));
Note that the RequirementDescription is binded correctly. The only problem is setting the css through testFunction being called when button is checked
function testFunction() {
// jQuery('#' + getSuffixRowID(this.attributes[6].nodeValue)).hide();
var nodeId = this.attributes['name'].nodeValue;
var stringValue = this.value;
switch (stringValue) {
case ('true'):
viewModel.Results()[nodeId].IsCompleted(true);
viewModel.Results()[nodeId].IsChecked(true);
break;
case ('false'):
viewModel.Results()[nodeId].IsCompleted(false);
viewModel.Results()[nodeId].IsChecked(false);
var idName = getSuffixRowID(nodeId);
$('#' + idName).css('display', 'block !important;');
break;
}
}
The id for checkbox elements are assigned via $index variable inside foreach. I realized the duplicate generation through taking a look at the generate html page. It has two duplicate foreach markups. Any help is really appreciated.
Thanks
This is not the way you should code with KnockoutJS :
onclick:'testFunction.apply(this);return true;'}
The Result object should have two properties (one for each checkbox).
So assuming your Result object looks like :
var Result = function() {
var self = this;
self.checkbox1 = ko.observable();
self.checkbox2 = ko.observable();
};
The binding the checkbox will be :
onclick: $parent:testFunction, value : checkbox2
You can add the id binding if you want.
And the TestFunction :
function testFunction(result/* the result item */) {
if(result.checkbox2()) {
}
[...]
};
With Knockout you souldn't modify the view directly. You have to leverage the viewModel and knockout will modify the view for you.
Please take a look at the visible binding too.
I hope it helps.

Categories