editable with ngrepeat: automatically editing the latest added item - angularjs

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/

Related

How do I control the execution flow so the ng-repeat completes before the parent directive?

I'd like extend-item directive to run after ng-repeat is rendered in DOM.
In the example below the ^ is added only to the static ul elements, since the dynamic ul elements are not yet generated by ng-repeat yet. See code in Plunker
How do I control the execution flow so the ng-repeat completes before the parent directive?
<ul id="nav" ng-controller="AppController as vm" extend-item>
<li ng-repeat="group in vm.groups">
<span>{{group.name}}</span>
<ul>
<li ng-repeat="item in vm.items">
<span>{{item.name}}</span>
</li>
</ul>
</li>
<!-- Static -->
<li>
<span>Static Group 10</span>
<ul>
<li><span>Static 11</span>
</li>
</ul>
</li>
</ul>
(function() {
'use strict';
angular.module('app')
.directive('extendItem', extendItem);
extendItem.$inject = ['$compile'];
/* #ngInject */
function extendItem($compile) {
var directive = {
restrict: 'A',
link: link
};
return directive;
function link(scope, element, attrs, ctrls) {
var $lists = element.find('ul').parent('li');
$lists.append('<i>^</i>');
console.log('extendItem - $lists length: ' + $lists.length);
}
}
})();
(function() {
'use strict';
angular.module('app')
.controller('AppController', AppController);
AppController.$inject = ['$scope'];
/* #ngInject */
function AppController($scope) {
var vm = this;
vm.groups = [{
id: 1,
name: 'Group 1'
}, {
id: 2,
name: 'Group 2'
}];
vm.items = [{
id: 1,
name: 'Item 1'
}, {
id: 2,
name: 'Item 2'
}];
activate();
///////////////////////////////
function activate() {
console.log('AppController');
}
}
})();
The easiest way to do that is use $timeout before executing your code: this will wait for the current digest cycle to be done and the DOM to be refreshed.
http://plnkr.co/edit/KVlzZHHwa3ECxteJBvD8?p=preview
function link(scope, element, attrs, ctrls) {
$timeout(function() {
var $lists = element.find('ul').parent('li');
$lists.append('<i>^</i>');
console.log('extendItem - $lists length: ' + $lists.length);
});
}
The problem with $timeout is that it will only work the first time your DOM is evaluated and relies on your model never changing. If your model changes (i.e, you add groups), the new groups will not have the ^ appended.
Here is a plunk that demonstrates this problem.
I would instead use css (if you are using a modern browser)
<style>
ul[extend-item] > li::after {
content: '^'
}
</style>
In general, any time you find yourself wanting to modify the DOM after ng-repeat "completes" or "is rendered", you are thinking about it "wrong" in the Angular sense. ng-repeat never "completes". It is always watching the model and can potentially add or remove elements based on changes in the model. Using $timeout does not guarantee that the DOM will not be modified at some time in the future.
You might want to also investigate using ng-repeat-start and ng-repeat-end for this particular use case, especially if the intent is to add more than just simple content after each list element.
From the docs:
To repeat a series of elements instead of just one parent element,
ngRepeat (as well as other ng directives) supports extending the range
of the repeater by defining explicit start and end points by using
ng-repeat-start and ng-repeat-end respectively. The ng-repeat-start
directive works the same as ng-repeat, but will repeat all the HTML
code (including the tag it's defined on) up to and including the
ending HTML tag where ng-repeat-end is placed.

Something like "with variable" in AngularJS?

Sample controller data:
$scope.items = [{ id: 1, name: 'First'}, { id: 2, name: 'Second'}];
Is there something in angular to make the following code work like "with variable"?
<ul>
<li data-ng-repeat="items">{{id}} {{name}}</li>
</ul>
Instead of:
<ul>
<li data-ng-repeat="i in items">{{i.id}} {{i.name}}</li>
</ul>
Please feel free to make a more understandable title/question.
Referring to Angular ngRepeat document, currently only followings expressions are supported.
variable in expression
(key, value) in expression
variable in expression track by tracking_expression
variable in expression as alias_expression
This means, you can't simply use ng-repeat="items" to iterate the collection.
BTW, ng-repeat will create a separate scope for each element and bind variable or (key, value) to the newly created scope. So "with variable" you refer to is not Angular built-in. You need to create a customized directive for this functionality.
My preferred answer would be "don't do this" but failing that, and because it's interesting, here's a proof of concept, assisted by this question and mostly adapted from this blog post:
app.directive('myRepeat', function(){
return {
transclude : 'element',
compile : function(element, attrs, linker){
return function($scope, $element, $attr){
var collectionExpr = attrs.myRepeat;
var parent = $element.parent();
var elements = [];
// $watchCollection is called everytime the collection is modified
$scope.$watchCollection(collectionExpr, function(collection) {
var i, block, childScope;
// check if elements have already been rendered
if(elements.length > 0){
// if so remove them from DOM, and destroy their scope
for (i = 0; i < elements.length; i++) {
elements[i].el.remove();
elements[i].scope.$destroy();
};
elements = [];
}
for (i = 0; i < collection.length; i++) {
// create a new scope for every element in the collection.
childScope = $scope.$new();
// ***
// This is the bit that makes it behave like a `with`
// statement -- we assign the item's attributes to the
// child scope one by one, rather than simply adding
// the item itself.
angular.forEach(collection[i], function(v, k) {
childScope[k] = v;
});
// ***
linker(childScope, function(clone){
// clone the transcluded element, passing in the new scope.
parent.append(clone); // add to DOM
block = {};
block.el = clone;
block.scope = childScope;
elements.push(block);
});
};
});
}
}
}
});
And then this will do what you want:
app.controller("myController", function($scope, $http) {
$scope.items = [
{a: 123, b: 234},
{a: 321, b: 432}
];
});
With the HTML structure you want:
<div ng-controller="myController">
<ul>
<li my-repeat="items">
{{ a }} {{ b }}
</li>
</ul>
</div>
Notice that given the attributes are copied into the child scopes, rather than referenced, if changes are made to the view, they won't affect the model (ie. the parent items list), severely limiting the usefulness of this directive. You could hack around this with an extra scope.$watch but it'd almost certainly be less fuss to use ng-repeat as it's normally used.
I can't see why other users are telling you that what you want has to be done via a new directive. This is a working snippet.
angular.module("Snippet",[]).controller("List",["$scope",function($scope){
$scope.items = [{ id: 1, name: 'First'}, { id: 2, name: 'Second'}];
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app="Snippet" ng-controller="List as list">
<ul>
<!-- Iterating the array -->
<li ng-repeat="item in items">
<!-- Iterating each object of the array -->
<span ng-repeat="(key,value) in item">{{value}} </span>
</li>
</ul>
</body>
Simply, you need to iterate the elements of the array via ng-repeat, then you can do what you want with the object item retrieved. If you want to show its values, for example, as it seems for your question, then a new ng-repeat gets the job done.

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.

AngularJS: How create a new directive joining existents directives?

I would like improve my current solution.
I have a big menu using <ul> and <li> tags. I need show to the user only the <li> tags that they have permission to access.
I have resolved this problem using two directives: ng-init and ng-show
...
<li ng-init="ok=hasPemission('item1')" ng-show="ok">
Item 1
</li>
...
I needed to use 'ng-init' to get a stable hasPermission result. I can't use ng-show="hasPemission('item1')" because 'hasPemission' returns a new 'promise' object and the ng-show has problem with not stable expressions ([$rootScope:infdig] infinit $digest loop).
Now, I wanted create a new directive that join this both directives. I created this one but I think that there is a way to reuse these two existents directives.
This is my new directive:
.directive('myPermissionShow',['$q','$animate','AccessControl',
function ($q, $animate, AccessControl) {
return {
restrict:'A',
scope: {
resourceName: '#myPermissionShow'
},
link: function ($scope, $element) {
var user = AccessControl.getLoggedUser();
AccessControl.hasPermission(user,$scope.resourceName).then(
function (value) {
// I copied the line below from ngShow directive:
$animate[value ? 'removeClass' : 'addClass']($element, 'ng-hide');
}
);
}
}
}])
So, my html changed to:
...
<li my-permission-show="item1">
Item 1
</li>
<li my-permission-show="item2">
Item 2
</li>
...
Is there a way to create this new directive reusing the directives 'ng-init' and 'ng-show'?
Something like this:
!!!NOT WORKING CODE!!!
.directive('myPermissionShow',['AccessControl',
function (AccessControl) {
return {
restrict:'A',
replaceAttribute: true, /* this property does not exists... */
template: 'ng-init="val=hasPermission(user,resourceName)" ng-show="val"',
scope: {
resourceName: '#myPermissionShow'
},
controller: function ($scope, $element) {
$scope.hasPemission = AccessControl.hasPermission;
$scope.user = AccessControl.getLoggedUser();
}
}
}])
!!!NOT WORKING CODE!!!

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