While it is not really a performance issue with my very small and very quick application, I am interested in becoming proficient in dealing with large applications BEFORE I get to working on large applications. This is causing me to identify ways to optimize AngularJS in as many ways as possible, and to integrate these optimizations as part of my "normal" coding routine. So! I've been scratching my head for several hours now, scouring articles and the documentation, trying to get ahold of the best method for me to achieve the following:
I have a drop-down select menu with options that I would like to be one-way data bound, from view to model, as there is no need to have the model update the view in this case.
Some questions about the above:
Will this save me time in the $digest cycle? Or does it not matter if I just use the two-way data binding of ng-model?
My other question is a little more involved.
EDIT: This issue has been fixed (as seen below), but I am now encountering a new issue. I have posted this new issue to the bottom of this question for better visibility.
Here is my template:
DropdownTemplate.html
<div class="form-group">
<label for="{{::regarding}}" class="col-sm-12 control-label">{{::regarding}}</label>
<div class="col-sm-12">
<select class="form-control" id="{{::regarding}}" data-ng-model="data" data-ng-change="update()">
<option selected>0</option>
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
<option>6</option>
<option>7</option>
<option>8</option>
<option>9</option>
<option>10</option>
<option>11</option>
<option>12</option>
<option>13</option>
<option>14</option>
<option>15</option>
<option>16</option>
<option>17</option>
<option>18</option>
<option>19</option>
<option>20</option>
</select>
</div>
</div>
Here is my directive:
.directive('trDropdown', ['Ticket', function(Ticket) {
return {
restrict: 'E',
templateUrl: 'DropdownTemplate.html',
scope: {
regarding: '#'
},
link: function($scope, $elem, $attrs){
$scope.data = 0;
$scope.update = function (){
Ticket[$scope.regarding] = $scope.data;
//console.log(Ticket[$scope.regarding]);
}
}
}
}]);
And here is what it looks like when I utilize this custom element in my main html page:
<tr-dropdown regarding="Adults"></tr-dropdown>
So far, my code runs the way it should, except for one small bug. The bug is that, if I don't include the bit about $scope.data = 0; in the link part of the directive, the attribute "selected" (or even selected="selected") doesn't register when my custom element is appended to the main html page. Despite the bug (which is not my primary concern), I don't feel like this is the most elegant way for me to handle the concept of one-way binding, if I even need it at all. I know that there is a plugin called bindonce that I am willing to use, although I have been searching first for a native solution.
Any discoveries found through the struggles of others is most welcome on this end!
**EDIT: One-time binding is now successfully working but I've encountered a strange anomaly. Batarang shows my expression {{::regarding}} as continuing to pull resources, each time one of the dropdowns is changed. As far as I understand, {{::regarding}} should be de-registered and not even a factor in the further performance of the page. What gives? **
Related
What is the advantage of using "Controller as" syntax in Angular? Is it just to create an alias for a controller or does it have some other technical reasons behind the scenes?
I am new to Angular and want to know more about this syntax.
controllerAs-syntax has multiple advantages:
Clartiy
Consider the following example:
<div ng-controller="containerController">
<h2>Improve your life!</h2>
<p ng-controller="paragraphController">
We talk about {{topic}} a lot, but do we really understand it?
Read this article to enhance your knowledge about {{topic}}
</p>
</div>
Just by reading this piece of code, you can not tell where topic comes from. Does it belong to the containerController, to the paragraphController or is it just a random floating scope variable from sone input above?
By using controllerAs it is very clear:
<div ng-controller="containerController as container">
<h2>Improve your life!</h2>
<p ng-controller="paragraphController as paragraph">
We talk about {{paragraph.topic}} a lot, but do we really understand it?
Read this article to enhance your knowledge about {{paragraph.topic}}
</p>
</div>
You can immediately see that topic is a property of paragraphController. This makes code a lot more readable overall, as it forces the developer to make clear who the functions and variables in the scope belong to.
Binding to properties
When you are using the old controller syntax, strange things can happen when you have multiple bindings in different scopes to the "same" variable. Consider this example:
<form ng-controller="myFormController">
<input type="text" ng-model="somefield">
<div ng-controller="someOtherController">
<input type="text" ng-model="somefield">
</div>
</form>
It looks like both inputs are bound to the same variable. They are not. Everything looks like it works fine when you edit the first input first, but as soon as you edit the second one, they won't sync up anymore. This has to do with the way scope-inheritance and binding work(and there is an excellent answer on this on SO). This does not happen when you bind to object properties (aka when there is a . in your ng-model-attribute). With controllerAs you bind to properties of the controller objects anyways, so it naturally solves that problem:
<form ng-controller="myFormController as myForm">
<input type="text" ng-model="myForm.somefield">
<div ng-controller="someOtherController as other">
<input type="text" ng-model="myForm.somefield">
</div>
</form>
It gets rid of scope(mostly)
The use of scope to create bindings to controllers in old angular code is hard to read, hard to understand and completely unnecessary if you use controllerAs. You no longer have to inject scope into every single controller, in fact you will probably not inject it in any in most applications (you still need to do so if you want to use the angular event system). This results in cleaner controller-code with less strange boilerplate.
It prepares for Angular 2
In angular 2, scope will be gone and we will write everything as components. Using controllerAs lets you work without scope and forces you to think more component-oriented, thus preparing you (and the applications you eventually will want to migrate) for the 2.0 update.
Learning angularjs at the moment, and I am confused as to how I can accomplish a task because I don't fully understand what ngModel is doing.
If I have a directive with two scope variables:
// An array of all my objects
$scope.allMyObjects
// The currently selected object from the array
$scope.selectedObject
and in the html
<span>{{ selectedObject.name }}</span>
<select id="select"
ng-model="selectedObject"
ng-options="object in allMyObjects">
</select>
This all works perfectly, when I select an object from the select, it updates the selectedObject on the scope and so the name of the currently selected object is displayed.
However, I don't want a select box, instead I want a list of all my objects with an editable name field, with a select button that I can use to select the specified object, so I came up with the following:
<div ng-repeat="object in allMyObjects">
<input class="object-name"
ng-model="object.name">
<a ng-click="loadObject(object)">Load</a>
</div>
and the loadObject() function on the scope:
function loadObject(object) {
$scope.selectedObject = object;
}
However, this doesn't work. I had assumed this was basically what ngModel was doing behind the scenes but am obviously confused. Is anyone able to shed some light or offer a better solution to what I wish to achieve?
Please see here :http://jsbin.com/jocane/1/edit?html,js,output
use ng-model="object.name" instead "sc.name"
<div ng-repeat="object in allMyObjects">
<input class="object-name"
ng-model="object.name">
<a ng-click="loadObject(object)">Load</a>
</div>
After an hour of debugging it came down to an issue with the scope being isolated by the ng-repeat, the problem didn't show up in any of the simplified jsfiddle examples because they used pure JS and it was the way I was accessing the scope via typescript that caused the issue.
Thanks for the answers that helped me narrow it down to my difficulty understanding typescript and not my difficulty understanding directives.
I'm working on a radio button list where a user can select from a pre-populated list of problems, or select an "other" radio button and then type in their specific problem.
I can get the pre-populated list of radio buttons to work and set the problem (outputting the scope variable confirms this), but introducing the "other" functionality is stumping me. When I select other, it doesn't seem to bind to the scope variable. I noticed in the dom it's missing an class="ng-scope" that the other radio buttons seem to get from the ng-repeat, but I'm not sure if that's the problem.
<form>
// This part loops through the list of problems and makess radio buttons
<div ng-repeat="problem in selectedType['nature_of_problem']">
<input type="radio" ng-model="$parent.natureOfProblem" ng-value="problem"/>
</div>
// Ideally this part is where the "other" radio is, it's still in the form
<input type="radio" ng-model="natureOfProblem" ng-value="other" ng-checked="">
</form>
Working JSFiddle:
http://jsfiddle.net/HB7LU/3794/
I saw a few issues, among them:
Using ng-value instead of plain old value for "other"
Using a primitive instead of dot notation (if you want your view to reliably write a variable, it needs to be something.yourVariable instead of just plain old yourVariable)
Hope this helps!
function MyCtrl($scope) {
$scope.uiState = {};
$scope.uiState.natureOfProblem = 1;
$scope.selectedType = {};
$scope.selectedType.nature_of_problem = [1,2,3];
}
<div ng-controller="MyCtrl">
<p>Nature of problem is: {{uiState.natureOfProblem}}</p>
<form>
<div ng-repeat="problem in selectedType['nature_of_problem']">
<input type="radio" ng-model="uiState.natureOfProblem" ng-value="problem"/><span ng-bind="problem"></span>
</div>
<input type="radio" ng-model="uiState.natureOfProblem" value="Other" /><span>Other</span>
</form>
</div>
EDIT to answer OP's questions:
I tend to use ng-bind out of habit -- in slower browsers like Firefox, it keeps "{{blah}}" from showing up on the screen as everything loads. Newer versions of Angular also have ng-cloak for this purpose, which I should probably get in the habit of using instead. :) (I also vaguely remember reading that "{{blah}}" can cause issues in IE, but I very possibly made that up.)
The use of dot notation relates to the fact that Angular can't maintain data bindings on brand-new objects. To try to explain it without using terms like "scope" and "inheritance": If you influence an existing object by changing yourObject.anAttribute, the overarching object consistently exists throughout that process and does not drop its binding. But if you have blahVariable that is equal to 8, and you set blahVariable equal to 7, you've basically tossed the old piece of data and created a new piece of data entirely. This new piece does not maintain the binding, so the controller never gets the memo from the view that the value has changed.
Sometimes I find this useful, actually -- you can briefly manipulate a variable in the view for some quick-and-dirty purpose without the controller finding out about it. :)
Maybe I'm crazy, or too used to KnockoutJS, but I keep looking for an ngWith directive in the docs to define the scope on an element, in a controller, or for an included (ngInclude) partial.
For example:
I'd like to write a controller that augments MyItem like:
MyModule.controller('MyItemCtrl', function($scope) {
$scope.doSomethingToItem = function() {
$scope.name = "bar";
};
});
Or a view/template for MyItem like:
<div ng-controller="MyItemCtrl">
{{name}}
<button ng-click="doSomethingWithItem()">Do Something</button>
</div>
But in both of these cases I'm imagining my $scope to be prototypically inherit from my model, MyItem.
But the scope doesn't inherit from the model!!
Which baffles me.
Instead, my model is a property on the scope.
In the case of a repeater:
<div ng-repeat="item in list">
<div ng-controller="MyItemCtrl">
{{item.name}}
<button ng-click="doSomethingWithItem()">Do Something</button>
</div>
</div>
which means everywhere I have to use item.this or item.that instead of just this and that. I have to remember which functions are native to the model, and which were applied directly to the scope by a controller.
If I want to have a partial to display names (stupid example, I know, but you get the idea):
<h3>{{name}}</h3>
I have to write it
<h3>{{item.name}}</h3>
and then ensure the model is always item. Usually by wrapping it in a directive simply to defines a scope with an item property.
What I often feel like I want to do is simply:
<div ng-include="'my/partial.html'" ng-with="item"></div>
or
<div ng-repeat="list" ng-controller="MyItemCtrl">
{{name}}
<button ng-click="doSomethingWithItem()">Do Something</button>
</div>
Is there some magical directive out there that I haven't found? Or am I completely wrong and just looking for trouble?
Thanks.
EDIT:
Many thanks to Brandon Tilley for explaining the dangers of using scopes as models. But I still often find the need for some quick declarative scope manipulation and wish for an ng-with directive.
Take, for example, you have a list of items which, when clicked, shows an expanded view of the selected item. You might write it something like:
<ul>
<li ng-repeat="item in items" ng-click="selection = item">{{item.minView}}</li>
</ul>
<div ng-controller="ItemController">
{{selection.maxView}}
</div>
now you have to get properties of the selected item using selection.property rather than what I'd want: item.property. I'd also have to use selection in ItemController! Making it wholly coupled with this view.
I know, in this simple example I could have a wrapping controller to make it work, but it illustrates the point.
I've written a very basic with directive:
myApp.directive('with', ['$parse', '$log', function(parse, log) {
return {
scope: true,
link: function(scope, el, attr) {
var expression = attr.with;
var parts = expression.split(' as ');
if(parts.length != 2) {
log.error("`with` directive expects expression in the form `String as String`");
return;
}
scope.$watch(parts[0], function(value) {
scope[parts[1]] = value;
}, true);
}
}
}]);
that simply creates a new scope parsing one expression onto another value, allowing:
<ul>
<li ng-repeat="item in items" ng-click="selection = item">{{item.minView}}</li>
</ul>
<div with="selection as item" ng-controller="ItemController">
{{item.maxView}}
</div>
This seems infinitely useful to me.
Am I missing something here? Just making trouble for myself down the line somehow?
I've found I can just put an array around the source of ng-repeat and it becomes functionally an ng-with. :)
<div ng-repeat="name in [vm.dto.Person.Name]" >
<input type="text" ng-model="name.First" />
<input type="text" ng-model="name.Last" />
</div>
Seems like some purests may not like this... Also doesn't seem like there is a good alternative.
This is a great question. I can see how this may be confusing coming from another front-end framework, but in Angular, the scope has a reference to the model, and the syntax you're describing is normal. I personally like to describe the scope as more like a view model.
Miško Hevery, the author of AngularJS, does a good job of explaining this concept in this video, starting at about the 30 minute mark and lasting about 3 minutes:
People oftentimes think that the scope is the model, and that's not the case. Scope has references to the model. [...] So in the view, you say model dot whatever property you want to access.
While it may be possible to write an ngWith directive that does kind-of what you're looking for, since Angular uses prototypal inheritance for scopes, you will likely run into the same issues that Miško describes in the aforementioned video at 31:10 (where you think you're updating a value on the parent scope but you're actually not). For more details on prototypal inheritance in AngularJS, check out the excellent article The Nuances of Scope Prototypal Inheritance on the AngularJS wiki.
Another approach would be to set a new variable via ng-init:
<div ng-init="name = vm.dto.Person.Name" >
<input type="text" ng-model="name.First" />
<input type="text" ng-model="name.Last" />
</div>
I think one side effect of this when coming from knockout is that is encourages you to create more components with less responsibility, and of course within a component the 'this' is the viewmodel for that component. If you get to a point where it is really annoying you then it might be a sign you need a new component.
And it sure is nice being able to just refer to something in the component view model without thinking about the scope you're in. And you can't tell me you miss $parents[2] and $root ;-)
PS. Yes I know you can use let in knockout, which means fewer $ stuff.
I definitely miss knockout when doing Angular stuff (I still use both) but it sure is nice to do certain things things like title="Customer #{{ row.customerId }} - {{ row.fullName }}" and not have everything under data-bind.
I'm building an AngularJS app and I have ran into an issue. I have been playing with the framework for a while and I have yet to see documentation for something like this or any examples. I'm not sure which path to go down, Directive, Module, or something that I haven't heard of yet...
Problem:
Basically my app allows the user to add objects, we will say spans for this example, that have certain attribute's that are editable: height and an associated label. Rather than every span have its own dedicated input fields for height and label manipulation I would like to use one set of input fields that are able to control all iterations of our span object.
So my approx. working code is something like this:
<span ng-repeat="widget in chart.object">
<label>{{widget.label}}</label>
<span id="obj-js" class="obj" style="height:{{widget.amt}}px"></span>
</span>
<button ng-click="addObject()" class="add">ADD</button>
<input type="text" class="builder-input" ng-model="chart.object[0]['label']"/>
<input type="range" class="slider" ng-model="chart.object[0]['amt']"/>
The above code will let users add new objects, but the UI is obviously hardcoded to the first object in the array.
Desired Functionality:
When a user clicks on an object it updates the value of the input's ng-model to bind to the object clicked. So if "object_2" is clicked the input's ng-model updates to sync with the object_2's value. If the user clicks on "object_4" it updates the input's ng-model, you get the idea. Smart UI, essentially.
I've thought about writing a directive attribute called "sync" that could push the ng-model status to the bound UI. I've though about completely creating a new tag called <object> and construct these in the controller. And I've thought about using ng-click="someFn()" that updates the input fields. All of these are 'possibilities' that have their own pros and cons, but I thought before I either spin out on something or go down the wrong road I would ask the community.
Has anyone done this before (if so, examples)? If not, what would be the cleanest, AngularJS way to perform this? Cheers.
I don't think you need to use a custom directive specifically for this situation - although that may be helpful in your app once your controls are more involved.
Take as look at this possible solution, with a bit of formatting added:
http://jsfiddle.net/tLfYt/
I think the simplest way to solve this requires:
- Store 'selected' index in scope
- Bind ng-click to each repeated span, and use this to update the index.
From there, you can do exactly as you proposed: update the model on your inputs. This way of declarative thinking is something I love about Angular - your application can flow the way you would logically think about the problem.
In your controller:
$scope.selectedObjectIndex = null;
$scope.selectObject = function($index) {
$scope.selectedObjectIndex = $index;
}
In your ng-repeat:
<span ng-repeat="widget in chart.object" ng-click="selectObject($index)">
Your inputs:
<input type="text" class="builder-input" ng-model="chart.object[selectedObjectIndex]['label']"/>
<input type="range" class="slider" ng-model="chart.object[selectedObjectIndex]['amt']"/>