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

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

Related

AngularJs watch angular.element

I'm trying to integrate AngularJs into a legacy Spring MVC application because there is lots of spaghetti javascript code (a huge mess!) to hide / show html elements based on conditions etc.
It uses jsp and lots of custom jsp tags and because the way it is written I'm hesitant to mess with the jsp tags themselves.
I'm trying to do is read the values of the spring input into angular scope and once I have it then can use angular to hide / show stuff.
Assume that my html is something like this
<div ng-app ng-controller="FooCtrl">
Backbone <input type="radio" name="yes" value="Backbone"/>
Angular <input type="radio" name="yes" value="Angular" />
</div>
I'm able to read these elements into Angular's scope like this
$scope.elements = angular.element("input[name='yes']");
But change to the value of these elements are not getting triggered or watched by Angular.
Ideally when the radio button gets checked I would like the model to change. How can I do this?
Thank you in advance.
Here is plnkr with the basic setup.
http://plnkr.co/edit/CRPBFF9FaGivBRa8OSdZ?p=preview
One thing is that in your controller you had:
$scope.$watch('$scope.elements',function(newValue){
console.log(newValue);
},true);
It should be 'elements' rather than '$scope.elements'. I'm not quite sure if a $watch or $watchCollection is going to be your best bet here. I tried it but was having issues.
Here is another idea:
var app = angular.module('myApp',[]);
app.controller('FooCtrl', function($scope){
$scope.message = "hi";
$scope.elements = angular.element("input[name='yes']");
angular.element("input[name='yes']").bind("input change", function(e) {
console.log(e);
});
});

Angular: update ng-include on CRUD event

I have a constant sidebar in my index.html file that lists projects using ng-include. When a project is created, or updated etc.. I would like the sidebar to automatically update along with it. I'm not sure which part of my code to provide, as hopefully it's a fundamental question that's easy to answer, though the solution eludes me.
Edit: feel I'm almost there, but src doesn't seem to pick up the controller property:
<div class="col col-md-4" data-ng-controller="ProjectsController" data-ng-include src="'{{sidebarUrl}}'"></div>
In my projects controller:
// Update existing Project
$scope.update = function() {
var project = $scope.project ;
project.$update(function() {
$location.path('projects/' + project._id);
$scope.$broadcast('projectUpdated');
}, function(errorResponse) {
$scope.error = errorResponse.data.message;
});
};
$scope.sidebarUrl = 'modules/projects/views/list-projects.client.view.html';
$scope.$on("projectUpdated",function(event,args) {
$scope.sidebarUrl=null;
$scope.sidebarUrl= 'modules/projects/views/list-projects.client.view.html';
});
This is where services are your friend. You should start by encapsulating your CRUD operations into a service.
function MyCrudService($http, ...){ ... }
angular.module('my-app')
.service('myCrudService', MyCrudService);
Now, there are several ways you could implement the updating.
Use $rootScope and broadcast a message saying something has changed, and listen for that event in your sidebar controller (assuming you have one).
//Inside your service
function updateProject(proj){
//Update project
$rootScope.$broadcast('project-updated', proj);
}
//Inside your controller
function MySidebarController($scope){
$scope.$on('project-updated', function(){ ... });
}
Encapsulate the eventing logic inside your service to avoid using $rootScope. Just maintain your own list of callbacks and execute them.
//Inside your controller
function MySidebarController(myCrudService){
myCrudService.onProjectChanged(function(){ ... });
}
Expose the shared data on your service that can be databound to.
//Inside your controller
function MySidebarController($scope, myCrudService){
$scope.projects = myCrudService.projects;
}
Personally, I try to avoid $scope in my controllers, but using it for eventing is OK. Still, I might write some kind of directive that would allow me to execute an expression whenever an event fired in order to avoid it.
<my-event-binding event='project-updated' expression='sideBar.updateProjects()' />
Okay, so I had the same requirement(dynamically changing menu items in an included side panel) what I did was to use a controller in the ng-include template. The template would then fetch the relevant menu items from a service and update the controller. The view had an ng-repeat directive to show all the menu items (projects in your case).
<div ng-controller="ProjectsCtrl">
<ul>
<li ng-repeat="project in projects">
<a ng-href="project.url">
{{project.name}}
</a>
</li>
</ul>
</div>
The controller function could look something like:
function($scope, projectsSvc){
$scope.projects = [];
loadProjects();
$scope.$on("updatedProjects", loadProjects);
function loadProjects(){
projectsSvc.getProjects.success(function(projects){
$scope.projects = projects;
});
}
}
Projects are fetched from a service. When you update a project, broadcast an event that triggers a load of the projects again.
So after the new projects have been committed into the service backend, the sidebar will update accordingly.

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.

Saving new models using AngularJS and $resource

I'm trying to get an understanding of AngularJS using $resource, however most of the examples I see out there don't explain how to actually create new instances of something using $resource (or how the entire setup should look).
I've posted my code at the bottom of this.
I have the following setup, where posting to '/entry/api' should create a new entry.
The entry object it self has three properties: name, description and id.
i thought that calling
$scope.save(); would do two things:
Map the input fields as POST data
make a POST request to the url defined in the $resource (in this case '/entry/api')
Some examples I've seen are manually mapping the data to the resource as such:
var entry = new Entry();
entry.name = $name; // defined in entryController
entry.description = $scope.description; // <-- defined in entryController
entry.$save()
I thought this wasn't supposed to be necessary, as these fields are defined in the html.
This solution results in:
Defining a model in the backend
Defining a model in the front end (the entryController div)
Adding the values from the from the entryController div to the JS version of the model and then finally saving it.
It might be the way AngularJS works, however I thought that the input fields in the html would automatically be mapped.
Otherwise you have at least 3 places in the code to update if you add or remove a property of your (backend) model.
How are you supposed to use AngularJS along with $resource to save new objects?
angular.module('entryManager', ['ngResource']);
function pollController($scope, $resource) {
$scope.polls = $resource('/entry/api/:id', {id: '#id'});
$scope.saveEntry = function() {
this.save();
}
}
<html ng-app="entryManager">
... <-- include angularjs, resource etc.
<div ng-controller="entryController">
<input type='text' ng-model="name"><br/>
<textarea ng-model="description" required></textarea><br/>
<button class="btn btn-primary" ng-click="saveEntry()">Save</button>
</div>
The first think you should note, that scope != model, but scope can contain model(s).
You should have some object in your scope and then save it.
So, there would be something like the following:
HTML:
<div ng-controller="entryController">
<input type='text' ng-model="poll.name"><br/>
<textarea ng-model="poll.description" required></textarea><br/>
<button class="btn btn-primary" ng-click="saveEntry()">Save</button>
</div>
JavaScript:
function pollController($scope, $resource) {
var polls = $resource('/entry/api/:id', {id: '#id'});
$scope.saveEntry = function() {
polls.save($scope.poll);
}
}
Note1: even if you do not have initialized poll object, AngularJS will automatically create new object when you start typing.
Note2: its better to wrap your form into ngForm (by adding ng-form="someformname" attribute to div with ng-controller or wrap with <form name='...'>..</form>. In this case you could check validity of form by $scope.someformname.$valid before saving.
Good example is on main page of AngularJS web site under "wiring the backend" section (btw, mine favorite).
Don't use save method over model object itself, use save method of model class, For example -
//inject User resource here
$scope.user = new User();
$scope.user.name = "etc";
User.save($scope.user,function(response){
});
previously i was using $scope.user.$save(function(response){}) it was clearing my $scope.user object

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