I created a custom directive in angular so that I can fade out a form on submit and replace it with a template with a custom message.
The desired workflow is as follows:
The user completes the form and clicks submit.
The controller updates the model with a new object and emits a "formSubmitted" event with some args.
The directive listens for the event and fades out the form.
The directive loads a partial html filled with the args from the event.
I resolved the first 3 steps, but I wasn't able to get around the final step (I don't want to hardcode the html as Strings, I want to pull it from another file if possible).
How can it be done?
Edit: some sample code (simplified):
This is the form:
<section class="hero-unit {{containerClass}}" tk-jq-replace-children>
<h2>{{formTitle}}</h2>
<form class="form-search" name="solform" novalidate>
<fieldset>
...
This is the controller:
if( formWasSavedOk ){
$scope.formSubmittedMsg = msg;
//Here emits the custom event
$scope.$emit( 'solicitud:formSubmitted' );
}
This is the directive:
.directive( 'tkJqReplaceChildren', function( $compile ){
return {
link: function( $scope, $iElement/*, $iAttrs*/ ){
//The directive listens for the event
$scope.$on( 'solicitud:formSubmitted', function( /*event*/ ){
$iElement
.children()
.fadeOut(1000)
.promise()
.done(function(){
var $detachedElments = $(this).detach();
//The html is compiled and added to the DOM
$iElement.html( $compile( "<h2>{{formSubmittedMsg}}</h2>" )($scope) );
$scope.$apply();
});
});
}
};
});
<h2>{{formSubmittedMsg}}</h2> is the code I want to pull from app/partials/create/createdOk.html (it is way more than just a header, that's why I want to load it from a file)
I'm not sure if you are looking for the $http service. I have created a plunker http://plnkr.co/edit/13kFLh9RTsIlO4TaFIFQ?p=preview, which doesn't cover the first three steps, but covers the 4th step you need.
In the plunker click on the text "Click here to submit the form", and notice the new text is is inserted. This new text is from the external file called tmpl.html. In the firebug console, you can notice a get call after you clicked the text, to fetch the tmpl.html
I believe the "Angular way" to fetch an external html snippet would be to use the ng-include directive:
<ng-include
src="{string}"
[onload="{string}"]
[autoscroll="{string}"]>
</ng-include>
As for why your directive didn't work, my guess is that you're fetching the html at the directive's link phase, rather than compile phase. There's a nice explanation on the difference on this answer, but it's pretty much this:
If you are going to make DOM transformation, it should be compile if
you want to add some features are behavior changes, it should be in
link.
I would recommend moving most of that logic away from the directive, to the controller: fetching the data using a $resource or $http service, and then passing the results to the newly created scope of the ng-directive.
Related
I know that if I want to create a reusable item, such as a date picker, then creating it as a Directive is recommended.
However, let's say that on my homepage, I have a Welcome section that displays the quote of the day with a background image that comes from a Rest service. Should this be a Directive that can encapsulate the markup and controller logic? Or should it be a simple AngularJs Controller that binds to markup in my index.html?
What constitutes whether or not something should be created as a Directive?
Directive is only a wrapper for a controller. It means if you have a controller you can use it. But you also may use the same controller as a controller of a directive for example instead of link function use controller.
This allow as to draw clear line where to use directive and where to use a controller.
We have to use Controller if we want to reproduce logic of piece of HTML markup. When we want to use the same $scope assignments, the same functions inside $scope, ... but HTML markup is always different for every other place where we use this controller.
We have to use directive when we have same logic in a controller of a directive and same HTML markup.
So in your case it is definitely a directive.
This is my own common sense of course, and may not be ideal.
There are three things you will require to implement this functionality:
AngularJS Template a.k.a. Markup to display quote with an image next to it.
AngularJS service to encompass the REST call in order to fetch above details from the server.
AngularJS controller to consume the AngularJS service to feed the data back the template (point 1) to update it accordingly after every rest call.
So the fact is you can achieve this without even writing an AngularJS Directive but what if you need to replicate the same feature in many places. In that sense, you will probably have to copy the same template somewhere else which will again need a separate controller to consume the same service (as using the same controller multiple times in the DOM is not recommended and a bad practice).
With the Directive API, you can put the markup in a directive template and consume the service in a directive controller to render the UI. So the next time if you want multiple instance of the widget, you just have to inject the directive, that's it - rest will work without any issue.
App = angular.module('App', []);
App.directive('welcomeQuote', function(QuoteService) {
return {
restrict: 'E',
template: '<div><img ng-src="{{quote.img}}" /><span ng-bind="quote.title"></span></div>',
controller: function(scope) {
// returns {img: 'angular.png', title: 'AngularJS';
QuoteService.fetch().then(function(data) {
scope.quote = data;
});
}
}
});
App.factory('QuoteService', function($http) {
return function() {
fetch: function() {
return $http.get('http://quote-server.com/new')
}
};
});
Finally you can use the widget as:
<welecome-quote></welcome-quote>
I'm writing an AngularJS app that gets a list of posts from a server, and then uses an ngRepeat and a custom post directive to output all the posts.
Part of the post object is a blob of html, which I currently add to the directive by first doing an $sce.trustAsHtml(blob), and then using the ng-bind-html directive and passing the trusted html blob to it. It works fine, but now I want to modify the html before adding it to the output. For instance, I want to find all link tags and add a target="_blank" to it. I also want to remove any content editable attributes from any element. etc.
What is the best way of doing this? I was thinking of just loading it up in a document fragment and then recursively iterating through all of the children doing what I need to do. But I assume there is a better AngularJS way to do this?
EDIT:
here is a codepen with an example of what I have:
http://codepen.io/niltz/pen/neqlC?editors=101
You can create a filter and pipe (|) your content through it. Something like:
<p ng-bind="myblob | myCleanupFilter">
Your myCleanupFilter would look something like this (not tested):
angular.module('myApp').filter('myCleanupFilter', function () {
return function cleanup (content) {
content.replace('......') // write your cleanup logic here...
};
});
If you want to add attributes that are themselves directives, then the best place to add them is in the compile function in a custom directive.
If they are just plain old attributes, then there's nothing wrong with hooking into DOM ready in your run block, and adding your attributes with jquery.
var app = app.module('app',[]);
app.run(function ($rootScope){
$(document).ready(function()
$rootScope.$apply(function(){
$('a').attr('title','cool');
});
})
});
If you want add the attributes after the compile phase but before the linking phase in the angular life cycle then a good place to do it is in the controller function for a directive that's placed on the body element.
<body ng-controller="bodyCtrl">
</body>
app.controller('bodyCtrl', function($element){
$('a', $element).attr('title','cool');
});
During the compile phase angular will walk the DOM tree, matching elements to directives, and transforming the HTML along the way. During the link phase, directives will typically set up watch handlers to update the view when the model changes. By placing a directive on the body element, it ensures that all directives have been compiled, but the linking phase hasn't started yet.
I'm using this http://plnkr.co/edit/sqXVCJ?p=preview on my Angular UI accordion. I've put the directive attribute on the anchor (which is in the template) and I've overriden the template to remove the ng-click="isOpen = !isOpen" and replaced it with a function call "callSubmit". All the accordions have views loaded into them, all of the views have forms as well. The purpose of the callSubmit function is to submit the form which works fine but I get the error above.
I've read through various posts regrding adding the $timeout service (which didnt't work) and adding the safe apply method which gave me recursion and digest errors.
I'm not sure what else I can try, the function works fine, but I just keep getting that error.
!--- button
<button type="submit" class="btn btn-default hidden" id="btnSubmit_step1" ng-click="submitForm()">Trigger validation</button>
!-- function
$scope.callSubmit = function() {
document.getElementById('btnSubmit_step1').click();
};
edit: The reason that I have been triggering the button click from the controller is due to the fact that the form method is in another scope & a different controller.
So if I use broadCast from the rootScope like below. I get the broadcast event to the other controller without issue. The controller that receives the broadcast has the form in it's scope but when I try to execute the function I get nothing, no form submission at all.
$$scope.callSubmit = function() {
//document.getElementById('btnSubmit_step1').click();
$rootScope.$broadcast('someEvent', [1,2,3]);
};
$scope.$on('someEvent', function(event, mass) {
$scope.submitForm();
});
Don't click the button from a controller.
$scope.callSubmit = function() {
$scope.submitForm();
};
The documentation is clear...
Do not use controllers to:
Manipulate DOM — Controllers should contain only business logic.
Putting any presentation logic into Controllers significantly affects
its testability. Angular has databinding for most cases and directives
to encapsulate manual DOM manipulation.
Clicking a button on a page is manipulating the DOM.
Using AngularJS and UI Bootstrap, I want to dynamically add alerts to DOM. But if I dynamically add an <alert> element to DOM, it's not compiled automatically. I tried to use $compile but it doesn't seem to understand tag names not present in core AngularJS. How can I achieve this? Is it even the right way to "manually" add elements to DOM in services?
See Plunker. The alert in #hardcodedalert is compiled and shown correctly but the contents of #dynamicalert are not being compiled.
Edit:
I'd later want to have alerts shown on different context and locations on my web page and that's why I created a constructor function for the alerts, to have a new instance in every controller which needs alerts. And just for curiosity's sake, I was wondering if it's possible to add the <alert> tags dynamically instead of including them in html.
I've updated your plunker to do what you're trying to do the "angular way".
There are a few problems with what you were trying to do. The biggest of which was DOM manipulation from within you controller. I see you were trying to offset that by handling part of it in the service, but you were still referencing the DOM in your controller when you were using JQuery to select that element.
All in all, your directives weren't compiling because you're still developing in a very JQuery-centric fashion. As a rule of thumb you should let directives handle the adding and removing of DOM elements for you. This handles all of the directive compiling and processing for you. If you add things manually the way you were trying, you will have to use the $compile provider to compile them and run them against a scope... it will also be a testing and maintenance nightmare.
Another note: I'm not sure if you meant to have a service that returned an object with a constructor on it, so I made it just an object. Something to note is that services are created and managed in a singleton fashion, so every instance of that $alertService you pass in to any controller will be the same. It's an interesting way to share data, although $rootScope is recommended for that in most cases.
Here is the code:
app.factory('alertservice', [function() {
function Alert() {
this.alerts = [];
this.addAlert = function(alert) {
this.alerts.push(alert);
};
}
return {
Alert: Alert
};
}]);
app.controller('MainCtrl', function($scope, alertservice) {
var myAlert = new alertservice.Alert();
$scope.alerts = myAlert.alerts;
$scope.add = function() {
myAlert.addAlert({"text": "bar"});
};
});
Here are the important parts of the updated markup:
<body ng-controller="MainCtrl">
<div id="dynamicalert">
<alert ng-repeat="alert in alerts">{{alert.text}}</alert>
</div>
<button ng-click="add()">Add more alerts...</button>
</body>
EDIT: updated to reflect your request
I've been using directives in AngularJS which build a HTML element with data fetched from the $scope of the controller. I have my controller set a $scope.ready=true variable when it has fetched it's JSON data from the server. This way the directive won't have to build the page over and over each time data is fetched.
Here is the order of events that occur:
The controller page loads a route and fires the controller function.
The page scans the directives and this particular directive is fired.
The directive builds the element and evaluates its expressions and goes forward, but when the directive link function is fired, it waits for the controller to be "ready".
When ready, an inner function is fired which then continues building the partial.
This works, but the code is messy. My question is that is there an easier way to do this? Can I abstract my code so that it gets fired after my controller fires an event? Instead of having to make this onReady inner method.
Here's what it looks like (its works, but it's messy hard to test):
angular.module('App', []).directive('someDirective',function() {
return {
link : function($scope, element, attrs) {
var onReady = function() {
//now lets do the normal stuff
};
var readyKey = 'ready';
if($scope[readyKey] != true) {
$scope.$watch(readyKey, function() {
if($scope[readyKey] == true) {
onReady();
}
});
}
else {
onReady();
}
}
};
});
You could use $scope.$emit in your controller and $rootScope.on("bradcastEventName",...); in your directive. The good point is that directive is decoupled and you can pull it out from project any time. You can reuse same pattern for all directives and other "running" components of your app to respond to this event.
There are two issues that I have discovered:
Having any XHR requests fire in the background will not prevent the template from loading.
There is a difference between having the data be applied to the $scope variable and actually having that data be applied to the bindings of the page (when the $scope is digested). So if you set your data to the scope and then fire an event to inform the partial that the scope is ready then this won't ensure that the data binding for that partial is ready.
So to get around this, then the best solution is to:
Use this plugin to manage the event handling between the controller and any directives below:
https://github.com/yearofmoo/AngularJS-Scope.onReady
Do not put any data into your directive template HTML that you expect the JavaScript function to pickup and use. So if for example you have a link that looks like this:
<a data-user-id="{{ user_id }}" href="/path/to/:user_id/page">My Page</a>
Then the problem is that the directive will have to prepare the :user_id value from the data-user-id attribute, get the href value and replace the data. This means that the directive will have to continuously check the data-user-id attribute to see if it's there (by checking the attrs hash every few moments).
Instead, place a different scope variable directly into the URL
My Page
And then place this in your directive:
$scope.whenReady(function() {
$scope.directive_user_id = $scope.user_id;
});