sorry for a noob question, just started with Knockout.js. I have an array of objects and I want to update the view when object property favorite: changes but every time I click on an icon that triggers the change nothing happens. When I add a new object to an array UI gets rerendered. I would really appreciate some help with this. Thanks
<div id="container" data-bind="foreach:savedSearches">
<div class="save-search-item" data-bind="attr:{'data-name': $data.name, 'data-id':$data.id, 'favourite':$data.favorite() === 1}">
<div data-bind="text: $data.name"></div>
<div class="icons">
<a href="#" class="favourite-search">
<i class="fas fa-star" data-bind="css: {favourite: $data.favorite() === 1}"></i>
</a>
<a href="#" class="edit-search">
<i class="fas fa-edit"></i>
</a>
<a href="#" class="delete-search">
<i class="fas fa-trash-alt"></i>
</a>
</div>
</div>
</div>
var searches = [
{
activation_time: null,
activation_time_ms: null,
favourite: 1,
enabled: 1,
id: 66,
name: "adfdfafs"
},
{
activation_time: null,
activation_time_ms: null,
favourite: 0,
enabled: 1,
id: 66,
name: "adfdfafs"
}
];
ko.applyBindings(AppViewModel, $('#container'));
function AppViewModel(data) {
self.savedSearches = ko.observableArray([]);
self.favourite = ko.observable();
self.populateSavedSearches = function(data) {
data.forEach(function(search) {
search.favorite = ko.observable();
});
self.savedSearches(data);
}
}
$('.favourite-search').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
// get parent element with id
var parent = e.currentTarget.closest('.save-search-item');
var searchId;
var isFavourite = false;
if (parent) {
searchId = parseInt(parent.getAttribute('data-id'));
isFavourite = parent.getAttribute('favourite');
searches.map(function(search) {
if (search.id === searchId) {
search.favorite = 0;
ko.populateSavedSearches(search);
}
});
}
});
When using knockout, you should not add your own event listeners via jQuery.
In this case, use the click binding to react to user behavior.
I did the bare minimum to make your snippet work, but I think it gets the point across:
You already found out you have to make the favorite property observable! Great start
I added a toggle function to each of the searches that swaps the favorite observable between 1 and 0
In the view, I added a click binding that calls toggle
In the view, I moved your favourite attribute binding to be a css binding. This makes sure favorited searches get the favourite class
In CSS, I styled .favourite elements to have a yellow background.
In applyBindings, I use new to create a new viewmodel and pass the app container using [0]
You can see these changes in action in the snippet below.
var searches = [
{
activation_time: null,
activation_time_ms: null,
favourite: 1,
enabled: 1,
id: 66,
name: "adfdfafs"
},
{
activation_time: null,
activation_time_ms: null,
favourite: 0,
enabled: 1,
id: 66,
name: "adfdfafs"
}
];
ko.applyBindings(new AppViewModel(searches), $('#container')[0]);
function AppViewModel(data) {
const self = this;
self.savedSearches = ko.observableArray([]);
self.favourite = ko.observable();
self.populateSavedSearches = function() {
data.forEach(function(search) {
search.favorite = ko.observable(search.favorite);
search.toggle = function() {
search.favorite(search.favorite() ? 0 : 1);
}
});
self.savedSearches(data);
}
self.populateSavedSearches();
}
.favourite { background: yellow }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div id="container" data-bind="foreach: savedSearches">
<div class="save-search-item" data-bind="
click: toggle,
attr: {
'data-name': $data.name,
'data-id':$data.id
},
css: { 'favourite': $data.favorite() === 1 }
">
<div data-bind="text: $data.name"></div>
<div class="icons">
<a href="#" class="favourite-search">
<i class="fas fa-star" data-bind="css: {favourite: $data.favorite() === 1}"></i>
</a>
<a href="#" class="edit-search">
<i class="fas fa-edit"></i>
</a>
<a href="#" class="delete-search">
<i class="fas fa-trash-alt"></i>
</a>
</div>
</div>
</div>
Related
I have a grid of styled books and their images, created with the gridstack library:
I also have a "save" button that calls a saveGrid() function to save the location of each book on the grid, and loads its serialized data with grid.load(serializedData)
HTML:
<div>
<h1 class="title">Your Books</h1>
<button onclick="saveGrid(); grid.load(serializedData)">Save</button>
</div>
<div class="grid-stack"></div>
JS:
saveGrid = () => {
serializedData = [];
grid.engine.nodes.forEach((node) => {
serializedData.push({
x: node.x,
y: node.y,
width: node.width,
height: node.height,
noResize: true,
el: node.el
});
});
};
The problem is that, when the "save" button is clicked, the positions of the items are saved but not the actual HTML images and content (like buttons), as shown below:
I've figured out the problem and edited my saveGrid() function, as well as made a new loadGrid() function that loads the grid after saving and an addBooks() function that loads books from an array upon page load. Below is the HTML and the full JS code:
HTML:
<div class="grid-stack"></div>
JS:
var booksArray = [
{"ISBN": '0385504209', "bookURL": "https://images.penguinrandomhouse.com/cover/9780767926034"},
{"ISBN": '0143039431', "bookURL": "https://upload.wikimedia.org/wikipedia/commons/a/ad/The_Grapes_of_Wrath_%281939_1st_ed_cover%29.jpg"},
{"ISBN": '0743273567', "bookURL": "https://prodimage.images-bn.com/pimages/9780743273565_p0_v8_s550x406.jpg"},
{"ISBN": '0743297334', "bookURL": "https://upload.wikimedia.org/wikipedia/commons/8/8b/The_Sun_Also_Rises_%281st_ed._cover%29.jpg"},
{"ISBN": '0553283685', "bookURL": "http://books.google.com/books/content?id=wDVV6y-8YHEC&printsec=frontcover&img=1&zoom=1&source=gbs_api"}
]
var grid = GridStack.init({
column: 8,
animate: true,
//kinda wonky --> float: true,
removable: true,
rtl: true
})
const addBooks = () => {
grid.batchUpdate()
booksArray.forEach((book, index) => {
grid.addWidget(`
<div>
<div class="grid-stack-item-content">
<img src="${book.bookURL}" class="book-cover" id=${book.ISBN}></img>
<div class="button-group">
<i class="fa fa-minus-circle"></i>
<button class="btn button4" data-toggle="modal" data-target="#exampleModalLong" style="background-color:#4f21cf"><i class="fa fa-info-circle"></i></button>
<i class="fa fa-check"></i>
</div>
</div>
</div>`, {
width: 2,
height: 3,
noResize: true,
staticGrid: true
})
grid.engine.nodes[index].ISBN = book.ISBN
grid.engine.nodes[index].bookURL = book.bookURL
console.log(document.querySelector('.book-cover').id)
})
grid.commit()
console.log(grid.engine.nodes)
}
const saveGrid = () => {
serializedData = []
console.log(grid.engine.nodes)
grid.batchUpdate()
grid.engine.nodes.forEach((node, index) => {
console.log(node.el)
console.log(node.ISBN)
console.log(node.bookURL)
serializedData.push({
x: node.x,
y: node.y,
width: 2,
height: 3,
noResize: true,
ISBN: node.ISBN,
bookURL: node.bookURL
})
})
serializedData.sort()
grid.commit()
console.log(JSON.stringify(serializedData, null, ' '))
console.log(grid.engine.nodes.length)
}
const loadGrid = () => {
// empty the grid first:
grid.removeAll({detachNode:false}) //{detachNode: false}
// grid.assign()
console.log(serializedData)
var items = GridStack.Utils.sort(serializedData)
console.log(items)
grid.batchUpdate()
items.forEach((node, index) => {
grid.addWidget(`
<div>
<div class="grid-stack-item-content">
<img src="${node.bookURL}" class="book-cover"></img>
<div class="button-group">
<i class="fa fa-minus-circle"></i>
<i class="fa fa-info-circle"></i>
<i class="fa fa-check"></i>
</div>
</div>
</div>`, node)
grid.engine.nodes[index].ISBN = node.ISBN
grid.engine.nodes[index].bookURL = node.bookURL
console.log(node)
})
grid.commit()
console.log(grid.engine.nodes.length)
console.log(grid.engine.nodes)
}
window.onload = addBooks()
I have a search mask and I would like that when a double click on a line is made, the code of the selected line is returned.
I am using the shieldGrid from shieldui.
How can I get the selected row?
I try to retrieve the selected row but it still empty.
#using TheBetterWayStoreHandler.Resources
#model TheBetterWayStoreHandler.Models.CustomerGroupModel
#{
ViewBag.Title = "Ricerca Clienti";
Layout = "~/Views/Shared/_PagesLayout.cshtml";
}
<form id="searchCustomerForm" action='#Html.Raw(#Url.Action("SearchSelection", "Customer"))?selectedCustomer=' + selectedCustomer.value method="post">
<nav class="navbar navbar-expand-sm bg-dark navbar-dark fixed-top" style="background-color: #e3f2fd;">
<a class="navbar-brand" href="#Url.Action("Index", "Dashboard")">The Better Way - #ViewBag.Title</a>
<ul class="navbar-nav">
<li class="nav-item">
<button type="submit" class="btn btn-outline-success" data-toggle="tooltip" title="Salva" data-placement="bottom" value="Input Button">
<span class="fa fa-hdd fa-lg" aria-hidden="true"></span>
</button>
</li>
</ul>
</nav>
<div class="container-fluid body-content" style="height: 100%">
<input id="selectedCustomer" name="selectedCustomer" type="hidden" value="">
<div id="customerSearchGrid"></div>
</div>
</form>
<script>
var selectedCustomerVar = "";
jQuery(function ($) {
$("#customerSearchGrid").shieldGrid({
dataSource: {
remote: {
read: "#Session["ApiPath"]" + "GetCustomerSearch?searchString=" + "#Session["SearchString"]",
}
},
rowHover: true,
columns: [
{ field: "CustomerCode", width: "80px", title: "#Resources.Tb_CustomerCode", type: String },
{ field: "CustomerDescription", title: "#Resources.Tb_CustomerDescription", type: String }
],
sorting: {
multiple: true
},
scrolling: true,
height: "700px",
selection: {
type: "row",
multiple: false,
toggle: true
},
editing: {
enabled: false
},
events: {
selectionChanged: function (e) {
var selected = e.target.contentTable.find(".sui-selected");
if (selected.length > 0) {
selectedCustomer.value = selected[0].cells[0].textContent;
}
else {
selectedCustomer.value = "";
}
},
ondblClickRow: function(rowId) {
var rowData = jQuery(this).getRowData(rowId);
var customerCode = rowData['CustomerCode'];
location.href = '#Url.Action("SearchSelection", "Customer")?selectedCustomer=' + customerCode;
}
}
});
});
$("#customerSearchGrid").dblclick(function () {
var sel = $("#customerSearchGrid").swidget().selectedRowIndices();
alert(sel);
window.searchCustomerForm.submit();
});
the selectedCustomer is empty, and the sel variable is empty too ...
Thanks.
Probably the dblclick event is raised before the selection is handled...
You can try using the selectionChanged event for handling the selection change.
I have a collection of Items that can be marked as "read" by my users.
In order to mark a collection as "read", a subdocument is added:
"readby": [
{
"action": "read",
"owner": "w5XzMrCCJJfDxCn6d"
}
]
I then use the following helper to set up an array and push any "read" entries that match the current owner. If the array has a length bigger than 0, the helper returns "true" and we know the logged in user has read this item:
itemHasBeenRead() {
var subscribers = [];
var readItems = this.readby;
if(!readItems) {
return false;
}
var readiness = readItems.forEach(function(currentSubscriber) {
// loop over current users expenses
var newSubscriber = { owner: currentSubscriber.owner };
if (currentSubscriber.owner == Meteor.userid()) {
subscribers.push(newSubscriber);
}
});
return subscribers && subscribers.length > 0
}
This all works perfectly BUT as I understand it, subdocuments aren't reactive in Meteor, so the code doesn't pick up changes reactively. Refresh the page and it works fine.
Is there a way to do this reactively, rather than just on page load?
--
Edits as requested:
--
Template code:
{{#each playlists}}
<h2 class="playlistheader">{{playlistName}}<span class="badge badge-playlist badge-playlist-first badge-primary"><i class="fas fa-users playlist-fa"></i>{{numberOfSubscribers}} enrolled</span>{{#unless isPlaylistOwner}}{{#unless userIsSubscribed}}<span class="badge badge-playlist badge-success subscribe-unsubscribed" data-id="{{this._id}}"><i class="far fa-heart playlist-fa"></i>Enrol</span>{{/unless}}{{#if userIsSubscribed}}<span class="badge badge-playlist badge-success subscribe-subscribed" data-id="{{this._id}}"><i class="fas fa-heart playlist-fa"></i>Enrolled</span>{{/if}}{{/unless}}
<span class="badge badge-playlist badge-completion" data-id="{{this._id}}"><i class="fas fa-check-circle" style="margin-right:3px;"></i>0/6 items marked complete</span>
{{#if isPlaylistOwner}}<span class="badge badge-playlist badge-danger badge-delete" data-id={{this._id}}><i class="fas fa-ban playlist-fa"></i>Delete</span>{{/if}}</h2>
<span class="playlist-subheader">{{playlistPrivacyType}} collection by {{playlistOwnerName}}</span>
<div class="row" style="display:inherit; margin-left:0px; margin-bottom:20px;margin-top:8px;">
<div class="scrolling-wrapper-playlist">
{{#if isPlaylistOwner}}
<div class="playlist-product playlist-product-add" data-playlistid="{{playlistid}}">
<div class="clampcontainer">
<div class="add-playlist-plus" data-playlistid="{{playlistid}}">+</div>
</div>
</div>
{{/if}}
{{#each playlistItems this._id}}
<div class="playlist-product" data-id={{this._id}}>
{{#if isPlaylistItemOwner}}
<div class="playlist-product-delete">
<i class="fas fa-times-circle product-delete"></i>
</div>{{/if}}
<div class="playlist-product-overlay" id="overlay-{{this._id}}" style="opacity:0;">
<div class="playlist-product-overlay-description">
"{{itemDescription}}"</div>
<div class="playlist-product-overlay-icons">
{{#unless itemHasBeenRead}}
<i class="far fa-check-circle playlist-circle"></i>
{{/unless}}
{{#if itemHasBeenRead}}
<i class="fas fa-check-circle playlist-circle"></i>
{{/if}}
{{#if hasPrice}}
<i class="fas fa-shopping-cart playlist-cart"></i>
{{/if}}
<i class="fas fa-external-link-alt playlist-external"></i>
</div>
</div>
<div style="width:90px; float:left; margin-right:10px;"><img src="{{itemImage}}" width="90"></div>
<div class="clampcontainer">
<div class="itemTitle linkColor">{{itemTitle}}</div>
<p class="itemDescription">{{itemDescription}}...</p>
<div class="fadeout"></div>
</div>
</div>
{{/each}}
</div>
</div>
{{/each}}
Publication code:
Meteor.publish('UserPlaylists', function() {
var loggedinuser = Meteor.user();
// Reveal ALL expenses if it's an admin who's logged in
return Playlists.find({
owner: loggedinuser._id
});
});
// Publish public playlists, as long as they're both public and from the same company
Meteor.publish('PublicPlaylists', function() {
var loggedinuser = Meteor.user();
var companyid = loggedinuser.userofcompanyid;
// Reveal ALL expenses if it's an admin who's logged in
return Playlists.find({
companyid: companyid,
published: true
});
});
// Publishes absolutely all expenses for superadmins
Meteor.publish('Playlists', function() {
var loggedinuser = Meteor.user();
if (loggedinuser.issuperadmin) {
return Playlists.find({});
}
});
Subscribing to the publications in the template js:
Template.Playlists.onCreated(function playlistsOnCreated() {
var self = this;
self.autorun(function() {
self.subscribe('UserPlaylists');
self.subscribe('PublicPlaylists');
self.subscribe('UserPlaylistItems');
self.subscribe('PublicPlaylistItems');
});
});
The playlists helper:
playlists() {
return Playlists.find({}, {
sort: {
timestamp: -1
}
});
},
The playlistItems helper:
playlistItems(playlistid) {
return PlaylistItems.find({
playlistid: playlistid
}, {
sort: {
timestamp: -1
}
});
},
Attempting to make list items clickable without a checkbox. I want those items to to get a strike through when clicked and still have the delete option at the end. This functions properly, but I can't seem to maintain that when I try to make the items clickable. How do I need to modify this code to make it work?
<p class="lead" ng-bind="vm.list.content"></p>
<div class="list-group">
<span data-ng-repeat="item in vm.list.items|orderBy:'name'"
class="list-group-item" ng-class="{strike: item.check}">
<input type="checkbox" ng-model="item.check" ng-click="vm.cross(item)">
<a class="btn btn-default pull-right" ng-click="vm.remove(item)">
<i class="glyphicon glyphicon-trash"></i></a>
<h4 class="list-group-item-heading" ng-bind="item.name + ' - ' + item.priority"></h4>
</span>
</div>
Controllers:
function remove(item){
var removedItem = $scope.vm.list.items.indexOf(item);
$scope.vm.list.items.splice(removedItem, 1);
if (vm.list._id) {
vm.list.$update(successCallback, errorCallback);
} else {
vm.list.$save(successCallback, errorCallback);
}
function successCallback(res) {
$state.go('lists.view', {
listId: res._id
});
}
function errorCallback(res) {
vm.error = res.data.message;}
}
function cross(item){
if (vm.list._id) {
vm.list.$update(successCallback, errorCallback);
} else {
vm.list.$save(successCallback, errorCallback);
}
function successCallback(res) {
$state.go('lists.view', {
listId: res._id
});
}
function errorCallback(res) {
vm.error = res.data.message;}
}
Why not to move the checkbox behavior to the item wrapper? In this case, if click on the trash, the outer click handler will not be triggered because we stop event from further propagation.
<div class="list-group">
<span data-ng-repeat="item in vm.list.items|orderBy:'name'" class="list-group-item" ng-class="{strike: item.check}" ng-click="item.check = true; vm.cross(item)">
<a class="btn btn-default pull-right" ng-click="vm.remove(item);$event.stopPropagation();">
<i class="glyphicon glyphicon-trash"></i>
</a>
<h4 class="list-group-item-heading" ng-bind="item.name + ' - ' + item.priority"></h4>
</span>
</div>
If you want to cross/uncross item by click, you can implement a method like toggleCross and use it instead of "item.check = true" statement:
item.toggleCheck = function() {
this.check = !this.check;
}
I have the following Javascript that I am using to make a sort of flowchart where the user clicks through a set of questions. For certain responses i want to link to an external site where more info can be found. How do I add these links?
HTML
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<div class="wrapper">
<div class="container">
<div class="row">
<div class="col-xs-12 text-right">
<button class="btn btn-default btn-corner" type="submit" data-bind="click: startOver, visible: queryData().id > 0">Start over</button>
</div>
</div>
</div>
<div class="container main">
<div class="row">
<div class="c12 text-center">
<h1 data-bind="text: queryData().text"></h1>
<h3 data-bind="text: queryData().subhead"></h3>
<div class="option-group" data-bind="foreach: queryData().answers">
<button class="btn btn-default btn-lg" type="submit" data-bind="click: $parent.goToTarget, text: text"></button>
</div>
<button class="btn btn-default" type="submit" data-bind="click: stepBack, visible: navHistory().length > 1">Previous Step</button>
</div>
</div>
</div>
<div class="push"></div>
</div>
<script src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-3.3.0.js"></script>
<script src="app.js?v=0.4.0"></script>
<script>
</script>
</body>
</html>
The Javascript is as follows:
JS
var queries = [{
id: 0,
text: "Where to start?",
answers: [{
text: "Let's Begin!",
target: 1
}]
}, {
id: 1,
text: "Which genre do you want to start in?",
answers: [{
text: "Fantasy",
target: 100
}, {
text: "SciFi",
target: 2
}, {
text: "Neither",
target: 59
}]
}, {
id: 2,
text: "It's huge but it's worth it. The Cryptonomicon by Neal Stephenson",
answers: [{
text: "Amazon.co.uk",
target: "_blank"
}, {
text: "Amazon.com"
}]
}];
function QueryViewModel() {
var self = this;
self.querySet = ko.observable();
self.currentStep = ko.observable();
self.queryData = ko.observable();
self.sfw = ko.observable();
self.navHistory = ko.observableArray();
// Operations
self.goToTarget = function(obj) {
self.navHistory.push(self.currentStep());
self.currentStep(obj.target);
self.queryData(self.querySet()[obj.target]);
}
self.startOver = function() {
self.navHistory.removeAll();
self.goToTarget({target: 0});
}
self.stepBack = function() {
var lastStep = self.navHistory().length > 1 ? self.navHistory.pop() : 0;
self.currentStep(lastStep);
self.queryData(self.querySet()[lastStep]);
}
var paramsString = document.location.hash.substring(1);
var params = new Array();
if (paramsString) {
var paramValues = paramsString.split("&");
for (var i = 0; i < paramValues.length; i++) {
var paramValue = paramValues[i].split("=");
params[paramValue[0]] = paramValue[1];
}
}
params ? paramTarget = params['target'] : params = [];
self.sfw() ? self.querySet(queriesSFW) : self.querySet(queries);
if (paramTarget) {
self.navHistory.push(0);
self.currentStep(0);
self.goToTarget({target: paramTarget})
} else {
self.goToTarget({target: 0});
}
}
ko.applyBindings(new QueryViewModel());
In html you can do something like this:
<button type="button" onclick="window.open('https://google.com/', '_self')">Button</button>
You don't have to use a button, different elements can use onclick like text or images. This can also call js functions, just put the function name where "window.open..." is.
Of course the standard way to do it is
<a href='https://www.google.com/'>Link</a>
You can practice using js here: http://www.w3schools.com/js/tryit.asp?filename=tryjs_intro_inner_html
and learn more about it here: http://www.w3schools.com/js/js_intro.asp
I am not sure why you would show us the JSON for open a link to another page. Unless I misunderstood. This kind of basic information can be found by a quick Google search.
Add your link in the object like:
text: "Fantasy",
link: "http://www.stackoverflow.com",
target: 2
Now when you need to go to that link, use this function:
var link = obj.link;
window.open(link, "_blank");