Access child scope corresponding to a model variable - angularjs

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>

Related

How to update model in ui-codemirror

I have two separate controllers which shared a property. If the first controller changes the property the second controller should recognize it and should change the text in the codemirror text area. I tried to figure it out in this fiddle example but I could not find a solution.
var app = angular.module('myApp', ['ui.codemirror']);
app.service('sharedProperties', function() {
var objectValue = {
data: 'test object value'
};
return {
setText: function(value) {
objectValue.data = value;
},
getText: function() {
return objectValue;
}
}
});
app.controller('myController1', function($scope, $timeout, sharedProperties) {
$scope.setText = function(text){
sharedProperties.setText(text);
console.log(sharedProperties.getText().data);
}
});
app.controller('myController2', function($scope, sharedProperties) {
$scope.editorOptions = {
lineWrapping: true,
lineNumbers: true,
readOnly: 'nocursor',
mode: 'xml'
};
$scope.mappingFile = sharedProperties.getText();
console.log($scope.mappingFile);
});
<div ng-app="myApp">
<div ng-controller="myController1">
<input type="text" ng-model="newText"></input>
<button ng-click="setText(newText)">Set Text</button><br/>
</div>
<div ng-controller="myController2">
<ui-codemirror ui-codemirror-opts="editorOptions" ng-model="mappingFile.data" ui-refresh="true"></ui-codemirror>
</div>
</div>
At first, the way that you're doing you have 2 controllers in the same page but without any relation, I'd suggest you to make one of them as child of another.
So, to achieve what you want you need do a kind of watch on that variable from the parent controller.
Steps:
Use the $broadcast to send data to the child controller
$scope.$broadcast('newText', $scope.newText);
Use $on to receive the data from the parent controller:
$scope.$on('newText', function(event, text) {
...
});
Here's the code working based on your original code:
(function() {
'use strict';
angular
.module('myApp', ['ui.codemirror'])
.controller('myController1', myController1)
.controller('myController2', myController2);
myController1.$inject = ['$scope', '$timeout'];
function myController1($scope, $timeout) {
$scope.setText = function(text) {
console.log('Sent...', $scope.newText);
$scope.$broadcast('newText', $scope.newText);
}
}
myController2.$inject = ['$scope'];
function myController2($scope) {
$scope.editorOptions = {
lineWrapping: true,
lineNumbers: true,
readOnly: 'nocursor',
mode: 'xml'
};
$scope.$on('newText', function(event, text) {
if (!text) return;
$scope.mappingFile = text;
console.log('Received... ', $scope.mappingFile);
});
}
})();
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.17.0/codemirror.min.js"></script>
<script src="https://rawgit.com/angular-ui/ui-codemirror/master/src/ui-codemirror.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
</head>
<body>
<div ng-app="myApp">
<div ng-controller="myController1">
<input type="text" ng-model="newText">
<button ng-click="setText()">Set Text</button>
<hr>
<div ng-controller="myController2">
<textarea ui-codemirror-opts="editorOptions" ng-model="mappingFile"></textarea>
</div>
</div>
</div>
</body>
</html>
Some notes:
You don't need to pass ngModel as parameter in your ngClick, you can access it directly in your controller simply calling $scope.newText (as I did);
<input> is a self-closing tag, so of course, you don't need to close it.
I hope it helps.

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

How to display custom layout with ng-repeat

Edit: My original question was not good enough, so edited it now.
Hi Im new to angular and im trying to display a custom Layout
for my data list with ng-repeat. I cant use ng-class, as I would like to display diffent model vaules too.
I implemented a function in my controller, that calculates true or false according to my desired design. Then im trying to use ng-if to display my desired HTML with the data. The way I implemeted seems a bit awkward, especially if the layout gets more complicated, is there a better way to achieve this behaviour?
Here a sample: http://plnkr.co/edit/puE7OE?p=info
Controller:
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.eventModels=[{name:'event1', description: 'description1'}, {name:'event2', description: 'description2'},
{name:'event3', description: 'description3'}, {name:'event4', description: 'description4'},
{name:'event5', description: 'description5'}, {name:'even6', description: 'description6'}, {name:'even7', description: 'description7'}];
var counter = 0;
this.isLarge = false;
$scope.isLargeContainer = function() {
if(counter === 0) {
this.isLarge = true;
counter++;
} else {
this.isLarge = false;
if(counter === 2) {
counter = 0;
} else {
counter++;
}
}
};
});
View:
<body ng-controller="MainCtrl">
<section ng-repeat="(key, eventModel) in eventModels" >
<div ng-init="isLargeContainer(eventModel)"></div> <!--the only way I found to call a function within the repeat directive.-->
<div ng-if="isLarge">
<!--Display large content -->
<p class='large'>my large event: {{eventModel.name}}, {{eventModel.description}}</p>
</div>
<div ng-if="!isLarge">
<!--Display content small-->
<p class='small'>{{eventModel.name}}</p>
</div>
</section>
</body>
Thanks a lot in advance!
Im using Anguler 1.3.3
I just solved your problem. You can find the solution here:
http://jsfiddle.net/anasfirdousi/46saqLmw/
Here is the HTML
var myApp = angular.module('myApp',[]);
function myController($scope){
$scope.msg = 'Learning Angular JS ng-class Directive';
$scope.menu = [ 'Menu Item 1','Menu Item 2','Menu Item 3','Menu Item 4'];
}
.large {
font-size:16px;
}
body{
font-size:12px;
}
<div ng-app="myApp" ng-controller="myController">
{{ msg }}
<ul>
<li ng-repeat="(key, value) in menu" ng-class="{'large': $index==2 }"> {{ value }} </li>
</ul>
</div>
In your case, you actually have to use ng-class with expressions. The class is applied only if the condition holds true.
http://jsfiddle.net/anasfirdousi/cuo6cn3L/
Check the above link. This is another example if you want to do it on any specific condition. You can call a function which checks a condition and returns either a true or a false rather than using an inline expression.
var myApp = angular.module('myApp',[]);
function myController($scope){
$scope.msg = 'Learning Angular JS ng-class Directive';
$scope.menu = [ 'Menu Item 1','Menu Item 2','Menu Item 3','Menu Item 4'];
$scope.CheckSometing = function(v){
if(v=='Menu Item 3'){
return true;
}
else
{
return false;
}
}
}
.large {
font-size:16px;
}
body{
font-size:12px;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp" ng-controller="myController">
{{ msg }}
<ul>
<li ng-repeat="(key, value) in menu" ng-class="{'large': CheckSometing(value) }"> {{ value }} </li>
</ul>
</div>

Angular accordion doesn't update UI when data source has been changed

I start learning Angular and faced with some strange behaviour.
I want to add a new header to accordion dynamically but I accordion doesn't reflect it on UI till I explicitly click on some of his items. By some reason he doesn't react on items changes before it starts load itserlf aggain durnig DOM rendering.
var mainApp = angular.module('ui.bootstrap.demo', ['ui.bootstrap', 'ngResource']);
mainApp.factory('teamSharedObj',['$rootScope', function($rootScope) {
return {
teams: [],
peopleInTeam: [],
addNewTeam: function(item) {
console.log("add new team: " + item);
this.teams.push(item);
$rootScope.$broadcast('team.new');
},
addTeamMembers: function(team, teamMembers) {
for (var i = 0; i < teamMembers.length; i++) {
var temp;
// put in team as key-value pair
temp[team] = teamMembers[i]
console.log("add new team member: " + temp);
peopleInTeam.push(temp);
}
if (teamMembers.length != 0) {
$rootScope.$broadcast('teamMember.new');
}
}
}
}]);
mainApp.directive("addNewTeam", ['teamSharedObj', 'teamSharedObj', function (teamSharedObj) {
return {
restrict: 'A',
link: function( scope, element, attrs ) {
element.bind('click', function() {
console.log(scope.teamName)
teamSharedObj.addNewTeam(scope.teamName)
});
}
}
}])
mainApp.controller('teamListCtrl', ['$scope', 'teamSharedObj', function($scope, teamSharedObj) {
$scope.$on('team.new', function(event) {
console.log('new team ' + event);
$scope.items = teamSharedObj.teams;
}
);
$scope.oneAtATime = true;
$scope.items = ['new', 'another one'];//teamSharedObj.teams;
}]);
<!DOCTYPE html>
<html>
<head lang="en">
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.26/angular.js"></script>
<script src= "http://ajax.googleapis.com/ajax/libs/angularjs/1.2.26/angular-resource.js"></script>
<script src="//angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.11.2.js"></script>
<script src="js/test.team.js"></script>
<link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body ng-app="ui.bootstrap.demo">
<div id="teamBlock">
<input type="text" ng-model="teamName" >
<input add-new-team type="submit" value="Add new team" >
<!--<button add-book-button>Add data</button>-->
</div>
<div>
{{teamName}}
</div>
<div ng-controller="teamListCtrl">
<accordion close-others="oneAtATime" >
<accordion-group heading="{{d}}" ng-repeat="d in items">
This content is straight in the template.
</accordion-group>
</accordion>
<div ng-repeat="item in items">{{item}}</div>
</div>
</body>
</html>
Can you suggest me please a right way to notifu component about changes in its datasource?
bind is jqLite/jQuery method and does not automatically trigger the digest loop for you. This means no dirty checking will take place and the UI will not be updated to reflect the model changes.
To trigger it manually wrap the code in a call to $apply:
element.bind('click', function() {
scope.$apply(function () {
teamSharedObj.addNewTeam(scope.teamName);
});
});
And since teamSharedObj contains a reference to the array the controller can reference it directly. Then you do not need to use $broadcast:
addNewTeam: function(item) {
this.teams.push(item);
},
And:
mainApp.controller('teamListCtrl', ['$scope', 'teamSharedObj',
function($scope, teamSharedObj) {
$scope.oneAtATime = true;
$scope.items = teamSharedObj.teams;
}
]);
Demo: http://plnkr.co/edit/ZzZN7wlT10MD0rneYUBM?p=preview

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)

Resources