Angular two way data binding inside directive - angularjs

I have a problem with understanding how two way data binding works inside directive. Seems it's working only one way cause when i update value by typing directly in input field model is not being updated even though the datasource is set with '='. When i click change item button model is also updated. Why typing inside input fields doesn't update the model?
Also if factories/services are singletons why i'am able to create new instances of Item by clicking the button and each one of the holds separate value?
I wasn't sure if those two problems are related to each other or not that's why asking it this way. PLUNKR
angular.module('singletonServicesTest', [])
.controller('pageCtrl', function(Item) {
vm = this;
vm.items = [];
vm.addItem = function() {
console.log('changeItem');
var item = new Item();
vm.items.push(item);
};
vm.addItemWithContent = function() {
var item = new Item('some content');
vm.items.push(item);
};
vm.changeItem = function() {
console.log('change');
vm.items[0].content = Math.random();
};
})
.factory('Item', function() {
function Item(data) {
if (data) {
this.content = data;
}
}
Item.prototype.content = '';
return Item;
})
.directive('itemDirective', function() {
return {
scope: {
datasource: '='
},
template: '<div><input value="{{datasource.content}}" /></div>'
}
});
<body ng-app="singletonServicesTest" ng-controller="pageCtrl as page">
<h1>Hello Plunker!</h1>
<button ng-click="page.addItem()">Add item</button>
<button ng-click="page.addItemWithContent()">Add item with content</button>
<button ng-click="page.changeItem()">change item</button>
<div ng-repeat="item in page.items">
<item-directive datasource="item"></item-directive>
</div>
{{page.items}}
</body>

Related

Factory value not updated in model ...what I am doing wrong?

I am new to angular-js. I have two controllers (welcomeContoller,productController) and both handling the same model within the factory.
When the model getting updating by one controller(productController) it should reflect the update in another controller. (welcomeContoller)
But its not happening now.
HTML code :
<body ng-app="myApp">
<div ng-controller="welcomeContoller">
{{totalProductCnt}}
</div>
<div ng-controller="productController">
<div class="addRemoveCart">
<span class="pull-left glyphicon glyphicon-minus" ng-click="removeProduct()"></span>
<span class="pull-right glyphicon glyphicon-plus" ng-click="addProduct(1)"></span>
</div>
</div>
JS code
var myApp = angular.module("myApp", ['ui.bootstrap']);
myApp.factory("productCountFactory", function() {
return {
totalProducts:0
};
});
myApp.controller("welcomeContoller", function($scope, productCountFactory)
{
$scope.totalProductCnt = productCountFactory.totalProducts;
});
myApp.controller("productController", function($scope, productCountFactory) {
$scope.addProduct = function() {
productCountFactory.totalProducts++;
alert(productCountFactory.totalProducts);
};
$scope.removeProduct = function() {
if(productCountFactory.totalProducts >=1)
productCountFactory.totalProducts--;
alert(productCountFactory.totalProducts);
};
});
Even after the addProduct is called the totalProductCnt is displaying as zero. I want to display the value for each increment.
Plunkr Link
Put the factory object reference on scope:
myApp.controller("welcomeContoller", function($scope, productCountFactory) {
$scope.productCountFactory = productCountFactory;
});
Watch the property of the object.
{{productCountFactory.totalProducts}}
The DEMO on PLNKR.
By putting a reference on scope, on every digest cycle the watcher looks up the value of the property and updates the DOM if there is a change.
The totalProductCnt from your welcomeController isn't updated because it is assigned only once when the controller is created.
You can use several solutions to refresh the displayed value. Use a getter for your totalProducts in the factory :
myApp.factory("productCountFactory", function() {
var totalProducts = 0;
return {
getTotalProducts: function() {
return totalProducts;
},
addProduct: function() {
totalProducts++;
},
removeProduct: function() {
totalProducts--;
}
};
});
myApp.controller("welcomeContoller", function($scope, productCountFactory) {
$scope.getTotalProducts = productCountFactory.getTotalProducts;
});
myApp.controller("productController", function($scope, productCountFactory) {
$scope.addProduct = function() {
productCountFactory.addProduct();
};
$scope.removeProduct = function() {
if (productCountFactory.getTotalProducts() >= 1)
productCountFactory.removeProduct();
};
});
And update the view accordingly:
<div ng-controller="welcomeContoller">
{{getTotalProducts()}}
</div>
Plunkr Link

ng-if inside ng-repeat not updating from controller

So I am new to Angular, like just started right now so bear with me please... I want to loop thru an array and then inside the loop evaluate a condition to display some HTML it looks like this:
<div ng-controller="BuilderController">
<div ng-repeat="row in formRows" class="fe-form-row">
<div class="fe-form-draggable-item">
<div class="fe-form-l-side-menu bg-primary col-md-1 pull-left">
<i class="fa fa-cog"></i>
<i class="fa fa-close"></i>
</div>
<div class="gray-lighter col-md-11 pull-left">
<div ng-if="row.columns.length == 0">
<div ng-click="open('lg','layout')" id="fe-form-container-base" class="fe-form-r-side-menu gray-lighter col-md-12">
<div class="fe-form-insert-menu">
<i class="fa fa-plus-square-o"></i>
<span>Add Column(s)</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
The first time arround column is an empty array so it evaluates to true and the HTML piece is displayed, all good. And I tried setting it up as not an empty array and evaluates to false and the HTML is not showed. So far so good. The problem is when I change the value of column inside the controller:
admin_app.controller('BuilderController', function ($scope, $modal, $log) {
//default value, empty row
//TODO: assign value to these and the form should be automatically created
$scope.formRows = [
{
'layout': 'empty',
columns: []
}
];
$scope.animationsEnabled = true;
//TODO: these should be loaded from server-side somehow...
$scope.columnsOptions = ['1', '2', '3', '4', '5', '6'];
$scope.open = function (size, content) {
var modalInstance = $modal.open({
animation: $scope.animationsEnabled,
templateUrl: 'fe-form-' + content + '-container.html',
controller: 'ModalInstanceCtrl',
windowClass: 'fe-form-builder',
size: size,
resolve: {
columnsOptions: function () {
return $scope.columnsOptions;
}
}
});
modalInstance.result.then(function (selectedItem) {
$scope.selected = selectedItem;
}, function () {
$log.info('Modal dismissed at: ' + new Date());
});
$scope.toggleAnimation = function () {
$scope.animationsEnabled = !$scope.animationsEnabled;
};
};
});
admin_app.controller('ModalInstanceCtrl', function ($scope, $modalInstance, columnsOptions) {
$scope.columnsOptions = columnsOptions;
$scope.selected = {
columnsOption: $scope.columnsOptions[0]
};
$scope.ok = function () {
var columns = [];
for (var i = 0; i < $scope.selected.columnsOption; i++) {
columns.push({
type: $scope.selected.columnsOption,
elements: []
});
}
//update formRows var with new columns
$scope.formRows = [
{
'layout': $scope.selected.columnsOption,
columns: columns
}
];
console.log(' $scope.formRows ', $scope.formRows[0].columns.length);
$modalInstance.close($scope.selected.item);
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
});
The console.log shows that the columns have been correctly updated based on the user input, but the HTML is not re-rendered to hide the HTML piece since columns is no longer an empty array. Am I missing something here, is there something wrong with my approach? As I mentioned I am very new to Angular.
If I understand correct, your data structure is something like this:
formRows -> Array
row -> Object which contains another object columns
columns -> Array
I think the problem is that Angular is tracking any changes to formRows, which are not happening when your columns object change because there is no direct change in formRows. Angular is not trying to check changes at the sub-sub object level, so to speak. You could either create a new object everytime you're changing rows and add it back to formRows. Or you can add a deepwatch to check changes to your columns array. Check this:
How to deep watch an array in angularjs?

angularjs - Sharing data between controllers through service

I have a 2 controllers [FirstController,SecondController] sharing two arrays of data (myFileList,dummyList) through a service called filecomm.
There is one attribute directive filesread with isolated scope that is bound to a file input in order to get the array of files from it.
My problem is that myFileList array in my service never gets updated when I select the files with the input. However, dummyList array gets updated immediately in the second div (inner2). Does anybody know why is this happening?
For some reason in the second ngrepeat when I switch from (fi in secondCtrl.dummyList) to (fi in secondCtrl.myFileList) it stops working.
Any help would be greatly appreciated.
Markup
<div ng-app="myApp" id="outer">
<div id="inner1" ng-controller="FirstController as firstCtrl">
<input type="file" id="txtFile" name="txtFile"
maxlength="5" multiple accept=".csv"
filesread="firstCtrl.myFileList"
update-data="firstCtrl.updateData(firstCtrl.myFileList)"/>
<div>
<ul>
<li ng-repeat="item in firstCtrl.myFileList">
<fileuploadrow my-file="item"></fileuploadrow>
</li>
</ul>
</div>
<button id="btnUpload" ng-click="firstCtrl.uploadFiles()"
ng-disabled="firstCtrl.disableUpload()">Upload
</button>
</div>
<div id="inner2" ng-controller="SecondController as secondCtrl">
<ul ng-repeat="fi in secondCtrl.dummyList">
<li>Hello</li>
</ul>
</div>
</div>
JS
angular.module('myApp', [])
.controller('FirstController',
['$scope','filecomm',function ($scope,filecomm) {
this.myFileList = filecomm.myFileList;
this.disableUpload = function () {
if (this.myFileList) {
return (this.myFileList.length === 0);
}
return false;
};
this.uploadFiles = function () {
var numFiles = this.myFileList.length;
var numDummies = this.dummyList.length;
filecomm.addDummy('dummy no' + numDummies + 1);
console.log('Files uploaded when clicked:' + numFiles);
console.log('dummy is now:'+ this.dummyList.length);
};
this.updateData = function(newData){
filecomm.updateData(newData);
console.log('updated data first controller:' + newData.length);
};
this.dummyList = filecomm.dummyList;
console.log('length at init:' + this.myFileList.length);
}]) //FirstController
.controller('SecondController',
['$scope', 'filecomm', function($scope,filecomm) {
var self = this;
self.myFileList = filecomm.myFileList;
self.dummyList = filecomm.dummyList;
console.log('SecondController myFileList - length at init:' +
self.myFileList.length);
console.log('ProgressDialogController dummyList - length at init:' +
self.dummyList.length);
}]) //Second Controller
.directive('filesread',[function () {
return {
restrict: 'A',
scope: {
filesread: '=',
updateData: '&'
},
link: function (scope, elm, attrs) {
scope.$watch('filesread',function(newVal, oldVal){
console.log('filesread changed to length:' +
scope.filesread.length);
});
scope.dataFileChangedFunc = function(){
scope.updateData();
console.log('calling data update from directive:' +
scope.filesread.length);
};
elm.bind('change', function (evt) {
scope.$apply(function () {
scope.filesread = evt.target.files;
console.log(scope.filesread.length);
console.log(scope.filesread);
});
scope.dataFileChangedFunc();
});
}
}
}]) //filesread directive
.directive('fileuploadrow', function () {
return {
restrict: 'E',
scope: {
myFile: '='
},
template: '{{myFile.name}} - {{myFile.size}} bytes'
};
}) //fileuploadrow directive
.service('filecomm', function FileComm() {
var self = this;;
self.myFileList = [];
self.dummyList = ["item1", "item2"];
self.updateData = function(newData){
self.myFileList= newData;
console.log('Service updating data:' + self.myFileList.length);
};
self.addDummy = function(newDummy){
self.dummyList.push(newDummy);
};
}); //filecomm service
Please see the following
JSFiddle
How to test:
select 1 or more .csv file(s) and see each file being listed underneath.
For each file selected the ngrepeat in the second div should display Hello. That is not the case.
Change the ngrepat in the second div to secondCtrl.dummyList
Once you select a file and start clicking upload, you will see that for every click a new list item is added to the ul.
Why does dummyList gets updated and myFileList does not?
You had a couple of issues.
First, in the filecomm service updateData function you were replacing the list instead of updating it.
Second, the change wasn't updating the view immediately, I solved this by adding $rootScope.$apply which forced the view to update.
Updated JSFiddle, let me know if this isn't what you were looking for https://jsfiddle.net/bdeczqc3/76/
.service('filecomm', ["$rootScope" ,function FileComm($rootScope) {
var self = this;
self.myFileList = [];
self.updateData = function(newData){
$rootScope.$apply(function(){
self.myFileList.length = 0;
self.myFileList.push.apply(self.myFileList, newData);
console.log('Service updating data:' + self.myFileList.length);
});
};
}]); //filecomm service
Alternately you could do the $scope.$apply in the updateData function in your FirstController instead of doing $rootScope.$apply in the filecomm service.
Alternate JSFiddle: https://jsfiddle.net/bdeczqc3/77/
this.updateData = function(newData){
$scope.$apply(function(){
filecomm.updateData(newData);
console.log('updated data first controller:' + newData.length);
});
};

Angular best practices for splitting components

So i have this simple listing of system users (attached screenshot), client can edit the users or create new user and a new panel is displayed accordingly.
What I'm doing is updating the different fields of the edit/create form depending on the user action. But since im coming from a Backbone background, the usual way I would do something like this is to create a new view, initialize it with a parameter indicating whether it's a create or edit mode, and then destroy this view when clicking on cancel/save changes/create user. But right now im just depending on angular event binding to update the same form elements, is this the proper way to do it in angular?
$scope.prepare_form = function (user_id) {
if (user_id) {
// edit mode
var user = filterFilter($scope.users, { id: user_id })[0];
$scope.fdata_create.name = user.name;
$scope.fdata_create.email = user.email;
$scope.fdata_create.role_name = user.role_name;
$scope.fdata_create.id = user.id;
} else {
// create mode
$scope.fdata_create.name = '';
$scope.fdata_create.email = '';
$scope.fdata_create.role_name = '';
$scope.fdata_create.id = '';
}
$scope.form_create_open = true;
}
You should think based on Models rather than forms. You should create Teammate model with its sync methods, update, create, replace, etc. It is cleaner, here is example extract from this tutorial
Book html form.
<div ng-controller="BookController">
<div ng-style="{ backgroundImage: 'url(' + book.getImageUrl(100, 100) + ')' }"></div>
Id: <span ng-bind="book.id"></span>
<br/>
Name:<input type="text" ng-model="book.name" />
<br/>
Author: <input type="text" ng-model="book.author" />
<br/>
Is Available: <span ng-bind="book.isAvailable() ? 'Yes' : 'No' "></span>
<br/>
<button ng-click="book.delete()">Delete</button>
<br/>
<button ng-click="book.update()">Update</button>
</div>
Book Model define as factory.
app.factory('Book', ['$http', function($http) {
function Book(bookData) {
if (bookData) {
this.setData(bookData):
}
// Some other initializations related to book
};
Book.prototype = {
setData: function(bookData) {
angular.extend(this, bookData);
},
load: function(id) {
var scope = this;
$http.get('ourserver/books/' + bookId).success(function(bookData) {
scope.setData(bookData);
});
},
delete: function() {
$http.delete('ourserver/books/' + bookId);
},
update: function() {
$http.put('ourserver/books/' + bookId, this);
},
getImageUrl: function(width, height) {
return 'our/image/service/' + this.book.id + '/' + width + '/' + height;
},
isAvailable: function() {
if (!this.book.stores || this.book.stores.length === 0) {
return false;
}
return this.book.stores.some(function(store) {
return store.quantity > 0;
});
}
};
return Book;
}]);
Book controller that uses Book Model
app.controller('BookController', ['$scope', 'Book', function($scope, Book) {
$scope.book = new Book();
$scope.book.load(1);
}]);
If you want to update book name for example, you dont need to manually assign the new name because the Book's name instance is binded to ng-model attribute. To update it just only need call your update function from Book Factory.
$scope.book = new Book();
if(book_id){
$scope.book.load(book_id);
$scope.submit_book = $scope.book.update // submit_book function need to be triggered after submit the form. Here it
}
else{
$scope.submit_book = $scope.book.create // submit_book function need to be triggered after submit the form. Here it will create the book
}
I would reuse the same form, but bind to different models:
app.controller('ctrl', function($scope){
$scope.addUser = function(){
$scope.add= true;
$scope.user = {};
};
$scope.editUser =function(user){
$scope.add=false;
$scope.user ={};
angular.copy($scope.user, user);
};
$scope.onOK = function(user){
...
angular.copy(user, $scope.user);
};
});
HTML:
<div ng-controller="ctrl">
<form>
<input type ="text" ng-model="user.name" />
<input type="text" ng-model="user.role" />
<button ng-click="onOK(user)"> ok </button>
</form>
</div>

AngularJS - ng-click does not remove previous click's modifications when clicked again

So I am trying to acomplish this example: http://jsfiddle.net/pkozlowski_opensource/WXJ3p/15/
However, for some reason, when I click on one div, and then on another, it does not remove the "active" class from the previous div so it highlights both , hence all my divs end up with the class active if I click all of them. I want to make it to where it will actually remove the class if I click anywhere else on the body and also if I click on any other div, like the fiddle example.
My jsFIddle
http://jsfiddle.net/GeorgiAngelov/jUj56/4/
<div ng-controller="MyCtrl">
<button class="addQuestionButton btn btn-primary" ng-click="AddRootQuestion(questions)">Add node</button>
<div ng-repeat="question in questions" ng-include="question"> </div>
<script type="text/ng-template" id="question">
<!-- Single question starts here -->
<div ng-controller="QuestionController" ng-class="{active : isSelected(question)}">
<button class="btn btn-primary" ng-click="AddSubQuestion(question)">Add node</button>
<button class="btn btn-primary" ng-click = "editQuestion(question)">Edit Question</button>
</div>
<div ng-repeat="question in question.nodes" ng-include="question">
</div>
</script>
</div>
Since each single question has its own QuestionController, and QuestionControlleris where $scope.selected is being set, they don't interact with each other. That is to say, when you click edit, it sets selected for that individual controller.
The easy way to fix it would be to set a property on a parent scope (the containing controller) when clicking edit:
function MyCtrl($scope) {
$scope.questions = [];
$scope.AddRootQuestion = function(questions) {
questions.push({name: 'Question', nodes: []});
};
// added this method --v
$scope.setSelected = function(q) {
$scope.selected = q;
};
}
function QuestionController($scope) {
$scope.choices = [];
$scope.choice = {text: ''};
$scope.AddSubQuestion = function(question, $element) {
var newName = 'Name of question goes here';
question.nodes.push({name: newName, id: 'it goes in here', nodes: []});
};
// modified this method --v
$scope.editQuestion = function(question){
$scope.setSelected(question);
};
$scope.isSelected = function(question) {
return $scope.selected === question;
};
}
But this makes QuestionController dependent upon a parent controller. A much better design would be to move all the data and data manipulation methods into a service:
var myApp = angular.module('myApp',[]);
myApp.factory('Question', function() {
return {
questions: [],
addRootQuestion: function() {
this.questions.push({name: 'Question', nodes: []});
},
addSubQuestion: function(question, data) {
question.nodes.push(data);
},
setSelectedQuestion: function(question) {
this.selectedQuestion = question;
},
isSelectedQuestion: function(question) {
return this.selectedQuestion == question;
}
};
});
You can then inject the service into your controllers:
function MyCtrl($scope, Question) {
$scope.questions = Question.questions;
$scope.AddRootQuestion = function() {
Question.addRootQuestion();
};
}
function QuestionController($scope, Question) {
$scope.choices = [];
$scope.choice = {text: ''};
$scope.AddSubQuestion = function(question, $element) {
var newName = 'Name of question goes here';
Question.addSubQuestion(question, {
name: newName, id: 'it goes in here', nodes: []
});
};
$scope.editQuestion = function(question){
Question.setSelectedQuestion(question);
};
$scope.isSelected = function(question) {
return Question.isSelectedQuestion(question);
};
}
Example: http://jsfiddle.net/BinaryMuse/pZkBv/
If you wanted, you could build up a richer data model using JavaScript OOP principles; for example, you could have Question instances with methods for manipulating the questions, which themselves live inside an instance of QuestionContainer (or Survey or Test or whatever).

Resources