Setting a scope variable from a directive with AngularJS - javascript

I've gone through what must be 20 similar questions asked on SO but have yet to find a solution for my situation, so I hope you guys can help me out.
The goal is to sort the list of names by the property that's provided in the "sort-type" attribute of the directive, but only for the list within each controller (not all lists at the same time).
HTML
<div ng-controller="TestController as testOne">
<b>By:</b> {{testOne.sortType}}<br>
<b>Reverse:</b> {{testOne.sortReverse}}<br>
<div ng-repeat="item in testOne.list">
<p table-sort sort-type="name" sort-reverse="false">Sort by name</p>
<ul>
<li ng-repeat="childItem in testOne.childList | orderBy:testOne.sortType">{{childItem.name}}</li>
</ul>
</div>
</div>
<br><br>
<div ng-controller="TestController as testTwo">
<b>By:</b> {{testTwo.sortType}}<br>
<b>Reverse:</b> {{testTwo.sortReverse}}<br>
<div ng-repeat="item in testTwo.list">
<p table-sort sort-type="name" sort-reverse="false">Sort by name</p>
<ul>
<li ng-repeat="childItem in testTwo.childList | orderBy:testTwo.sortType">{{childItem.name}}</li>
</ul>
</div>
</div>
Javascript (Angular)
var app = angular.module('demo', []);
app.controller('TestController', TestController);
function TestController() {
var vm = this;
vm.sortType = 'oldOrder';
vm.sortReverse = false;
vm.list = [1];
vm.childList = [{ name: 'Jimmy' },
{ name: 'Danny' },
{ name: 'Bobby' }];
}
/////////////////////////////////////
app.directive('tableSort', tableSort);
function tableSort() {
var directive = {
restrict: 'A',
link: linkFunc,
};
return directive;
function linkFunc(scope, element, attr) {
element.on('click', function() {
if(scope.sortType === attr.sortType) {
scope.sortReverse = !scope.sortReverse;
} else {
scope.sortType = attr.sortType;
}
});
}
}
JSFiddle here
My actual application is a bit more complex but I've tried to abstract it as much as possible.
Thanks for looking :)

Ok Several things going on here:
you are using the controllerAs syntax on your templates but
you are changing scope variables in your directive. hence your
controller variables are never changed.
your directive is inside of the ng-repeat which means that
you are actuating actually on a child scope so if you are setting
variables directive on the scope your ng-repeat won't be able to
reach them because they are being set after the child scope are
created.
you are using element.on which executes outside of angular
digest which means you would have to call scope.$apply to let
angular know that something happened.
Take a look at this
https://jsfiddle.net/rez8ey12/
i hope it helps

Related

Remove object from ng-repeat

I have a PhoneGap + Onsen UI + AngularJS app in the works, where I have a list in the view, where the items will be fetched from the controllers variable.
I want to be able to remove items from this list, by clicking on them.
The list looks like this:
<ons-list>
<ons-list-item modifier="tappable" class="item" ng-repeat="citem in completeditems" ng-click="delete(citem)">
<ons-row>
<ons-col>
<div class="titlediv">
<header>
<span class="item-title">{{citem.name}}</span>
</header>
</div>
<div class="item-dates">
<span class="item-start">{{citem.start}}</span>
</div>
</ons-col>
</ons-row>
</ons-list-item>
</ons-list>
The completeditems object in the $scope looks like this:
var completeditemname = "item" + i;
$scope.completeditems[completeditemname] = {
id : "ID",
name : "Name for it",
start: "Start date"
}
Tried the following method, but it didn't work out:
$scope.delete = function(item) {
var index = $scope.completeditems.indexOf(item);
$scope.completeditems.splice(index,1);
//$scope.completeditems.remove(item); //tried this aswell
$scope.$apply() //i need this to update the view
}
You do not need the $scope.$apply() invocation. As you are making alterations to scope variables the digest cycle will be triggered anyhow and you will be encountering an error because of this I believe.
UPDATED:: You're working with an actual object by the looks of it so I've updated the code in the plunker to help you out. It means altering the ng-repeat to use both key and value.
Here is a simple plunkr showing a basic example of what you are trying to do with a one liner in the delete function http://plnkr.co/edit/NtQD....
<body ng-app="myApp">
<div ng-controller="myController as ctrl">
<ul ng-repeat="(key, value) in ctrl.items track by key">
<li ng-click="ctrl.delete(key)">{{value}}</li>
</ul>
</div>
</body>
var myApp = angular.module('myApp', [])
.controller('myController', [
'$scope',
function($scope) {
var self = this;
self.items = {
item1: {
id: 1,
name: 'a'
},
item2: {
id: 2,
name: 'b'
},
item3: {
id: 3,
name: 'c'
}
};
self.delete = function(key) {
delete self.items[key];
};
}
]);
Hope that helps you out!
$scope.$apply() should only be used when changes are coming in from outside the Angular framework. Since your delete() function is being called from an ng-click, it is already being managed by Angular and calling $apply() will raise a "$digest is already in progress" error (check your browser console). Removing that call will most likely get your code working.

AngularJs - binding passed in paramter to object from different controller

I'm having difficulties in solving this. What I'm trying to achieve is to update iterated objects which is passed in to a function in a different controller.
Here is my controllers -
angular.module('eatmapp.controllers', ['eatmapp.services'])
.controller('AppCtrl', function($scope) {
$scope.intoCart = function(item) {
if(item.type == 'variations'){
item = newItemObj;
}
}
})
.controller('BrowseCtrl', function($scope, dataService, $localstorage) {
dataService.getItems().then(function(returnData) {
$scope.items = returnData.products;
})
});
Here is my view -
<div ng-controller="BrowseCtrl">
<div class="list card product" ng-repeat="item in items" ng-click="intoCart(item)">
<div class="item item-text-wrap">
<span class="ifs-productcat" ng-repeat="category in item.categories">{{category}}<span ng-if="$index != item.categories.length - 1">,</span></span><br>
<h3>{{item.title}}</h3>
<h3>Rs.{{item.price}}</h3>
</div>
</div>
</div>
I need to update item object with newItemObject in iteration(ng-repeat) implemeted in template view after doing some condition check with method (intoCart) in another controller(AppCtrl). I'm fairly new to javascript programming and I'm looking for some help.
The problem I had was not able to get access to 'ng-repeat' child scope in controller.
I solved this using 'this.item' in controller rather than passing the whole object or index.
HTML -
<div class="list card product" ng-repeat="item in items" ng-click="intoCart()"></div>
Controller -
angular.module('eatmapp.controllers', ['eatmapp.services'])
.controller('AppCtrl', function($scope) {
$scope.intoCart = function() {
item = this.item; // current (ng-click) child scope of ng-repeat
if(item.type == 'variations'){
item = newItemObj;
}
}
})
Now, whenever I made changes to 'item' object, it automatically updates scope in view (ng-repeat).
Once way I like to handle this is by using services as setters and getters. The problem is you have to include the service with every controller that needs to access it, but if you don't have too many it's no big deal. So something like this:
.service('userFirstName', function() {
var userFirstNameProp;
return {
getter: function() {
return userFirstNameProp;
},
setter: function(value) {
userFirstNameProp = value;
}
};
})
Then you can call userFirstName.getter() or userFirstName.setter("John") as appropriate.

Changing $scope inside controller

I want to change $scope within controller from the wrapping div to the object I'm currently clicking on. My code is as follows:
var blogApp = angular.module('blogApp', ['ngSanitize', 'ngRoute']);
blogApp.controller('blogPostsCtrl', function($scope, $http) {
$http.get('http://jsonplaceholder.typicode.com/posts').success(function(data) {
$scope.posts = data;
$scope.postsLoaded = 'article--loaded';
});
$scope.getPost = function(postID) {
var currentPost = document.getElementById('post-'+postID);
$scope.postsLoaded = 'article--loaded';
$http.get('http://jsonplaceholder.typicode.com/posts/'+postID).success(function(data) {
$scope.body = data.body;
currentPost.insertAdjacentHTML('beforeend', '<div class="body body--hidden" id="body-'+postID+'">'+$scope.body+'</div>');
var currentBody = document.getElementById('body-'+postID);
setTimeout(function() { currentBody.className = currentBody.className + ' body--visible'; }, 1000);
currentPost.classname = 'article one-half desk-one-whole';
});
};
});
html:
<div class="site-wrapper">
<div class="grid-wrapper" ng-controller="blogPostsCtrl">
<article ng-repeat="post in posts" ng-class="postsLoaded" class="article one-half desk-one-whole" id="post-{{post.id}}" ng-click="getPost(post.id)">
<header><h2>{{post.title}}</h2></header>
</article>
</div>
</div>
As you can see, I use function getPost inside controller and there I'm using $scope, but it's (as it should be) set for, like I said, global wrapper. How can I solve this? Please note I'm new to Angular, so I don't know if it's the valid way ;-)
I agree with #Matthew Green. Your question a bit confused. But as far as I can see, you should use directives.
Create a directive and assign to it own $scope.
Hope that will help you.

Angular JS breaking two way binding on isolate scope when binding to a primitive and using ng-include to dynamically load a template

I have a bit of a strange scenario that is a little different to the other childscope and two way binding issues I have seen on Stackoverflow.
I have a field generation directive that receives a configuration object and some data and dynamically creates the correct type of field on screen and populates the data.
directive.js
.directive('myField', function () {
var stringTemplate = "scripts/directives/templates/my-string.tpl.html";
var textTemplate = "scripts/directives/templates/my-text.tpl.html";
var selectTemplate = "scripts/directives/templates/my-select.tpl.html";
var linker = function ($scope, elem, attrs) {
// Function to dynamically select the correct template
$scope.getTemplateUrl = function () {
var template = '';
if ($scope.options) {
if ($scope.options.optionList) {
template = selectTemplate;
} else {
switch ($scope.options.type) {
case 'String':
template = stringTemplate;
break;
case 'Text':
template = textTemplate;
break;
}
}
return template;
}
};
return {
restrict: 'E',
replace: true,
scope: {
options: '=',
data: '=',
fieldName: '#',
fieldWidth: '#',
labelWidth: '#',
},
link: linker,
template: '<ng-include src="getTemplateUrl()"/>'
}
});
I then have the corresponding template... I'm showing just the string template in this case.
my-string.tpl.html
<div class="form-group col-md-12">
<label for="{{fieldName}}" class="{{labelWidth}}">
{{options.label}}
</label>
<div class="{{fieldWidth}}">
<input type="text" class="form-control input-sm" id="{{fieldName}}" placeholder="{{options.watermark}}" ng-model="data" tooltip="{{options.tipText}}" ng-disabled="options.editable === false">
</div>
</div>
An example of how this might then be used would be
controller.js
$scope.person.firstName = "John";
$scope.person.lastName = "Doe";
$scope.options.person.firstName.type = "String";
index.html
<div class="row">
<my-field options="options.person.firstName" data="person.firstName" field-name="firstName" label-width="small" field-width="medium"></my-field>
The problem is the usual one, my-field directive has an isolated scope with a "data" property that is two-way bound to the controller. Because I am then using ng-include to dynamically load the correct template I am creating a further child scope that due to prototypical inheritance still populates correctly as it doesn't have its own data property so reaches to the parent. However when I modify the field, a shadow property is created on my child scope called data that doesn't propagate upwards the way that two way binding should.
I hope you are still with me
controller > my-field
ng-include causes the following scopes to exist
controller > my-field > ng-include
From reading around I understand that what I need to do to rectify this is pass an object rather than a primitive, however as there is effectively an intermediate layer between my controller and my final directive this is not straightforward.
I thought about changing the isolate scope in my-field to look like this
scope: {
....
data: {value: '=data'}
....
}
and then updating the template to refer to the object
my-string.tpl.html
<div class="form-group col-md-12">
<label for="{{fieldName}}" class="{{labelWidth}}">
{{options.label}}
</label>
<div class="{{fieldWidth}}">
<input type="text" class="form-control input-sm" id="{{fieldName}}" placeholder="{{options.watermark}}" **ng-model="data.value"** tooltip="{{options.tipText}}" ng-disabled="options.editable === false">
</div>
</div>
but this kills angular.
I have successfully got it to work by reaching back to the controller scope for binding by using
ng-model="$parent.$parent.data"
but I am not really happy with this as a solution as A it is ugly and B it involves knowing the depth of scope you are at which could vary.
Really stumped with this. Any help would be appreciated.

Instantiate and initialize controller in AngularJS

I have a problem instanciating controller with Angular. I have a main controller AlkeTypeDefListController from which I want to dynamically create/remove controllers of type AlkeTypeDefController, so I have done that :
Code of AlkeTypeDefListController:
// Create main controller
Alke.controller('AlkeTypeDefListController', ['$scope', '$controller', function($scope, $controller)
{
var primitives =
[
];
// Add some properties to the scope
angular.extend($scope,
{
typedefs : primitives,
addTypeDef : function()
{
var controller = $controller("AlkeTypeDefController", {$scope:$scope.$new()});
$scope.typedefs.push(controller);
}
});
}]);
Code of AlkeTypeDefController:
// Create main controller
Alke.controller('AlkeTypeDefController', ['$scope', '$controller', function($scope, $controller)
{
// Add some properties to the scope
angular.extend($scope,
{
name : "New Type",
fields : [],
addField : function()
{
}
});
}]);
The html code is this one:
<div id="typedefs-editor" ng:controller="AlkeTypeDefListController">
<button ng:click="addTypeDef()">Add</button>
<button>Remove</button>
<div id="typedef-list">
<ul class="list">
<li ng:repeat="typedef in typedefs">{{typedef.name}}</li>
</ul>
</div>
</div>
The problem does not really come from the instantiation (which works fine), but from the initialization. In fact, when the new "li" appears when I push the "Add" button, the text "New type" (initialized in the controller) does not appear.
I think it is about the scope or something like that, but I can't really find how to fix this.
I wanted to know if this method seems correct, and also how could I fix the problem I have.
Thanks
Reading the code, I understand that you want to create typedefs dynamically and those typedef items have to be controlled by an AlkeTypeDefController.
In that case I would create AlkeTypeDefController using ng:controller directive, so you don't need to create the controller programmatically, because then you would need to attached it to the view and that's just what the ngController directive does for you.
Notice AlkeTypeDefListController does not create a AlkeTypeDefController controller, this is done in the view
Demo on Plunker
Controllers:
.controller('AlkeTypeDefListController', ['$scope', function($scope) {
var primitives = [];
$scope.typedefs = primitives;
$scope.addTypeDef = function() {
var typeDef = { name: 'New Type' };
$scope.typedefs.push(typeDef);
}
}])
.controller('AlkeTypeDefController', ['$scope', function($scope) {
$scope.addField = function() {
alert('add Field');
}
}]);
View (notice how ng-controller directive is specified in li element):
<div id="typedefs-editor" ng:controller="AlkeTypeDefListController">
<button ng:click="addTypeDef()">Add</button>
<button>Remove</button>
<div id="typedef-list">
<ul class="list">
<li ng:repeat="typedef in typedefs" ng:controller="AlkeTypeDefController">
{{typedef.name}}
</li>
</ul>
</div>
In the code above, ngRepeat is going to create a new $scope for each typedef. AlkeTypeDefController then decorates that scope with functions and values.
I hope it helps
When you call $controller("AlkeTypeDefController") it will essentially call new on the AlkeTypeDefController constructor and give you back the return value not the scope. You are assign the name attrubute to the scope though so it is not being accessed in your html when you have typedef.name.
Try changing your AlkeTypeDefController to this:
Alke.controller('AlkeTypeDefController', function() {
this.name = "New Type";
this.fields = [];
this.addField = function() {};
});
Then you can instantiate it with: var controller = $controller("AlkeTypeDefController"); and you shouldn't need to worry about creating nested scopes.
If I get what you're saying correctly then I think I'd try to leverage the power of a custom directive here instead of dynamically generating controllers.
plunker
Controller:
Alke.controller('alkeTypeDefListController', ['$scope', '$controller',
function($scope, $controller) {
var primitives = [];
var addTypeDef = function() {
$scope.typedefs.push({
name: 'new name'
});
};
var removeTypeDef = function(){
$scope.typedefs.pop();
};
var properties = {
typedefs: primitives,
addTypeDef: addTypeDef,
removeTypeDef: removeTypeDef
};
// Add some properties to the scope
angular.extend($scope, properties);
}
]);
Directive:
Alke.directive('alkeTypeDef', function() {
return {
restrict: 'A',
scope: {
typeDef: '=alkeTypeDef'
},
template: '{{typeDef.name}}',
link: function(scope, element, attr) {
var properties = {
fields: [],
addField: function() {
}
};
angular.extend(scope, properties);
}
};
});
HTML:
<div ng-app='Alke'>
<div id="typedefs-editor" ng-controller="alkeTypeDefListController">
<button ng-click="addTypeDef()">Add</button>
<button ng-click="removeTypeDef()">Remove</button>
<div id="typedef-list">
<ul class="list">
<li alke-type-def='typedef' ng-repeat="typedef in typedefs"></li>
</ul>
</div>
</div>
</div>
If you want a controller then you can use one in the directive instead of a linking function.

Categories

Resources