Using controller inside repeat, do I need a directive? - angularjs

When using a ng-repeat, how should I use a controller inside it?
For example, if I'm looping over a set of days in a week:
<ul ng-controller="WeekCtrl">
<li ng-repeat="d in days">
<span ng-controller="DayCtrl">
{{dayOfWeek}} {{date}}: {{info}}
</span>
</li>
</ul>
But my DayCtrl wants to know what day d it is, so has to pull that out of the scope:
app.controller('DayCtrl', function($scope){
$scope.date = $scope.d.date;
$scope.dayOfWeek =
['Mon','Tue','Wed','Thr','Fri','Sat','Sun']
[$scope.d.date.getDay()];
$scope.info = '... extra info...';
});
But this creates a dependency between the display and the controller. Ideally, I'd like to pass in d.date as an argument.
I could write a directive+controller and pass d.date in as an attribute. But that means I have to write a lot more and move the day's html into separate template, and I don't intend to use the DayCtrl elsewhere.
Alternatively, I could try and use <span ng-init="date=d.date"> but again, this feels dirty.
What's the right way of doing this.
Full example code on Plunker: http://plnkr.co/edit/wUxNFSEGjcDN7KlOLYdv which shows the problem I'm having with days and weeks.

To me, this seems like a job for a directive, with d.date specified as an attribute. I don't know how info is populated, but potentially your directive would not need a controller.
The day's HTML doesn't have to be in a separate/directive template, it can remain in the HTML:
<li ng-repeat="d in days">
<day date="d.date">
{{dayOfWeek}} {{date}}: {{info}}
</day>
</li>
Directive:
app.directive('day', function() {
return {
restrict: 'E',
scope: { date: '=' },
link: function(scope) {
scope.dayOfWeek = ['Mon','Tue','Wed','Thr','Fri','Sat','Sun'][scope.date.getDay()];
scope.info = '... extra info...';
}
};
});
Plunkr.
FYI, if you want to use ng-init, you can include it with the ng-controller:
<span ng-controller="DayCtrl" ng-init="date=d.date">

Related

Angular "bind twice"

I'm trying to keep my watches down by using one-time binding (::) in most places.
However, I've run into the situation where I need to wait for one property of an object to arrive from our server.
Is there someway I can make Angular bind twice (first to a placeholder and second to the actual value)?
I tried accomplishing this using bindonce but it did not seem to work (I am guessing this is because bindonce wants to watch an entire object, not a single property).
Another solution would be if I could somehow remove a watch from the templates after the value comes in, if that is possible.
My objects look something like this:
{
name: 'Name',
id: 'Placeholder'
}
And my template:
<div ng-repeat="object in objects">
{{::object.name}}
{{::object.id}}
</div>
Id will change once and only once in the application life time, having a watch forever for a value that will only change once feels wasteful as we'll have many of these objects in the list.
I think this is what you are looking for! Plunkr
I just wrote a bind-twice directive and if I did not completely missed the question it should solve your problem:
directive("bindTwice", function($interpolate) {
return {
restrict: "A",
scope: false,
link: function(scope, iElement, iAttrs) {
var changeCount = 0;
var cancelFn = scope.$watch(iAttrs.bindTwice, function(value) {
iElement.text(value === undefined ? '' : value);
changeCount++;
if (changeCount === 3) {
cancelFn();
}
});
}
}
});
What I do is, I add a watcher on the scope element we need to watch and update the content just like ng-bind does. But when changeCount hit the limit I simply cancel $watch effectively cleaning it from watchlist.
Usage:
<body ng-controller="c1">
<div ng-repeat="t in test">
<p>{{ ::t.binding }}</p>
<p bind-twice="t.binding"></p>
<p>{{ t.binding }}</p>
</div>
</body>
Please see Plunkr for working example.

Creating a Reusable Component in AngularJS

I am new to Stackoverflow. I'm also new to AngularJS. I apologize if I'm not using this correctly. Still, I'm trying to create a UI control with AngularJS. My UI control will look like this:
+---------------------+
| |
+---------------------+
Yup. A textbox, which has special features. I plan on putting it on pages with buttons. My thought is as a developer, I would want to type something like this:
<ul class="list-inline" ng-controller="entryController">
<li><my-text-box data="enteredData" /></li>
<li><button class="btn btn-info" ng-click="enterClick();">Enter</button></li>
</ul>
Please note, I do not want buttons in my control. I also want the enteredData to be available on the scope associated with child controls. In other words, when enterClick is called, I want to be able to read the value via $scope.enteredData. In an attempt to create my-text-box, I've built the following directive:
myApp.directive('myTextBox', [function() {
return {
restrict:'E',
scope: {
entered: '='
},
templateUrl: '/templates/myTextBox.html'
};
}]);
myApp.controller('myTextBoxController', ['$scope', function($scope) {
$scope.doSomething = function($item) {
$scope.entered = $item.name;
// Need to somehow call to parent scope here.
};
}]);
myTextBox.html
<div ng-controller="myTextBoxController">
<input type="text" class="form-control" ng-model="query" placeholder="Please enter..." />
</div>
entryController.js
myApp.controller('entryController', ['$scope', function($scope) {
$scope.enteredData = '';
$scope.enterClick = function() {
alert($scope.enteredData);
};
});
Right now, I have two issues.
When enterClick in entryController is called, $scope.enteredData is empty.
When doSomething in myTextBoxController is called, I do not know how to communicate to entryController that something happened.
I feel like I've setup my directive correctly. I'm not sure what I'm doing wrong. Can someone please point me in the correct direction.
Three suggestions for you.
1) You really shouldn't create a directive with a template that references a controller defined elsewhere. It makes the directive impossible to test in isolation and is generally unclear. If you need to pass data into a directive from a parent scope use the isolate scope object on your directive to bind to that data (Note how the directive template doesn't have a controller) http://jsfiddle.net/p4ztunko/
myApp.directive('myTextBox', [function () {
return {
restrict: 'E',
scope: {
data: '='
},
template: '<input type="text" class="form-control" ng-model="data" placeholder="Please enter..." />'
};
}]);
myApp.controller('entryController', ['$scope', function ($scope) {
$scope.enteredData = 'Stuff';
$scope.enterClick = function () {
alert($scope.enteredData);
};
}]);
<div>
<ul class="list-inline" ng-controller="entryController">
<li>{{enteredData}}
<my-text-box data="enteredData" />
</li>
<li>
<button class="btn btn-info" ng-click="enterClick();">Enter</button>
</li>
</ul>
</div>
2) Don't obfuscate HTML when you don't need to. One of the goals of angular is to make the markup more readable, not replace standard elements with random custom elements. E.g. If you want to watch the value of the input and take action depending on what it is you could do that in the linking function (Note: still not referencing an external controller) http://jsfiddle.net/Lkz8c5jo/
myApp.directive('myTextBox', function () {
return {
restrict: 'A',
link: function(scope, element, attrs){
function doSomething (val) {
alert('you typed ' + val);
}
scope.$watch(attrs.ngModel, function (val) {
if(val == 'hello'){
doSomething(val);
}
});
}
};
});
myApp.controller('entryController', ['$scope', function ($scope) {
$scope.enteredData = 'Stuff';
$scope.enterClick = function (data) {
alert('You clicked ' + data);
};
}]);
<div>
<ul class="list-inline" ng-controller="entryController">
<li>{{enteredData}}
<input type="text" ng-model="enteredData" my-text-box />
</li>
<li>
<button class="btn btn-info" ng-click="enterClick(enteredData)">Enter</button>
</li>
</ul>
</div>
3) Pass data into controller functions from the UI instead of referencing the $scope object in the function like in ng-click="enterClick(enteredData)" It makes testing easier because you remove the $scope dependency from that method
$scope.enteredData is empty because you're not using the correct scope. The $scope in entryController is not the same $scope as in myTextBoxController. You need to specify one of these controllers on your directive, and then use that to reference the proper scope.
It seems like you should move the enterClick and corresponding button into your directive template. Then move the enter click into your text box controller and you will be able to reference $scope.enteredData.
You can notify a parent scope of a change by using $emit. (This is in reference to your comment "// Need to somehow call to parent scope here.")
Furthermore, you may have an issue of not using the proper variable. In myTextBox directive, you declare $scope.entered, yet you are effectively setting $scope.data equal to the value of enteredData in the html.

Read existing data from DOM into model, ng-model for static data?

I'm currently playing around with rewriting the functionality of an existing page using Angular. The gist of it is that I have a plain HTML page with a list of stuff, like this:
<ul>
<li class="item">
<h1>Foo</h1>
<ul class="categories">
<li class="category">Bar</li>
</ul>
</li>
...
</ul>
This is augmented by some Javascript which parses this data once and adds a dynamic category filter menu to the page. I.e. it extracts all li.category elements and displays a menu with them, and clicking on one of these categories filters the item list to display only items with the chosen category.
I've replicated the basics of that in Angular with a lot less code than I had before. However, I'm still doing a lot of jQuery traversing of the .item elements to build that initial list of categories:
myApp.controller('MyController', function ($scope) {
$scope.categories = [];
angular.element('.item').each(function () {
angular.element(this).find('.categories .category').each(function () {
var category = this.textContent;
for (var i = 0, length = $scope.categories.length; i < length; i++) {
if ($scope.categories[i].name == category) {
$scope.categories[i].count++;
$scope.categories[i].items.push(this);
return;
}
}
$scope.categories.push({ name : category, count : 1, items : [this] });
});
});
});
This does not seem to be in the spirit of Angular, and I'd like to replace it with something like:
<ul>
<li class="item" ng-item>
<h1>Foo</h1>
<ul class="categories">
<li class="category" ng-category>Bar</li>
</ul>
</li>
...
</ul>
A directive should then be able to parse all ng-item/ng-category elements and add them to the model/scope once. Something like ng-model, but for static data.
I have virtually no experience with Angular, how can I accomplish this; or shouldn't I want to do something entirely different in the first place?
For creating your own ng-item and ng-category directives, I suggest that you can go through Creating Custom Directives part in Angular Offical Develop Guide:
http://docs.angularjs.org/guide/directive
It will tell you how to begin creating your directive from add directive to module like this:
.directive('myCustomer', function() {
return {
template: 'Name: {{customer.name}} Address: {{customer.address}}'
};
});
Edit:
This two is also useful tutorial:
http://www.ng-newsletter.com/posts/directives.html
http://www.befundoo.com/university/tutorials/angularjs-directives-tutorial/
Edit2:
To answer your comment:
Do you have a concrete sample of how I'd write a directive that reads data from its element and modifies the controller's scope?
I thought that it has clear explanation in Angular Official Guide:
.directive('myCustomer', function() {
return {
restrict: 'E',
scope: {
customerInfo: '=info'
},
templateUrl: 'my-customer-iso.html'
};
});
In this example:
restrict: 'E' : directive name match element name
So directive look like this:
<my-customer></my-customer>
scope: { customerInfo: '=info'}
<my-customer info="myInfo"></my-customer>
this will bind myInfo to scope just like this expression:
$scope.customerInfo = myInfo;
this is a concrete sample of how to read data from its element and modify the controller's scope.

editable with ngrepeat: automatically editing the latest added item

I need to add new items to a collection, that gets rendered with ngrepeat and using xeditable make it automatically editable.
BTW, I'm using the "manual trigger" method for xeditable.
Here it is the HTML
<h4>Angular-xeditable demo</h4>
<div ng-app="app" ng-controller="Ctrl" style="margin: 50px">
<div class="btn btn-default" ng-click="addNew()">+</div>
<ul>
<li ng-repeat="item in array | orderBy:'-value'">
{{ item.field }}
<i ng-show="!itemForm.$visible" ng-click="itemForm.$show()">edit</i>
</li>
</ul>
</div>
and here the controller:
var app = angular.module("app", ["xeditable"]);
app.run(function(editableOptions) {
editableOptions.theme = 'bs3';
});
app.controller('Ctrl', function($scope, $filter) {
$scope.array = [
{value: 1, field: 'status1'},
{value: 2, field: 'status2'},
{value: 3, field: 'status3'},
{value: 4, field: 'status4'}
];
$scope.addNew = function(){
$scope.array.push({value:$scope.array.length+1, field: 'enter text here'});
//MAKE IT EDITABLE????????
}
});
Take a look to the issue in this fiddle: http://jsfiddle.net/dpamio/hD5Kh/1/
Here is a updated fiddle that works. Because of how the directive was written, and how ng-repeat works, it required an extremely hacky solution...
app.controller('Ctrl', function($scope, $filter, $timeout) {
$scope.itemForms = {};
$scope.addNew = function(){
$scope.array.push({value:$scope.array.length+1, field: 'enter text here'});
// Use timeout to force evaluation after the element has rendered
// ensuring that our assignment expression has run
$timeout(function() {
$scope.itemForms[0].$show(); // last index since we sort descending, so the 0 index is always the newest
})
}
Background on how ng-repeat works: ng-repeat will create a new child scope for each element that is repeated. The directive assigns a variable on that scope using the string passed into e-form for its name (in this case itemForm). If it was smarter, it'd allow for expression evaluation for assignment. (Then we could assign it to the parent scope, and access it in the controller, but that's a different matter).
Since we don't have any way to access this child scope outside of the directive, we do something very bad. We use the mustache expression in a span of display none to assign the itemForm variable to the parent scope so that we can use it later. Then inside our controller we use the look up value to call the itemForm.$show() method that we expect.
Abstracting that bit of nastyness into an angular directive, we could write the following:
.directive('assignFromChild', function($parse) {
return {
restrict: 'A',
link: function(scope, el, attrs) {
scope.$watch(function() { return $parse(attrs.assignFromChild)(scope); }, function(val) {
$parse('$parent.' + attrs.toParent).assign(scope, val);
})
}
};
});
Allowing our HTML to go back down to:
<ul>
<li ng-repeat="item in array | orderBy:'-value'" assign-from-child="itemForm" to-parent="itemForms[{{$index}}]">
{{ item.field }}
<i ng-show="!itemForm.$visible" ng-click="itemForm.$show()">edit</i>
</li>
</ul>
Here is a fiddle with my final solution
I found a very simple solution using ng-init="itemForm.$show()", that will activate the xeditable form when the new item is inserted.
Here's the updated jsFiddle answering the question: http://jsfiddle.net/hD5Kh/15/

AngularJS : Toggle to modify attribute in directive

In the project I am working on, I am applying a ui-sort via Angular on a to-do list and am trying to get a toggle to work for when a user is editing tasks. My current method of testing this toggle is employing the use of a button to toggle sorting on and off.
My strategy is this:
Employ an angular directive to generate an initial template with sorting on.
Add a button which, when clicked, modifies a scope variable in the controller ($scope.sortingEnabled) to toggle between true and false.
Inside my directive, I have a watch set on 'sortingEnabled' in a link function to add/remove the sorting attribute from a .
Here is the in todo.html before I tried employing a directive:
sortableOptions is a function written to re-order the todos on internal records.
<ul class="unstyled" ng-model="todos" ui-sortable="sortableOptions">
<!-- list items here via ng-repeat -->
</ul>
The following is the code in todo.html after my directive:
<sortable></sortable>
And my current draft for the directive inside todo-directives.js:
app.directive('sortable', function() {
var innerHtml = '<li ng-repeat="todo in todos" class="item">' +
'<span ng-model="todo.name" >{{todo.name}}</span> ' +
'</li>';
var link = function (scope, element, attrs) {
scope.$watch('sortingEnabled', function() {
if(scope.sortingEnabled === true) {
element.contents().attr("ui-sortable", "sortableOptions");
//needed else ui-sortable added as a class for <ul> initially for
//some reason
element.contents().removeClass("ui-sortable");
}
else {
element.contents().removeAttr("ui-sortable");
//needed else ui-sortable added as a class for <ul> initially for
//some reason
element.contents().removeClass("ui-sortable");
}
});
};
return {
restrict: 'E',
transclude: true,
template: '<ul class="unstyled" ng-model="todos" ui-sortable="sortableOptions" ng-transclude>' + innerHtml + '</ul>',
link: link
};
});
This code works in the source code view of Chrome's debugger, but the view does not properly refresh. I have tried scope.$apply() within the watch function but get a $digest already running error. I have also tried $compile, but my understanding of how that works is severely lacking, so I get errors of which I do not remember.
Am I missing something crucial, or doing things incorrectly? I am unsure, as my understanding is low, being that I have been leaning Angular for a few weeks. Any help would be greatly appreciated!
The angular directive supports watching when the sortable options change:
scope.$watch(attrs.uiSortable, function(newVal, oldVal){
So all you had to do was look at the jqueryui sortable documentation, and update the correct property on the plugin.
Plunker: http://plnkr.co/edit/D6VavCW1BmWSSXhK5qk7?p=preview
Html
<ul ui-sortable="sortableOptions" ng-model="items">
<li ng-repeat="item in items">{{ item }}</li>
</ul>
<button ng-click="sortableOptions.disabled = !sortableOptions.disabled">Is Disabled: {{sortableOptions.disabled}}</button>
JS
app.controller('MainCtrl', function($scope) {
$scope.items = ["One", "Two", "Three"];
$scope.sortableOptions = {
disabled: true
};
});

Resources