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">
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.
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
Use Case
The application I'm working on is fairly dynamic. Inputs change dynamically based on a template variable. The directive I have, based on a variety of scope variables, will grab the necessary templates and build the html output. Events are also associated with each of these templates and so the childScope needs to be destroyed in order to fire the appropriate destroy events when the scope changes. In particular, I found that if I don't call the destroy method, a memory leak will occur.
[edits]
To expand the definition further, essentially we have a generic widget. For people that can see a button is rendered that changes value based on the number of times it is clicked. For the visually impaired, this button is rendered as a list of radio buttons. The buttons are essentially "hotspots" rendered on top of an image. This is used to evaluate pain scores on joints. However, visually impaired patients can't see the buttons, but the screen reader can read out the radio inputs. A tablet is used for data capturing and passed between researcher, patient and doctor. A toggle button will switch between visual mode and visually impaired mode as it is quicker for the doctors to comprehend the image view. WiFi is an issue as connection in the hospital is spotty, so I used angular to make a SPA for this.
So how it works is there is a variable called template which is passed into the directive. The link will read this variable and load the appropriate templates which is cached. When the button is pressed, template may change and the link will re-read this. The issue I found is that the jQuery slider will cause a memory leak when I switch it from a slider to a radio list input for the visually impaired. The more times the view is switched, the more memory is consumed. However, if the destroy method is called before the $compile, the memory leak issue disappears. Unfortunately, calling scope.$destroy destroys everything, so a childScope was used to prevent this.
This could be completed using ng-if or ng-switch on the template variable, however, we took the programmatic route because essentially, a requirement that is coming up is that these widget should change dynamically in multiple views. Currently we only have visual and visually impaired mode, but they also want it to have "doctor view", "researcher view", "patient view", "patient visually impaired view", "elderly view", etc... Depending on the view, the widget will change its appearance and functionality. i.e. some views may have additional behaviours.
Problem
I'm unable to bind the model to the generated childScope. I think the fundamental problem is the $new() creates an isolated scope, so I suspect that it can't communicate with the outside world. However, what I really want to do is destroy the scope properly to avoid the memory leak.
My question is:
Is there a way to have the childScope bind with the parent ng-model?
Is there a another method to fire destroy?
Code
Here is a stripped down and simplified version of my non-working code. I've removed most of the complexity to reduce it down to the fundamental components.
Template
<div ng-app="app" ng-controller="ctrl">
<div>
<h1>Input</h1>
<doodad ng-model="foo"></doodad>
<nested-doodad ng-model="bar"></nested-doodad>
<nested-doodad ng-model="qux.value"></nested-doodad>
</div>
<div>
<h1>Output</h1>
<span>{{ foo }}</span>
<span>{{ bar }}</span>
<span>{{ qux | json }}</span>
</div>
</div>
Application
function ctrl($scope) {
$scope.foo = "foo";
$scope.bar = "bar";
$scope.qux = { value : "qux" };
}
angular
.module('app', [])
.directive('doodad', function($compile) {
var linker = function(scope, element) {
var childScope;
/* Used to fire destroy on child widgets */
var getNewScope = function(oldScope) {
if (oldScope) {
oldScope.$destroy();
oldScope = null;
element.html('');
}
return scope.$new();
};
var renderTemplate = function() {
childScope = getNewScope(childScope);
/* template is dynamic - hardcoded for example */
/* events & lots of other funny stuff are bound here */
element.html('<input type="text" ng-model="ngModel" />');
$compile(element.contents())(childScope);
}
scope.$watch('template', renderTemplate);
}
return {
restrict: 'E',
replace: true,
require: '^ngModel',
scope: {
ngModel : '=',
template: '='
},
link: linker
}
})
.directive('nestedDoodad', function() {
return {
restrict: 'E',
replace: true,
scope: {
ngModel : "="
},
template: '<div><doodad ng-model="ngModel"></doodad></div>'
}
});
JSFiddles
Non-working Code
Working Example without the childScope
Is there a way to have the childScope bind with the parent ng-model?
$scope = $parent.$scope;
Is there a another method to fire destroy?
angular.element(document).injector().get('$rootElement').removeClass("ng-scope")
References
AngularJS: Developer Guide: Working With CSS
Background
If any of you are familiar with the Evernote desktop application, when adding tags to a note, you are able to start typing the name of the tag. Once you begin typing, "help text" appears to assist you in selecting the right tag. So it functions as a filter. I need a control that functions just like this for my web application.
What I Have So Far
In my controllers.js file, I have defined the following property for temporary testing. This property will be populated with data from an API once I get this working with test data.
$scope.data.types = {Types: [{name: 'Developer'}, {name: "\"Developer Company\""}, {name: 'Accountant'}, {name: "\"Legal Counsel\""}]};
In my route template file contacts-edit.html, I have the following piece of relevant code. Basically, I have an editable <div> that pulls in and displays persistent tags (i.e. tags that have been saved for this contact in the database) from $scope.data.contacts.
<div class="form-group">
<label for="entity_types" class="control-label col-sm-2">Tags</label>
<div class="col-sm-5">
<div class="form-control" contenteditable="true" ng-model="data.contact.EntityTypes" my-Directive my-Other-Directive></div>
</div>
</div>
In my directives.js file resides code for myDirective and myOtherDirective. The myDirective directive uses the ngModel to provide 2-way data binding. Thus, when a user types in the div, on blur the tags are added to the model and formatted to look similar to a tag. These tags are space delimited and will allow for spaces for tags between double quotes to be ignored. So myDirective functions the way I want it to.
However, I'm a little stumped on how to provide the filter functionality which I'll look to provide through the myOtherDirective directive. I'm probably going about this all wrong so any help would be beneficial.
The myOtherDirective Directive
So the idea here is that as soon as the end user starts typing, a <span> displays directly beneath the <div> that displays filtered results that is using data from scope.data.types.Types as the model data to filter against. The results do not need to be clickable or anything, they are merely there to assist the end user in entering the correct tag for the contact.
Code
directives.directive('myOtherDirective', [function() {
var template = "<span class='help-block'>" +
"<ul class='list-unstyled list-inline'>" +
"<li ng-repeat='type in data.types.Types'><small>{{type.name}}</small></li>" +
"</ul></span>";
function link(scope, element, attrs) {
element.on('focus', function(e) {
scope.$apply(function() {
element.parent().append(template);
});
});
}
return {
restrict: 'A',
link: link,
scope: {
type: '='
},
template: template
};
}]);
So, I was able to get the help block to display as soon as the <div> gains focus (it's removed on blur by the way). I couldn't get the data binding to work on data.types.Types and was at a loss for where to go next. Any help would be much appreciated.
If you are trying to get autocomplete working for an input box I would use ui-bootstrap's typeahead
When you pass the type parameter into your directive it causes it to have an isolated scope. This means that you won't be able to access $scope.data form inside your directive. To fix this you could pass data in as well.
scope: {
type: '='
data: '='
}
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.