One time binding: update model and re-render view - angularjs

I was wondering if possible, using angular one time binding, to completely re-render the view/template after a model update, also by recompiling the template.
For instance, on a button press, maybe in the way react works: I update the model and explicitly force to update the view.
Basically here is what I am trying to achieve:
// controller
angular.module('app', []).controller('AppCtrl', function($scope) {
$scope.items = [
{id: 1},
{id: 2},
{id: 3}
];
$scope.addAndRefresh = function() {
$scope.items.push({id: 4});
// manually call render logic here???
};
});
<!-- HTML template -->
<div ng-repeat="item in ::items">
{{item.id}}
</div>
<button ng-click="addAndRefresh()">Add</button>
By clicking on the "Add" button I would like to refresh the view to see the newly added item.

I was trying to figure out some way to do this elegantly as well. I wish there was something built into the framework to refresh one-time bindings. All I came up with is using ngIf to remove the element I wanted to refresh and the add it back.
Here's a demo. Click the Add Item button, you'll see that the list does not refresh due to the one-time binding on the repeat. Check the refresh values and click again, and the items will be updated:
var app = angular.module('demo', []);
app.controller('RefreshCtrl', function($scope, $timeout) {
var counter = 4;
$scope.visible = true;
$scope.items = ['Item1', 'Item2', 'Item3'];
$scope.addItem = function() {
if ($scope.refresh) {
$scope.visible = false;
}
$scope.items.push('Item' + counter);
counter++;
$timeout(function() {
$scope.visible = true;
});
};
});
<script src="https://code.angularjs.org/1.3.17/angular.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<div ng-app="demo" ng-controller="RefreshCtrl" class="container">
<button class="btn btn-default" ng-click="addItem()">Add Item</button>
<input type="checkbox" ng-model="refresh" />Refresh Values
<div ng-if="visible">
<h3 ng-repeat="item in ::items">{{item}}</h3>
</div>
<p>Items Array: {{items}}</p>
</div>

Depending on what you are after, I would recommend one of two solutions:
Get angular-bind-notifier.
Does not recompile your template, only refreshes the bound values.
Get kcd-recompile.
Recompiles the template along with the bound values.
I'm the author of the former, and the big difference between it and other solutions is the choice of hooking into the $parse service.
As such, you can use the introduced {{:refreshkey:expression}}/:refreshkey:expression syntax in most (if not all) areas of Angular where an expression is accepted.
In your case, the implementation could look something like this:
js
angular.module('app', []).controller('AppCtrl', function($scope) {
$scope.items = [
{id: 1},
{id: 2},
{id: 3}
];
$scope.addAndRefresh = function() {
$scope.items.push({id: 4});
/**
* '$$rebind' is the internal namespace used by angular-bind-notifier.
* 'refresh' is the refresh key used in your view.
*/
$scope.$broadcast('$$rebind:refresh');
};
});
markup
<!-- HTML template -->
<div ng-repeat="item in :refresh:items">
{{::item.id}}
</div>
<button ng-click="addAndRefresh()">Add</button>
Or, if you wanted something semi-dynamic
js
angular.module('app', []).controller('AppCtrl', function($scope) {
$scope.items = [
{id: 1},
{id: 2},
{id: 3}
];
$scope.add = function() {
$scope.items.push({id: 4});
};
});
markup
<!-- HTML template -->
<div bind-notifier="{ refresh: items.length }">
<div ng-repeat="item in :refresh:items">
{{::item.id}}
</div>
</div>
<button ng-click="add()">Add</button>
Check out the README and this jsBin for some usage examples.

Related

Ionic , angular , Show or disable a component within controller

I have the following code. The following code displays 5 tag item.
What I wanted to do is when user click on any of the tag item, it will be hidden or blank-out. But when I clicked on any of it, all the tag item got blanked out instead of the item I selected. Can anyone give me some idea ?
<div data-ng-repeat="x in mylist">
<a class="button" ng-click="hideme();" style="{{visibility}}">{{x}}</a>
</div>
.controller('MyCtrl', function($scope, $stateParams, chats) {
$scope.hideme = function(index, value) {
$scope.visibility = "background-color: #000000 !important;";
}
});
Firstly, you might want to change your object structure a bit. Remeber always have a dot (.) in your model value.
Try this.
<div ng-app ng-controller="myCtrl">
<div ng-repeat="x in myList">
{{x.value}}
</div>
</div>
function myCtrl($scope){
$scope.myList = [{
value: "a"
}, {
value: "b"
}, {
value: "c"
}];
$scope.hide = function(obj){
obj.hide = true;
}
}
JSFiddle

Access child scope corresponding to a model variable

I have a list of items and I've rendered them in a template via ng-repeat. Each of them is controlled by an itemController which exposes some behavior for item (e.g. grabing focus). Here is the HTML:
<body ng-controller="mainController">
<button ng-click="addItem()">add</button>
<ul>
<li ng-repeat="item in items" ng-controller="itemController">
<div ng-if="item.isEditing">
<input ng-model="item.name"/>
<button ng-click="item.isEditing=false">done</button>
</div>
<span ng-if="!item.isEditing">{{item.name}}</span>
</li>
</ul>
</body>
In the mainController, I have a function for adding a new item to items. Here is the code for mainController:
app.controller('mainController', function($scope){
$scope.items = [
{
name: "alireza"
},
{
name: "ali"
}
];
$scope.addItem = function(){
$scope.items.push({isEditing: true});
}
});
Whenever I add an item to items array, a corresponding li element is added into the view which is controlled by an instance of itemController, and the corresponding model is the new item I've just added (or maybe the scope of the itemController, which contains item).
Problem:
When I add some item to items, I only have access to item and not the scope of the recently created item. So I can't run some function (like grabFocus) on the scope of new item.
Is it something semantically wrong in my design? What is the canonical approach for this problem?
Plunker link
Here is the plunker link with related comments
You can use $broadcast from the parent scope, along with $on from the child scope, to notify the child scopes of the newly added item. And by passing (as an argument) the $id of the child scope that corresponds to the newly added item, each child catching the event can know whether or not it's the one that needs to have grabFocus() called.
Here's a fork of your Plunker that uses that approach. I wasn't sure what you were trying to accomplish with $element.find(":text").focus(); in your original Plunker, so I tweaked it to toggle a $scope property that in turn controlled a style in the view. The newly added item will be red (by calling its own grabFocus function to toggle the flag to true), and the others will be black (by calling their own loseFocus function to toggle the flag to false).
Modified HTML (just the repeated li):
<li ng-repeat="item in items" ng-controller="itemController">
<div ng-if="item.isEditing">
<input ng-model="item.name"/>
<button ng-click="item.isEditing=false;handleItemAdded($index);">done</button>
</div>
<span ng-if="!item.isEditing" ng-style="{ color: isFocused ? 'red' : 'black' }">{{item.name}}</span>
</li>
Full JavaScript:
var app = angular.module("app",[]);
app.controller('mainController', function($rootScope, $scope){
$scope.items = [ { name: "alireza" }, { name: "ali" } ];
$scope.addItem = function(){
$scope.items.push({isEditing: true});
};
$scope.handleItemAdded = function (index) {
// $rootScope.$broadcast('item-added', { index: index });
for(var cs = $scope.$$childHead; cs; cs = cs.$$nextSibling) {
if (cs.$index === index) {
$rootScope.$broadcast('item-added', { id: cs.$id });
break;
}
}
};
});
app.controller('itemController', function($scope, $element){
$scope.$on('item-added', function (event, args) {
if ($scope.$id === args.id + 1) {
$scope.grabFocus();
} else {
$scope.loseFocus();
}
});
$scope.grabFocus = function() {
$scope.isFocused = true;
};
$scope.loseFocus = function() {
$scope.isFocused = false;
};
});
I changed your approach a little bit by creating an unique id for every input, based on its index number. See code below, hope it helps.
// Code goes here
var app = angular.module("app",[]);
app.controller('mainController', function($scope,$timeout){
$scope.items = [
{
name: "alireza"
},
{
name: "ali"
}
];
$scope.addItem = function(){
$scope.items.push({isEditing: true});
$timeout(function(){
document.getElementById("newItem"+($scope.items.length-1)).focus();
},0)
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<!DOCTYPE html>
<html ng-app="app">
<head>
<script data-require="angular.js#1.3.0" data-semver="1.3.0" src="//code.angularjs.org/1.3.0/angular.js"></script>
<link href="style.css" rel="stylesheet" />
<script src="script.js"></script>
</head>
<body ng-controller="mainController">
<button ng-click="addItem()">add</button>
<ul>
<li ng-repeat="item in items">
<div ng-if="item.isEditing">
<input ng-model="item.name" id="newItem{{$index}}"/>
<button ng-click="item.isEditing=false">done</button>
</div>
<span ng-if="!item.isEditing">{{item.name}}</span>
</li>
</ul>
</body>
</html>

Adding directive dynamically using ng-click

I am working on a modal for a client. I have created a directive that works great, the problem is that one modal is made ahead each time it is used like..
What I have
<div ng-repeat="item in items">
<a data-toggle="modal" data-target="{{item.id}}">Click</a>
<my-dialog element-id="item.id">
<h1>This is the body of the modal</h1>
</my-dialog>
</div>
This works great for a small amount of modals but we are using a very large number of modals. So I would like to add the directive at runtime, something closer to...
What I want...
<div id="warning"></div>
<div ng-repeat="item in items">
<a data-toggle="modal" data-target="{{item.id}}" ng-click="showModal(item)">Click</a>
</div>
...
// inside controller
$scope.showModal = function(item){
$http.get('/someUrl').success(function(data){
var result = $compile('<my-dialog element-id="'+item.id+'">'+data+'</my-dialog>').($scope);
$("#warning").append(result);
});
}
$scope.hideModal = function(){
$( "#warning" ).empty();
}
This of course isn't working yet. Also it doesn't feel like the best way. This would allow me to remove the directive once it has been closed.
Please include a plunker or equivalent for the check.
One way you could do this is to use ng-repeat with your items, then call $scope.$apply() after you push a new item to the list. The HTML could look like this ...
<div ng-repeat="item in items">
<span dialog>
<a class="dialog-anchor">{{item.name}}</a>
<div class="dialog-body">{{item.id}}</div>
</span>
</div>
... and the directive like this
.directive('dialog', [function () {
return {
scope: {
id: '#elementId',
}
, link: function (scope, el, attrs) {
var body = $(el).find('.dialog-body').detach();
$(el).find('.dialog-anchor').on('click', function () {
$('body').append(body);
});
}};
}])
... and the controller like this
.controller('app', ['$scope', function ($scope) {
$scope.items = [
{name: 'first', id: 001},
{name: 'second', id: 002}
];
setTimeout(function () {
$scope.items.push({name: 'three', id: 003});
if (!$scope.$$phase) $scope.$apply();
}, 2000);
}])
Here's the plunker... http://plnkr.co/edit/2ETbeCKGcHW3CJCfD9d7?p=preview. You can see the $scope.$apply call in the setTimeout where I push a new item to the array.
Try this:
var result = $compile('<my-dialog element-id="'+item.id+'">'+data+'</my-dialog>')($scope);

Edit Mode inside Angular ng-repeat

I have a requirement to implement an editable list for a project I am working on. When you click on an item it changes to an edit state that has a bunch of options related to the item you clicked on. Our UX wants the items to be edited in-line but I am not sure of the best way to do this in angular and would like to know which way is better.
Example 1 Template
<div class="person" ng-repeat="person in people" ng-click="editing=!editing">
<div class="details-view" ng-hide="editing" ng-bind="person.name"></div>
<div class="edit-view" ng-show="editing">
<input type="text" /> <button type="button">Save</button> <a>Cancel</a>
</div>
</div>
Example 1 Controller
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.people = [
{name: 'Joe', age:23},
{name: 'Jim', age:32},
{name: 'Jill', age:13}
]
});
The first way (example here) is to have an ng-repeat and inside of each ng-repeat item create an edit mode that is specific to the ng-repeat item. This works great but I don't want to leave edit mode until I have a successful response from the server and I don't understand how to handle that using this method.
Example 2 Template
<div class="person" ng-repeat="person in people" ng-click="toggleEditing(person)">
<div class="details-view" ng-hide="person.editing" ng-bind="person.name"></div>
<div class="edit-view" ng-show="person.editing">
<input type="text" /> <button type="button">Save</button> <a>Cancel</a>
</div>
</div>
Example 2 Controller
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.people = [
{name: 'Joe', age:23},
{name: 'Jim', age:32},
{name: 'Jill', age:13}
];
$scope.toggleEditing = function(person) {
person.editing = !person.editing;
};
});
The second way (example here) I thought of is to duck punch the view state onto the object. I don't like this way because I don't want to modify the object handed to me by the ng-repeat.This method does allow me to handle the successful save that the first way above doesn't.
Are there any better options?
If you don't want to clutter the object with the view state, you can save the view state in an different object.
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.editedItems = {};
$scope.people = [
{name: 'Joe', age:23},
{name: 'Jim', age:32},
{name: 'Jill', age:13}
];
$scope.toggleEditing = function(person) {
$scope.editedItems[person.name] =
!$scope.editedItems[person.name] || true;
};
});
HTML
<div class="person" ng-repeat="person in people" ng-click="toggleEditing(person)">
<div class="details-view" ng-hide="editedItems[person.name]" ng-bind="person.name"></div>
<div class="edit-view" ng-show="editedItems[person.name]">
<input type="text" /> <button type="button">Save</button> <a>Cancel</a>
</div>
</div>
Have you tried ng-grid instead of ng-repeat? They have a good edit in-line model and it seems to have better UX than the ng-hide/ng-show options.

Display list of items and edit them via AngularJS

I try to solve a classic problem using AngularJS: I need to display list of some entities and provide ability to add, edit and view details of this entities.
I implement two controllers: ListController to iterate list of entities and ItemController to display and save entity details. This is html code:
<div ng-app="myApp">
<a class="btn" data-toggle="modal" data-target="#modal">Add new item</a>
<div ng-controller="ListController">
<h4>List</h4>
<ul>
<li ng-repeat="item in list">
{{item.name}}
<a class="btn" data-toggle="modal" data-target="#modal" ng-click="editItem(item)">Edit item</a>
</li>
</ul>
</div>
<div id="modal" role="dialog" class="modal hide fade">
<div ng-controller="ItemController">
<div class="modal-header">
Item Dialog
</div>
<div class="modal-body">
<label for="txtName" />
<input type="text" id="txtName" ng-model="item.name" />
</div>
<div class="modal-footer">
<button class="btn btn-primary" ng-click="saveItem()" data-dismiss="modal">OK</button>
</div>
</div>
</div>
and controllers code:
var db_list = [{ name: "Test1" }, { name: "Test2" }];
var app = angular.module('myApp', []).
controller('ListController', function($scope, $rootScope) {
$scope.list = db_list;
$scope.editItem = function(item) {
$rootScope.item = item;
}
}).
controller('ItemController', function($scope, $rootScope) {
$scope.saveItem = function() {
db_list.push($rootScope.item);
$rootScope.item = null;
}
});
Also you can find the working ptototype at http://jsfiddle.net/yoyoseek/9Qntw/16/.
The general problem in this code that I store entity to display its description using scope of the ListController (via editItem()), but I need this stored entity details in the ItemController. I use $rootScope for sharing entity to edit and it looks like hack for me. Is it a normal practice?
This code has one more drawback: $rootScope.item have to been cleared on modal dialog hide.
It looks like the main problem here is that events triggered by data-toggle happen outside of your control and it's not part of the AngularJS bindings (I am new to it so I may be wrong).
Anyway, it seems like there is no way to cross-reference controllers in Angular, and the only way to get hold of them is via inspecting the DOM. But, once you get into that, you may as well initialize the scope directly (http://jsfiddle.net/B4kAW/4/):
var db_list = [{ name: "Test1" }, { name: "Test2" }];
var app = angular.module('myApp', []);
app.controller('ListController', function($scope) {
$scope.list = db_list;
$scope.editItem = function(item) {
angular.element(document.getElementById("modal")).scope().item = item;
};
});
app.controller('ItemController', function($scope) {
$scope.saveItem = function(item) {
//db_list.push(item);
//$rootScope.item = null;
};
});
Note:
The modal dialog here has no way of knowing whether it's opened for editing, or adding a new item (I commented out push).
Since the dialog is linked with "main" item in the list, it updates it instantly (can be seen while the dialog is open, on the background). You may need to copy it instead of using a reference.
Inspired by this answer. It looks like "the Angular way" around dialogs is to convert them into services.

Resources