I'm just getting started with Knockout.js and i have a view(html) which is supposed to be populated by data from a rest api via jquery's $.getJSON method.
When i run the app, nothing shows but using firebug i can see that the 'GET' query returns a status code of 200 and the right data.
I'm at a fix as to why nothing shows in the view since the bindings in Knockout.js are supposed to be automatic.
Below is my code.
Thanks
<div id ='main'>
<!-- ko foreach: posts -->
<p>Hello</p><span data-bind="text: title"></span></p><p data-bind="text: content"></p>
<p data-bind="text: author"></p><p data-bind="text: date"></p>
<!-- /ko -->
</div>
</body>
<script type="text/javascript">
function Post(data){
this.title = ko.observable(data.title);
this.content = ko.observable(data.content);
this.author = ko.observable(data.author);
this.date = ko.observable(data.date)
}
function PostListViewModel(){
var self = this;
self.posts = ko.observableArray([]);
$.getJSON("/posts", function(getPost){
var mappedPost = $.map(getPost, function(item){
return new Post(item)
});
self.posts(mappedPost);
});
}
var postlistviewmodel = new PostListViewModel();
ko.applyBindings(postlistviewmodel);
</script>
This should be:
$.getJSON("/posts", function(getPost){
var mappedPosts = $.map(getPost, function(item){
return new Post(item)
});
self.posts(mappedPosts);
});
wouldn't do self.posts.push(mappedPosts[i]) at all. You should just pass mappedPosts through the ko binding in order to update the listeners.
If your just getting the latest posts and want to update your current list simply do:
var allPosts = self.posts().concat(mappedPosts);
self.posts(allPosts);
You don't need the model to have ko.observable if you're just displaying them. If you want to edit model as well, then leave as.
Also, I tend to do this for single or multiple view models:
ko.applyBindings({viewModel : new viewModel() };
This allows for having multiple named view models. Access scope using: $root.viewModel
This is what I did earlier: http://jsfiddle.net/jFb3X/
Check your code against this fiddle then.
Script tags also need to be above the closing body tags
<html>
<head>
</head>
<body>
<!-- all your html content -->
<script type="text/javascript">
var viewModel = function () {
}
ko.applyBindings({viewModel : new viewModel()});
</script>
</body>
</html>
Is it something as simple as waiting for the DOM to be ready?
Are you able to try the following:
$(function () {
ko.applyBindings(postlistviewmodel);
});
Source: I've done this a few times and been stumped for a bit trying to see what I did wrong. :-)
(As a style thing, I'd also move the /body to after the /script - probably not related to your issue though).
I suspect you get multiple posts from /posts. You only push a single item (array).
...
$.getJSON("/posts", function(getPost){
var mappedPosts = $.map(getPost, function(item){
return new Post(item)
});
for(var i = 0; i < mappedPosts.length; i++) {
self.posts.push(mappedPosts[i]);
}
});
...
Related
Working with the dynamic JavaScript Technology called Knockout, I would like to send new data to my webpage and ask Knockout to do the dynamic UI update for me.
The below example shows a very simple webpage that shows two Scores (P1 and P2). The JavaScript creates a ViewModel() using Knockout. Unfortunately, I only achieved this by providing a JSON-data property. And I don't know how to dynamically load and also dynamically update new score-data.
Now my question: How can I "inject" (i.e. load at first and update at any time) new data to my webpage and Knockout would update the Scores UI dynamically ?
I guess, I would need some sort of...
a) $.getJSON(".... for the initial loading!
b) Post-request (REST) call from anywhere for the data-update
But how do I do a) and b) ???
Thanks for any help on this.
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>iKK_ScoreMirror</title>
</head>
<script type='text/javascript' src='Knockout/knockout-3.4.2.js'></script>
<body>
<h3>Game</h3>
<p>Score P1 = <span data-bind="text: scoreP1"></span> </p>
<p>Score P2 = <span data-bind="text: scoreP2"></span> </p>
</body>
<script>
function ViewModel() {
var self = this;
// !!!!!!! Here is the json-data given fix
var jsonData = {
sP1: 13,
sP2: 23
};
self.scoreP1 = ko.observable(jsonData.sP1)
self.scoreP2 = ko.observable(jsonData.sP2)
};
var vm = new ViewModel();
ko.applyBindings(vm);
</script>
</html>
you mean something like this? here is a runnable fiddle https://jsfiddle.net/ca0xv62w/2/
function ViewModel() {
var self = this;
self.scoreP1 = ko.observable('13')
self.scoreP2 = ko.observable('23')
self.loadDataFromServer = function() {
$.ajax({
type: 'POST',
cache: false,
data: {
json: JSON.stringify(data)
},
url: '/echo/json/',
success: function(response) {
self.scoreP1(response.sP1);
self.scoreP2(response.sP2);
}
});
}
}
I am using a framework called Framework7.
In my index.html, I have some Template7 code, like this format
<script type="text/template7" id="commentsTemplate">
{{#each this}}
<div> test this template 7 code </div>
</script>
However, I want to have this part of code into an another separated file (Just like I can have many other *.js files in, say, a static folder and refer to the file by "static/*.js).
I have tried to use a typical way to import js
<script type="text/template7" id="storiesTemplate" src="js/template.js"></script>
But it doesn't work, there is also no demo/sample code in the documentation.
Any help is appreciated!
You can do it. The idea behind is to include a HTML file in a HTML file. I can tell at least 3 ways that this can happen, but personally I fully validated only the third.
First there is a jQuery next sample is taken from this thread
a.html:
<html>
<head>
<script src="jquery.js"></script>
<script>
$(function(){
$("#includedContent").load("b.html");
});
</script>
</head>
<body>
<div id="includedContent"></div>
</body>
</html>
b.html:
<p> This is my include file </p>
Another solution, I found here and doesn't require jQuery but still it's not tested: there is a small function
My solution is a pure HTML5 and is probably not supported in the old browsers, but I don't care for them.
Add in the head of your html, link to your html with template
<link rel="import" href="html/templates/Hello.html">
Add your template code in Hello.html. Than use this utility function:
loadTemplate: function(templateName)
{
var link = document.querySelector('link[rel="import"][href="html/templates/' + templateName + '.html"]');
var content = link.import;
var script = content.querySelector('script').innerHTML || content.querySelector('script').innerText;
return script;
}
Finally, call the function where you need it:
var tpl = mobileUtils.loadTemplate('hello');
this.templates.compiledTpl = Template7.compile(tpl);
Now you have compiled template ready to be used.
=======UPDATE
After building my project for ios I found out that link import is not supported from all browsers yet and I failed to make it work on iphone. So I tried method number 2. It works but as you might see it makes get requests, which I didn't like. jquery load seems to have the same deficiency.
So I came out with method number 4.
<iframe id="iFrameId" src="html/templates/template1.html" style="display:none"></iframe>
and now my loadTemplate function is
loadTemplate: function(iframeId, id)
{
var iFrame = document.getElementById(iframeId);
if ( !iFrame || !iFrame.contentDocument ) {
console.log('missing iframe or iframe can not be retrieved ' + iframeId);
return "";
}
var el = iFrame.contentDocument.getElementById(id);
if ( !el ) {
console.log('iframe element can not be located ' + id );
return "";
}
return el.innerText || el.innerHTML;
}
How about lazy loading and inserting through the prescriptions?
(function (Template7) {
"use strict";
window.templater = new function(){
var cache = {};
var self = this;
this.load = function(url)
{
return new Promise(function(resolve,reject)
{
if(cache[url]){
resolve(cache[url]);
return true;
}
if(url in Template7.templates){
resolve(Template7.templates[url]);
return true;
}
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = function() {
if(this.status == 200 && this.response.search('<!DOCTYPE html>') == -1){
cache[url] = Template7.compile(this.response);
resolve(cache[url]);
}else{
reject(`Template ${url} not found`);
}
};
xhr.send();
})
}
this.render = function(url, data)
{
return self.load(url)
.then(function(tpl){
return tpl(data) ;
});
}
this.getCache = function()
{
return cache;
}
}
})(Template7);
Using :
templater.render('tpl.html').then((res)=>{ //res string })
Or :
templater.load('tpl.html').then( tpl => { Dom7('.selector').html( tpl(data) ) } )
It is possible to define your templates in .js-files. The template just needs to be a string.
Refer to this [JSFiddle] (https://jsfiddle.net/timverwaal/hxetm9rc/) and note the difference between 'template1' and 'template2'
var template1 = $$('#template').html();
var template2 = '<p>Hello, my name is still {{firstName}} {{lastName}}</p>'
template1 just extracts the content of the <script> and puts it in a string.
template2 directly defines the string
I have a cordova app in which I want to show the details of a location. For some reason when I try to display a variable in HTMl which is being successfully assigned in JS, nothing appears.
JS controller:
app.controller('placeCtrl', function($scope, LocDat){
LocDat.async().then(function(d){
$scope.item= places.selectedItem;
$scope.locs = [];
for(var i=0; i<d.length; i++){
if(d[i].attributes.Joint.id === places.selectedItem.id){
getDistance(d[i]);
$scope.locs.push(d[i]);
}
}
$scope.showSite = function(){
//var ref = navigator.app.loadUrl($scope.item.attributes.Website, '_blank');
var ref = window.open($scope.item.attributes.Website,'_blank','location=yes');
}
$scope.showDetail = function(index){
var selectedItem = d[index];
d.selectedItem = selectedItem
$scope.l = selectedItem;
console.log($scope.l.attributes.City);
$scope.ons.navigator.pushPage('location_detail.html', { title : d.selectedItem.attributes.Address });
}
});
HTML:
<!DOCTYPE html>
<html>
<body>
<div ng-controller="placeCtrl">
<ons-page class="center" ng-device-backbutton="myNavigator.popPage()">
<ons-toolbar>
<div class="left"><ons-back-button ons-if-platform="ios">Back</ons-back-button></div>
<div id="title" class="center">{{l.attributes.City}}, {{l.attributes.State}}</div>
<!--<div class="left" onclick=".myNavigator.popPage()"><ons-back-button>Back</ons-back-button></div>-->
<!--<div class="center">Page 2</div>-->
</ons-toolbar>
<h2 align="center">Location Details Go Here</h2>
<!--enter more content here-->
</ons-page>
</div>
</body>
</html>
Image of the Console output:
Apparently my reputation is too low to post images... Seriously? Anyway, it displays the City name in the console successfully, but the html only shows the comma
Services that make async calls, such as your LocDat, do not automagically trigger a digest event when they return. If you're writing a service it should call a $scope.$apply() chained to the end of the promise. Alternatively you can wrap any changes to $scope variables in an apply and that should get you where you need.
$scope.$apply( function() { $scope.l = selectedItem; } );
In angularjs data binding, if the data type is list or object, it will pass by reference value in view.
When you do like $scope.l = selectedItem, the reference is changed, but the watched reference is previous one. So it will be always better to bind by an attribute on an object, but not the object itself. like:
<div id="title" class="center">{{obj.l.attributes.City}}, {{obj.l.attributes.State}}</div>
And update in controller with:
$scope.obj.l = selectedItem;
The issue was that the scope changed when I loaded the new page. I'm now passing the data through the parameters of onsenui's pushpage function and assigning them to the scope variables in a separate controller.
I have a list of messages. When a particular message is clicked it loads the details. This part works fine.
I want to load some other related data asynchronously when the clicked message is loaded. For that I'm nesting a view inside my messageView. However I am unable to load and access the data.
Here is my template
<script type="text/x-handlebars" id="message">
{{#view "messageThread" contentBinding="this"}}
{{#each message in view.getConversation}}
<div>Message body</div>
{{message.body}}
{{/each}}
</div>
{{/view}}
</script>
Here is the messageThreadView used in the template above
App.MessageThreadView = Ember.View.extend({
getConversation: function(){
var msg = this.get('context').model;
var sender_id = msg.sender.id;
var recipient_id = msg.recipient.id;
downloadConversation(recipient_id, sender_id);
return this.get('current_conversation');
}.property('current_conversation'),
});
Here is the asynchronous data load function called in the view above
function downloadConversation(recipient_id, sender_id){
$.getJSON(<a url>)
.then(function(data){
App.set('current_conversation', data['objects']);
});
}
How do I get view.getConversation to work as expected i.e load the data when it becomes available?
Here's the simplest pattern for asynchronous properties, especially when they are a collection. You essentially return a collection reference (in this case convo), then you asynchronously populate that collection from the reference.
App.MessageThreadView = Ember.View.extend({
getConversation: function(){
var msg = this.get('context').model,
sender_id = msg.sender.id,
recipient_id = msg.recipient.id,
convo = [];
$.getJSON(<a url>).then(function(data){
data.forEach(function(item){
convo.pushObject(item);
});
});
return convo;
}.property(), // this should be watching sender and receipient
});
I have an MVC site that uses Knockout JS. Basically, the MVC handles routing to a few different pages, and each page has a viewmodel.
One of the pages requires a parameter to filter the data. The code for the MVC Controller for that page is as follows:
public ActionResult Transactions(int policyId)
{
ViewData["policyId"] = policyId;
return View();
}
The View for that page includes a hidden field.
<input type="hidden" name="hldPolicy" value="#ViewData["policyId"]">
Then after the html for the page,
#section scripts
{
#Scripts.Render("~/bundles/myBundle")
<script>
$(document).ready(function () {
var policyId = $('#hldPolicy').val();
var transactionViewModel = new TransactionViewModel(policyId);
ko.applyBindings(transactionViewModel);
});
</script>
}
The problem is this doesn't work because the hidden field is undefined when the script runs. That doesn't make sense to me as I thought that was what the $(document).ready was protecting against. What am I doing wrong here? And is there a better way to pass a parameter from the URL params into the viewmodel?
You can use it like this. Here you dont actually have to pass the parameter instead define a function which will be called on viewmodel initialization and get the data according to your requirements.
#section scripts
{
#Scripts.Render("~/bundles/myBundle")
<script type="text/javascript">
function TransactionViewModel(){
var self = this
self.SomeProperty = ko.observable()
self.LoadData = function(){
var policyId = $('#hldPolicy').val();
self.SomeProperty(policyId)
}
self.LoadData()
}
$(document).ready(function () {
ko.applyBindings(new TransactionViewModel());
});
</script>
}
When knockout model will be initialized it will call self.LoadData() automatically.
EDIT
I found you are missing id attribute at your input
<input type="hidden" id="hldPolicy" name="hldPolicy" value="#ViewData["policyId"]">
Now it should work properly.
EDIT:
You can also do it like this
#section scripts
{
#Scripts.Render("~/bundles/myBundle")
<script type="text/javascript">
function TransactionViewModel(policyId){
var self = this
self.SomeProperty = ko.observable()
self.LoadData = function(policyId){
self.SomeProperty(policyId)
}
self.LoadData(policyId);
}
$(document).ready(function () {
var policyId = $('#hldPolicy').val();
ko.applyBindings(new TransactionViewModel(policyId));
});
</script>
}