Knockout.js mapping. How to update only part of view model? - javascript

I want to build simple page where user can select one photo from list and modify some properties, so i had built the model:
model = {
newPhotos: [],
currentPhoto: {
id: 0,
description: ""
},
setCurrent: function (photo, e) {
ko.mapping.fromJS(ko.mapping.toJS(photo), viewModel.currentPhoto);
}
}
and markup:
<div class="content" data-bind="foreach: newPhotos">
<div class="photo" data-bind="click: $parent.setCurrent">
<div class="frame" data-bind="img: imageSrc">
</div>
</div>
</div>
<div class="edit-area" data-bind="with: currentPhoto">
<canvas id="image-edit" />
<textarea class="multi-line" name="PhotoDesc" data-bind="value: description"></textarea>
</div>
Main idea is that when user click on photo, setCurrent function called and there i can update currentPhoto with view model that click binding pass into setCurrent, but there is problem ko.mapping.fromJS(jsObj, viewModel) (as i can see from source) expecting that viewModel will be root model.
I know that i can manually go through all observables and refresh their values, or unmap rootModel, set property and then update root, but i belive that there is more complex and elegant way to do this.
Thank you.

Set up your current photo as an observable, and then you can use map to load the observable with JSON details.
self.currentPhoto(ko.mapping.fromJS(data));
Here's a working JSFiddle to demonstrate:
http://jsfiddle.net/jearles/wgZ59/7/

Related

Filter Events with collections

I have projects and workflows with separate collections.
collections:
Project = new Meteor.Collection("project");
Workflow = new Meteor.Collection("workflow");
After creating project, I am selecting project in the form and displaying Work flow card.
HTML:
<!-- Workflow Card -->
<div id="wCard">
{{#each workflow}}
**<div class="pheader">
<h2>{{project}}</h2>
<hr width="100%">
</div>**
<br>
<div class="workflowcard">
<div class="module-card">
<div class="card-header wfmodule">{{workflowTitle}}</div>
<div class="casting">
<div class="assigned-tag">Assigned To:</div>
<div class="assigned-to">{{team}}<hr></div>
<div class="actions">No Actions Created</div>
</div>
<div class="due">
Due on:
<div>
<div class="day-stamp" >{{weekday d_date}}</div>
<div class="date-stamp">{{date d_date}}</div>
<div class="month-stamp">{{month d_date}}</div>
</div>
</div>
</div>
<div class="btn-box showmuloption">
<button type="edit" class="editw" style="display:none">Edit Workflow</button>
<button type="hide" class="hidew" style="display:none">Hide Actions</button>
<div class="actionBtn"><button class="btn-wf stage-blue-wf button-x-small-wf" id="newAction">New Action</button></div>
</div>
</div>
{{/each}}
</div>
.JS:
Template.workflow.helpers({
getWorkflow: function(){
return Workflow.find();
},
user: function(){
return Meteor.users.find({});
},
getNewaction: function(){
return Newaction.find();
},
});
Now the workflow cards are displaying in a list. I want to display workflows according to projects. If I select a project, the workflow should go to that project, and I create another workflow with same project, it should display in that project. And if I select another project, the workflow should display in that related project.
Please help through this.
I use reactive varrible for this.
At first you need add meteor package:
$ meteor add reactive-var
Your form with project selecting might look like this:
<select id="project" value="{{getProject}}">
{{#each getProjectsList}}
<option id='{{_id}}' value="{{_id}}">{{projectname}}</option>
{{/each}}
</select>
Don't forget create helpers getProject and getProjectsList.
Now it's time for reactive varrible, you can initialise this in onCreated event.
Template.workflow.onCreated(function() {
var instance = this;
instance.projectId = new ReactiveVar(null);
});
When you change selected project, you should change you reactive varrible.
Template.workflow.events({
'change #project': function(){
Template.instance().projectId.set($("#project").val());
});
Every workflow need to have field with project id. You get workflows list like this
{{#each getWorkflows}}
And there is helper for getting workflow's list:
Template.workflow.helpers({
getWorkflows: function() {
var id = Template.instance().projectId.get();
return Workflow.find({projectId: id});
},
...// another helpers
})
So, when you will change project in form, list of workflow also will change, because it depends on reactive var. Also you can sibscribe only needed workflow object, and not all collection.
I hope this will helpfull to someone.

knockout click binding failing to call JS function

I have a list of results from an API call rendering in a list (HTML below) and have added a click binding with Knockout to a JS function that basically takes the name of the result, searches an array of google maps markers for a match, then pans to the marker on the map element and opens the previously stored infoWindow. When I open developer tools I can see that there is a listener on the element for a 'click' action that points to a Knockout function, but when I click, nothing happens. Here is the gitHub repo : https://github.com/ZackBoyd/BrewMap, if that is useful and the specific code is below. I feel like I'm overlooking something very basic, thank you in advance for any help.
HTML
<ul class="results list" data-bind="foreach: filteredBreweries">
<li data-bind="click: $parent.goToMarker">
<div class="item">
<a class="image loaded">
<img data-bind="attr: { src: iconImage }">
</a>
<div class="wrapper">
<h4 data-bind="text: name"></h4>
<figure data-bind="text: address"></figure>
<figure data-bind="text: yearEstablished"></figure>
<figure data-bind="text: type"></figure>
</div>
</div>
</li>
</ul>
JS
function goToMarker(clickedBrewery){
var clickedBreweryName = clickedBrewery.name;
console.log(clickedBrewery);
console.log(clickedBreweryName);
for (var key in self.mapMarkers()) {
if (clickedBreweryName === self.mapMarkers()[key].marker.title) {
map.panTo(self.mapMarkers()[key].marker.position);
infowindow.setContent(self.mapMarkers[key].marker.content);
infowindow.open(map, self.mapMarkers()[key].marker);
}
}
};
This code is within the scope of the 'appViewModel' function, you can see it in the repo, I didn't want to include all of it for simplicity's sake.
Try to assign the function to a variable in your viewmodel. For instance:
self.goToMarker = function(clickedBrewery){
var clickedBreweryName = clickedBrewery.name;
console.log(clickedBrewery);
console.log(clickedBreweryName);
for (var key in self.mapMarkers()) {
if (clickedBreweryName === self.mapMarkers()[key].marker.title) {
map.panTo(self.mapMarkers()[key].marker.position);
infowindow.setContent(self.mapMarkers[key].marker.content);
infowindow.open(map, self.mapMarkers()[key].marker);
}
}
};
I don't know what your viewmodel looks like tho.
But yeah that internal function wont be exposed to the bindings i guess.

Nested Angular Controllers and Views management

I guess it is best to describe it with a picture. I have an angular app and here is a simple view.
Obvious explanation: list shows all the entities, if you click on an entity you can edit it in the form that is hidden by default and similar action applies to adding a new entity.
the issue
I know it is basic example so here the solution might be an overkill but I want to separate the logic of 'Add new entity', 'Edit entity' and 'Entities list'. I thought I could implement it like this:
<div ng-include="'userAddForm.html'"
ng-show="???"
ng-controller="AddUser as add">
</div>
<div ng-include="'userEditForm.html'"
ng-show="???"
ng-controller="AddEdit as edit">
</div>
<div class="panel panel-default">
... list managed by the current controller
</div>
What I miss
I have a difficulty in sharing a state of the hidden parts. For example some boolean flag. For instance:
Click on the entity shows the edit form
Save/Cancel in the edit form hides the part
Then, I think the first step is the responsibility of list-controller, but save/cancel part goes to edit-controller. It would be only possible to share the value with a service included in both - but that does not seem reasonable either.
I think there is some simple solution I can not see and I am open for any advice. Thanks!
If your goal is a simple solution with just a boolean being toggled in the model, you can use child controllers like this:
http://plnkr.co/edit/P1ncToJwqvxt9F9MTF5E?p=preview
The child controllers will inherit the scope of the parent controller and can directly edit the values. I have the edit child controller filtering for editMode==true, so when the parent changes that value, the child controller automatically shows the item. All changes are updated live and the child controller simply toggles the editMode property to remove it from the editing area.
Similar logic is used for the add child controller.
The views look like this:
index.html
<div ng-controller="myCtrl">
<div ng-controller="addCtrl" ng-include="'userAddForm.html'">
</div>
<div ng-controller="editCtrl" ng-include="'userEditForm.html'">
</div>
<h1>Listing</h1>
<ul>
<li ng-repeat="item in items | filter:{addMode:false}">
{{item.id}}
{{item.name}}
<button ng-click="startEditing(item)">[ edit ]</button>
</li>
</ul>
<button ng-click="startAdding()">[ add ]</button>
<div>Debug:<br>{{items}}</div>
</div>
userAddForm.html
<ul>
<li ng-repeat="item in items | filter:{addMode:true}">
<input type="text" ng-model="item.id">
<input type="text" ng-model="item.name">
<button ng-click="add(item)">[ add ]</button>
<button ng-click="cancel(item)">[ cancel ]</button>
</li>
</ul>
userEditForm.html
<ul>
<li ng-repeat="item in items | filter:{editMode:true}">
<input type="text" ng-model="item.id">
<input type="text" ng-model="item.name">
<button ng-click="save(item)">[ save ]</button>
</li>
</ul>
And the controllers look like this:
angular.module('myApp.controllers',[])
.controller('addCtrl', function($scope) {
$scope.add = function(item) {
item.addMode = false;
}
$scope.cancel = function(item) {
$scope.items.pop(item);
}
})
.controller('editCtrl', function($scope) {
$scope.save = function(item) {
item.editMode = false;
}
})
.controller('myCtrl', function($scope) {
$scope.items = [
{name:'aap', id:"1", editMode:false, addMode:false},
{name:'noot', id:"2", editMode:false, addMode:false},
{name:'mies', id:"3", editMode:false, addMode:false},
{name:'zus', id:"4", editMode:false, addMode:false}
];
$scope.startAdding = function(){
$scope.items.push({addMode:true});
};
$scope.startEditing = function(item){
item.editMode = true;
};
});
You can achieve this using Angular state routing.In which you will create state (different views) like -
header
addEntity
editEntity
listEntity
refer https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views
Sharing state can be implemented by creating a service which is than injected to all interested párties (controllers), service can hold data which controllers can be bound to and display in template. Services in Angular JS are singletons so all the controllers are accesing and mutating shared state.

Editing property from Object in ng-repeat from LocalStorage

I'm listing an array of objects saved into Localstorage in a table-like layout.
Each row displays data saved in a particular object. I want to be able to edit and update certain properties from the object once it has already been saved into LocalStorage.
This is how a couple of my objects looks like:
[{
"date":"2014 10 16",
"time":"20.22",
"car":"396",
"driver":"Seb",
"from":"A",
"destination":"B",
"pax":"3",
"arrival":"23.10"
},
{
"date":"2014 10 16",
"time":"23.22",
"car":"46",
"driver":"Eric",
"from":"C",
"destination":"E",
"pax":"3",
"arrival":"00.10"
}]
So far my frontend code displaying the Destination property looks like this:
HTML
<div class="col-md-3"
ng-show="editItem == false"
ng-hide="editItem">{{record.destination}}</div>
// Shows current value
<div class="col-md-3"
ng-show="editItem == true"
ng-hide="!editItem">
<select class="form-control"
ng-model="locationList2"
ng-options="location.place for location in locationlist | orderBy:'place'">
<option value="">Destination</option>
</select>
</div>
// Shows select with options to be picked to update property
<div class="col-md-1">
<button ng-click="editItem = !editItem"
ng-show="!editItem">Edit</button>
<button ng-click="editData(record); editItem = !editItem"
ng-show="editItem">Ok</button>
</div>
//Toggles between current value and select and triggers editData function
Relevant JS:
$scope.editData = function (record) {
record.destination = $scope.locationList2;
jsonToRecordLocalStorage($scope.recordlist);
}
So far when I trigger editData it just deletes the Destination property, it doesn't update it with the model of locationList2 from the Select.
What am I missing?
EDIT
Here's the complete ng-repeat piece of code:
<div class="row msf-row" ng-repeat="record in recordlist | filter: search">
<div class="col-md-1">{{record.time}}</div>
<div class="col-md-1"><strong>{{record.car}}</strong></div>
<div class="col-md-1">{{record.driver}}</div>
<div class="col-md-3">{{record.from}}</div>
<div class="col-md-3"
ng-show="editItem == false"
ng-hide="editItem">
{{record.destination}}
</div>
<div class="col-md-3"
ng-show="editItem == true"
ng-hide="!editItem">
<select class="form-control"
ng-model="locationList2"
ng-options="location.place for location in locationlist | orderBy:'place'">
<option value="">Destination</option>
</select>
</div>
<div class="col-md-1">{{record.pax}}</div>
<div class="col-md-1">
<button
ng-click="editItem = !editItem"
ng-show="!editItem">
<i class="fa fa-pencil"></i>
</button>
<button
ng-click="editData(record); editItem = !editItem"
ng-show="editItem">
<i class="fa fa-check"></i>
</button>
</div>
</div>
Also, I here's a Plunkr to ilustrate the issue!
Add a driver, car code and location before starting to see the app running and the mentioned problem.
You could use angular-local-storage as an abstraction over LocalStorage API.
If you want to just hack it, you can do something along localStorage.setItem('data', JSON.stringify(data)) when setting data and use JSON.parse(localStorage.getItem('data')) to extract it. LocalStorage doesn't deal with objects by default so we have to serialize it.
Regardless of the solution you choose, it could be a good idea to extend your edit a bit:
$scope.editData = function (recordlist) {
$scope.recordlist.destination = $scope.locationList2;
// replace whole LocalStorage data here now. no need to "patch" it
updateLocalStorage('data', <data containing your objects goes here>);
}
If you have multiple ways to modify the data and want to avoid explicit update, you could set up a watcher instead:
$scope.$watch(<data name goes here>, function(newVal) {
// update your LocalStorage now
});
Why it fails with ng-repeat?
The reason you see the behavior is quite simple. $scope.locationList2 is a single variable that gets bound for each member created by ng-repeat. That explains why it stays empty during edit.
You will need to bind the data using some other way. Consider binding it directly to your record models. Example: AngularJS - Using $index in ng-options .
Solution
The original code had bits like this:
JS:
$scope.editData = function (record) {
record.destination = $scope.location;
jsonToRecordLocalStorage($scope.recordlist);
};
HTML:
<select class="form-control" ng-model="location" ng-options="location.place for location in locationlist | orderBy:'place'">
<option value="">Destination</option>
</select>
Note that the markup is inside a ng-repeat and effectively each item created by it points at the same location! This isn't good.
To make it work I changed it like this:
JS:
$scope.editData = function () {
jsonToRecordLocalStorage($scope.recordlist);
};
HTML:
<select class="form-control" ng-model="record.destination" ng-options="location.place as location.place for location in locationlist | orderBy:'place'">
<option value="">Destination</option>
</select>
As mentioned above the JS could be replaced by a watcher. The important thing to note here is that I bind the data directly to the records. That avoid hassle at editData and more importantly gets rid of the problematic ng-model reference.

How can a data-bind to an element within a Kendo-Knockout listview?

I have a rather sophisticated template for Kendo ListView using knockout-kendo.js bindings. It displays beautifully. My problem is that I need to use the visible and click bindings in parts of the template, but I can't get them to work. Below is a simplified version of my template. Basically, deleteButtonVisible determines whether the close button can be seen, and removeComp removes the item from the array.
<div class='template'>
<div >
<div style='display:inline-block' data-bind='visible: deleteButtonVisible, event: {click: $parent.removeComp}'>
<img src='../../../Img/dialog_close.png'></img>
</div>
<div class='embolden'>#= type#</div><div class='label1'> #= marketArea# </div>
<div class='label2'> #= address# </div>
<!-- more of the same -->
</div>
The view model:
function CompViewModel() {
var self = this;
self.compData = ko.observableArray().subscribeTo("compData");
self.template = kendo.template(//template in here);
self.removeComp = function (comp) {
//do something here
}
}
html:
<div class="row" >
<div class="col-md-12 centerouter" id="compDiv" >
<div class="centerinner" id="compListView" data-bind="kendoListView: {data: compData, template: template}"></div>
</div>
</div>
finally, sample data:
{
type: "Comparable",
marketArea: "",
address: "2327 Bristol St",
deleteButtonVisible: true
},
Take in count that the deleteButtonVisible must be a property on the viewModel linked to the view.You are not doing that right now. The click element can v¡be access from the outer scope of the binding and remove the $parent.He take the method from the viewmodel. Take in count that every thing that you take on the vie must be present on the view model for a easy access.

Categories

Resources