Keeping multiple controllers in scope - javascript

Is it good practice to have multiple controllers in scope so that you can go back and forth between binding to each throughout a document?
For instance, if I want to interleave the values of two sets of price/quantity/total, is it bad design to do the following: On Plnkr
<html ng-app="invoiceTest">
<head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.12/angular.min.js"></script>
<script type="text/javascript" src="invoiceTest.js"></script>
</head>
<body>
<span ng-controller="InvoiceController as invoice1">
<span ng-controller="InvoiceController as invoice2">
<div>Cost 1: <input type="number" ng-model="invoice1.cost" required ></div>
<div>Cost 2: <input type="number" ng-model="invoice2.cost" required ></div>
<div>Quantity 1: <input type="number" ng-model="invoice1.qty" required ></div>
<div> Quantity 2: <input type="number" ng-model="invoice2.qty" required ></div>
<div><b>Total 1: </b>{{invoice1.total('USD') | currency}}</div>
<div><b>Total 2: </b>{{invoice2.total('USD') | currency}}</div>
</span>
</span>
</body>
</html>
Alternatively, is there a way to carry multiple controls in scope without nesting them, or point the scope of an element to a previously declared controller instance?
Thanks for any advice. I'm just getting started with angular. I'm used to (MVVM) frameworks where the element you're binding to is instantiated in the ViewModel, not in the View itself.

I'm going to say that your use case is a little contrived. Your example really only needs one controller that had an array of objects, each one having a quantity and price.
I would say that I can't think of a situation where two controllers would be a good idea; controllers are supposed to be pretty independent, and if you have two controllers over the same code, you open yourself up to collisions and trouble. That's not to say that you shouldn't nest logic -- putting logic in directives can be a great idea, and that's kind of a controller inside a controller -- but using two controllers together usually implies that you want to share data between them, which can get clumsy fast.
Getting user to angular takes time, but in your example, I would try refactoring your controller to have an array of objects, then using directives like ng-repeat to generate your html for you, rather than writing it yourself. Angular is very powerful at turning your model into a view, and once you use it enough I think you'll see that using multiple controllers isn't the worst idea, but there's almost always a better solution that's both easier to write and easier to maintain.
EDIT:
As an example, here's your original example under one controller, and a full JSFiddle showing it working.
app.controller('OneController', function($scope){
$scope.invoices = [
{ cost: 100,
qty: 5,
total: function(){return this.cost*this.qty}
},//etc.
];
});
app.directive('compareInvoices', function(){
return {
restrict: 'E',
replace: true,
scope: {
invoices: '='
},
template:'<div>'+
'<div ng-repeat="invoice in invoices">Cost {{$index+1}}: <input type="number" ng-model="invoice.cost" required ></div>'+
'<div ng-repeat="invoice in invoices">Quantity {{$index+1}}: <input type="number" ng-model="invoice.qty" required ></div>'+
'<div ng-repeat="invoice in invoices"><b>Total {{$index+1}}: </b>{{invoice.total()}}</div>'+
'</div>'
}
});
It creates the same HTML you had originally, but could also handle any number of invoices instead of just 2, and if you wanted to compare the two invoices, you could easily do that inside your controller because it had immediate access to both.

Related

AngularJS - Accessing to an input array with ng-model dynamic name

I have 3 rows with 13 input fields in each row.
All the follow the rule of have a ng-model like:
First row: field[1][{1-13}]
Second row: field[2][{1-13}]
Third row: field[3][{1-13}]
So if I want to access to the first row and to the field #6 I do it like $scope.field[1][6]. The problem is that I cannot access like this to the field.
This is the HTML of the input:
<input ng-model='field[1][6]' type='text' />
I've tried to access by using: $scope.field[1][6] but it says that "field" is undefined.
Here is how I am trying to access it from my AngularJS controller:
angular.module("myModule")
.controller("myModuleController", ["$scope","$http", function($scope,$http) {
console.log($scope.field[1][2]);
}]);
This fields are created when the DOM loads and they cannot be generated with ng-repeat because some deeper factors.
I am new at AngularJS, thanks for the patience.
There should be no problem accessing that way. Maybe you have some error elsewhere.
angular.module('test', [])
.controller('Test', Test);
function Test($scope) {
$scope.fields = [
[
{name: '1-1'},
{name: '1-2'},
{name: '1-3'}
],
[
{name: '2-1'},
{name: '2-2'}
],
[
{name: '3-1'}
]
]
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.min.js"></script>
<div ng-app='test' ng-controller='Test'>
<div ng-repeat='(i, lv1) in fields'>
<div ng-repeat='(j, lv2) in lv1'>
<input type='text' ng-model='lv2.name'>
<input type='text' ng-model='fields[i][j].name'>
</div>
</div>
<div>
<input type='text' ng-model='fields[1][1].name'>
</div>
</div>
Well, I found what the issue is in case someone needs to know.
Basically, in order for the array of inputs to be inside the $scope it needs to be looped with ng-repeat. So, even though you give the ng-model directive to the input it won't know about it because it wasn't processed by AngularJS.
Conclusion: You cannot just read the array of inputs this way. I worked it around by giving a dynamic ID, like field-1-6 and then using:
angular.element("#myApp").find("#field-" + firstIndex + "-" + secondIndex );
In my point of view is much better to just redo your code and make it be generated by angular. In my case it can't be done because the client doesn't want that piece of code upgraded. So, its a good workaround if you are in my situation.
;)
The code you have updated your question with unfortunately does not show
how you are initializing $scope.field
how you are using it in ng-repeat
<input ng-model='field[1][6]' type='text' /> can mean different things depending on the first two points.
Without context angular will read it like this http://jsfiddle.net/3tsw98yf/
Is that what you were looking for?

Disabling submit button based on fields added with ng-bind-html

JSFiddle here: http://jsfiddle.net/c6tzj6Lf/4/
I am dynamically creating forms and buttons and want to disable the buttons if the required form inputs are not completed.
HTML:
<div ng-app="choicesApp">
<ng-form name="choicesForm" ng-controller="ChoicesCtrl">
<div ng-bind-html="trustCustom()"></div>
<button ng-repeat="button in buttons" ng-disabled="choicesForm.$invalid">
{{button.text}}
</button>
</ng-form>
</div>
JavaScript:
angular.module('choicesApp', ['ngSanitize'])
.controller('ChoicesCtrl', ['$scope', '$sce', function($scope, $sce) {
$scope.custom = "Required Input: <input required type='text'>";
$scope.trustCustom = function() {
return $sce.trustAsHtml($scope.custom);
};
$scope.buttons = [
{text:'Submit 1'},
{text:'Submit 2'}];
}]);
choicesForm.$invalid is false and does not change when entering text into the input field.
Solution:
I ended up using the angular-bind-html-compile directive from here: https://github.com/incuna/angular-bind-html-compile
Here is the relevant bit of working code:
<ng-form name="choicesForm">
<div ng-if="choices" bind-html-compile="choices"></div>
<button ng-click="submitForm()" ng-disabled="choicesForm.$invalid">
Submit
</button>
</ng-form>
And choices might be a snippit of HTML like this:
<div><strong>What is your sex?</strong></div>
<div>
<input type="radio" name="gender" ng-model="gender" value="female" required>
<label for="female"> Female</label><br>
<input type="radio" name="gender" ng-model="gender" value="male" required>
<label for="male"> Male</label>
</div>
The main problem is that ngBindHtml doesn't compile the html - it inserts the html as it is. You can even inspect the dynamic input and see that it doesn't have the ngModel's CSS classes (ng-pristine, ng-untouched, etc) which is a major red flag.
In your case, the form simply doesn't know that you've added another input or anything has changed for that matter. Its state ($pristine, $valid, etc) isn't determined by its HTML but by the registered NgModelControllers. These controllers are added automatically when an ngModel is linked.
For example this <input required type='text'> won't affect the form's validity, even if it's required, since it doesn't have ngModel assigned to it.
But this <div ng-model="myDiv" required></div> will affect it since it's required and has ngModel assigned to it.
The ngDisabled directive on your buttons works as expected since it depends on the form's $invalid property.
See this fiddle which showcases how ngModel registers its controller. Note that the html containing the dynamic input gets compiled after 750ms just to show how NgModelControllers can be added after FormController has been instantiated.
There are a few solutions in your case:
use a custom directive to bind and compile html - like this one
use ngInclude which does compile the html
use $compile to compile the newly added HTML but this is a bit tricky as you won't know exactly when to perform this action
This is an answer yet imcomplete because i cannot do the code at the moment.
I think your html will be included, not compiled. So the inputs are not bind to angular and are not part of the angular form object.
The only way i see is to use a directive that will compile the passed html and add it to your form. This may be quite tricky though, if you want to go on this way i suggest to edit your question to ask for the said directive.
However i'm not really familiar with $compile so i don't know if it'll work to just add $compile around $sce.trustAsHtml()
You can write a method as ng-disabled does not work with booleans, it works with 'checked' string instead:
So on your controller place a method :
$scope.buttonDisabled = function(invalid){
return invalid ? "checked" : "";
};
And on your view use it on angular expression :
<button ng-repeat="button in buttons" ng-disabled="buttonDisabled(choicesForm.$invalid)">
Here is a working fiddle
Working DEMO
This is the solution you are looking for. You need a custom directive. In my example I have used a directive named compile-template and incorporated it in div element.
<div ng-bind-html="trustCustom()" compile-template></div>
Directive Code:
.directive('compileTemplate', function($compile, $parse){
return {
link: function(scope, element, attr){
var parsed = $parse(attr.ngBindHtml);
function getStringValue() { return (parsed(scope) || '').toString(); }
//Recompile if the template changes
scope.$watch(getStringValue, function() {
$compile(element, null, -9999)(scope); //The -9999 makes it skip directives so that we do not recompile ourselves
});
}
}
});
I found the directive in this fiddle.
I believe what is really happening though due to jsfiddle I'm unable to dissect the actual scopes being created here.
<div ng-app="choicesApp">
<ng-form name="choicesForm" ng-controller="ChoicesCtrl">
<div ng-bind-html="trustCustom()"></div>
<button ng-repeat="button in buttons" ng-disabled="choicesForm.$invalid">
{{button.text}}
</button>
</ng-form>
</div>
The first div is your top level scope, your form is the first child scope. Adding the div using a function creates the dynamically added input field as a child of the first child, a grandchild of the top level scope. Therefore your form is not aware of the elements you're adding dynamically causing only the static field to be required for valid form entry.
A better solution would be to use ng-inclue for additional form fields or if your form isn't to large then simply put them on the page or template you're using.

Setting ng-model value dynamically to the value of js variable in AngularJS

Similar questions have bee asked before and I have tried solutions given to them, but my problem is little different.
Lets say I have an array of objects like this in my controller
$scope.objArray = [
{
id:1,
name:"placeholder1"
},
{
id:2,
name:"placeholder2"
}
];
now based on this array I am generating a form dynamically like this
<div ng-repeat="field in fields">
{{field.field_name}} </br>
<input type="text" ng-model="field.field_name"><br>
</div>
here in this form i want ng-model value to be set as placeholder1 and placeholder2 and not like javascript variable like field.field_name because my targets are coming from another source which contains html like below.
<p>this is sample html {{placeholder1}}. again sample html {{placeholder2}} </p>
i don't have those JS variables in here. I could not find any solution which will work this way for me so far.
here is link to plunk:
http://plnkr.co/edit/ttaq0l3PDRu4piwSluUX?p=preview
In your case it will look like this:
<div ng-repeat="field in fields">
{{field.field_name}}
<br>
<input type="text" ng-model="$parent[field.field_name]">
<br>
</div>
Note, that you have to use $parent reference because in your case you want to set property of the outer scope (where objArray is defined), however ngRepeat creates child scopes per iteration, so you need one step up.
Demo: http://plnkr.co/edit/35hEihmxTdUs6IofHJHn?p=info

AngularJS Model Location

I'm writing a simple AngularJS example that doesn't have a controller. It just uses the default global controller/model.
<!DOCTYPE html>
<html lang="en" ng-app=>
<body>
<input type="text" ng-model="firstName" placeholder="first name">
<input type="text" ng-model="lastName" placeholder="last name">
<br />
<h2 ng-style=style>Welcome {{firstName + ' ' + lastName}}</h2>
<button ng-disabled="!(firstName.length && lastName.length)">Sign Up</button>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.js"></script>
</body>
</html>
Where are the firstName and lastName values stored? This form works as expected so the two-way binding is working. I've tried looking through the window object and I didn't see anything in there that might be the global Angular controller. If I wanted to submit these values, where would the "handle" to them be in this example?
UPDATE
As I said, this is working as evidenced by this jsFiddle. The name is computed correctly and the button is only enabled once both firstName and lastName have values. I also tried using Batarang and it did not help. It told me that there wasn't a scope defined yet the application still functions.
Try installing Batarang and inspecting for yourself, though I believe those two values would be applied to $rootScope
You can view your scope using ng-inspector. It may not work in js-fiddle, because they do things with iframes that hide the global variables the inspector depends on.
I tried it on a test application with an unnamed module, as in your example, and it discovers the scope as $rootScope 002
Because of this I theorized, and have found through experiment, that the scope of an application without a declared module name (and probably with one, too) is attached to whatever DOM node the ng-app directive is on.
So, if you put ng-app on the html tag, this would get you your scope:
angular.element(
document.getElementsByTagName('html')[0]
).scope();
Using JSFiddle will complicate this testing, since it does iFrame stuff and has multiple html and body tags. Try it in an isolated environment and it's pretty straight-forward.
In console, select the result frame, and type:
angular.element($('body')).scope().firstName
You should see the firstName value. With regards to "If I wanted to submit these values" part, I believe you want to do an ajax call with these values. The best place to do that is by attaching a controller, where you will have direct access to these variables, and then you can use $http service to post to a server.

Angular directive inserts wrong DOM node

I have an app with many forms. Each field has several HTML elements, so I thought I could extract some directives (one per type of field) to keep my forms tidy.
I've created a sample app to demonstrate the problem, but I'm getting inconsistent behavior. In the sample app, a <link /> element replaces the <input />. In my real app, <input /> just gets removed from the DOM completely. I feel like this should be easy; why doesn't it work?
To answer your stated question, it's because you told it to, with ng-transclude. That replaces the contents of the tag with the original element, which I don't think you wanted; you probably wanted the original contents to be transcluded as the label instead.
This is probably what you're looking for:
<div class="form-group" >
<label for="{{htmlId}}" ng-transclude></label>
<input id="{{htmlId}}" class="form-control" type="text" ng-model="model" />
<span ng-repeat="error in errors">{{error}}</span>
</div>
I've moved the tranclusion into the label. While this works, I would also recommend the style of actually passing a label attribute, rather than transclude it, just for the sake of having a consistent API and simpler code; it's functionally equivalent, though, so don't let me bully you.
Also, you've got a few errors in your .js as well. First, you want to use = in your scope instead of &
scope: {
model: '=',
errors: '='
},
& is used to pass methods, while = is used for objects (this is a simplification). Since your model and errors are objects, you'll want to use = instead.
Finally, in your example, your html template and your directive's template don't have the same name... you've got an extra 's' in your .js, but that's probably just in the plunker and not your real app.

Categories

Resources