Directive implementation advice using attribute string binding - angularjs

I am new to AngularJS and directives and am seeking some advice or guidance on a directive I have implemented. The directive in question will be used to display pdf's to the user. The directive exposes two attributes, documentPath and documentType that are defined using isolated scope as follows:
var directiveDefinition = {
templateUrl: 'app/views/directives/document.html',
scope: {
documentPath: '#documentPath',
documentType: '#documentType'
},
restrict: 'E',
templateNamespace: 'html',
link: linkFunc
};
In the view that uses the directive, I bind the properties using a model property for the view controller and a string.
<my-document document-path="{{ application.documentpath }}" document-type="Application"></my-document>
When I initially ran this, I found that the directive would sometimes run before the data had been returned by the model. So an empty document would be displayed. Other times, the model would load before the directive ran, so the document path would be present when the link function ran, allowing the document to be displayed.
I determined that one way to resolve this was to use a $watch listener on the documentPath attribute of the directive. This seems to resolve the issue.
Being new to AngularJS and also directive implementation my question is...was this the best solution? Any advice would be greatly appreciated. Thanks!

The $watch solution should work fine but depending on the complexity of your app and the number of total watchers this could lead to a slow $digest cycle.
Another option would be to send the parameters as a reference using the "." dot notation so you it will get updated when the data gets loaded from model (from the API most of the time since this is the slow part) instead of sending a string primitive.
Your directive scope declaration would become:
scope: {
documentPath: '=',
documentType: '='
},
and you will use it from the parent view like this:
<my-document document-path="application.documentpath" document-type="application.documenttype"></my-document>
If the documentType is always the same you may leave it as a string scope parameter.

Related

angular.js $scope not attaching to form in external directive

I am working on an angular app. right now we have these directives that are breadcrumbs that guide you through long wizard forms. One of the problems I have run into is if you have an error in one part of the form and click on a breadcrumb to go back, it doesnt let you progress throughout the form (the next button breaks). So the solution to this (I believe) is to not let you click on these breadcrumbs unless the form doesn't have any errors.
These breadcrumbs are in directives. How can I access the form from this external directive? I have tried:
$scope.competitionCreateForm
$scope.$competitionCreateForm
competitionCreateForm.$error
etc and nothing seems to work. However, when I console.log competitionCreateForm I do see the actual form object, so I know that part is working.
Here is what the directive looks like (coffeescript)
angular.module('App').directive 'breadcrumb', [
'WizardHandler'
(WizardHandler) ->
{
restrict: 'E'
scope:
breadcrumbs: '=breadcrumbs'
placeholder: '#'
templateUrl: 'directives/templates/breadcrumbs.html'
#require: ''
controller: ['$scope', ($scope) ->
# checking right here
$scope.goToStep = (step) ->
$scope.currentStepValid = true
WizardHandler.wizard().goTo(step)
]
}
]
I have looked through other answers and it hasn't seemed to work. maybe I need to add something when I'm first creating the form, but I'm not sure. Does anyone know how I can check the status of my form object from an external directive?
So by using the scope property on the directive you are creating an isolated scope for the directive, this means that the directive can't access the parent directive's scope.
You have two options if you wish to continue using an isolated scope:
You can use two way binding like you did with breadcrumbs.
Another option is to use one way bindings i.e. '<' to pass the form state into your directive. This will update the directive when that property changes just as two way binding will but will not allow the form to be modified from the directive.
I was able to get this working by adding
scope:
breadcrumb: '=breadcrumbs'
placeholder: '#'
form: '?=form'
which adds the ability to optionally add a form to the breadcrumb directive
breadcrumb[breadcrumb='breadcrumb' form='createForm']

Angular translation not updating placeholder of Chosen plugin within ng-include $scope

I have an ng-view with multiple instances of the localytics (angular) Chosen plugin. I also have an ng-include with one instance of the plugin. Both rendered on the same page.
I'm using the data-placeholder attribute to render a value which is filtered through the angular-translate plugin.
Initially I was having issues with all Chosen instances rendering the translated text when the method to update the language was being called.
I got around this by calling $route.reload() at the end of the method (not ideal, but acceptable).
I tried:
binding the values for the translations and the translate filter inline
setting them in controllers
watching the properties on the $scope (which never
triggered)
destroying the template before reloading the route
However, the placeholder within the ng-include refuses to update without the use of a hard refresh. Calling $window.location.reload() at the end of the method allows all instances to show the correct translation, but short of this I've not been able to find a way to fix the issue.
I'm assuming it's a scoping issue. Perhaps the Chosen plugin (which is a directive) creates its own scope, then the ng-include has its own scope, as does the ng-view.
All properties that are being translated, outside of the Chosen plugins, are working as expected.
Currently the angular-translate objects look like this:
var translationEN = {
SEARCH: {
'SEARCH-BTN': 'Search'
}
}
So I'm binding them inline as per the following:
<div ng-bind="'SEARCH.SEARCH-BTN' | translate">
I've also attempted some of the methods on $translate, such as $translate.refresh() to no avail.
If anyone has any ideas, any help and / or comments are very much appreciated.
Thanks in advance.
You can use the chosen attribute to pass in some configurations instead of using the data-placeholder attribute, like this:
<select chosen="{'placeholder_text_single': 'Select the options'}"></select>
Or you can write custom attributes that the chosen directive will also accept as configurations. However, when using attributes, the directive will evaluate the expression instead of using the literal value, which won't work as expected for translation purposes, as '{{ ... }}' is not a valid expression. The attributes would be like this:
<select chosen placeholder-text-single="'Select the options'"></select>
A similar problem occurred for me when the options were loaded by a promise.
With just an empty array, the translation worked fine, but putting the promise back in the code caused this behaviour.
A quick debugging in the chosen directive showed that, the element, from which the chosen angular plugin takes the template for the chosen widget, is not linked (or compiled... I'm really new with angular), it still contains the {{placeholder.string | translate}} value for the data-placeholder attribute, however, the attr.placeholder contains the tranlated value.
So this line sets wrong value as the default text: https://github.com/localytics/angular-chosen/blob/master/chosen.js#L57
I extended the chosen directive with a preLink function, which modified the element's data-placeholder attribute with the right value:
angular.module('myModule').directive('chosen', function() {
return {
priority : 1,
restrict: 'A',
link : {
pre : function(scope, element, attr, ngModel) {
var defaultText = attr.placeholder;
angular.element(element[0]).attr('data-placeholder', defaultText);
}
}
}
});

angularjs - get the filters attached to a directive

Using angular, I have a situation where I've written a custom directive, and then some filters.
I have done a lot of searching, and haven't been able to find a clear way to actually get the filters out of the directive once attached. They are attached like this;
<div ng-data-bind="Model.Tags | format:'json'"></div>
The directive looks like ...
.directive('ngDataBind', ["$parse", "$filter", function($parse, $filter){
return {
restrict: "A",
scope: {
ngDataBind: "="
},
link: function(scope, element, attributes, controller) {
// I am hoping to get the value of 'format' here (which is 'json' in this case)
}
}
});
Right now, the filter is just extremely bare bones. I haven't added any real functionality to it yet, because a lot of what I need to do is in the directive.
.filter('format', function(){
return function(text, value) {
}
});
So in the ngDataBind directive, that I wrote, I want to get the format filter and the parameter passed to it.
I've looked at the $filter service and it doesn't seem to do this. I've attempted to parse it off of the attributes parameter passed through link on the directive, but all that gives me is a huge string that isn't all that useful.
Is there any information on this, anywhere?
Update
After being reviewed by people with a lot more experience in this than I have, I'm taking a different approach, since this is apparently not the appropriate use of filters.
The method I am going with is to create properties on the directive that are assigned like expressions, for instance..
<div c-data-bind="{ value: 'Model.Tags', format: 'json' }"></div>
I went with this method because there is a certain consistency in the expected input (always requiring content to be enclosed in '' instead of mismatching between types of quotations) and it allows the directive to be expanded without having to add more directives later. I'm unsure if this is a good approach or not, but ... it seems to be working.
Your approach is off. The directive should not concern itself with the filter.
The filter will process the bound data according to its logic.
The directive will receive the filtered data and act on it according to its logic.
None of the two need to know about the other. If you need them to, your design is flawed.
See Separations of concern

AngularJS Custom Directive isolated scope model binding

I'm trying to implement a double select list form component just to try out angular and try to understand it better.
The functionality of this directive is that the parent model contains the list of Available Items to choose from, and contains the destination list (selected) where chosen items are placed. When an item is chosen it will be removed from the available array and placed in the selected array.
As a form may require more than one of these double select list components I don't want to rely on hard coded property names but instead allow the lists to be passed in for two way data binding.
Here is a JSBin of the code: Double Select Lists
I haven't gone any further than just trying to bind the data to the lists via the passed in model property names.
On the directive, the following scope properties are defined. The availableItems and selectedItems should both be bound to the parent controllers properties. The highlightedAvailable and highlightedSelected are to store the user selections prior to the button clicks.
scope: {
availableItems: '=',
selectedItems: '=',
highlightedAvailable: [],
highlightedSelected: []
},
Can someone please tell me why the controller properties never bind to the directive properties?
Thanks!
First, you have an error being caused by your scope:
scope: {
availableItems: '=',
selectedItems: '=',
highlightedAvailable: [],
highlightedSelected: []
},
should be:
scope: {
availableItems: '=',
selectedItems: '='
},
Declare the arrays somewhere else, like in the link function:
link: function (scope, element, attrs) {
scope.highlightedAvailable = [];
scope.highlightedSelected = [];
The next problem was the way you specified the attributes to the directive, you had:
<div ng-Select-Lists availableItems='availableItems' selectedItems='selectedItems'>
Try this instead:
<div ng-Select-Lists available-items='availableItems' selected-items='selectedItems'>
To expand on aet's answer, the reason that the way you specified your directive attributes in your html did not work is because HTML is case-insensitive. So the 'availableItems' attribute was actually being passed to your directive scope as 'availableitems'. On the other hand, snake cased words like 'available-items' will be converted to camel case in your angular code.
That's the reason you write angular directives in the html as 'ng-repeat', 'ng-model', and so on, but in the angular source code you'll see these directive names camel cased: 'ngRepeat', 'ngModel'...
Be super careful to use snake-case in HTML, and camel case in your Javascript (Angular)! I've spent way too long on some bugs caused by that confusion.

AngularJS - add http prefix to url input field

Our app is being ported from jQuery to AngularJS with bootstrap (angular-ui bootstrap).
One handy feature that was covered by the following excellent post was to add "http://" prefix to a URL field if it did not already have a prefix: http://www.robsearles.com/2010/05/jquery-validate-url-adding-http/
I am trying to achieve the same in AngularJS via a directive, but cannot get the directive to alter the value of the ng-model as it is being typed.
I've started simple by trying to get a fiddle to add a "http://" prefix on EVERY change for now (I can add the logic later to only add it when needed). http://jsfiddle.net/LDeXb/9/
app.directive('httpPrefix', function() {
return {
restrict: 'E',
scope: {
ngModel: '='
},
link: function(scope, element, attrs, controller) {
element.bind('change', function() {
scope.$apply(function() {
scope.ngModel = 'http://' + scope.ngModel;
});
});
}
};
});
Can anyone please help me to get this to write back to the ngModel. Also, the field I need to apply this new directive to already has a directive on it with isolate scope so I'm assuming I can't have another one with isolate scope - if this is so can I achieve it without isolate scope?
A good way to do this is by using the parsers and formatters functionality of ng-model. Many people use use ng-model as just a binding on isolated scope, but actually it's a pretty powerful directive that seems to lack documentation in the right places to guide people on how to use it to its full potential.
All you need to do here is to require the controller from ng-model in your directive. Then you can push in a formatter that adds 'http://' to the view, and a parser that pushes it into the model when needed. All the binding work and interfacing with the input is done by ng-model.
Unless I can find a good blog on this (very much open to comments from anyone who finds them), an updated fiddle is probably the best way to describe this, this support for URL to be entered manually with 'http' or 'https', as well as auto-prefixing if none of them: http://jsfiddle.net/jrz7nxjg/
This also solves your second problem of not being able to have two isolated scopes on one element, as you no longer need to bind to anything.
The previous comment provided by Matt Byrne doesn't work for the https prefix. Checkout the updated version based on previous answers that works with **https prefix too!
This was missing there
/^(https?):\/\//i
http://jsfiddle.net/ZaeMS/13

Resources