What Im trying to accomplish: I have jQuery plugin that I want to wrap to be an angular directive. And to do this i need to pass params to it, and the plugin have it ownonchange` even where I'm trying to change passed values so it will be reflected in the original scope. And I get some really unexpected and strange results.
Here is fiddle number one:
http://jsfiddle.net/q1915b38/2/
Here I tried to simulate minimal example of what i want to accomplish. But as you see it just does not work at all. Value in the original controller scope doesn't change. But in real world example it act a bit differently.
And here goes fiddle number 2.
http://jsfiddle.net/ne5hbgxp/
The only thing i changed from first one is template from template:
template: "<input type='text' id='blah' />",
to
template: "<input type='text' id='blah' ng-model='abc' />",
Basically i added to template an ng-model attribute which I don't use anywhere at all. But it just goes from totally not working to working with glitches. Now when change trigger first time nothing happens. But when it triggers second time - value from previous change got passed into original scope. When I change 3 time value - the value from second time goes to controller. And so on. So basically it have a delay with one step back for unknown for me reason. And this is exact behavior that I face in my real world example, although there no ng-model at all and all content generate via jQuery plugin.
So basically my questions are following:
1) Why its not working in first example
2) Why its working in second example with this strange behavior with one step delay? What the logic on this behavior?
3) What is a correct way to solve this ?
Since you're using jQuery to update something in your directive, a call to $apply() is needed to trigger an angular digest cycle
link: function(scope, iElement, iAttrs, controller) {
$('#blah').change(function() {
scope.value = $(this).val();
scope.$apply();
});
}
JSFiddle Link
However looking at this a bit closer, is there a reason why you prefer jQuery .change() in this example? Angular offers ngChange, which may be just what you are looking for, since you will be alleviated from explicitly calling a digest cycle since we're in Angular world and not battling jQuery so to speak. An example may include...
<input type='text' id='blah' ng-model='abc' ng-change='update()'/>
scope.update = function() {
scope.value = scope.abc;
}
JSFiddle Link with ng-change
Issue is pretty simple... events that change scope that are outside of angular's core directives aren't visible to angular so you you need to notify angular to perform a digest so the view can be updated.
This is done with $apply() or can use $timeout() to prevent calling $apply() while another digest cycle is in progress
link: function (scope, iElement, iAttrs, controller) {
$('#blah').change(function () {
var $el =$(this);
scope.$apply(function () {
scope.value = $el.val();
})
});
}
I would suggest taking advantage of the iElement being exposed in the directive. This is a jQuery object when jQuery is included in page prior to angular
Related
I've recently switched from jQuery to Angularjs and I am in the process of re-coding some pagination logic for the links ("Next", "Previous", etc.) that were written in jQuery-style Javascript previously.
Each link has an ngIf condition (for example, the "Previous" link won't show if you're on page 1) plus an ngClick event, which essentially updates a scope variable called $scope.pagination.position that determines which results are displayed in the table.
My original code was something like this (simplified for clarity):
Template
<a ng-if="pagination.position > 0" ng-click="pagination.first()">First</a>
Controller
$scope.pagination = {
first: function() {
this.position = 0;
}
}
Then I learned more about directives, and how most DOM elements that aren't static HTML should be created using a directive. So I switched each link (since each has it's own display rules and behaviour on clicks) to its own directive, like so:
Template
<a pagination-first></a>
Directive
app.directive('paginationFirst', function() {
return {
link: function(scope,el,attr) {
scope.pagination.first = function() {
scope.pagination.position = 0;
}
},
replace: true,
template: '<a pagination-first ng-if="pagination.position > 0" ng-click="pagination.first()">First</a>'
}
});
I'll cut straight to the chase : am I doing directives wrong? All that's happened, from my perspective, is I've flipped from having logic in my template to having a template in my logic, and I've defined the click event function in the directive rather than in the controller.
Is this even an appropriate time to be using a directive?
I'd like to learn best practices, so I'd love to know if I've missed the point and if the original templated-based ngIf and controller function approach was fine, even with longer and more complex ngIf conditions than the one shown.
If I want to add specific behaviors to a dom or dom list then I normally create a directive. As per angular js perspective the dom manipulation should only be done through directive (For me it is the best place, sometime I have to disobey this due to my lack of knowledge ). I specially found directive use full while creating a widget. In one of my project there was a part where a section is dedicated to display an image and also upload the image. I just use the directive on the top div, with the help of link function I attached the event handlers to various child dom. And as my project doesnot require an isolated scope (as this widget was all used in a single project and the outer scope was under my control) . So it worked like a charm. I cerarted the directive once. And used that widget through rest of the project as it's behavior and design (of the widget ) was same through out the project. For the pagination widget you can create a directive. Take the directive attibutes value as the input of the pagination parameters. Like calling script, limit offset. Container identifier to update the content. Then you can solely concentrate on the pagianation behavior. But from my experience (as I am also not so experienced in angular js), sometimes it becomes a little hectic to develop a directive and and use that throughout the project. As in some places we need to modify the behavior of the directive. And for this it may breaks elsewhere. But I know as I learn more I will be more efficient to handle this kind of situation. Hope my experience will help you.
So I have been working on this issue for a week now and i cannot seem to get my head around this whole Directive thing. I have read lots of posts ...
Demystifying Directives
Directives
Compile, Pre and Post Linking
a bunch of videos ...
Creating Reusable Directives in AngularJS
Writing Directives
And gone through StackOverflow and other forums (links to follow) hoping something will sink in ... I think that the problem that I am running into is that I want to UNDERSTAND why/how these work so that I am not cut/pasting someone else's solution into my code but then having to ask again later when something else crops up because I don't know what my pasted code is doing.
I am finding however that everyone has a different way to skin this cat and none of them seem to match up with my understanding of HOW this is supposed to work.
What I am attempting to do is build a form using the Metro UI CSS library. I thought I would start with a simple text-box. yep ... just a simple text box. A Metro UI text-box has some nice built in functionality that I wanted to preserve so I thought that was good place to start.
I read that in order to leverage Metro UI behaviors with AngularJS I would need to wrap it in a custom directive (Custom data-directives inside an AngularJS ng-repeat). While this example wasn't exactly what I was looking for it seemed to easily explain what I needed to do. Just call the function that applies the behavior in the LINK function of the directive and add the directive attribute to the input element ...
So I created a directive called 'metroInputTransform" and added it as an attribute to an input element.
<div data-ng-controller="pageOneFormCtrl as page">
<input type="text" id="txProductName"
data-ng-model="page.data.productName"
data-metro-input-transform=""
placeholder="product name" />
</div>
In the LINK function of the directive I simply called the method that applies the behavior I was looking for. I know that this is a little more verbose than it needs to be but I am trying to learn it so I am stepping through it as best as I can. ... (for full code see this fiddle)
var metroDirectives = angular.module('metroDirectives', []);
metroDirectives.directive('metroInputTransform', function ($compile) {
function postLink($scope, element, attrs, controller) {
$(element).inputTransform();
};
return {
priority: 100,
compile: function (element, attrs) {
return { postLink };
}
};
});
So this worked, partially. It created the Metro look and feel and associated behavior, but ... ngModel was not binding to the element. So this began a long journey through concepts such as isolate scope, breaking out the various compile, controller, pre-link, post-link functions, at least two different ways of persisting ngModel ... all of which did not work.
After a variety of reading it was my understanding that the DOM manipulation should happen in the COMPILE function so that any DOM transformations would be available for the compile and then linking stages of the digest process. So I moved the inputTransform() call to the COMPILE function ... (fiddle)
return {
priority: 100,
terminal: true, // if I didn't put this everything would execute twice
compile: function (element, attrs) {
$(element).inputTransform();
return {
pre: preLink,
post: postLink
};
}
};
No Luck ... same thing ... not binding to ngModel. So I discovered the concept of "isolate scope" ...
Understanding Isolate Scope - video
Using Isolate Scopes in Directives - video
Using ngModel with Isolate Scope
Based on that I tried the following (fiddle)...
return {
priority: 100,
scope: {
ngModel : '='
},
terminal: true, // if I didn't put this everything would execute twice
compile: function (element, attrs) {
$(element).inputTransform();
return {
pre: preLink,
post: postLink
};
}
};
No change ...
I tried a number of other things but am afraid I may lose you attention soon if I have not already. The closest I got was ONE-WAY binding doing something like below ... and even here you can see that the extraction of the ngModel reference is utterly unacceptable. (fiddle)
var metroDirectives = angular.module('metroDirectives', []);
metroDirectives.directive('metroInputTransform', function () {
function postLink($scope, element, attrs, controller) {
//
// Successfully perfomes ONE-WAY binding (I need two-way) but is clearly VERY
// hard-coded. I suppose I could write a pasrsing function that would do this
// for whatever they assign to the ngModel ... but ther emust be a btter way
$(element).on("change", '[data-metro-input-transform]', function(e) {
$scope.$apply(function(){
$scope['page']['data']['productName'] = e.currentTarget.value;
});
});
};
return {
priority: 100,
terminal: true, // if I didn't put this here the compile would execute twice
compile: function (element, attrs) {
$(element).inputTransform();
return {
pre: function ($scope, element, attrs, controller, transcludeFn) { },
post: postLink
};
}
};
});
I am EXHAUSTED and have absolutely no idea what's left to try. I know that this is a matter of my ignorance and lack of understanding on how/why AngularJS works the way it does. But every article I read leaves me asking as many questions as were answered or takes me down a rabbit hole in which I get more lost than I was when I started. Short of dropping $3000 on live in-person seminars that I cannot afford where I can ask the questions I need answered, I am at a complete dead end with Angular.
I would be most grateful if anyone could provide guidance, direction ... a good resource ... anything that can help shed some light on this issue in particular, but anything that might help me stop spinning my wheels. In the mean-time I will continue to read and re-read everything I can find and hopefully something will break.
Thanks
G
UPDATE - 10/30/2014
I am soooo over this issue but want to follow it through. I need and want to learn this. Also I really want to express appreciation for the effort that folks have put into this and while they have presented some solutions, which ultimately may be the best way to go, they have both skirted the issue, which is that I am attempting to use the behaviors provided with the Metro UI CSS library. I would prefer to not have to rewrite them if possible.
Both solutions provided so far have eliminated the key statement from the solution ... which is the line ...
$(element).inputTransform()
I don't want to post the entire jQuery widget that comprises the "inputTransform" definition, but I cut the meat of it out and included it here ...
function createInputVal(element, name, buttonName) {
var wrapper = $("<div/>").addClass("input-control").addClass(name);
var button = $("<button/>").addClass(buttonName);
var clone = element.clone(true); // clone the original element
var parent = element.parent();
$(clone).appendTo(wrapper);
$(button).appendTo(wrapper);
$(wrapper).insertBefore(element);
$(element).remove(); // delete the original element
return wrapper;
};
So, I have applied the directive as an attribute because the Metro code behind it wants to CLONE the text-box (which would not do if it was an element directive) and then REMOVES the original input element. It then creates the new DOM elements and wraps the cloned input element in the newly created DIV container. The catch, I believe is ... that the binding is being broken when the original element is being cloned and removed from the DOM. Makes sense, if the "ng-model" attribute assignment is bound to a reference of the text-box. So the expectation that I originally had was, since the "ng-model" attribute was cloned along with the rest of the element, that in the compile event/function/phase of the directive the reference would be(re)established to the newly created input element. This apparently was not the case. You can see in this updated fiddle that I have made some attempts at reconnecting the ng-model to the new DOM elements with no success.
Perhaps this is impossible ... it certainly seems that just re-building these things may ultimately be the easier way to go.
Thanks again Mikko Viitalia and 'azium' ...
Directives are not the easiest concepts out there and documentation is really not that good and it's scattered around the interwebs.
I struggled with compile, pre-compile and such when I tried to write my first directives but to date I have never needed those functions. It might be due to my lack of understanding but still...
Looking at your examples I see there's some basic things that needs clarification. First of all, I'd restrict your directive to Element since it's replacing the control in HTML. I'd use Attribute e.g. to add functionality to existing control.
There is a (mandatory) naming convention where you use dashed naming in HTML and camel casing inside your JavaScript. So something-cool becomes somethingCool. When you "bind" variables to directive's scope, there's a major difference on how you do it. Using = you bind to variable, using # to variables evaluated (string) value. So first allows the "two-way binding" but latter of course, not. You can also use & to bind to parent scope's expression/function.
If you use e.g. plain = then directive's scope expects same name in your HTML. If you wish to use different name, then you add variable name after the =. An example
ngModel : '=' // <div ng-model="data"></div>
otherVar: '#someVar' // <div some-var="data></div> or <some-var="data"></some-var>
I took liberty to take your first Fiddle of metro-input-transform as starting point and rewrite it in Plunker. I'm trying to explain it here (and hope I understood you right).
Metro input directive
directives.directive('metroInput', function () {
return {
restrict: 'E',
scope: {
ngModel: '=',
placeholder: '#watermark'
},
link: function (scope) {
scope.clear = function () {
scope.ngModel = null;
};
},
templateUrl: 'metro-template.html'
};
});
Directive expects ngModel to bind to and watermark to show when ngModel has no value (text input is empty). Inside link I've introduced clear() function that is used within directive to reset ngModel. When value is reset, watermark is show. I have separated the HTML parts into a separate file, metro-template.html.
Metro input HTML template
<input type="text" ng-model="ngModel" placeholder="{{ placeholder }}">
<button type="button" class="btn-clear" ng-click="clear()">x</button>
Here we bind ngModel to input and assign placeholder. Button showing [X] is bound to clear() method.
Now when we have our directive set up, here's the HTML page using it.
HTML page
<body>
<div ng-controller="Ctrl">
<section>
The 'Product name' textbox in the 'Directive'
fieldset and the textbox in the 'Controls'<br>
fieldset should all be in sync.
</section>
<br>
<fieldset>
<legend>Directive</legend>
<label for="productName">Product name</label>
<br>
<metro-input name="productName"
ng-model="data.productName"
watermark="product name">
</metro-input>
</fieldset>
<br>
<fieldset>
<legend>Control</legend>
<input detect-mouse-over
type="text"
ng-model="data.productName">
</fieldset>
</div>
</body>
So in above example usage of metro directive is as follows. This will be replaced with directive's HTML template.
<metro-input name="productName"
ng-model="data.productName"
watermark="product name">
</metro-input>
The other input has detect-mouse-over directive applied to it, restricted to Attribute just to show usages/differences between A and E. Mouse detection directive makes input change background-color when mouse is moved over/out of it.
<input detect-mouse-over
type="text"
ng-model="data.productName">
.
directives.directive('detectMouseOver', function () {
return {
link: function (scope, element, attrs) {
element.bind('mouseenter', function () {
element.css('background-color', '#eeeeee');
});
element.bind('mouseleave', function () {
element.css('background-color', 'white');
});
}
};
});
It also has same ng-model to mirror changes between controls.
In your example you also had a productService that provided the value to above input controls. I rewrote it as
Product service
app.service('productService', function () {
return {
get: function () {
return { productName: 'initial value from service' };
}
};
});
So get() function just gets the hard coded value but it still demonstrates use of services. Controller, named Ctrl is really simplistic. Important part here is that you remember to inject all services and such into your controller. In this case angular's $scope and our own productService.
Controller
app.controller('Ctrl', function ($scope, productService) {
$scope.data = productService.get();
});
Here a screen capture of above solution.
Changing value in any of the inputs changes value of both. Input below has "mouseover" so it's greyish, mouseout would turn it white again. Pressing [X] clears the value and makes placeholder visible.
Here's the link to plunker once more http://plnkr.co/edit/GGGxp0
Ok I'm not exactly sure what other advantages from the Metro UI you're getting, but here's a simple fiddle that doesn't need your directive at all to capture what you had in your first fiddle that works for me. http://jsfiddle.net/f0sph1vp/7/
<input placeholder="{{page.placeholder}}"
ng-model="page.data.productName"
ng-focus="page.data.productName=''">
<button ng-click="page.data.productName=''">x</button>
The second fiddle you posted, http://jsfiddle.net/gary_stenstrom/xcx2y8uk/64/, is pretty weird to me, because it doesn't seem like you want the second input box to be the same model as your first one. It kind of seems like you want the clicking of the x button to assign the value of the first input to the second. Which makes a lot more sense.
<input ng-model="data.first">
<button ng-click="data.second = data.first; data.first=''">X</button
<input ng-model="data.second">
I have defined a directive in angular.js. That directive has a link function and a controller function, and no template, so all the view is generated in the link function. In the link function I am doing the following:
var button=angular.element("<a>");
button.addClass("ng-click: previousLink();");
//previousLink() is a function defined in the scope.
//I am doing it like that, because before that one I attempted to do:
//button.prop("ng-click", "previousLink()");
//button.text("Previous");
//but for some reason it was showing on html as <a>Next</a>, without adding the property.
It does not work. If I click the button it does nothing. If, instead of doing this in the link function in code I were doing it using a template, it would work. For some reason I need to make some manipulations using jquery in the link function. What should I do? Is there anyway to make this work, or would I have to use both template and link function and combine things there?
To use $compile you need to follow your existing code with something like:
$compile(button.contents())(scope);
If you want it to be dynamic, you can put this inside a $watch like so:
link: function (scope, ele, attrs) {
scope.$watch(attrs.yourval, function(html) {
var button=angular.element("<a>");
button.addClass("ng-click: previousLink();");
$compile(button.contents())(scope);
});
}
$compile attaches the scope (supplied as a parameter) to the html you have defined. This will make your button clicks work properly.
i am trying to write a directive that replaces an input field with an custom made input field. However, I can not get the databinding to work as the model does not show in the directive input field.
I have created a jsFiddle here:
http://jsfiddle.net/6HcGS/392/
I guess i dont really know what to place here for the databinding to work:
tElement.replaceWith('<input ng-model="ngModel" type="text" />');
If anybody could help me out i would be very grateful as this has been a problem for me for a whole day now.
Cheers!
tElement.replaceWith('<input ng-model="ngModel" type="text" />');
Angularjs doesn't know that ngModel is a binding. It's interpreted as a simple string. So you need to tell angular this.
I've updated your jsfiddle to show you how to do this:
http://jsfiddle.net/6HcGS/393/
But you can do it even simpler by removing the isolated scope in the directive:
http://jsfiddle.net/6HcGS/394/.
Like lort already mentioned the attributes are getting passed to the element during replacement. Of course only if you dont use isolated scope.
I don't understand what you're trying to do but it seems that following code example is all you need:
angular.module('zippyModule', [])
.directive('zippy', function(){
return {
restrict: 'C',
replace: true,
template: '<textarea></textarea>',
}
});
This one changes initial input into textarea. Binding through ng-model still works because other attributes are not deleted from element during replacement.
I am having a serious performance issue in my application. I am using angular and ng-grid. After some reading for why my app is slow, I was directed to use bindonce directive to overcome potential Angular performance issues.
So I added bindonce.js in my solution and injected the directive in my module
HomeIndexModule = angular.module("HomeIndexModule", ['ngGrid', 'pasvaz.bindonce']);
and I am using as below in markup
<div class="gridStyle " bindonce data-ng-grid="gridOptions"></div>
I am not sure whether this is actually unbinding the grid.
Question 1: Has anyone undergone the process could direct me how to do this as I could find examples only for ng-repeat in the bindonce website.
Question 2: how to verify whether the bindonce is actually working?
I have mentioned this twice in other posts, I have created my own bind-once directive that is tiny and does a perfect job, personally i think the plugin is OVER-COMPLICATING things.
Check this out
app.directive('bindOnce', function() {
return {
scope: true,
link: function( $scope ) {
setTimeout(function() {
$scope.$destroy();
}, 0);
}
};
});
<div class="gridStyle" bind-once ng-grid="gridOptions"></div>
Demo: http://plnkr.co/edit/4cBOEG?p=preview
Similar Post:
Genuinely stop a element from binding - unbind an element - AngularJS
This change fixed the performance lag, the change is commenting out the self.resizeOnData() in ng-grid.js line number 1420.
$scope.$on("ngGridEventData", function () {
//self.resizeOnData(temp);
Chrome event pro-filer showed this method being called too many times and looks like it is re sizing all the cells in the grid on change of data-source. I am still testing to find the side effects but till now all the previous functionalities are working and the performance was increase 5X than my previous one.
if you see this change break any thing else let me know
You should read the documentation thoroughly. Using just bindonce won't give you the effect you want. Look at this example I've created: http://plnkr.co/edit/GXkLWfFpfdJvPVyRMtpO - $timeout is used to call $apply every second. Two elements have bindings to the same functions which logs to console the text passed as a parameter. As you can see, using just bindonce doesn't work - the just bindonce text is still being logged, whilst with bo-text appears only once. One of bo-text, bo-html etc. must be used to achieve binding only once.
So, in your case, you need to modify templates of the ngGrid directive and replace every normal binding you want with bo-* directives. Here: How to render html formatted text in ng-grid column header I've explained how to do that.