Knockout JS bind view model to multiple distributed element IDs - javascript

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>

Related

Child view model modying a different child viewmodel

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

Knockout JS parameters with Grapnel routing

I'm working on a knockout app that now requires routing to be implemented. Grapnel looks like a great solution however I've hit a bit of a brick wall with it.
Knockout click events pass the current 'view model' to whatever function you define in your view - as documented here. As mentioned this is really useful when working with collections and the app I mention uses this quite a lot.
I'm looking for a way of being able to make use of this from within grapnel routes however I'm lost when it comes to a solution.
I've put together a rather simple fiddle to try to help explain things:
https://jsfiddle.net/nt0j49x7/4/
HTML
<div id="app">
<ul class="playlist" data-bind="foreach: albumList">
<li class="album">
<a href="" data-bind="click: $root.showAlbumInfo">
<span data-bind="text: title"></span> -
<strong data-bind="text: artist"></strong>
</a>
</li>
</ul>
<div data-bind="with: selectedAlbum">
<img data-bind="attr:{src: coverImg}" />
<div>
<span data-bind="text: title"></span> - <span data-bind="text: artist"></span>
<a data-bind="attr:{href: spotifyLink}">Listen on spotify</a>
</div>
</div>
</div>
Javascript
var appView = {
albumList: ko.observableArray([
{id: 1, title:'Helioscope' , artist: 'Vessels', coverImg: 'http://ecx.images-amazon.com/images/I/91nC-KVZBBL._SX466_.jpg', spotifyLink: 'https://open.spotify.com/album/3dARFB98TMzKLHwZOgKZhE'},
{id: 2, title:'Dilate' , artist: 'Vessels', coverImg: 'http://ecx.images-amazon.com/images/I/31AvNaBtnpL._SX466_PJautoripBadge,BottomRight,4,-40_OU11__.jpg', spotifyLink: 'https://open.spotify.com/album/7yapNLdtqlYiGFbuEuGRIt'},
{id: 3, title:'White fields and open devices' , artist: 'Vessels', coverImg: 'http://ecx.images-amazon.com/images/I/918vEDkM5PL._SX522_PJautoripBadge,BottomRight,4,-40_OU11__.jpg', spotifyLink: 'https://open.spotify.com/album/4kB1vlgei2DvIweeBoiNVu'}
]),
selectedAlbum: ko.observable(),
showAlbumInfo: function(album, event) {
// knockout supplies the clicked model value as the first parameter
appView.selectedAlbum(album);
}
};
var routes = {
'album/:id' : function(req, event){
// Any ideas on how to pass the 'album' object knockout is
// passing to the appView.showAlbumInfo method into this
// route handler? I can use the ID request param to
// get the model from albumList and set the selectedAlbum
// but that isn't what I'm trying to achieve.
}
};
//var router = new Grapnel(routes);
ko.applyBindings(appView, document.getElementById('app'));
I have been using knockout for year but only took a quick look at Grapnel. I don't see a way of passing objects as parameters. But this is obviously a single page app approach and you have declare your view model as a global. So you can access the "appView" within the router code:
var router = new Grapnel();
//must include the id in the route for function to fire on id change
router.navigate('/album/' + album.id);
console.info(appView.selectedAlbum());
}
);
Then in your viewModel event you can navigate after you set the observable.
showAlbumInfo: function(album, event) {
// knockout supplies the clicked model value as the first parameter
appView.selectedAlbum(album);
router.navigate('/album/' + album.id);
}
fiddle:example
Not sure what your app is going to be but Angular js will do what you are trying to do all in one package with observables and routeing. You won't need knockout.

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

Backbone: get data of item that was clicked

I have a simple page where I display a list of books and a few details about the book, title, price, description. Data is pulled from a JSON file.
When I click on any of the books listed, a lightbox (bootstrap modal) fires up where I'd like to show the title of the book that was clicked.
User will be able to write a comment so I'd also like to get then send the book ID.
Not sure what is the best way to get the data from the book that was clicked?
Here is my code so far (including lightbox):
Backbone:
var Book = Backbone.Model.extend();
var BookList = Backbone.Collection.extend({
model: Book,
url: 'json/books.json'
});
var BookView = Backbone.View.extend({
el: '.booksList',
template: _.template($('#booksTemplate').html()),
render: function(){
_.each(this.model.models, function(model){
this.$el.append(this.template({
data: model.toJSON()
}));
}, this);
return this;
}
});
var AppView = Backbone.View.extend({
el: 'body',
initialize: function(){
var bookList = new BookList();
var bookView = new BookView({
model: bookList
});
bookList.bind('reset', function(){
bookView.render();
});
bookList.fetch();
}
});
var appView = new AppView();
Template:
<script id="booksTemplate" type="text/template">
<div class="book">
<div class="bookDetails">
<h3><%= data.title %></h3>
<p><%= data.price %></p>
</div>
<p><%= data.description %></p>
bid
</div>
<div id="myModal" class="modal hide fade">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3><%= data.title %></h3>
</div>
<div class="modal-body">
<form action="#" method="post">
<input type="text" name="comment" id="comment" />
</form>
</div>
<div class="modal-footer">
Close
</div>
</div>
</script>
Listen to the events in your view. Source.
So basically you'll have something like this in your view:
var BookView = Backbone.View.extend({
el: '.booksList',
template: _.template($('#booksTemplate').html()),
render: function(){
_.each(this.model.models, function(model){
this.$el.append(this.template({
data: model.toJSON()
}));
}, this);
return this;
},
events: {
'click': 'openModal'
// you could also use 'click selector', see the doc
},
openModal: function() {
// here the context is your view
// so this.model will give you your collection, hence access to your data
}
});
However, I personally think that you should have several views, each for one model (=book), instead of a whole view for the collection. But hey, that's just an opinion.
Edit: details
I personally never create views for a collection. I prefer wrapping collections in another model (eg, as you have a list of books, a bookshelf...). But that's just if you need a unique element on top of the list of views.
To illustrate, say you ordered your books by genre. You'd want a wrapping view to display a title (to tell the user the genre). So you could use a wrapping model for your collection.
Now you simply want to display all of your books as one. You could only add as many views as you have books, inside some div or ul element. Hence you'd not need to wrap your collection.
I could go on forever about where, when and how I'm creating my views but that's not the point, nor am I qualified to do so (haven't had any computer science education, so you may question everything I'm saying, I won't resent you). So basically, you could just change your code to:
initialize: function(){
var bookList = new BookList; // I'm removing the parenthesis here
// I simply like to separate "new Booklist" which makes a new object
// from "Booklist()" which just calls the function
bookList.each(function(book) {
new BookView({model: book});
// here you may not need "book" and use "this" instead, not sure though
});
Then there's the question of the binding. Again, I'll let you search for your solution, but it could be as easy as doing the binding inside the views' initialize function. There are a lot of possibilities.

Using Knockout with repeated user controls

I have a web page that contains a list of games. Each game is presented by a user control, that contains a few labels that hold the properties of the game (time, scores, players, etc.). So the same user control is repeated a few times on the page.
The data changes every minute to support live covarage of the game.
I was hoping to use knockout to update all labels in the user control, but since every user control should bind to a different game data, and a user control cannot have its own view model, I dont know what is the best approach to this scenario.
I need something like a dynamic ViewModel and a dynamic data-bind attributes, but I couldnt find any information on the subject.
Here is a demonstration of the template binding using both data and foreach with the same template. You can see in the JS that the data is the type, a game, but we are dislpaying them separately in the HTML.
HTML
<!-- ko if: favoriteGame -->
<h1>Favorite Game</h1>
<div data-bind="template: { name: 'gameTemplate', data: favoriteGame }"></div>
<!-- /ko -->
<h1>All Games</h1>
<div data-bind="template: { name: 'gameTemplate', foreach: games }"></div>
<script type="text/ko" id="gameTemplate">
<div>
<span class="gameName" data-bind="text: name"></span>
<span data-bind="text: publisher"></span>
<input data-bind="value: score" />
<button data-bind="click: $parent.favoriteGame">Favorite</button>
</div>
</script>
Javascript
var Game = function(data) {
this.name = ko.observable(data.name || "");
this.publisher = ko.observable(data.publisher || "");
this.score = ko.observable(data.score || 0);
};
var ViewModel = function(init) {
var self = this;
self.favoriteGame = ko.observable();
self.games = ko.observableArray(ko.utils.arrayMap(init, function(g) {
return new Game(g);
}));
};
Note that the click: $parent.favoriteGame binding selects the favorite game directly. Knockout passes the current context as the first parameter to function bindings, and since observables are functions, this updates the observable directly, without needing a wrapper function.
You can take a look at this in this fiddle. Its not perfectly clear what you where after, you don't have any code in your question. I hope this isn't too far off.

Categories