Scope inside ng-repeat isn't being defined - angularjs

In the following code I'm trying to create a form at the end of each ng-repeat and assign a value to the scope.
For some reason the value I'm assigning (with ng-model) isn't being passed.
If you prefer fiddle:
https://jsfiddle.net/U3pVM/27716/
otherwise here is the code:
app.js:
var app = angular.module('qmaker', []);
app.controller('MainCtrl', [
'$scope',
function($scope){
$scope.qstnrs = [
//object #1
{
title: 'questionnaire 1',
author: 'dave',
questions:
[
{qid: 1, qtype: 'multi'},
{qid: 2, qtype: 'cross'}
]
},
//object #2
{
title: 'questionnaire 2',
author: 'raul',
questions:
[
{qid: 1, qtype: 'lol'},
{qid: 2, qtype: 'foreal'}
]
}
];
$scope.newQuestion = function(index) {
console.log($scope.type);
var question_id = $scope.qstnrs[index].questions.length +1;
$scope.qstnrs[index].questions.push({
qid: question_id,
qtype: $scope.type
}
);
};
$scope.newQstnr = function () {
$scope.qstnrs.push({
title: $scope.title,
author: 'admin',
questions: []
});
$scope.title = '';
};
}]);
When I try to log $scope.type to console I receive undefined.
Here is the HTML:
<html>
<head>
<title>QMaker app</title>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.10/angular.min.js"></script>
<script src="app.js"></script>
</head>
<body ng-app="qmaker" ng-controller="MainCtrl">
<!-- This form works fine, the next is problematic -->
<form ng-submit="newQstnr()">
<input required type="text" ng-model="title">
<button type="submit">New Questionnaire</button>
</form>
<div ng-repeat="qstnr in qstnrs">
{{qstnr.title}} by {{qstnr.author}}<br>
<ul ng-repeat="question in qstnr.questions">
<li>#{{question.qid}}: {{question.qtype}}</li>
</ul>
<!-- Form we're speaking about -->
<form ng-submit="newQuestion($index)">
<input required type="text" ng-model="type">
<button type="submit">[+] Question</button>
</form>
</div>
</body>
</html>
When we attempt to add a new question to a questionnaire, the type doesn't appear, or appears undefined.
Why does this happen and how can I make it work?

Change your form to this:
<form ng-submit="newQuestion($index, type)">
<input required type="text" ng-model="type">
<button type="submit">[+] Question</button>
</form>
And your function to this:
$scope.newQuestion = function(index, type) {
var question_id = $scope.qstnrs[index].questions.length +1;
$scope.qstnrs[index].questions.push({
qid: question_id,
qtype: type
}
);
};
And it works... My hunch is that it creates a new scope in the ng-repeat for the ng-model so that all the inputs that are repeated don't share the same values. Otherwise, when you type in one textbox, all the repeated textboxes would show the same value.
Actually, I proved this is the case by changing the form to this:
<form ng-submit="newQuestion($index)">
<input required type="text" ng-model="$parent.type">
<button type="submit">[+] Question</button>
</form>
Adding the $parent attaches it to the parent scope. Do that and you'll see that your logic works but with the unexpected result I was talking about.

Related

Check if state of dropdown has changed

I am using ng-options to display drop-down with options. Suppose I have three options in it for example option1, option2, option3.By default option1 is selected, now if a user selects option2, then $pristine becomes False and again if he selects option1 then from angularjs's prospective $pristine should be false but according to user he has not changed the option.So I was looking for a way to detect this change
Here is the Js fiddle demo
HTML
<div ng-app="myApp">
<div ng-controller="ctrl">
<form name="myForm">
{{myForm.$pristine}}
<select ng-model='list' ng-options='item.name for item in data'>
</select>
</form>
</div>
</div>
JS code
var app = angular.module('myApp', []);
app.controller('ctrl', function($scope) {
$scope.data = [{
id: 1,
name: 'test'
}, {
id: 2,
name: 'test1'
}];
$scope.list = $scope.data[0];
$scope.$watch('list', function(o, n) {
if ($scope.list == $scope.data[0])
$scope.myForm.$pristine = true;
})
});
You have add watch on your list model then this can be achieved
That is exactly what ng-change is for.
Usage would be like this (added $index to show you another option):
HTML
<div ng-app="myApp">
<div ng-controller="listController">
<form name="myForm">
<select ng-model="currOption"
ng-options='item.name for item in data'
ng-change="optionChanged(currOption, $index)">
</select>
</form>
</div>
</div>
Controller
angular.module('myApp')
.controller('listController', function($scope) {
$scope.data = [{
id: 1,
name: 'option1'
}, {
id: 2,
name: 'option2'
}, {
id: 3,
name: 'option3'
}];
$scope.optionChanged(option, index) {
// option = the selection the user just made.
// index = the index of the selection in your data. I.E. $scope.data[index]
};
});

Angular: How to $broadcast from Factory?

I have a list of items and I need to get a message (saying Item added!) in the navbar whenever a new item is added.
The function addItem() (ng-click on the Add Item button) is in the ItemFactory and from there I seem to not be able to broadcast it.
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js"></script>
</head>
<body ng-app="MyApp" ng-controller="MainCtrl">
<div>{{ text }}
<nav class="navbar navbar-inverse" ng-controller="NavCtrl">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">List of items | {{ alertItemAdded }}</a>
</div>
<form class="navbar-form navbar-right" role="search">
<div class="form-group">
<input type="text" class="form-control" ng-model="newItem" placeholder="Add an item">
</div>
<button type="submit" class="btn btn-primary" ng-click="addItem(newItem)">Add Item</button>
</form>
</div>
</nav>
<div class="container" ng-controller="ContentCtrl">
<div class="row">
<div class="col-xs-12">
<form class="form-inline">
<div class="form-group">
<input type="text" class="form-control" ng-model="newItem" placeholder="Add an item">
</div>
<button type="submit" class="btn btn-primary" ng-click="addItem(newItem)">Add Item</button>
</form>
<br />
<br />
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div ng-repeat="item in items">
<form class="form-inline">
<div class="form-group">
<div>{{ item }}</div>
</div>
<button type="button" class="btn btn-default btn-s" ng-click="removeItem($index)">Remove Item</button>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
angular.module('MyApp',[]);
angular.module('MyApp').controller('MainCtrl', function($scope, ItemFactory){
$scope.text = "Text from the Main Controller";
$scope.addItem = function(newItem){
ItemFactory.addItem(newItem);
}
});
angular.module('MyApp').controller('NavCtrl', function($scope){
// $on
$scope.$on('itemAdded', function(event, data){
$scope.alertItemAdded = data;
});
});
angular.module('MyApp').controller('ContentCtrl', function($scope, ItemFactory){
$scope.items = ItemFactory.getItem();
$scope.removeItem = function($index){
ItemFactory.removeItem($index);
}
});
angular.module('MyApp').factory('ItemFactory', function(){
var items = [
'Item 1',
'Item 2',
'Item 3'
];
return {
getItem : function() {
return items;
},
addItem : function(item){
items.push(item);
// $broadcast
$scope.$broadcast('itemAdded', 'Item added!');
},
removeItem : function($index){
items.splice($index, 1);
}
};
});
You can inject $rootScope into your factory and use $broadcast from there.
angular.module('MyApp').factory('ItemFactory', ["$rootScope", function($rootScope){
var items = [
'Item 1',
'Item 2',
'Item 3'
];
return {
getItem : function() {
return items;
},
addItem : function(item){
items.push(item);
// $broadcast
$rootScope.$broadcast('itemAdded', 'Item added!');
},
removeItem : function($index){
items.splice($index, 1);
}
};
}]);
Here is a clean solution for you.
See it working in this plunker
Let me explain how all of this works.
Your message looks like this :
<span ng-if="alertItemAdded.recentAdd">Item added !</span>
It will show only when "alterITemAdded.recenAdd" is true. You'll use this to make the message disapear if you need.
You factory look like this now :
angular.module('MyApp').service('ItemService', function(){
var service = {};
//I'll always wrap my data in a sub object.
service.notification = {};
service.notification.recentAdd=false;
service.items = {};
service.items.list = [
'Item 1',
'Item 2',
'Item 3'
];
service.items.addItem = function(item){
service.items.list.push(item);
service.notification.recentAdd=true;
console.log(service);
}
service.items.removeItem = function($index){
service.items.list.splice($index, 1);
}
return service;
});
I'm using service instead of factory. But there is almost no difference, it's just a matter of taste.
Here is your controllers
angular.module('MyApp').controller('MainCtrl', function($scope, ItemService){
$scope.text = "Text from the Main Controller";
});
angular.module('MyApp').controller('NavCtrl', function($scope, ItemService){
//IMPORTANT POINT : I bind it the sub object. Not to the value. To access the value i'll use $scope.alterItemAdded.recentAdd
$scope.alertItemAdded = ItemService.notification;
//I don't have to redeclare the function. I just bind it to the service function.
$scope.addItem = ItemService.items.addItem;
});
angular.module('MyApp').controller('ContentCtrl', function($scope, ItemService){
$scope.items = ItemService.items.list;
$scope.addItem = ItemService.items.addItem;
$scope.removeItem = function($index){
ItemService.items.removeItem($index);
}
});
Important point :
I always bind my vars to a sub object. Why ? In fact if i did
$scope.alertItemAdded = ItemService.notifications.recentAdd
When i do something like this in my service
service.notifications.recentAdd = true;
It will create a new variable and put the reference into service.notifications.recentAdd. The $scope.alertItemAdded was bind to the previous reference and wont see the update.
Doing this :
$scope.alterItemAdded = ItemService.notification
And using the value in the ng-if clause or anything else. I prevent the reference link to break. If i do in the service
service.notification.recentAdd = true
I'll create a new var with a new reference for "recentAdd" but i keep the same reference for "notification". The binding in the controller will be keep and the value recentAdd will be updated in the view.
If you have more question feel free to ask.
You not injected $scope to factory, and you cant actually, use $rootScope instead
$broadcast goes from top to bottom so you should use $rootScope to perform a $broadcast to all $scope elements below it.
Inject $rootScope in your factory
$rootScope.$broadcast('itemAdded, 'Item added!')

Enable button when i have selected two values

I have two select i want to enable my button when i have selected values in my two select, i don't know how i can do this ?
JSFIDDLE
My HTML:
<div ng-app="myapp">
<fieldset ng-controller="FirstCtrl">
<select
ng-options="people.first for people in people_1"
ng-model="selectedPerson1"></select>
<select
ng-options="people.first for people in people_2"
ng-model="selectedPerson2"></select>
</fieldset>
<br/><button class="btn btn-primary" ng-click="platform=true">Comparer</button>
</div>
You can do this using the ng-disabled directive. I have amended your jsfiddle here: http://jsfiddle.net/p0t4cw84/3/. I moved the button into the scope of the controller FirstCtrl and added a method on the scope that will return true or false if values are selected.
$scope.enableCompare = function () {
return !($scope.selectedPerson1 && $scope.selectedPerson2);
};
I hope this helps.
Here you go.There are lot of issue in you fiddle.I fixed it.
http://jsfiddle.net/purpz7u3/
<div ng-app="myapp">
<div ng-controller="FirstCtrl">
<fieldset >
<select
ng-options="people.first for people in people_1"
ng-model="selectedPerson1" ng-change="check()"></select>
<select
ng-options="people.first for people in people_2"
ng-model="selectedPerson2" ng-change="check()"></select>
</fieldset>
<br/><button class="btn btn-primary" ng-disabled="checked">Comparer</button>
</div>
</div>
var myapp = angular.module('myapp', []);
myapp.controller('FirstCtrl', function ($scope) {
$scope.checked = true;
$scope.people_1 = [
{ id: 1, first: 'John' },
{ id: 2, first: 'Rocky' }
];
$scope.people_2 = [
{ id: 1, first: 'Rambo' },
{ id: 2, first: 'Balboa'}
];
$scope.check = function(){
if($scope.selectedPerson1 && $scope.selectedPerson2){
$scope.checked=false;
}
}
});

How can I use Angular to output dynamic form fields?

I want to render a form, based on a dynamic field configuration:
$scope.fields = [
{ title: 'Label 1', type: 'text', value: 'value1'},
{ title: 'Label 2', type: 'textarea', value: 'value2'}
];
This should output something that behaves like:
<div>
<label>{{field.title}}<br />
<input type="text" ng-model="field.value"/>
</label>
</div>
<div>
<label>{{field.title}}<br />
<textarea ng-model="field.value" rows="5" cols="50"></textarea>
</label>
</div>
The simple implementation would be to use if statements to render the templates for each field type. However, as Angular doesn't support if statements, I'm lead to the direction of directives. My problem is understanding how the data binding works. The documentation for directives is a bit dense and theoretical.
I've mocked up a bare bones example of what I try to do here: http://jsfiddle.net/gunnarlium/aj8G3/4/
The problem is that the form fields aren't bound to the model, so the $scope.fields in submit() isn't updated. I suspect the content of my directive function is quite wrong ... :)
Going forward, I need to also support other field types, like radio buttons, check boxes, selects, etc.
The first problem you are running into regardless of the directive you are trying to create is using ng-repeat within a form with form elements. It can be tricky do to how ng-repeat creates a new scope.
This directive creates new scope.
I recommend also instead of using element.html that you use ngSwitch instead in a partial template.
<div class="form-row" data-ng-switch on="field.type">
<div data-ng-switch-when="text">
{{ field.title }}: <input type="text" data-ng-model="field.value" />
</div>
<div data-ng-switch-when="textarea">
{{ field.title }}: <textarea data-ng-model="field.value"></textarea>
</div>
</div>
This still leaves you with the problem of modifying form elements in child scope due to ng-repeat and for that I suggest using the ngChange method on each element to set the value when an item has changed. This is one of the few items that I don't think AngularJS handles very well at this time.
You might consider Metawidget for this. It uses JSON schema, but is otherwise very close to your use case. Complete sample:
<html ng-app="myApp">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js" type="text/javascript"></script>
<script src="http://metawidget.org/js/3.5/metawidget-core.min.js" type="text/javascript"></script>
<script src="http://metawidget.org/js/3.5/metawidget-angular.min.js" type="text/javascript"></script>
<script type="text/javascript">
angular.module( 'myApp', [ 'metawidget' ] )
.controller( 'myController', function( $scope ) {
$scope.metawidgetConfig = {
inspector: function() {
return {
properties: {
label1: {
type: 'string'
},
label2: {
type: 'string',
large: true
}
}
}
}
}
$scope.saveTo = {
label1: 'value1',
label2: 'value2'
}
$scope.save = function() {
console.log( $scope.saveTo );
}
} );
</script>
</head>
<body ng-controller="myController">
<metawidget ng-model="saveTo" config="metawidgetConfig">
</metawidget>
<button ng-click="save()">Save</button>
</body>
</html>
The type attribute can be changed when the element is out of DOM, so why not a small directive which removes it from DOM, changes it type and then add back to the same place?
The $watch is optional, as the objective can be change it dynamically once and not keep changing it.
var app = angular.module('app', []);
app.controller('MainCtrl', function($scope) {
$scope.rangeType = 'range';
$scope.newType = 'date'
});
app.directive('dynamicInput', function(){
return {
restrict: "A",
link: linkFunction
};
function linkFunction($scope, $element, $attrs){
if($attrs.watch){
$scope.$watch(function(){ return $attrs.dynamicInput; }, function(newValue){
changeType(newValue);
})
}
else
changeType($attrs.dynamicInput);
function changeType(type){
var prev = $element[0].previousSibling;
var parent = $element.parent();
$element.remove().attr('type', type);
if(prev)
angular.element(prev).after($element);
else
parent.append($element);
}
}
});
span {
font-size: .7em;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="MainCtrl">
<h2>Watching Type Change</h2>
Enter Type: <input ng-model="newType" /><br/>
Using Type (with siblings): <span>Before</span><input dynamic-input="{{newType}}" watch="true" /><span>After</span><Br>
Using Type (without siblings): <div><input dynamic-input="{{newType}}" watch="true" /></div>
<br/><br/><br/>
<h2>Without Watch</h3>
Checkbox: <input dynamic-input="checkbox" /><br />
Password: <input dynamic-input="{{ 'password' }}" value="password"/><br />
Radio: <input dynamic-input="radio" /><br/>
Range: <input dynamic-input="{{ rangeType }}" />
</div>
Tested in latest Chrome and IE11.

Setting the default value for a radio inside of ng-repeat (angular-js)

How can I set a default value for the radios if this value is an object and not a string?
EDIT:
To make things more clear I updated 's fiddle:
Check out the fiddle:
http://jsfiddle.net/JohannesJo/CrH8a/
<body ng-app="app">
<div ng-controller='controller'>
oConfigTerminal value= <input type="text" ng-model="oConfigTerminal"/><br><br>
<div ng-show="!oConnection.aOptions"
ng-repeat="oConnection in oFormOptions.aStationaryConnections">
<label>
<input type="radio" name="connection" ng-model="$parent.oConfigTerminal" value="{{oConnection.id}}"
/>{{oConnection.sId}}</label>
</div>
</div>
app = angular.module('app', []);
app.controller('controller', function ($scope) {
$scope.oFormOptions = {};
$scope.oConfigTerminal=0;
$scope.oFormOptions.aStationaryConnections = [{
id: 1,
sId: "analog"
}, {
id: 2,
sId: "isdn"
}, {
id: 3,
sId: "dsl"
}];
// !!! Trying to set the default checked/selected value !!!
$scope.oConfigTerminal = $scope.oFormOptions.aStationaryConnections[0];
});
Inspect the live html in browser console and you will see that oConnection is an object and value of radio becomes: {"id":1,"sId":"analog"}. You probably want to use oConnection.id for value
One other issue within ng-repeat there is a scope inheritance issue that needs to be resolved for ng-model by setting ng-model to $parent.variableName
Your oConnectionTmpView didn't make any sense to me so for simplification I removed it:
HTML:
<div ng-show="!oConnection.aOptions" ng-repeat="oConnection in oFormOptions.aStationaryConnections">
<label>
<input type="radio"
name="connection"
ng-model="$parent.oConfigTerminal"
value="{{oConnection.id}}"
/>
{{oConnection.sId}}
</label>
</div>
JS:
app = angular.module('app', []);
app.controller('controller', function ($scope) {
$scope.oFormOptions = {};
$scope.oConfigTerminal = 0;
$scope.oFormOptions.aStationaryConnections = [{
id: 1,
sId: "analog"
}, {
id: 2,
sId: "isdn"
}, {
id: 3,
sId: "dsl"
}];
}
Demo: http://jsfiddle.net/CrH8a/14/

Resources