Angular: How to $broadcast from Factory? - angularjs

I have a list of items and I need to get a message (saying Item added!) in the navbar whenever a new item is added.
The function addItem() (ng-click on the Add Item button) is in the ItemFactory and from there I seem to not be able to broadcast it.
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js"></script>
</head>
<body ng-app="MyApp" ng-controller="MainCtrl">
<div>{{ text }}
<nav class="navbar navbar-inverse" ng-controller="NavCtrl">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">List of items | {{ alertItemAdded }}</a>
</div>
<form class="navbar-form navbar-right" role="search">
<div class="form-group">
<input type="text" class="form-control" ng-model="newItem" placeholder="Add an item">
</div>
<button type="submit" class="btn btn-primary" ng-click="addItem(newItem)">Add Item</button>
</form>
</div>
</nav>
<div class="container" ng-controller="ContentCtrl">
<div class="row">
<div class="col-xs-12">
<form class="form-inline">
<div class="form-group">
<input type="text" class="form-control" ng-model="newItem" placeholder="Add an item">
</div>
<button type="submit" class="btn btn-primary" ng-click="addItem(newItem)">Add Item</button>
</form>
<br />
<br />
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div ng-repeat="item in items">
<form class="form-inline">
<div class="form-group">
<div>{{ item }}</div>
</div>
<button type="button" class="btn btn-default btn-s" ng-click="removeItem($index)">Remove Item</button>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
angular.module('MyApp',[]);
angular.module('MyApp').controller('MainCtrl', function($scope, ItemFactory){
$scope.text = "Text from the Main Controller";
$scope.addItem = function(newItem){
ItemFactory.addItem(newItem);
}
});
angular.module('MyApp').controller('NavCtrl', function($scope){
// $on
$scope.$on('itemAdded', function(event, data){
$scope.alertItemAdded = data;
});
});
angular.module('MyApp').controller('ContentCtrl', function($scope, ItemFactory){
$scope.items = ItemFactory.getItem();
$scope.removeItem = function($index){
ItemFactory.removeItem($index);
}
});
angular.module('MyApp').factory('ItemFactory', function(){
var items = [
'Item 1',
'Item 2',
'Item 3'
];
return {
getItem : function() {
return items;
},
addItem : function(item){
items.push(item);
// $broadcast
$scope.$broadcast('itemAdded', 'Item added!');
},
removeItem : function($index){
items.splice($index, 1);
}
};
});

You can inject $rootScope into your factory and use $broadcast from there.
angular.module('MyApp').factory('ItemFactory', ["$rootScope", function($rootScope){
var items = [
'Item 1',
'Item 2',
'Item 3'
];
return {
getItem : function() {
return items;
},
addItem : function(item){
items.push(item);
// $broadcast
$rootScope.$broadcast('itemAdded', 'Item added!');
},
removeItem : function($index){
items.splice($index, 1);
}
};
}]);

Here is a clean solution for you.
See it working in this plunker
Let me explain how all of this works.
Your message looks like this :
<span ng-if="alertItemAdded.recentAdd">Item added !</span>
It will show only when "alterITemAdded.recenAdd" is true. You'll use this to make the message disapear if you need.
You factory look like this now :
angular.module('MyApp').service('ItemService', function(){
var service = {};
//I'll always wrap my data in a sub object.
service.notification = {};
service.notification.recentAdd=false;
service.items = {};
service.items.list = [
'Item 1',
'Item 2',
'Item 3'
];
service.items.addItem = function(item){
service.items.list.push(item);
service.notification.recentAdd=true;
console.log(service);
}
service.items.removeItem = function($index){
service.items.list.splice($index, 1);
}
return service;
});
I'm using service instead of factory. But there is almost no difference, it's just a matter of taste.
Here is your controllers
angular.module('MyApp').controller('MainCtrl', function($scope, ItemService){
$scope.text = "Text from the Main Controller";
});
angular.module('MyApp').controller('NavCtrl', function($scope, ItemService){
//IMPORTANT POINT : I bind it the sub object. Not to the value. To access the value i'll use $scope.alterItemAdded.recentAdd
$scope.alertItemAdded = ItemService.notification;
//I don't have to redeclare the function. I just bind it to the service function.
$scope.addItem = ItemService.items.addItem;
});
angular.module('MyApp').controller('ContentCtrl', function($scope, ItemService){
$scope.items = ItemService.items.list;
$scope.addItem = ItemService.items.addItem;
$scope.removeItem = function($index){
ItemService.items.removeItem($index);
}
});
Important point :
I always bind my vars to a sub object. Why ? In fact if i did
$scope.alertItemAdded = ItemService.notifications.recentAdd
When i do something like this in my service
service.notifications.recentAdd = true;
It will create a new variable and put the reference into service.notifications.recentAdd. The $scope.alertItemAdded was bind to the previous reference and wont see the update.
Doing this :
$scope.alterItemAdded = ItemService.notification
And using the value in the ng-if clause or anything else. I prevent the reference link to break. If i do in the service
service.notification.recentAdd = true
I'll create a new var with a new reference for "recentAdd" but i keep the same reference for "notification". The binding in the controller will be keep and the value recentAdd will be updated in the view.
If you have more question feel free to ask.

You not injected $scope to factory, and you cant actually, use $rootScope instead

$broadcast goes from top to bottom so you should use $rootScope to perform a $broadcast to all $scope elements below it.
Inject $rootScope in your factory
$rootScope.$broadcast('itemAdded, 'Item added!')

Related

Adding ng-model directive to dynamically created input tag using AngularJs

I am trying that on a button click, a div and and input tag are created and the input tag contain ng-model and the div has binding with that input.
Kindly suggest some solution.
You can create the div and input beforehand and and do not show it by using ng-if="myVar". On click make the ng-if="true".
<button ng-click="myVar = true">
In controller : $scope.myVar = false;
$scope.addInputBox = function(){
//#myForm id of your form or container boxenter code here
$('#myForm').append('<div><input type="text" name="myfieldname" value="myvalue" ng-model="model-name" /></div>');
}
Here is another solution, in which there's no need to create a div and an input explicitly. Loop through an array of elements with ng-repeat. The advantage is that you will have all the values of the inputs in that array.
angular.module('app', [])
.controller('AppController', AppController);
AppController.$inject = ['$scope'];
function AppController($scope) {
$scope.values = [];
$scope.add = function() {
$scope.values.push('');
};
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="AppController">
<button ng-click="add()">Click</button>
<div ng-repeat="value in values track by $index">
<input type="text" ng-model="values[$index]"/>
<div>{{values[$index]}}</div>
</div>
<pre>{{values}}</pre>
</div>
UPDATE. And if you want only one input, it's even simpler, using ng-show.
angular.module('app', []);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app">
<button ng-click="show = true">Click</button>
<div ng-show="show">
<input type="text" ng-model="value"/>
<div>{{value}}</div>
</div>
</div>
You should use $compile service to link scope and your template together:
angular.module('myApp', [])
.controller('MyCtrl', ['$scope', '$compile', '$document' , function MyCtrl($scope, $compile, $document) {
var ctrl = this;
var inputTemplate = '<div><span ng-bind="$ctrl.testModel"></span>--<span>{{$ctrl.testModel}}</span><input type="text" name="testModel"/></div>';
ctrl.addControllDynamically = addControllDynamically;
var id = 0;
function addControllDynamically() {
var name = "testModel_" + id;
var cloned = angular.element(inputTemplate.replace(/testModel/g, name)).clone();
cloned.find('input').attr("ng-model", "$ctrl." + name); //add ng-model attribute
$document.find('[ng-app]').append($compile(cloned)($scope)); //compile and append
id++;
}
return ctrl;
}]);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="//code.angularjs.org/1.6.2/angular.js"></script>
<div ng-app="myApp">
<div ng-controller="MyCtrl as $ctrl">
<input type="button" value="Add control dynamically" ng-click="$ctrl.addControllDynamically()"/>
</div>
</div>
UPDATE: to add a new compiled template each time the button is clicked, we need to make a clone of the element.
UPDATE 2: The example above represents a dirty-way of manipulating the DOM from controller, which should be avoided. A better (angular-)way to solve the problem - is to create a directive with custom template and use it together with ng-repeat like this:
angular.module('myApp', [])
.controller('MyCtrl', ['$scope', function MyCtrl($scope) {
var ctrl = this;
ctrl.controls = [];
ctrl.addControllDynamically = addControllDynamically;
ctrl.removeControl = removeControl;
function addControllDynamically() {
//adding control to controls array
ctrl.controls.push({ type: 'text' });
}
function removeControl(i) {
//removing controls from array
ctrl.controls.splice(i, 1);
}
return ctrl;
}])
.directive('controlTemplate', [function () {
var controlTemplate = {
restrict: 'E',
scope: {
type: '<',
ngModel: '='
},
template: "<div>" +
"<div><span ng-bind='ngModel'></span><input type='type' ng-model='ngModel'/></div>" +
"</div>"
}
return controlTemplate;
}]);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="//code.angularjs.org/1.6.2/angular.js"></script>
<div ng-app="myApp">
<div ng-controller="MyCtrl as $ctrl">
<input type="button" value="Add control dynamically" ng-click="$ctrl.addControllDynamically()"/>
<div ng-repeat="control in $ctrl.controls">
<control-template type="control.type" ng-model="control.value"></control-template>
</div>
</div>
</div>

ng-dialog: input radio button value is undefined

I'm trying to get the radio value button value from ng-dialog but it always got undefined.
Here is the dialog template:
<script type="text/ng-template" id="flag-reasons.html">
<div style="padding:10px;">
<span>
What's wrong ?
</span>
<div class="form-group" ng-repeat="flagreason in flagreasons">
<input type="radio" ng-model="fr" name="frname" value="{{flagreason.id}}"> {{flagreason.title}}
</div>
<div class="form-group">
<button type="button" ng-click="validFlag()">
Valider
</button>
</div>
</div>
</script>
Here is the partial js where I start with the dialog:
$scope.openFlag = function(){
$scope.dialog = ngDialog.open({ template: 'flag-reasons.html',
className: 'ngdialog-theme-default', scope: $scope });
$scope.validFlag = function(){
console.log($scope.fr);
}
}
I have tried ng-value as below:
<input type="radio" ng-model="fr" name="frname" ng-value="flagreason.id"> {{flagreason.title}}
but it still got undefined
Notice that it works when I directly set the value of the radio button like:
<input type="radio" ng-model="fr" name="frname" value="5"> {{flagreason.title}}
The issue is with ngmodel that's not getting update. You have to initialise ngmodel first in the template.
flag-reasons.html
<script type="text/ng-template" id="flag-reasons.html">
<div style="padding:10px;">
<span>
What's wrong ?
</span>
<div class="form-group" ng-repeat="flagreason in flagreasons">
<input type="radio" ng-model="cb.fr" name="frname" value="{{flagreason.id}}">{{flagreason.id}} - {{flagreason.title}}
</div>
<div class="form-group">
<button type="button" ng-click="validFlag()">
Valider
</button>
</div>
</div>
</script>
Controller
angular.module("app", ['ngDialog'])
.controller('Ctrl',function ($scope, ngDialog) {
'use strict';
$scope.cb = {};
$scope.flagreasons = [
{id: 1, title: 'title1'},
{id: 2, title: 'title2'},
{id: 3, title: 'title3'}
];
$scope.openFlag = function(){
$scope.dialog = ngDialog.open({ template: 'flag-reasons.html',
className: 'ngdialog-theme-default', scope: $scope });
$scope.validFlag = function(){
console.log($scope.fr);
}
}
$scope.$watch('cb.fr', function (v) {
console.log(v);
});
});
Working Fiddle: JSFiddle

Bind data to modal

I am trying to pass some data with a function to display on a modal, yet the usual approaches to binding are not working, I was hoping someone could point me in the right direction.
$scope.openModal = function (obj) {
//$scope.data = {type: obj.type, descriptions: obj.description, isDone: obj.isDone, createDate: obj.createDate, priority: obj.priority};
$scope.data = obj;
console.log($scope.data);
var modalInstance = $uibModal.open({
animation: $scope.animationsEnabled,
templateUrl: 'modalTemplate.html',
controller: 'View1Ctrl',
resolve: {
data: function () {
return $scope.data;
}
}
});
}
Template
<!-- MODAL -->
<div>
<div ng-controller="View1Ctrl">
<script type="text/ng-template" id="modalTemplate.html">
<div class="modal-header">
<h3 class="modal-title">Item Details</h3>
</div>
<div class="modal-body">
<ul>
<li>Type: <span ng-model="data.type"></span></li>
<li>Description: <span ng-model="data.description"></span></li>
<li>Date: <span ng-model="data.createDate"></span></li>
<li>Priority: <span ng-model="data.priority"></span></li>
<li>Finished: <span ng-model="data.isDone"></span></li>
</ul>
</div>
<div class="modal-footer">
<button class="btn btn-primary" ng-click="$close()">OK</button>
</div>
</script>
</div>
Also tried {{data.type}} etc, and ng-bind. I now my $scope.data is populated because it is showing as much in the console.
You should inject data (resolve object) into your modal controller then add it to the $scope object.
You should remove ng-controller="View1Ctrl" from template.

How to $watch changes on models created by ng-repeat?

Consider this Plnkr for example. I don't know how many members of fooCollection will be created beforehand. So I don't know how many bar models are going to exist.
But I know they are going to be angular models, and I know where they are going to be.
How do I do a $watch on these?
I need to do that because I need to trigger behavior when a bar model is changed. Watching the fooCollection itself is not enough, the $watch listener does not fire when a bar is changed.
Relevant html:
<body ng-controller="testCtrl">
<div ng-repeat="(fooKey, foo) in fooCollection">
Tell me your name: <input ng-model="foo.bar">
<br />
Hello, my name is {{ foo.bar }}
</div>
<button ng-click="fooCollection.push([])">Add a Namer</button>
</body>
Relevant JS:
angular
.module('testApp', [])
.controller('testCtrl', function ($scope) {
$scope.fooCollection = [];
$scope.$watch('fooCollection', function (oldValue, newValue) {
if (newValue != oldValue)
console.log(oldValue, newValue);
});
});
Create individual list-item controllers: demo on Plnkr
js
angular
.module('testApp', [])
.controller('testCtrl', function ($scope) {
$scope.fooCollection = [];
})
.controller('fooCtrl', function ($scope) {
$scope.$watch('foo.bar', function (newValue, oldValue) {
console.log('watch fired, new value: ' + newValue);
});
});
HTML
<html ng-app="testApp">
<body ng-controller="testCtrl">
<div ng-repeat="(fooKey, foo) in fooCollection" ng-controller="fooCtrl">
Tell me your name: <input ng-model="foo.bar" ng-change="doSomething()">
<br />
Hello, my name is {{ foo.bar }}
</div>
<button ng-click="fooCollection.push([])">Add a Namer</button>
</body>
</html>
If you have your collection populated, you can place a watch on each item of the ng-repeat:
html
<div ng-repeat="item in items">
{{ item.itemField }}
</div>
js
for (var i = 0; i < $scope.items.length; i++) {
$scope.$watch('items[' + i + ']', function (newValue, oldValue) {
console.log(newValue.itemField + ":::" + oldValue.itemField);
}, true);
}
You can pass true as third argument into $watch
$scope.$watch('something', function() { doSomething(); }, true);
https://docs.angularjs.org/api/ng/type/$rootScope.Scope
You can also create a custom directive that will tell your main controller for the changes
YourModule.directive("batchWatch",[function(){
return {
scope:"=",
replace:false,
link:function($scope,$element,$attrs,Controller){
$scope.$watch('h',function(newVal,oldVal){
if(newVal !== oldVal){
Controller.updateChange(newVal,oldVal,$scope.$parent.$index);
}
},true);
},
controller:"yourController"
};
}]);
assume your markup is like this
<ul>
<li ng-repeat="h in complicatedArrayOfObjects">
<input type="text" ng-model="someModel" batch-watch="$index" />
</li>
</ul>
and this is your controller
YourModule.controller("yourController",[$scope,function($scope){
this.updateChange = function(newVal,oldVal,indexChanged){
console.log("Details about the change");
}
}]);
You can also play around the value provided by the directive link function which sits on first 3 arguments, scope,element and attr.
Since I didn't want another controller I ended up using ng-change instead.
Simple jsFiddle: https://jsfiddle.net/maistho/z0xazw5n/
Relevant HTML:
<body ng-app="testApp" ng-controller="testCtrl">
<div ng-repeat="foo in fooCollection">Tell me your name:
<input ng-model="foo.bar" ng-change="fooChanged(foo)">
<br />Hello, my name is {{foo.bar}}</div>
<button ng-click="fooCollection.push({})">Add a Namer</button>
</body>
Relevant JS:
angular.module('testApp', [])
.controller('testCtrl', function ($scope) {
$scope.fooCollection = [];
$scope.fooChanged = function (foo) {
console.log('foo.bar changed, new value of foo.bar is: ', foo.bar);
};
});
Try to do this
<div ng-repeat="foo in fooCollection" ng-click="select(foo)">Tell me your ame:
<input ng-model="foo.bar" ng-change="fooChanged(foo)">
<br />Hello, my name is {{foo.bar}}</div>
<button ng-click="fooCollection.push({})">Add a Namer</button>
</div>
There is the code in Directive/Controller
$scope.selectedfoo = {};
$scope.select = (foo) => {
$scope.selectedfoo = foo;
}
$scope.$watch('selectedfoo ', (newVal, oldVal) => {
if (newVal) {
}
},true)

$first in ngRepeat

I have an array $scope.items.
I want if the item is the $first one of the ngRepeat to add the css class in.
Is it something that can be done with angular? Generally how can I handle the booleans in Ajs?
<div ng-repeat="item in items">
<div class='' > {{item.title}}</div>
</div>
It should be
<div ng-repeat="item in items">
<div ng-class='{in:$first}' > {{item.title}}</div>
</div>
Look at ng-class directive in http://docs.angularjs.org/api/ng.directive:ngClass and in this thread What is the best way to conditionally apply a class?
You can try approach with method invocation.
HTML
<div ng-controller = "fessCntrl">
<div ng-repeat="item in items">
<div ng-class='rowClass(item, $index)' > {{item.title}}</div>
</div>
</div>
JS
var fessmodule = angular.module('myModule', []);
fessmodule.controller('fessCntrl', function ($scope) {
$scope.items = [
{title: 'myTitle1', value: 'value1'},
{title: 'myTitle2', value: 'value2'},
{title: 'myTitle3', value: 'value1'},
{title: 'myTitle4', value: 'value2'},
{title: 'myTitle5', value: 'value1'}
];
$scope.rowClass = function(item, index){
if(index == 0){
return item.value;
}
return '';
};
});
fessmodule.$inject = ['$scope'];
Demo Fiddle

Resources