How to get Angular to work for dynamically added object - angularjs

I have two files as described below. I am defining the controller in file_1.php
In file_2.php, I am 'require'ing the file_1.php, and then moving the ul into the div that is described in file_1.php
What I want to be able to do is - get the functions within the controller to work for the ul which was dynamically added. My guess is that, when the page was loaded, the ul block is seen as being outside the controller and so it doesn't work. On searching, I was able to see a solution that involved $compile, but that works for ng-model, and not for repeat or {{}} either. I am new to Angular and would appreciate any help.
file_1.php
<?php
<div id="box_1" ng-controller="MyCtrl"></div>
?>
file_2.php
<?php
require 'file_1.php';
?>
<ul>
<li ng-repeat="item in items">item.text</li>
</ul>
{{items}}
<script>
function MyCtrl($scope) {
$scope.items = [{text: "Item 1", text: "Item 2"}];
}
$(document).ready(function() {
$('#box_1').append($('ul'));
})
</script>
Some information that I found:
In the documentation here, under section "Reasons behind the compile/link separation", they have explained why compiling is different for ng-repeat. Could anyone explain what it means exactly and/or the way to go about it? I tried compile - anything that is not in an ng-repeat works, but anything inside ng-repeat doesn't.

Are you wedded to this file structure for some reason? What you're trying to do goes against the grain of angular and will thus be a pain. Angular is supposed to free you from the pain of DOM manipulation with jQuery.
It seems like using $compile should work, though. Can you show us what you're trying to do with it?
Why is it that you can't do something like this?
<div id="box_1" ng-init="items = [{text: "Item 1", text: "Item 2"}]">
<ul>
<li ng-repeat="item in items">{{item.text}}</li>
</ul>
{{items}}
</div>
(The syntax might be a bit off on ng-init.)

It is not good practice to manipulate the DOM in a controller. Aside of that, doesn't $scope.$apply() help?
$(document).ready(function() {
$('#box_1').append($('ul'));
$scope.$apply();
})

Related

Angular 1: How to add directive to DOM from view controller

What is the recommended Angular way of adding custom directives conditionally? No google post or SO article has seemed to really answer my question.
The app allows the user to display and interact with customer information. In one view, the person can click on the order details, which makes an API call to return the data. If the order comes back as a certain type, I want to add an indicator to the already rendered sidebar. It seems like a pretty simple and common use case so I'm wondering what I'm missing.
I'm using Angular 1.4x,
Angular Bootstrap
It is a big app but here is a contrived example of the relevant code:
sidebar.html - before API call:
<div>
Customer Info
Notes
</div>
sidebar.html - what I want to have after API call:
<div>
Customer Info
<a href="#/orders" id="ordertype">
<order-type-directive uib-tooltip="Phone Order" tooltip-trigger="mouseenter" tooltip-placement="top"/>
</a>
Notes
</div>
main-controller.js
angular.module('app.main')
.controller('mainCtrl', function(){
var vm = this;
vm.orderType;
...
vm.getOrderData(memberID)
.then(function(data){
vm.orderType = data.type;
//This is where my question arises
})
})
So I can make a directive:
.directive('orderTypeDirective', function(){
...
})
Or I can just do something like this:
var el = angular.element('#orderType')
.html('<span uib-tooltip="Phone Order" popover-trigger="mouseenter"></span>');
var aNewScope = $scope.$new();
$compile(el)(aNewScope)
But I feel like the above is ugly and hacky. As to how I insert it into the DOM after creating the element, the documentation and all googling assumes I already know how to do that.
For the directive option I've tried to use an ng-if but it doesn't update when the API call comes back and updates vm.orderType.
For the create-element option, I could do something like this inside an if block in mainCtrl:
el.replaceWithEl(el);
But I know there must be a better, more "Angular" way and that I'm missing something glaring.
Have you tried maybe with a ng-if directive ? like:
<div>
Customer Info
<a href="#/orders" id="ordertype">
<order-type-directive uib-tooltip="Phone Order"
tooltip-trigger="mouseenter"
tooltip-placement="top"
data-ng-if="mainCtrl.orderType && mainCtrl.orderType.length>0"/>
</a>
Notes
</div>
Or you can put any other condition in your ng-if directive that maybe match well your case

Delimiting some sibling nodes without additional element

What is the equivalent angularjs template for the following handlebar template? Is there any way to achieve same result without wrapping the if block with another tag?
(foo is false)
<ul>
<li>a</li>
{{if foo}}
<li>b</li>
…
<li>c</li>
{{/if}}
<li>d</li>
</ul>
The rendered template should be exactly:
<ul>
<li>a</li>
<li>d</li>
</ul>
ng-if with one time binding(if you are in version 1.3.x else resort to some other libraries like bindonce to avoid any unnecessary watches) might be more appropriate for you. But ideally it is clearly unclear because you can solve this with many ways in angular. It does not even has to get to the view, you could just filter it out from the controller itself while setting up the view model which is used to repeat (ng-repeat) the lis. ng-show can also be used if you are trying to show and hide them. Difference between ng-if and ng-show/ng-hide is that ng-if removes the element completely from dom (and it cannot be animated with nganimate). ng-show just sets the css property display:none if condition set is false.
<ul>
<li>a</li>
<li ng-if="::foo">b</li><!-- Using :: for one time binding V1.3.x so no more watchers -->
<li ng-if="::foo">c</li>
<li>d</li>
</ul>
Update based on the comment that OP is looking for "a block statement to show/hide a bunch of elements together without adding a container tag".
Angular is not just a templating library like handlebars. So first thing before providing any specific answer is to recommend to learn how angular works. It is much more than a templating engine, it binds data to DOM that is already rendered and view is more of a reflection of the view model/model built from the controller. So in your case, as i explained earlier you just have to filter out the data based on a specific condition. Take a look at ng-repeat, event DOM filters that can be used with ng-repeat. So in short looking for a a block statement to show/hide a bunch of elements together without adding a container tag in angular (just what you would in handlebars) is thinking in wrong direction in my opinion. A possible solution for you can as well just be to identify when foo becomes true do not event provide those items (to be filtered out) to be rendered to view (or works case use filters in the view). And adding a block statement can just result in an invalid html in your case and browser will just strip it off before even angular has a chance to process it (unlike handlerbars where you transform your template to html before even rendering).
Here is one possible, better way (Using view filters are bad if filtering is one time, if it is just one time do the filtering in the controller) to do this in my opinion.
angular.module('app', [])
.controller('ctrl', function($scope) {
$scope.items = [{
name: 'a',
hideWhenFoo: false
}, {
name: 'b',
hideWhenFoo: false
}, {
name: 'c',
hideWhenFoo: true
}, {
name: 'd',
hideWhenFoo: true
}, {
name: 'e',
hideWhenFoo: true
}, {
name: 'f',
hideWhenFoo: false
}, {
name: 'g',
hideWhenFoo: false
}];
$scope.foo = true; // due to some condition
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<ul>
<li ng-repeat="item in items | filter:{hideWhenFoo:!foo}">{{item.name}}</li>
</ul>
</div>
The following works, similar to ng-repeat-start and ng-repeat-end. However, I did not find it on the docs.
<ul>
<li>a</li>
<li ng-if-start='foo'>b</li>
…
<li ng-if-end>c</li>
<li>d</li>
</ul>

Conditionally apply hasDropdown directive on ng-repeated element

I'm working on a project where I use both angularJS and foundation, so I'm making use of the Angular Foundation project to get all the javascript parts of foundation working. I just upgraded from 0.2.2 to 0.3.1, causing a problem in the top bar directive.
Before, I could use a class has-dropdown to indicate a "top-bar" menu item that has a dropdown in it. Since the menu items are taken from a list and only some have an actual dropdown, I would use the following code:
<li ng-repeat="item in ctrl.items" class="{{item.subItems.length > 0 ? 'has-dropdown' : ''}}">
However, the latest version requires an attribute of has-dropdown instead of the class. I tried several solutions to include this attribute conditionally, but none seem to work:
<li ng-repeat="item in ctrl.items" has-dropdown="{{item.subItems.length > 0}}">
This gives me a true or false value, but in both cases the directive is actually active. Same goes for using ng-attr-has-dropdown.
this answer uses a method of conditionally applying one or the other element, one with and one without the directive attribute. That doesn't work if the same element is the one holding the ng-repeat so i can't think of any way to make that work for my code example.
this answer I do not understand. Is this applicable to me? If so, roughly how would this work? Due to the setup of the project I've written a couple of controllers and services so far but I have hardly any experience with custom directives so far.
So in short, is this possible, and how?
As per this answer, from Angular>=1.3 you can use ngAttr to achieve this (docs):
If any expression in the interpolated string results in undefined, the
attribute is removed and not added to the element.
So, for example:
<li ng-repeat="item in ctrl.items" ng-attr-has-dropdown="{{ item.subItems.length > 0 ? true : undefined }}">
angular.module('app', []).controller('testCtrl', ['$scope',
function ($scope) {
$scope.ctrl = {
items: [{
subItems: [1,2,3,4], name: 'Item 1'
},{
subItems: [], name: 'Item 2'
},{
subItems: [1,2,3,4], name: 'Item 3'
}]
};
}
]);
<div ng-app="app">
<ul ng-controller="testCtrl">
<li ng-repeat="item in ctrl.items" ng-attr-has-dropdown="{{ item.subItems.length > 0 ? true : undefined }}">
{{item.name}}
</li>
</ul>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
Ok, I made a directive. All <li> will need an initial attr of:
is-drop-down="{{item.subItems.length > 0}}"
Then the directive checks that value and for somereason its returning true as a string. Perhaps some onc can shed some light on that
app.directive('isDropDown', function () {
return {
link: function (scope, el, attrs) {
if (attrs.isDropDown == 'true')
{
return el.attr('has-dropdown', true); //true or whatever this value needs to be
}
}
};
});
http://jsfiddle.net/1qyxrcd3/
If you inspect test2 you will see it has a has-dropdown attribute. There is probably a cleaner solution, but this is all I know. I'm still new to angular.
edit I noticed a couple extra commas in my example json data..take note, still works, but they shouldn't be there.

How to avoid "sausage" type binding when having nested model

I have nested model and I am trying to avoid vm.someObject.someChild.ChildOfChild.name type of situations. Is there a way to set the model for outer <div> so that I can instead do ChildOfChild.name or even name. In Silverlight this was called DataContext. I put "vm" on the $scope, but in html I would like to avoid having to type the full path to attribute.
For example:
<div>
{{someObject.Id}}
<div>
{{someObject.name.first}}
{{someObject.name.last}}
</div>
<div>
{{someObject.someChild.name.first}}
</div>
</div>
I would like to do something like this
<div datacontext = someObject>
{{Id}}
<div datacontext = name>
{{first}}
{{last}}
</div>
<div datacontext = someChild.name>
{{first}}
</div>
</div>
You can do this with a custom directive.
HTML:
<div ng-app="myApp" ng-controller="myCtrl as ctrl">
<div>
Access from deepObj: {{ctrl.deepObj.one.two.three.four}}
</div>
<div scope-context="ctrl.deepObj.one.two.three">
Access from third level: {{four}}
</div>
</div>
JS:
var myApp = angular.module('myApp', []);
var myCtrl = function() {
this.deepObj = {one: {two: {three: {four: "value"}}}};
};
myApp.directive('scopeContext', function () {
return {
restrict:'A',
scope: true,
link:function (scope, element, attrs) {
var scopeContext = scope.$eval(attrs.scopeContext);
angular.extend(scope, scopeContext);
}
};
});
See the documentation on $compile for information on what scope: true does.
Make sure you don't call the directive something like data-context as an attribute starting with data- has a special meaning in HTML5.
Here is the plunker: http://plnkr.co/edit/rMUQlaNsH8RTWiRrmohx?p=preview
Note that this can break two-way bindings for primitive values on the scope context. See this plunker for an example: http://plnkr.co/edit/lCuNMxVaLY4l4k5tzHAn?p=preview
You could try/abuse ng-init
Try ng-init, you'll have one more ., but it's better than the other answer I've seen proposed:
<div ng-init="x = foo.bar.baz">
{{x.id}}
{{x.name}}
</div>
BUT Be warned, doing this actually creates a value on your scope, so doing this with something like ng-model if you're reusing the same name, or in a repeater, will produce unexpected results.
Why a custom directive for this probably isn't a good idea
What #rob suggests above is clever, and I've seen it suggested before. But there are issues, which he touches on, in part at least:
Scope complexity: Adding n-scopes that need to be created (with prototypical inheritence) whenever views are compiled.
View processing complexity: Adding an additional directive (again for no real functional benefit) that needs to be checked on each node when the view is compiled.*
Readability? The next Angular developer will likely less readable because it's different.
Forms Validation: If you're doing anything with forms in Angular, this might break things like validation.
ng-model woahs: Setting things with ng-model this way will not be at all intuitive. You'll have to use $parent.whatever or $parent.$parent.whatever depending on how may contexts deep you are.
* For reference, views are $compiled more than you think: For every item in a repeater, whenever it's changed for example.
A common idea that just doesn't jive with Angular
I feel like this question comes up frequently in StackOverflow, but I'm unable to find other similar questions ATM. ... regardless, if you look at the approaches above, and the warnings given about what the side effects will be, you should be able to discern it's probably not a good idea to do what you're trying to do just for the sake of readability.

How to reload / refresh model data from the server programmatically?

Background
I have the most basic "newbie" AngularJS question, forgive my ignorance: how do I refresh the model via code? I'm sure it's answered multiple times somewhere, but I simply couldn't
find it.
I've watched some great videos here http://egghead.io and went quickly over the tutorial, but still I feel I'm missing something very basic.
I found one relevant example here ($route.reload()) but I'm not sure I understand how to use it in the example below
Here is the setup
controllers.js
function PersonListCtrl($scope, $http) {
$http.get('/persons').success(function(data) {
$scope.persons = data;
});
}
index.html
...
<div>
<ul ng-controller="PersonListCtrl">
<li ng-repeat="person in persons">
Name: {{person.name}}, Age {{person.age}}
</li>
</ul>
</div>
...
This all works amazingly well, each time the page is reloaded I see the list of people as expected
The questions
Let's say I want to implement a refresh button, how do I tell the model to reload programmatically?
How can I access the model? it seems Angular is magically instantiating an instance of my controller, but how do I get my hands on it?
EDIT added a third question, same as #1 but how can it be done purely via JavaScript?
I'm sure I'm missing something basic, but after spending an hour trying to figure it out, I think it deserves a question. Please let me know if it's duplicate and I'll close + link to it.
You're half way there on your own. To implement a refresh, you'd just wrap what you already have in a function on the scope:
function PersonListCtrl($scope, $http) {
$scope.loadData = function () {
$http.get('/persons').success(function(data) {
$scope.persons = data;
});
};
//initial load
$scope.loadData();
}
then in your markup
<div ng-controller="PersonListCtrl">
<ul>
<li ng-repeat="person in persons">
Name: {{person.name}}, Age {{person.age}}
</li>
</ul>
<button ng-click="loadData()">Refresh</button>
</div>
As far as "accessing your model", all you'd need to do is access that $scope.persons array in your controller:
for example (just puedo code) in your controller:
$scope.addPerson = function() {
$scope.persons.push({ name: 'Test Monkey' });
};
Then you could use that in your view or whatever you'd want to do.
Before I show you how to reload / refresh model data from the server programmatically? I have to explain for you the concept of Data Binding. This is an extremely powerful concept that will truly revolutionize the way you develop. So may be you have to read about this concept from this link or this seconde link in order to unterstand how AngularjS work.
now I'll show you a sample example that exaplain how can you update your model from server.
HTML Code:
<div ng-controller="PersonListCtrl">
<ul>
<li ng-repeat="person in persons">
Name: {{person.name}}, Age {{person.age}}
</li>
</ul>
<button ng-click="updateData()">Refresh Data</button>
</div>
So our controller named: PersonListCtrl and our Model named: persons. go to your Controller js in order to develop the function named: updateData() that will be invoked when we are need to update and refresh our Model persons.
Javascript Code:
app.controller('adsController', function($log,$scope,...){
.....
$scope.updateData = function(){
$http.get('/persons').success(function(data) {
$scope.persons = data;// Update Model-- Line X
});
}
});
Now I explain for you how it work:
when user click on button Refresh Data, the server will call to function updateData() and inside this function we will invoke our web service by the function $http.get() and when we have the result from our ws we will affect it to our model (Line X).Dice that affects the results for our model, our View of this list will be changed with new Data.

Resources