Child view model modying a different child viewmodel - javascript

I have a main View Model for my screen. It consists of 2 child view models.
One handles the registration section.
One handles the login section.
One handles the menu section (If authenticated and what menu items can appear, as well as the "Welcome "Username" type stuff).
$(document).ready(function () {
// Create the main View Model
var vm = {
loginVm: new LoginViewModel(),
registerVm: new RegisterViewModel(),
layoutVm: new LayoutViewModel()
}
// Get the Reference data
var uri = '/api/Reference/GetTimezones';
$.getJSON({ url: uri, contentType: "application/json" })
.done(function (data) {
vm.registerVm.Timezones(data);
});
// Bind.
ko.applyBindings(vm);
});
Once my Login model's "Login" method completes, I want to set the "IsAthenticated" value within the Menu model, as well as some other user info.
So in my login model, I have a SignIn method.
$.post({ url: uri, contentType: "application/json" }, logindata)
.done(function (data) {
toastr[data.StatusText](data.DisplayMessage, data.Heading);
if (data.StatusText == 'success') {
alert($parent.layoutVm.IsAuthenticated());
}
else {
}
})
.fail(function () {
toastr['error']("An unexpected error has occured and has been logged. Sorry about tbis! We'll resolve it as soon as possible.", "Error");
});
The alert code is my testing. I am hoping to access (and set) the IsAuthenticated property of the layoutVm model. That's one of the child models on my main View model.
However, "$parent" is not defined.
How can I update values in the layoutVm, from my loginVm?

$parent is part of the binding context, which is only available during the evaluation of the data-bind (i.e. to the binding handler).
In your viewmodel structure, you'll have to come up with a way to communicate between models yourself. For example, by passing parent view models, or by passing along shared observables. The problem you're describing can be solved by using data-bind="visible: $root.userVM.IsAuthenticated", like I answered in your previous question.
If you'd like to go with the other approach, here's an example on how to share an observable between viewmodels.
var ChildViewModel = function(sharedObs) {
this.myObs = sharedObs;
this.setObs = function() {
this.myObs(!this.myObs());
}.bind(this);
}
var RootViewModel = function() {
this.myObs = ko.observable(false);
this.vm1 = new ChildViewModel(this.myObs);
this.vm2 = new ChildViewModel(this.myObs);
this.vm3 = new ChildViewModel(this.myObs);
}
ko.applyBindings(new RootViewModel());
div { width: 25%; display: inline-block; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="with: vm1">
<h4>vm1</h4>
<p data-bind="text: myObs"></p>
<button data-bind="click: setObs">
flip
</button>
</div>
<div data-bind="with: vm2">
<h4>vm2</h4>
<p data-bind="text: myObs"></p>
<button data-bind="click: setObs">
flip
</button>
</div>
<div data-bind="with: vm3">
<h4>vm3</h4>
<p data-bind="text: myObs"></p>
<button data-bind="click: setObs">
flip
</button>
</div>
Note that each of the child view models also have write permission, so you'll have to be careful to not accidentally update the observable

Related

Cannot work out how to access property of self in ViewModel

I am very new to js and html - trying to make a basic front end for a C# web api.
I'm making a simple app for tracking bugs. I have a panel for the list of bugs, where I can click "Details" to see more info on each bug (I would post an image, but my reputation is too low). Then a new panel opens with with the details of the bug, including a button to close the bug, ie change set the status to "closed". It's with this button that I have the problem.
I have this in my Index.cshtml:
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h2 class="panel-title">Bugs</h2>
</div>
<div class="panel-body">
<ul class="list-unstyled" data-bind="foreach: bugs">
<li>
<strong><span data-bind="text: Title"></span></strong>: <span data-bind="text: Description"></span>
<small>Details</small>
</li>
</ul>
</div>
</div>
<div class="alert alert-danger" data-bind="visible: error"><p data-bind="text: error">
</p></div>
<!-- ko if:detail() -->
#* Bug Detail with Close Button *#
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h2 class="panel-title">Detail</h2>
</div>
<table class="table">
<tr><td>Title</td><td data-bind="text: detail().Title"></td></tr>
<tr><td>Description</td><td data-bind="text: detail().Description"></td></tr>
<tr><td>Status</td><td data-bind="text: detail().Status"></td></tr>
<tr><td>Created</td><td data-bind="text: detail().Created"></td></tr>
<tr><td>Owner</td><td data-bind="text: detail().Owner"></td></tr>
</table>
<div class="panel-body">
<form class="form-horizontal" data-bind="submit: closeBug(detail())">
<button type="submit" class="btn btn-default">Close bug</button>
</form>
</div>
</div>
</div>
<!-- /ko -->
Then this is the relevant stuff in app.js:
var ViewModel = function () {
var self = this;
self.bugs = ko.observableArray();
self.error = ko.observable();
self.detail = ko.observable();
self.getBugDetail = function (item) {
ajaxHelper(bugsUri + item.Id, 'GET').done(function (data) {
self.detail(data);
});
}
var bugsUri = '/api/bugs/';
function ajaxHelper(uri, method, data) {
self.error(''); // Clear error message
return $.ajax({
type: method,
url: uri,
dataType: 'json',
contentType: 'application/json',
data: data ? JSON.stringify(data) : null
}).fail(function (jqXHR, textStatus, errorThrown) {
self.error(errorThrown);
});
}
// get open bugs
function getAllBugs() {
ajaxHelper(bugsUri, 'GET').done(function (data) {
self.bugs(data);
});
}
// Fetch the initial data.
getAllBugs();
//close bug
self.closeBug = function (localDetail) {
var closedBug = {
OwnerId: self.localDetail.OwnerId,
Description: self.localDetail.Description,
Status: "closed",
Title: self.localDetail.Title,
Created: self.localDetail.Created
};
ajaxHelper(bugsUri + self.localDetail.Id, 'DELETE', self.localDetail.Id);
ajaxHelper(bugsUri, 'POST', closedBug).done(function (item) {
self.bugs.push(item);
});
}
};
To update the status of a bug, I want to take the Id of the bug currently open in the detail panel and create an identical bug except with Status set to "closed". The trouble is that there's always a problem access self.localDetail in the new variable closedBug. I've tried it here by parameterizing the closeBug method, but I've also tried accessing self.Detail, but it's done no good, so I'm here. My next step, if this fails, is to create a separate panel entirely with a form for bugId which closes the bug when you submit, but it would be better to be in the bug details window.
you're already passing localDetail as the param in the closeBug fn, so you don't need to refer to it by adding self. Try this (removed all references to self.):
//close bug
self.closeBug = function (localDetail) {
var closedBug = {
OwnerId: localDetail.OwnerId,
Description: localDetail.Description,
Status: "closed",
Title: localDetail.Title,
Created: localDetail.Created
};
ajaxHelper(bugsUri + localDetail.Id, 'DELETE', localDetail.Id);
ajaxHelper(bugsUri, 'POST', closedBug).done(function (item) {
self.bugs.push(item);
});
}
Your first issue is in your submit binding itself. It's being called as soon as it's rendered, not on submit. You want to pass the function object (optionally with bound arguments) instead of calling it in your html.
Explicit Bound Arguments
<form class="form-horizontal" data-bind="submit: closeBug.bind(null, detail)">
<button type="submit" class="btn btn-default">Close bug</button>
</form>
which will bind null as the this value of the function and pass the detail observable as the first argument. With this, your closeBug looks like
self.closeBug = function (localDetail) {
var closedBug = {
OwnerId: localDetail().OwnerId
}
}
Note, you want to unwrap it in the handler, not the html so you get the latest value and not the initial value.
Binding Context as this
Alternatively (and in more idiomatic knockout fashion) you can bind to the function object and it will be called with the binding context as this (the same as explicitly using closeBug.bind($data)).
<form class="form-horizontal" data-bind="submit: closeBug">
<button type="submit" class="btn btn-default">Close bug</button>
</form>
self.closeBug = function () {
var closedBug = {
OwnerId: this.localDetail().OwnerId
}
}
Aside: this may be helpful for better understanding this, self, and function.bind

Knockout JS bind view model to multiple distributed element IDs

This is similar to my question, but it seems the solution is to create a common parent that is close by. In terms of commonality, the only way I could do that is to bind to document or something like that, but then it defeats the purpose:
Can I applyBindings to more than one DOM element using Knockout?
Is it recommended to bind a single view model instance to multiple IDs like this. I tried it, and it works in simple cases:
ko.applyBindings(newVm, document.getElementById('grapes'));
ko.applyBindings(newVm, document.getElementById('apples'));
My reasons for doing so is that I would like to use the built in functionality to bind to specific elements on a single page application, but those elements don't have a common parent.
When a binding is applied, are any copies of the view model instance created that would cause this to be a memory hog?
This is not about multiple view models to a single page view, and this is not about multiple view models to the same element. An example use case would be a serverConnection view model, where the connect and disconnect buttons are at the top in a toolbar, while the connection status is at the bottom in a status bar.
Is it recommended to bind a single view model instance to multiple IDs
No, it's not recommended. But also not necessarily wrong...
The recommended way is to use the with binding. For example:
JS
const serverConnection = new ServerConnection();
const app = new App();
ko.applyBindings({ app, serverConnection });
HTML
<body>
<header data-bind="with: serverConnection">
<button data-bind="click: connect">Connect</button>
<button data-bind="click: disconnect">Disconnect</button>
</header>
<article data-bind="with: app">
...
</article>
<footer data-bind="with: serverConnection">
<div data-bind="text: statusCode"></div>
</footer>
</body>
Runnable snippet
const serverConnection = new ServerConnection();
const app = new App(serverConnection);
ko.applyBindings({ app, serverConnection });
function App(connection) {
this.user = connection.user;
this.heading = ko.pureComputed(
() => this.user() ? `Welcome, ${this.user()}` : `Connect to get started...`
);
}
function ServerConnection() {
this.connected = ko.observable(false);
this.connecting = ko.observable(false);
this.user = ko.observable(null);
this.connect = () => {
this.connecting(true);
setTimeout(
() => {
this.connected(true);
this.user("Jane Doe");
this.connecting(false);
},
1500
)
};
this.disconnect = () => {
this.user(null);
this.connected(false);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<header data-bind="with: serverConnection">
<button data-bind="click: connect, disable: connecting">Connect</button>
<button data-bind="click: disconnect, disable: connecting">Disconnect</button>
</header>
<article data-bind="with: app">
<h2 data-bind="text: heading"></h2>
</article>
<footer data-bind="with: serverConnection">
<div data-bind="text: connected() ? '✅' : '🛑'"></div>
</footer>

Async loading a template in a Knockout component

I'm pretty experienced with Knockout but this is my first time using components so I'm really hoping I'm missing something obvious! I'll try and simplify my use case a little to explain my issue.
I have a HTML and JS file called Index. Index.html has the data-bind for the component and Index.js has the ko.components.register call.
Index.html
<div data-bind="component: { name: CurrentComponent }"></div>
Index.js
var vm = require("SectionViewModel");
var CurrentComponent = ko.observable("section");
ko.components.register("section", {
viewModel: vm.SectionViewModel,
template: "<h3>Loading...</h3>"
});
ko.applyBindings();
I then have another HTML and JS file - Section.html and SectionViewModel.js. As you can see above, SectionViewModel is what I specify as the view model for the component.
Section.html
<div>
<span data-bind="text: Section().Name"></span>
</div>
SectionViewModel.js
var SectionViewModel = (function() {
function SectionViewModel() {
this.Section = ko.observable();
$.get("http://apiurl").done(function (data) {
this.Section(new SectionModel(data.Model)); // my data used by the view model
ko.components.get("dashboard", function() {
component.template[0] = data.View; // my html from the api
});
});
}
return SectionViewModel;
});
exports.SectionViewModel = SectionViewModel;
As part of the constructor in SectionViewModel, I make a call to my API to get all the data needed to populate my view model. This API call also returns the HTML I need to use in my template (which is basically being read from Section.html).
Obviously this constructor isn't called until I've called applyBindings, so when I get into the success handler for my API call, the template on my component is already set to my default text.
What I need to know is, is it possible for me to update this template? I've tried the following in my success handler as shown above:
ko.components.get("section", function(component) {
component.template[0] = dataFromApi.Html;
});
This does indeed replace my default text with the html returned from my API (as seen in debug tools), but this update isn't reflected in the browser.
So, basically after all that, all I'm really asking is, is there a way to update the content of your components template after binding?
I know an option to solve the above you might think of is to require the template, but I've really simplified the above and in it's full implementation, I'm not able to do this, hence why the HTML is returned by the API.
Any help greatly appreciated! I do have a working solution currently, but I really don't like the way I've had to structure the JS code to get it working so a solution to the above would be the ideal.
Thanks.
You can use a template binding inside your componente.
The normal use of the template bindign is like this:
<div data-bind="template: { name: tmplName, data: tmplData }"></div>
You can make both tmplData and tmplName observables, so you can update the bound data, and change the template. The tmplName is the id of an element whose content will be used as template. If you use this syntax you need an element with the required id, so, in your succes handler you can use something like jQuery to create a new element with the appropriate id, and then update the tmplname, so that the template content gets updated.
*THIS WILL NOT WORK:
Another option is to use the template binding in a different way:
<div data-bind="template: { nodes: tmplNodes, data: tmplData }"></div>
In this case you can supply directly the nodes to the template. I.e. make a tmplNodes observable, which is initialized with your <h3>Loading...</h3> element. And then change it to hold the nodes received from the server.
because nodesdoesn't support observables:
nodes — directly pass an array of DOM nodes to use as a template. This should be a non-observable array and note that the elements will be removed from their current parent if they have one. This option is ignored if you have also passed a nonempty value for name.
So you need to use the first option: create a new element, add it to the document DOM with a known id, and use that id as the template name. DEMO:
// Simulate service that return HTML
var dynTemplNumber = 0;
var getHtml = function() {
var deferred = $.Deferred();
var html =
'<div class="c"> \
<h3>Dynamic template ' + dynTemplNumber++ + '</h3> \
Name: <span data-bind="text: name"/> \
</div>';
setTimeout(deferred.resolve, 2000, html);
return deferred.promise();
};
var Vm = function() {
self = this;
self.tmplIdx = 0;
self.tmplName = ko.observable('tmplA');
self.tmplData = ko.observable({ name: 'Helmut', surname: 'Kaufmann'});
self.tmplNames = ko.observableArray(['tmplA','tmplB']);
self.loading = ko.observable(false);
self.createNewTemplate = function() {
// simulate AJAX call to service
self.loading(true);
getHtml().then(function(html) {
var tmplName = 'tmpl' + tmplIdx++;
var $new = $('<div>');
$new.attr('id',tmplName);
$new.html(html);
$('#tmplContainer').append($new);
self.tmplNames.push(tmplName);
self.loading(false);
self.tmplName(tmplName);
});
};
return self;
};
ko.applyBindings(Vm(), byName);
div.container { border: solid 1px black; margin: 20px 0;}
div {padding: 5px; }
.a { background-color: #FEE;}
.b { background-color: #EFE;}
.c { background-color: #EEF;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="byName" class="container">
Select template by name:
<select data-bind="{options: tmplNames, value: tmplName}"></select>
<input type="button" value="Add template"
data-bind="click: createNewTemplate"/>
<span data-bind="visible: loading">Loading new template...</span>
<div data-bind="template: {name: tmplName, data: tmplData}"></div>
</div>
<div id="tmplContainer" style="display:none">
<div id="tmplA">
<div class="a">
<h3>Template A</h3>
<span data-bind="text: name"></span> <span data-bind="text: surname"></span>
</div>
</div>
<div id="tmplB">
<div class="b">
<h3>Template B</h3>
Name: <span data-bind="text: name"/>
</div>
</div>
</div>
component.template[0] = $(data)[0]
I know this is old, but I found it trying to do the same, and the approcah helped me come up with this in my case, the template seems to be an element, not just raw html

How "Data" can help me

I'm doing a web game with javaScript and KnockoutJs library.
In my html file I have a array foreach, and the number that this array saves, is the same number of buttons that a I have to draw on the page. Like this:
<strong data-bind = "foreach: cena1.opcoes">
<button data-bind="click: $parent.teste">Opcão</button>
<font color="red"><strong data-bind="text: conteudo"> </strong></font><br>
What I want to know is, how will I know which button the player selected?
I put data like a parameter on my button function, but I don't know how this works. like this:
object.teste = function(data) {
}
You can call the function this way:
<button data-bind="click: function() { $parent.teste($data); }">Opcão</button>
or
<button data-bind="click: function() { $parent.teste($data/* here can be any arguments available in the current binding context */); }">Opcão</button>
Update
By default the first parameter is being passed to the click handler function is the current view model - $data in the current binding context.
For more details and advanced scenarios you can check the Knockout JS documentation.
The click binding passes the current item to the bound function.
var vm = {
items: [1, 2, 3],
click: function(data) {
alert('You clicked: ' + data);
}
};
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="foreach:items">
<button data-bind="click:$parent.click">Click</button>
</div>

Having issues tying together basic javascript chat page

I have the skeleton of a chat page but am having issues tying it all together. What I'm trying to do is have messages sent to the server whenever the user clicks send, and also, for the messages shown to update every 3 seconds. Any insights, tips, or general comments would be much appreciated.
Issues right now:
When I fetch, I append the <ul class="messages"></ul> but don't want to reappend messages I've already fetched.
Make sure my chatSend is working correctly but if I run chatSend, then chatFetch, I don't retrieve the message I sent.
var input1 = document.getElementById('input1'), sendbutton = document.getElementById('sendbutton');
function IsEmpty(){
if (input1.value){
sendbutton.removeAttribute('disabled');
} else {
sendbutton.setAttribute('disabled', '');
}
}
input1.onkeyup = IsEmpty;
function chatFetch(){
$.ajax({
url: "https://api.parse.com/1/classes/chats",
dataType: "json",
method: "GET",
success: function(data){
$(".messages").clear();
for(var key in data) {
for(var i in data[key]){
console.log(data[key][i])
$(".messages").append("<li>"+data[key][i].text+"</li>");
}
}
}
})
}
function chatSend(){
$.ajax({
type: "POST",
url: "https://api.parse.com/1/classes/chats",
data: JSON.stringify({text: $('input1.draft').val()}),
success:function(message){
}
})
}
chatFetch();
$("#sendbutton").on('click',chatSend());
This seems like a pretty good project for Knockout.js, especially if you want to make sure you're not re-appending messages you've already sent. Since the library was meant in no small part for that sort of thing, I think it would make sense to leverage it to its full potential. So let's say that your API already takes care of limiting how many messages have come back, searching for the right messages, etc., and focus strictly on the UI. We can start with our Javascript view model of a chat message...
function IM(msg) {
var self = this;
self.username = ko.observable();
self.message = ko.observable();
self.timestamp = ko.observable();
}
This is taking a few liberties and assuming that you get back an IM object which has the name of the user sending the message, and the content, as well as a timestamp for the message. Probably not too far fetched to hope you have access to these data elements, right? Moving on to the large view model encapsulating your IMs...
function vm() {
var self = this;
self.messages = ko.observableArray([]);
self.message = ko.observable(new IM());
self.setup = function () {
self.chatFetch();
self.message().username([user current username] || '');
};
self.chatFetch = function () {
$.getJSON("https://api.parse.com/1/classes/chats", function(results){
for(var key in data) {
// parse your incoming data to get whatever elements you
// can matching the IM view model here then assign it as
// per these examples as closely as possible
var im = new IM();
im.username(data[key][i].username || '');
im.message(data[key][i].message || '');
im.timestamp(data[key][i].message || '');
// the ([JSON data] || '') defaults the property to an
// empty strings so it fails gracefully when no data is
// available to assign to it
self.messages.push(im);
}
});
};
}
All right, so we have out Javascript models which will update the screen via bindings (more on that in a bit) and we're getting and populating data. But how do we update and send IMs? Well, remember that self.message object? We get to use it now.
function vm() {
// ... our setup and initial get code
self.chatSend = function () {
var data = {
'user': self.message().username(),
'text': self.message().message(),
'time': new Date()
};
$.post("https://api.parse.com/1/classes/chats", data, function(result) {
// do whatever you want with the results, if anything
});
// now we update our current messages and load new ones
self.chatFetch();
};
}
All right, so how do we keep track of all of this? Through the magic of bindings. Well, it's not magic, it's pretty intense Javascript inside Knockout.js that listens for changes and the updates the elements accordingly, but you don't have to worry about that. You can just worry about your HTML which should look like this...
<div id="chat">
<ul data-bind="foreach: messages">
<li>
<span data-bind="text: username"></span> :
<span data-bind="text: message"></span> [
<span data-bind="text: timestamp"></span> ]
</li>
</ul>
</div>
<div id="chatInput">
<input data-bind="value: message" type="text" placeholder="message..." />
<button data-bind="click: $root.chatSend()">Send</button>
<div>
Now for the final step to populate your bindings and keep them updated, is to call your view model and its methods...
$(document).ready(function () {
var imVM = new vm();
// perform your initial search and setup
imVM.setup();
// apply the bindings and hook it all together
ko.applyBindings(imVM.messages, $('#chat')[0]);
ko.applyBindings(imVM.message, $('#chatInput')[0]);
// and now update the form every three seconds
setInterval(function() { imVM.chatFetch(); }, 3000);
});
So this should give you a pretty decent start on a chat system in an HTML page. I'll leave the validation, styling, and prettifying as an exercise to the programmer...

Categories