Which element does myVar bind to? Problems with `ng-init` [duplicate] - javascript

This question already has answers here:
What are the nuances of scope prototypal / prototypical inheritance in AngularJS?
(3 answers)
Closed 5 years ago.
I'm wondering what's happening behind the scenes when using ng-init.
Sometimes ng-init does some unexpected things, and I have a hard time debugging.
Let's say I have the following structure:
<!-- App -->
<div ui-view>
<!-- Component -->
<custom-component>
<!-- Transcluded component -->
<transcluded-component>
<!-- Controller -->
<div ng-controller="MyCtrl">
<!-- Element -->
<div ng-init="myVar = 'hello'">
{{myVar}}
</div>
</div>
</transcluded-component>
</custom-component>
</div>
Which element does myVar bind to?
Edit 2017-07-21: Added an example
In the following template block (especially within an ng-repeat), I may use an ng-init:
<span ng-init="path = getPath()">
<a ng-if="path" ng-href="path">
Click here
</a>
<span ng-if="!path">
Some text
</span>
</span>
In this case, I skipped a function call twice, and kept my template clean.

Sometimes ng-init does some unexpected things, and I have a hard time debugging.
The ng-init directive evaluates an Angular Expression in the context of the scope of its element. Numerous directives (ng-repeat, ng-controller, ng-if, ng-view, etc.) create their own scope.
For more information, see
AngularJS Developer Reference - scopes
AngularJS Wiki - Nuances of Scope Prototypical Inheritance
Avoid using ng-init
Avoid using ng-init. It tangles the Model and View, making code difficult to understand, debug and maintain. Instead initialize the Model in the controller.
From the Docs:
ngInit
This directive can be abused to add unnecessary amounts of logic into your templates. There are only a few appropriate uses of ngInit, such as for aliasing special properties of ngRepeat, as seen in the demo below; and for injecting data via server side scripting. Besides these few cases, you should use controllers rather than ngInit to initialize values on a scope.
— AngularJS ng-init Directive API Reference
Update
Q: I've also added an example of a code block I sometime use.
<!-- Controller -->
<div ng-controller="MyCtrl">
<!-- Element -->
<div ng-init="myVar = 'hello'">
{{myVar}}
</div>
</div>
To do the equivalent initialization in the controller:
app.controller("MyVar", function($scope) {
this.$onInit = function() {
$scope.myVar = 'hello';
};
});
By separating concerns of Model and View, the code becomes easier to understand, debug and maintain.

Related

How does childscope work on an ng-if statement? Specifically in a <SELECT> element

NOTE: I'm a new member here so I couldn't directly comment and ask for clarification.
So, my question is: How can I work around ng-if creating a child scope for a select element?
I have the following code:
HTML
<select id="project-select"
ng-if="projects.length > 0"
ng-options="project.name for project in projects"
ng-model="currentProject"
ng-change="broadcastChange('project-changed', currentProject)">
</select>
And my controller is set up in the following format:
function Controller() {
//Do code stuffz
}
angular
.module('app')
.controller('Controller', Controller);
I learned from this post that the "ng-if" is creating a child scope.
So even when the model changes, this part stays the same because it is a primitive value: (name is just a string)
<div id="current-project" class="pull-left">
<strong>Project: </strong>{{currentProject.name}}
</div>
Furthermore in the aforementioned post there were a couple options.
a. Simply change to this: ng-model="$parent.currentProject" which feels a little hacky
b. Set the object value in the controller, which I'm not entirely sure how to do. I feel like it's an easy fix, but I'm somehow overcomplicating it.
Anyway, for now I simply changed ng-if to ng-show and that's solved the problem. However, I am trying to understand Angular more deeply and I feel like this issue could be explained a little bit better to me. Thanks in advance!
What you will find with Angular scope variables is: always use a dot.
That's the mantra from the excellent ng-book
In your case, what this means is this:
You have this code:
<select id="project-select"
ng-if="projects.length > 0"
ng-options="project.name for project in projects"
ng-model="currentProject"
ng-change="broadcastChange('project-changed', currentProject)">
</select>
Which means that you are binding to a $scope variable called $scope.currentProject.
Because of the mysterious and awesome way that javascript works, this does not get updated when you are inside of a child scope.
Thankfully, the solution is actually quite simple. Instead, create an object like so:
$scope.myData = {
currentProject: ''
}
And in your markup, bind to that like so:
<select id="project-select"
ng-if="projects.length > 0"
ng-options="project.name for project in projects"
ng-model="myData.currentProject"
ng-change="broadcastChange('project-changed', myData.currentProject)">
</select>
And voila. It will update, even though it's in a child scope.
This is actually quite useful, because you now have a way to "meaningfully" group variables together. Here's some other pseudo-code to demonstrate what I mean:
$scope.projectData = {
currentProjectID: 1,
currentProjectTitle: 'My Cool Project',
projects: [
{id: 1, name: 'My Cool Project'},
{id: 2, name: 'Another Project'}
],
someOtherProperty: false
// ...etc....
}
As a side-note, this section of this article might be helpful: http://docstore.mik.ua/orelly/webprog/jscript/ch11_02.htm#jscript4-CHP-11-SECT-2.1
If all you want to do is show/hide the select element based on the projects in your 'Controller' controller scope, then ng-show is the right way to go here. In my experience, I've used ng-if when I'm conditionally loading a larger "partial" view containing numerous controls where I felt a separate scope was necessary to avoid having a very large scope (or to facilitate re-use).
You are correct. Do not use $parent in any production Angular apps. It makes your model dependent on the structure of your view, which makes your code hard to refactor and less modularized.
Binding to object properties is the way to go, as you suggested in your "b" answer. The recommended way to do this in the latest version of Angular 1.x is by using the "controller as" syntax. This method makes use of "prototypical inheritance" in javascript. There is a good explanation on this here: http://javascript.info/tutorial/inheritance
I created a plunker for you to demonstrate how binding to object properties works in nested scopes. take a look at the "controller as" syntax". also, try changing the value of ctrl.testBinding in the input, you will see that reflected in the ng-if child scope. I will try to find some links to explain this in more detail.
https://plnkr.co/edit/Gx5xbkJXgzjPSG8kajPR?p=preview
<!DOCTYPE html>
<html >
<head>
<link rel="stylesheet" href="style.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.7/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body ng-app="testApp">
<div ng-controller="testCtrl as ctrl">
<input ng-model="ctrl.testBinding" type="text"/>
<button ng-click="ctrl.toggle()">toggle show</button>
<div ng-if="ctrl.show">
{{ ctrl.testBinding }}
</div>
</div>
</body>
</html>
//script.js
function testController($scope) {
var vm = this;
vm.show = true;
vm.toggle = function(){
vm.show = !vm.show
}
}
angular
.module('testApp', [])
.controller('testCtrl', testController);

AngularJS - change scope of HTML element to new scope

I am learning AngularJS and doing some freaking experiments to clear by concepts about scope, directives and dom.
AIM : Change the scope of HTML element <div id="grocery"> to newly created child scope'
index.html
<html ng-app="eggly" ng-controller="rootCtrl">
<body>
<div id="grocery">
{{box}}
</div>
</body>
</html>
rootCtrl
eggly.controller('rootCtrl',function($scope,$timeout,$compile){
$scope.box = "lunchbox"; --> first assign box = lunchbox
$timeout(function(){
//delete $scope;
var newScope = $scope.$new(); --> create a new scope
newScope.box = "dinnerbox"; ---> Assign new value
$compile($('#grocery'))(newScope); --> compile div with new scope
//newScope.$apply(); --> I think $timeout already do $apply so commented out
},1000);
});
My issue is:
If I do not create new scope and do not $compile in $timeout function then my view is updated with dinnerbox as I am expecting
If I create new scope and $compile in $timeout function then my view shows me old value i.e lunchbox however chrome console show me angular.element($0).scope().box shows me dinnerbox in scope (after $timeout execution).
What I am doing wrong?
When $compile is being called, #grocery contents are lunchbox and not {{ box }}. At this moment ng-controller directive was already linked and binding information is not available as plain HTML.
This shortcoming can be overcome by using directive substitute to {{ }} binding:
<div id="grocery"><span ng-bind="box"></span></div>

Can I use a JQuery plugin from within an angularjs directive?

I'm quite new to javascript in general. I've spent the past couple of weeks building a front end with Angular.js.
I have a number of directives I've defined that sit on my page, Angular has been great for this.
Here's what my main page looks like:
<body class="body" ng-controller="OverviewController as overview" font-size:1em>
<sidebar-menu ng-controller="PanelController as panel"></sidebar-menu>
<div id="content" >
<div>
<div class="list-group">
<div class="list-group-item" ng-repeat="site in overview.sites" ng-click="">
<div class="item-heading">
<h3>{{site.name}}</h3>
<p>Address: {{site.address}}</p>
Click Here
</div>
<installationsite-panels ng-controller="PanelController as panel"></installationsite-panels>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function(){
$('.paulund_modal').paulund_modal_box();
});
</script>
</body>
Note the javascript function to call a modal box at the bottom, using this tutorial.
I've spent the past few days trying different tutorials to get modals to work in my webapp, but with no success. I think it's down to my lack of understanding of Angular and Javascript in general.
In any case, I've managed to get this tutorial to work using JQuery, and when I click on the link, the modal opens as expected.
However, I don't want to call this modal from here. I want to call it from a directive that's embedded within the <installationsite-panels> directive in the above code, which looks like this (just a single section shown here):
Device Statuses
<div>
<div class="device-icon-group">
<div class="device-type1-icons" ng-click="panel.showDevices(3)" ng-show="showtype1Red"><img src="img/type1red.png" style="width:50%; height:50%;"/></div>
<div class="device-type2-icons" ng-click="panel.showDevices(3)" ng-show="showType2Red"><img src="img/type2red.png" style="width:50%; height:50%;" /></div>
</div>
<div class="service" ng-click="panel.showDevices(3)" ng-show="showService">
<b>{{panel.getServiceDeviceCount()}} device needs servicing</b>
</div>
<div ng-show="showServiceList">
<device-list-service></device-list-service>
</div>
</div>
The directive <device-list-service> shows a list of items like so:
<div ng-controller="DevicesController as deviceList" font-size:1em >
<div id="device-list-group">
<div id="device-list-group-item" ng-click="" ng-repeat="device in deviceList.devicesService">
<div ng-class="device.status"><img src="{{(device.type == 'type1') ? 'img/type1white.png' : 'img/type2white.png'}}"> </div>
<div class="device-params">
<b>ID: </b> {{device.id}}<br />
<b>Type: </b> {{device.type}}
</div>
<div class="device-params">
<b>Location: </b> {{device.location}}<br />
<b>Action: </b> {{device.action}} <br />
</div>
</div>
</div>
</div>
I want to show the modal when the user clicks on one of the list-group-item 's, and display some data relating to that item.
The modal works fine from the top level in the main app, but I cannot call it from within any of the directives. How can I do this?
Is it possible, or do I need to scrap my JQuery modal and do it the Angular way, which hasn't worked for me for the past few attempts.
Don't use jquery modals. You can, but you shouldn't.
Instead, I recommend using Angular UI, which has a pretty usable modal implementation: https://angular-ui.github.io/
Second alternative: if you don't like Angular UI, then use AngularJS + Bootstrap, and create your own custom directives
Third alternative: Use jQuery.
If you still want to go with the 3rd alternative, despite my advice against it, then here is how you do it:
var app = angular.module('app', []);
app.directive('modal', function($http, $timeout) {
return {
restrict: 'A',
link: function(scope, element, attr) {
$timeout(function() {
element.paulund_modal_box();
}, 0, false);
}
};
});
Usage:
<div modal></div>
Some explanation is needed here.
Why is the $timeout service necessary? jQuery plugins often require the DOM to be fully loaded in order to work properly. That is why most jQuery plugins are wrapped inside of a $(document).ready block. In AngularJS there is no concept of DOM ready, and there is no easy way in AngularJS to hook into the event. However, there is a well-known hack, which is to use the $timeout service. In Angular there are three phases:
1. compile - Angular walks the DOM tree looking for directives
2. Link - Angular calls the link function for each directive to setup watch handlers.
3. Render - Angular updates the views
Using $timeout within the Link function queues the $timeout function to be executed until after the current closure is done executing. It just so happens that the Render phase is within the current closure's scope of execution. Hence, the $timeout function will execute after the render phase, when the DOM has been loaded.
Mixing JQuery and Angular in that way is maybe a little messy, but sometimes you do want to use a well-built component. You could try to find a similar modal in Angular - angular-modal - or you could try and build the component into your Angular directive itself - jQuery Plugins in AngularJS

Should AngularJS logic be placed in HTML file?

I want to refactor code of which I post examples below. I am very new to AngularJS. Now when I saw the code, I was very curious about all the logic that is placed in the HTML code.
<p ng-show="countForm.count.$dirty && countForm.count.$error.min" class="error-message">
<button ng-click="step(2)" ng-show="data.step == 1 && countForm.count.$dirty" ng-disabled="countForm.$invalid" class="line-break">
<div ng-class="{selected: data.spreadChoice == 3}" ng-click="data.spreadChoice = 3; step(3)" ng-mouseover="data.preSpreadChoice = 3" ng-mouseout="data.preSpreadChoice = data.spreadChoice">
<div ng-show="data.step >= 2" class="step" ng-class="{active: data.step == 3, done: data.step > 3, left: data.preSpreadChoice == 1, right: data.preSpreadChoice == 3}" ng-scroll-here="data.step == 3">
<p ng-switch-when="false" class="large">[[data.emails.length]] von [[data.count]] – <span class="red">[[Math.max(0,data.count-data.emails.length)]]</span> Members</p>
<div ng-show="data.step >= 5 && data.multipleTeams" class="step" ng-class="{done: data.step > 5, active: data.step == 5}" ng-scroll-here="data.step == 5">
<button class="small" ng-disabled="!unitsForm.$dirty || unitsForm.$invalid" ng-click="addUnit(data.nextTeam, data.nextTeamleaderEmail)">
Shouldn't the HTML rather contain classes or attributes and the logic itself should be placed in JS files or JS code? Is this a good (or at least a common) way of developing AngularJS? Or should placing logic in HTML be avoided?
If you ask me:
Client side business logic sits in services that are injected into directives\controllers.
UI logic is suppose to be placed in the controllers.
Now about adding logic to the views, if we are talking about business logic then it's a big no no. Use a method on your controller that will evaluate stuff using the service.
If we are talking about ng-if/ng-show conditions then only if they are small and "readable" conditions I would add them to the view. When it's more than that, I move them to the controller for debugging issues and since I believe the HTML should be readable.
Placing logic in HTML using directives in angular is a good way. You cannot take full advantage of angular without placing logic in HTML.
Controllers should contain view logic, but shouldn’t actually reference the DOM (referencing the DOM should only be done through directives). ref
Two things to remember or the best practices for AngularJS are
Treat the scope as read-only in views
Treat the scope as write-only in controllers
ref
Since you are placing logic in HTML, if you treat it as read-only, you can check conditions or extract data using functions in scope, but the original data model isn't disturbed whatever you do in HTML.
Also tying dom elements to specific directives are the most powerful features of angular.
When you use a datepicker, in jQuery, you could do as follows:
<div id="datepicker"></div>
then in JS:
jQuery('#datepicker').datepicker({
start:'today',
end:'tomorrow',
showTime:true
})
You can do this in angular way as follows
This way even when a designer or someone who reads HTML will be able to read what and even you can pass the options from the element's attributes itself.
<div date-picker start="today" end="tomorrow" show-time="true"></div>
AngularJS's importance itself is declarative syntax and can contain expressions as attribute values like you posted. That is not at all a bad practice. Indeed it is common and good practice all developers do. Using logic in HTML in angularjs saves a lot of code writing by ourselves. All the heavylifting is done by angular behind the scenes.
See some best practices about AngularJS

AngularJS reusing the same controller on one page, with different configuration

I want to display two elements on a page controlled by different instances of the same controller, but I need to register some external information that will be unique (one "joystick" gets an identifying property set, like "player = one" while the other gets "player = two").I'm not sure of the best way of pulling this off exactly
Here's a generic example of what I'm trying to accomplish:
<!-- These need to have different configurations -->
<div ng-include src="'joystick/joy.tpl.html'"
ng-controller="JoystickCtrl">...</div>
<div ng-include src="'joystick/joy.tpl.html'"
ng-controller="JoystickCtrl">...</div>
Should I:
Use a directive?
<div ng-include src="'joystick/joy.tpl.html'"
ng-controller="JoystickCtrl" player="one">...</div>
<div ng-include src="'joystick/joy.tpl.html'"
ng-controller="JoystickCtrl" player="two">...</div>
Use $injector? (fyi - this might be an incorrect implementation)
<div ng-controller="DualJoyCtrl">
<div ng-include src="'joystick/joy.tpl.html'"
ng-controller="joyOne" player="one">...</div>
<div ng-include src="'joystick/joy.tpl.html'"
ng-controller="joyTwo" player="two">...</div>
</div>
-----
.controller('DualJoyCtrl', function ($injector, JoystickCtrl, $scope, $rootScope) {
$scope.joyOne = $injector.instantiate(JoystickCtrl, {$scope: $rootScope.$new(), player:"one"});
$scope.joyTwo = $injector.instantiate(JoystickCtrl, {$scope: $rootScope.$new(), player:"two"});
});
Or... not do this?
I realize this is similar to another, seemingly inconclusive stack post:
Edit
Since ngController is initialized before ngInit, in order to have data available in controller at once, you should wrap ngController in parent element with ngInit:
<div ng-init="player = 'one'">
<div ng-controller="JoystickCtrl">
...
</div>
</div>
Original answer
I think simple ng-init would suffice:
<div ng-controller="JoystickCtrl" ng-init="player='one'">...</div>
<div ng-controller="JoystickCtrl" ng-init="player='two'">...</div>
Store your config values in a data attribute, and retrieve it within the controller using $attrs. (The AngularJS ngInit documentation recommends to say clear of ng-init unless aliasing special properties of ngRepeat. ) A similar answer is here. This code snippet gives you the general idea:
Index.html:
<div ng-include ng-controller="JoystickCtrl" src="'same.html'" data-id="1"></div>
<div ng-include ng-controller="JoystickCtrl" src="'same.html'" data-id="2"></div>
Controller:
function joystickCtrl($scope, $attrs) {
$scope.id = $attrs.id;
};
View:
<h2>Joystick: {{id}}</h2>
Here is the full code in Plunker.

Categories

Resources