Monday, May 20, 2024
 Popular · Latest · Hot · Upcoming
55
rated 0 times [  62] [ 7]  / answers: 1 / hits: 17347  / 10 Years ago, thu, october 23, 2014, 12:00:00

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 ...





a bunch of videos ...





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 ...





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' ...


More From » html

 Answers
25

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.



imgur



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


[#69031] Tuesday, October 21, 2014, 10 Years  [reply] [flag answer]
Only authorized users can answer the question. Please sign in first, or register a free account.
kevonmoisesf

Total Points: 693
Total Questions: 101
Total Answers: 128

Location: Reunion
Member since Mon, Dec 28, 2020
3 Years ago
kevonmoisesf questions
Sat, Jan 23, 21, 00:00, 3 Years ago
Tue, Feb 18, 20, 00:00, 4 Years ago
Wed, Jun 12, 19, 00:00, 5 Years ago
;