Switch between add/remove button with KnockoutJS - javascript

The goal
Switch between add/remove button based on availability using KnockoutJS.
The problem
I need to add a product/item to my summary. If the product is already on summary, then changes the "add button" into "remove button" or else changes from "remove button" to "add button"
Until here, just concept, right? Yea, but I think I'm missing the logic of the trouble.
Look:
<!-- ko foreach: products -->
<!-- ko if: isAdded -->
<button class="btn btn-small btn-success action remove">
<i class="icon-ok"></i>
</button>
<!-- /ko -->
<!-- ko ifnot: isAdded -->
<form action="#" data-bind="submit: add">
<button class="btn btn-small action add">
<i class="icon-plus"></i>
</button>
</form>
<!-- /ko -->
<!-- /ko -->
As you can see, there is conditions to check if a specific product is added or not. If the list is empty, nothing appears; If I add something manually by code, the two buttons appears — remove and add button.
I made this CodePen to simulate the scenario.
Can someone help me?
Some details
I can use jQuery; I'm already working on it for about three weeks and until now, no success.

In my opinion you don't take full advantage of knockoutjs (but that could just be because I don't see the full application). I however rewrote it a bit and put up an example here (jsfiddle).
function Product(name, desc) {
var self = this;
self.name = ko.observable(name);
self.desc = ko.observable(desc);
self.isAdded = ko.observable(false);
};
function SummaryViewModel(products) {
var self = this;
self.products = ko.observableArray(products);
self.add = function (item) {
var i = self.products.indexOf(item);
self.products()[i].isAdded(true);
};
self.remove = function (item) {
var i = self.products.indexOf(item);
self.products()[i].isAdded(false);
};
};
var p = [new Product("One", "Yep one"), new Product("Two", "nope, two")];
ko.applyBindings(new SummaryViewModel(p));

Related

Why this Javascript DOM method doesn't work?

Basically, I'm trying to create a dynamic list, that allows the user to add list/remove list items.
The problem is for some reason
1) 'createTextNode' in this variable doesn't work.
2)
document.getElementById("myList").appendChild(list ).appendChild(inList);
works, while
var listContainer = document.getElementById("myList").appendChild(list ).appendChild(inList);
listCotnainer.appendChild(list).appendChild(inList);
DOES NOT!!
Any help would be appreciated. Thanks!!
Also, would appreciate if you could direct me to a readily written code for the dynamic list, it'll save me a great deal of time. Thanks!
function addItem(txt) {
var list = document.createElement("li");
var listAtr = list.setAttribute("class", "list-group-item");
listAtr.createTextNode(txt);
var inList = document.createElement("button");
var inListAtr = inList.setAttribute("class", "btn btn-default glyphicon glyphicon-minus");
document.getElementById("myList").appendChild(list ).appendChild(inList);
//listCotnainer.appendChild(listAtr);
}
function removeItems() {
}
function removeItem() {
}
<section>
<div ng-controller="addElements">
<h3>{{subtitle}}</h3>
<button class="btn btn-default glyphicon glyphicon-plus" onclick="addItem('This is a test text')"></button>
<button class="btn btn-default glyphicon glyphicon-minus" onclick="removeItems()"></button>
<ul class="list-group" id="myList">
<!-- dynamically generate list items here -->
</ul>
</div>
</section>
Your variable listContainer is wrong. It should be:
var listContainer = document.getElementById("myList");
listCotnainer.appendChild(list).appendChild(inList);

AngularJS: ng-switchs removed data from input fields

I have a Laravel application and I want to add users an schools through wizards. I have made a very simple wizard with AngularJS.
$scope.addSchool = {
items : [ 1, 2, 3 ],
current : 1,
progress : 0
}
$scope.addSchoolNext = function(next)
{
$scope.addSchool.current = next;
$scope.addSchool.progress = (100 / $scope.addSchool.items.length) * ($scope.addSchool.current - 1);
}
$scope.addSchoolPrevious = function(previous)
{
$scope.addSchool.current = previous;
$scope.addSchool.progress = (100 / $scope.addSchool.items.length) * ($scope.addSchool.current - 1);
}
I have ng-switch in my view:
<div class="create-school-wizard" ng-switch on="addSchool.current">
<section class="create-school-wizard-1" ng-switch-when="1">
Add data
<button class="btn btn-default" ng-click="addSchoolNext(2)">Next</button>
</section>
<section class="create-school-wizard-2" ng-switch-when="2">
Add data
<button class="btn btn-default" ng-click="addSchoolPrevious(1)">Previous</button>
<button class="btn btn-default" ng-click="addSchoolNext(2)">Next</button>
</section>
<section class="create-school-wizard-3" ng-switch-when="3">
Add data
<button class="btn btn-default" ng-click="addSchoolPrevious(2)">Previous</button>
<button class="btn btn-default">Save</button>
</section>
</div>
It works fine! I have just one problem. When I go the the next page and then go back, the input fields are empty. Actually, I dont want to save my inputy data in AngularJS models because of, Laravel takes care for posting and saving the data.
I also tried ng-show, but ng-show submits the entire form when I click on next.
Since you didn't have a plunker, I created a different one. What happens when you go back and forth between pages, scopes get destoryed, so you need to repopulate the data when it loads.
Plunker link: http://plnkr.co/edit/laSUM6RDcF4SeTzMSCIu?p=preview
<div ng-switch on="someVariableToSwitchOn">
<div ng-switch-when="true">
<input ng-model="data.value" />
</div>
<div ng-switch-default>
<input ng-model="data.value" placeholder="Not prepopulated" ng-change="onChange()"/>
</div>
</div>
<input ng-model="data.prepopulated"/>
In this example you see that I can set the ng-model on an input and it will be tied to my scope variables.
My JS is a bit overly complex, but the basics are understandable. When the page loads I create $scope.data. it is not initialized so the first input will not switch. Once the user types "blue" into the first input, it will switch and do the other input will not run the change function.
If I want to prepopulate some input, then I have to do that as the page loads, so that it's done in a timely manner.
$scope.someVariableToSwitchOn = false;
$scope.data = {};
$scope.onChange = function(){
if($scope.data.value === 'blue'){
$scope.someVariableToSwitchOn = true;
console.log('switched');
} else {
$scope.someVariableToSwitchOn = false;
}
};
$scope.data.prepopulated = "prepopulated";
You can prepopulate these fields in a manner of ways, you can get data using $http from your server, or you can persist the data with localStorage in your browser (if you're just moving between pages and not relying on a server to save this (search for ng-localstorage) )

Knockoutjs remove multiple by selecting checkbox

I have a list of "assets" that I am displaying using foreach bind.
Each asset has a delete button which calls $parent.removeAsset, which all works fine.
However, I want to add the option to select several "assets" by checking a checkbox, and then remove all the "assets" that are checked.
I am still learning the ropes of knockoutjs so I would really appreciate any help.
Here is the code I am using to display the "assets
<div style="height: 100%; overflow: auto;" data-bind="foreach: assets">
<!-- AssetList AssetItem Tmpl BEGIN -->
<div class="asset-item action" data-tooltip="Select Asset">
<div class="asset-type" data-bind="css: type"> </div>
<div class="asset-select"><input type="checkbox" /></div>
<!-- ko if: type() === 'Text' -->
<div class="asset-name" data-bind="text: content"></div>
<!-- /ko -->
<!-- ko if: type() === 'Image' -->
<div class="asset-name">Image</div>
<!-- /ko -->
<!-- ko if: type() === 'Video' -->
<div class="asset-name">Video</div>
<!-- /ko -->
<div class="asset-remove-cell">
<div class="asset-remove action" data-tooltip="Remove Asset" data-bind="click: $parent.removeAsset"></div>
</div>
</div>
<!-- AssetList AssetItem Tmpl END -->
</div>
And this is my delete function:
self.removeAsset = function(asset){
if (!confirm("Are you sure you want to delete this asset?")) {
event.stopImmediatePropagation();
return false;
}
self.selectedIndex(0);
$.ajax({
url: "/Assets/delete/"+asset.id(),
type: "POST",
success: function(response) {
self.assets.remove(asset);
//notify('good',response);
}
});
};
As #Kenneth suggested I added a boolean to my assets and then looped through the observable array and deleted each asset that was set to true. Here is the code I used for anyone looking to do something similar:
The array:
function Asset() {
var self = this;
self.id = ko.observable("");
self.type = ko.observable("");
self.selected = ko.observable(false);
};
The delete checkbox:
<input type="checkbox" data-bind="checked: selected" />
The function called when the delete button is pressed:
$('#deleteMultipleAssets').click(function(){
if (confirm('Are you sure you want to delete the selected asset?')) {
ko.utils.arrayForEach(viewModel.assets(), function(asset) {
if(asset.selected()){
viewModel.removeMultipleAsset(asset);
}
});
}
});
The removeMultipleAssets function:
self.removeMultipleAsset = function(asset){
self.assets.remove(asset);
};
You should add a field to your assets called selected and then bind that field to the checkbox you're displaying inside the foreach-template.
When you click on the general delete-button, you can loop over your assets and delete all the ones that have their selected-field equal to true and remove them.

Knockout: How do I toggle visibility of multiple divs on button click?

I want to toggle visibility of multiple divs using knockout. Below is the rough idea of my problem -
<button>Button 1</button>
<button>Button 2</button>
<button>Button 3</button>
<div> Div 1 </div>
<div> Div 2 </div>
<div> Div 3 </div>
By default, 'Div 1' should be visible.
When I click individual buttons it should display only the related divs based on the buttons clicked.
I have gone through the Knockout live examples but not getting how to do this efficiently.
Please help!
The following will do a job for you. It's not ideal, but should give you a platform to work on.
First, everything in Knockout is tied to a view model. You want to be able to control the visibility of 3 divs, so here's a view model which might suit. Like I said, not perfect :)
var buttonVm = new function(){
var self = this;
// Flags for visibility
// Set first to true to cover your "first should be open" req
self.button1Visible = ko.observable(true);
self.button2Visible = ko.observable(false);
self.button3Visible = ko.observable(false);
self.toggle1 = function(){
self.button1Visible(!self.button1Visible());
}
self.toggle2 = function(){
self.button2Visible(!self.button2Visible());
}
self.toggle3 = function(){
self.button3Visible(!self.button3Visible());
}
}
You'll need to change your markup to:-
<!-- events here. When clicked call the referenced function -->
<button type="button" data-bind="click: toggle1">Button 1</button>
<button type="button" data-bind="click: toggle2">Button 2</button>
<button type="button" data-bind="click: toggle3">Button 3</button>
<!-- Visibility set here -->
<div data-bind="visible: button1Visible"> Div 1 </div>
<div data-bind="visible: button2Visible"> Div 2 </div>
<div data-bind="visible: button3Visible"> Div 3 </div>
Couple of things to note here. First, I've added the type attribute. Without it, the default behaviour of the button will be to try and submit your form.
Tying it all up:-
// Create view model
var vm = new buttonVm();
ko.applyBindings(vm);

How to refer to the same parent/grandparent scope with nested loops?

I have nested loops with Knockout. I would like to refer to something in a parent "scope". If you see below I always want to refer to the same parent/grandparent regardless how deep I nest the loops. I have seen the "with" binding, not sure it will help me. Is there any way I can create an alias to a particular scope, so further down in the nested loop I can refer to this alias and still be able to refer to the scope of the current loop also?
<!-- Somewhere up there is the "scope" I want to capture -->
<!-- ko foreach: getPages() -->
<span data-bind="text: pageName" ></span>
<button data-bind="click: $parents[1].myFunction()" >Press me</button>
<!-- ko foreach: categories -->
<span data-bind="text: categoryName" ></span>
<button data-bind="click: $parents[2].myFunction()" >Press me</button>
<!-- ko foreach: questions -->
<span data-bind="text: questionText" ></span>
<button data-bind="click: $parents[3].myFunction()" >Press me</button>
<!-- /ko -->
<!-- /ko -->
<!-- /ko -->
The foreach binding supports as aliases as does the (custom) withProperties binding.
<!-- ko withProperties: { book: $root.getBook() } -->
<!-- ko foreach: {data: book.pages, as: 'page'} -->
<span data-bind="text: page.pageName" ></span>
<button data-bind="click: book.bookClicked" >Press me</button>
<!-- ko foreach: {data: page.categories, as: 'category'} -->
<span data-bind="text: category.categoryName" ></span>
<button data-bind="click: page.pageClicked" >Press me</button>
<!-- etc -->
<!-- /ko -->
<!-- /ko -->
<!-- /ko -->
None of my declarative bindings directly use $parent.
The problem with the #user2864740 answer is that it doesn't work out of the box jsFiddle.
The first issue:
The binding 'withProperties' cannot be used with virtual elements
To fix that simply add the following code:
ko.virtualElements.allowedBindings.withProperties = true;
After that, you'll get another exception:
Unable to parse bindings. Message: ReferenceError: 'book' is
undefined; Bindings value: foreach: {data: book.pages, as: 'page'}
Which indicates that the withProperties isn't working at all - it hasn't created the book property in the binding context for its child bindings as you might expect.
Below is the fully working and reusable custom binding (jsFiddle):
ko.bindingHandlers.withProperties = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
// Make a modified binding context, with a extra properties, and apply it to descendant elements
var value = ko.utils.unwrapObservable(valueAccessor()),
innerBindingContext = bindingContext.extend(value);
ko.applyBindingsToDescendants(innerBindingContext, element);
// Also tell KO *not* to bind the descendants itself, otherwise they will be bound twice
return { controlsDescendantBindings: true };
}
};
ko.virtualElements.allowedBindings.withProperties = true;
I think this will help you
Calling a function in a parent's scope in nested view model
and the jsfiddle demo
jsfiddle

Categories