Angular JS Directive - Template, compile or link? - javascript

I would like to create an Angular JS directive to check the length of a string, if it is too long to shorten it using a Filter, and show an Angular-UI popover on mouseover.
Where in the directive should I be placing the functionality to get this to work (link, template or compile)?
The view:
<div myapp-shorten="project">{{project.Description}}</div>
Here are my first attempts at the directive so far:
angular.module('myapp.directives', [])
.directive('myappShorten', function () {
function link(scope, element, attrs) {
var outputText = "";
if (myappShorten.Description.length > 20) {
outputText += "<div popover='{{myappShorten.Description}}' popover-trigger='mouseenter'>" +
"{{myappShorten.Description | cut:true:20:' ...'}}</div>";
} else {
outputText += "<div>{{myappShorten.Description}}</div>";
}
element.text(outputText);
}
return {
link: link,
scope: {
myappShorten: "="
}
};
});

First of all you can change the filter that it wouldn't alter string if it doesn't need to
Second, since you only need filter and popover - template is enough.
angular.module('myapp.directives', [])
.directive('myappShorten', function () {
return {
scope: { data : '=myappShorten',
template:"<div popover='{{data.Description}}' popover-trigger='mouseenter'>" +
"{{ data.Description | cut:true:20:' ...' }}</div>"
}
})
Alternatively you can use combination of ng-show and ng-hide
app.directive('shorten', function () {
return {
restrict: 'A'
, scope : {
shorten : '=',
thestring: '='
}
, template: "<div ng-show='sCtrl.isLong()' tooltip='{{ sCtrl.str }}'>{{ sCtrl.short() }}</div>"+
"<div ng-hide='sCtrl.isLong()'>{{ sCtrl.str }}</div>"
, controllerAs: 'sCtrl'
, controller: function ($scope) {
this.str = $scope.shorten || ''
this.length = $scope.thestring || 20
this.isLong = function() {
return this.str.length > this.length
}
this.short = function() {
if ( this.str.length > this.length) {
return this.str.substring(0,this.length) + '...'
}
}
}
}
})
Third option would be to actually use compile and $watch on myappShrten.Description but it seems to be overkill to me.

The above accepted answer works fine. But if the value of thestring changes this will not update as the controller compiles on first run and then will not update if the value changes. Putting code into the controller compiles upfront, but putting the code in the link function allows it to update if the value changes. This is my preferred solution inspired by the solution above:
The view:
<shorten thestring="project.Description" thelength="40"></shorten>
The directive:
.directive('shorten', function () {
return {
restrict: 'E'
, scope: {
thelength: '=',
thestring: '='
}
, link: function postLink(scope, iElement, iAttrs) {
scope.isLong = function () {
return scope.thestring.length > scope.thelength
}
scope.short = function () {
if (scope.thestring.length > scope.thelength) {
return scope.thestring.substring(0, scope.thelength) + '...'
}
}
}
, template: "<div class='handCursor' ng-show='isLong()' tooltip='{{ thestring }}'>{{ short() }}</div>" +
"<div ng-hide='isLong()'>{{ thestring }}</div>"
}
});

Related

Problem creating custom validator for an angularjs directive

So I have a custom directive which works fine as it is. This directive is being used at multiple places. This is an element directive.
This element directive has certain attributes. I have added a custom attribute for only 1 instance of this directive i.e. only at 1 particular usage of this directive I have added an extra attribute for this element.
Here is the directive being used in the HTML:
<attribute-types target-model="patient" attribute="::attribute"
field-validation="::fieldValidation"
is-auto-complete="isAutoComplete"
get-auto-complete-list="getAutoCompleteList"
get-data-results="getDataResults" is-read-only="isReadOnly"
handle-update="handleUpdate" validate-autocomplete="true">
</attribute-types>
The validate-autocomplete is the extra attribute I have used at 1 place use of this directive.
Here is the template for the directive:
<div class="left" data-ng-switch-when="org.openmrs.Concept" ng-if="attribute.name == 'PATIENT_OCCUPATION'" style="position: absolute">
<input type="text"
class="ui-autocomplete-input"
id="{{::attribute.name}}"
name="{{::attribute.name}}"
ng-model="targetModel[attribute.name].value"
ng-keyup="suggest(targetModel[attribute.name])"
ng-required="{{::attribute.required}}">
<ul class="ui-front ui-autocomplete ui-menu ui-widget ui-widget-content ui-corner-all" ng-if="showTag" ng-hide="hideList"
style="position:absolute; top:30px; width:192px">
<li class="ui-menu-item" role="presentation" ng-repeat="info in filterOcuppation"
ng-click="hideSuggestions(info)">
<a class="ui-corner-all" tabindex="-1">{{info.description}}</a>
</li>
</ul>
</div>
And this is the directive definition:
angular.module('bahmni.common.attributeTypes', [])
.directive('attributeTypes', [function () {
var link = function (scope, element, attrs, ngModelCtrl) {
var formElement = element[0];
if (attrs.validateAutocomplete) {
ngModelCtrl.$setValidity('selection', true);
}
};
return {
link: link,
scope: {
targetModel: '=',
attribute: '=',
fieldValidation: '=',
isAutoComplete: '&',
handleLocationChange: '&',
handleSectorChange: '&',
getAutoCompleteList: '&',
getDataResults: '&',
handleUpdate: '&',
isReadOnly: '&',
isForm: '=?'
},
templateUrl: '../common/attributeTypes/views/attributeInformation.html',
restrict: 'E',
controller: function ($scope) {
var dateUtil = Bahmni.Common.Util.DateUtil;
$scope.getAutoCompleteList = $scope.getAutoCompleteList();
$scope.getDataResults = $scope.getDataResults();
$scope.today = dateUtil.getDateWithoutTime(dateUtil.now());
// to avoid watchers in one way binding
$scope.isAutoComplete = $scope.isAutoComplete() || function () { return false; };
$scope.isReadOnly = $scope.isReadOnly() || function () { return false; };
$scope.handleUpdate = $scope.handleUpdate() || function () { return false; };
$scope.handleLocationChange = $scope.handleLocationChange() || function () { return false; };
$scope.handleSectorChange = $scope.handleSectorChange() || function () { return false; };
$scope.suggestions = $scope.attribute.answers;
$scope.showTag = false;
$scope.itisinvalid = true;
$scope.appendConceptNameToModel = function (attribute) {
var attributeValueConceptType = $scope.targetModel[attribute.name];
var concept = _.find(attribute.answers, function (answer) {
return answer.conceptId === attributeValueConceptType.conceptUuid;
});
attributeValueConceptType.value = concept && concept.fullySpecifiedName;
};
$scope.suggest = function (string) {
$scope.hideList = false;
$scope.showTag = true;
var output = [];
angular.forEach($scope.suggestions, function (suggestion) {
if (suggestion.description.toLowerCase().indexOf(string.value.toLowerCase()) >= 0) {
output.push(suggestion);
}
});
$scope.filterOcuppation = output;
};
$scope.hideSuggestions = function (object) {
$scope.targetModel[$scope.attribute.name] = object;
$scope.targetModel[$scope.attribute.name].value = object.description;
$scope.targetModel[$scope.attribute.name].conceptUuid = object.conceptId;
$scope.hideList = true;
};
}
};
}]);
When running this I get TypeError: ngModelCtrl.$setValidity is not a function
What I'm basically doing it validating whatever is entered into the input text is valid or not. For that I would also need the ng-model, how would I access that in my link function?
If I have written some wrong, feel free to correct me. I'm still in the process of learning AngularJS
You should use directive like that:
directive('attributeTypes', [function() {
return {
require: '?ngModel', // get a hold of NgModelController
link: function(scope, element, attrs, ngModel) {
...
ngModel.$setValidity(...

passing ngModel and ngChange with custom directive under md-select

I am making a custom directive on top of md-select. I am having issues with default behavior of ngModel and ngChange. I can't seem to make them both work together.
Currently I have this http://next.plnkr.co/edit/X34DUWtkyYhbwJP4?open=lib%2Fscript.js
The ngModel is being updated, but the ngChange doesnt seem to work.
I also tried a method shown in http://embed.plnkr.co/HZAHSyi9L8UQdE24zYYI/
but having issues when setting value with a timeout (assuming value comes from api).
app.controller("appCtrl", function($scope){
$scope.items = [1,2,3,4,5,6];
$scope.foo=2; // this works
$scope.bar = function(foo) {
$scope.aux = foo;
}
setTimeout(function(){
$scope.foo=5;
}, 0); // this doesnt work
});
I want to make these two attributes to work as default md-select does.
When working with ng-model and custom directives, you can specify ngModel as a require, and then automatically get access to other directives like ngChange and ngRequired. I've updated your plunkr: http://next.plnkr.co/edit/VzYpZ2elmzV6XkbM?open=lib
HTML
<md-custom-select
ng-model="vm.SelectItems"
ng-change="vm.onselectchange()"
list="vm.ItemList">
</md-custom-selector>
JavaScript
var app = angular.module("MaterialApp", ["ngMaterial"]);
app.directive("mdCustomSelect", ["$compile", mdCustomSelect]);
function mdCustomSelect($compile) {
return {
restrict: "E",
require: {
ngModelCtrl: '^ngModel'
},
scope: {
ngModel: "<",
list: "=",
options: "<",
},
replace: true,
link: function(scope, element, attrs, controllers) {
scope.ngModelCtrl = controllers.ngModelCtrl;
var searchTemplate = '<md-select-header aria-label="Select Header" class="demo-select-header"><input aria-label="InputSearchBox" ng-keydown="$event.stopPropagation()" ng-model="searchTerm" type="search" placeholder="Search items" class="md-text"></md-select-header>';
var selectAllTemplate = '<div style="padding: 0px 0px 15px 5px; background-color: #efefef;"><md-checkbox class="md-warn" title="Select All" ng-model="checkAllChecked" ng-change="toggleSelectAll()">Check/Uncheck All </md-checkbox></div>';
var multiSelectGroupTemplate = '<md-option ng-value="item.ItemID" ng-repeat="item in ItemList | filter: searchTerm">{{item.ItemName}}</md-option>';
var completeTemplate = "";
completeTemplate += '<md-select multiple ng-model="ngModel" ng-change="valChanged()" data-md-container-class="selectdemoSelectHeader">';
completeTemplate += searchTemplate; //2 begin and end
completeTemplate += selectAllTemplate; //3 begin and end
completeTemplate += multiSelectGroupTemplate; //4 begin and end
completeTemplate += " </md-select>"; //1 end
element.html(completeTemplate);
$compile(element.contents())(scope);
},
controller: ["$scope", function($scope) {
var defaultValueProperty = ($scope.options == undefined || $scope.options.Value === undefined) ? "value" : $scope.options.Value;
var defaultTextProperty = ($scope.options == undefined || $scope.options.Text === undefined) ? "name" : $scope.options.Text;
$scope.isMultipleSelected = angular.isUndefined($scope.multiple) ? true : $scope.multiple;
$scope.checkAllChecked = false;
$scope.ItemList = [];
var rawItemList;
$scope.$watch("list", function(newValue) {
$scope.ItemList = newValue.map(item => {
return { ItemID: item[defaultValueProperty], ItemName: item[defaultTextProperty] };
});
}, true);
$scope.valChanged = function(){
$scope.ngModelCtrl.$setViewValue($scope.ngModel);
}
$scope.toggleSelectAll = function() {
if ($scope.checkAllChecked == false) {
$scope.ngModelCtrl.$setViewValue([]);
} else {
$scope.ngModelCtrl.$setViewValue($scope.ItemList.map(item => item.ItemID));
}
};
}]
};
}

How to pass a custom directive attribute to custom directive child element in Angular 1.5?

I'm currently trying to pass a validation directive to a custom element directive. But I'm struggling to make it work since it should receive model as an input while I am using bind to controller.
I have to premise that I cannot upgrade to a more recent version of Angular, so 1.5 is the limitation, together with the fact I cannot edit validation directive.
I thought transclude would have helped but with directive attribute it looks not so promising.
What the following code should do is to validate vm.model on input element.
Here's the HTML:
<body ng-controller="MainCtrl">
<div class="myClass">
<my-custom-directive data-placeholder="No text"
data-id="myModel.id"
data-model="myModel.text"
not-editable-directive-attribute >
</my-custom-directive>
</div>
</body>
And here the app.js:
var myTemplate = '<div class="myContainer">' +
'<input class="myInput"' +
' ng-mousedown="$event.stopPropagation();"' +
' ng-show="vm.isFocused"' +
' ng-model="vm.model"' +
' ng-change="vm.onChange()"' +
' type="text">' +
'<span ng-show="!vm.isFocused">{{vm.model}}</span>' +
'<span ng-show="!vm.isFocused && !vm.model && vm.placeholder">{{vm.placeholder}}</span>' +
'</div>';
app.controller('MainCtrl', function($scope) {
$scope.myModel = {
id: 'test',
text: 'this is text'
};
});
app.directive('myCustomDirective', ['$timeout', function($timeout) {
return {
restrict: 'E',
replace: true,
template: myTemplate,
controllerAs: 'vm',
bindToController: {
id: '#',
model: '=',
onChange: '&',
placeholder: '#'
},
scope: {},
controller: angular.noop,
link: function(scope, element) {
var input = element.find('input')[0];
var spans = Array.from(element.find('span'));
var vm = scope.vm;
vm.isFocused = false;
vm.focus = function() {
vm.isFocused = true;
scope.$applyAsync(function() {
$timeout(function() {
input.focus();
input.select();
});
});
};
spans.forEach(span => span.addEventListener('click', vm.focus));
}
};
}]);
app.directive('notEditableDirectiveAttribute', [function() {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
ctrl.$validators.myCustomDirectiveAttribute = function(modelValue, viewValue) {
if (viewValue) {
return viewValue.indexOf('e') < 0;
}
return false;
};
}
};
}]);
I've created a plunker to make it clearer:
http://plnkr.co/edit/auminr?p=preview
So clicking on span element i should be able to edit text and directive should validate it (in this specific case check if it contains letter "e").
Is it even possible or am I struggling against windmills?
One approach to adding directives to templates based on component attributes is to use the function form of the template property:
<my-custom-directive data-placeholder="No text"
model="vm.data"
custom="not-editable-directive-attribute" >
</my-custom-directive>
app.directive("myCustomDirective", function() {
return {
template: createTemplate,
scope: {},
//...
});
function createTemplate(tElem, tAttrs) {
var placeholder = tAttrs.placeholder;
var model = tAttrs.model;
var custom = tAttrs.custom;
return `
<input placeholder=${placeholder}
ng-model=${model}
${custom} />
`;
}
})
The createTemplate function copies attributes and used them in a template literal.
For more information, see
AngularJS Comprehensive Directive API - template
MDN JavaScript Reference - Template Literals

Angular directive dynamic template not working with bindings

This is a simplified version of a real problem but I want to understand what is happening and how to fix it. I have a directive that should load different templates deppending on a parameter that is being bind to it.
var app = angular.module('my_app', [])
.controller('controller', Controller)
.directive('dirTwo', DirTwo);
function Controller() {
var este = this;
this.list = ["number", "text", "text"];
this.change = function() {
this.list = this.list.reverse();
}
}
function DirTwo() {
return {
restrict: 'E',
//template: ' Name: {{name}}{{type}}',
template: my_template,
scope: {
type: "="
},
link: function($scope) {
$scope.name = "Pepito";
console.log("scope type: " + $scope.type);
}
}
}
var my_template = function(elem, attrs, $scope) {
console.log("template type: " + attrs.type);
switch (attrs.type) {
case 'number':
return '{{type}}';
break;
default:
return ' ---- '
break;
}
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js"></script>
<div class="app" ng-app="my_app" ng-controller="controller as App">
<p>{{App.list}}
<button ng-click="App.change()">Reverse</button>
</p>
<dir-two ng-repeat="item in App.list track by $index" type="item"></dir-two>
</div>
The log in the template function prints the word item instead of number or text. How can I fix it to load the template correctly?
Unfortunately there is no way to use template like that because the template resolution happens before the compile phase where your type would assume a value on scope. To change the template of a directive based on a scope value, you must watch that value and recompile the directive element when it gets changed. It will recompile the directive entirely with a new template but with the same scope. You could do this once (i.e., without the $watch) as well.
The following snippet implements this solution.
var app = angular.module('my_app', [])
.controller('controller', Controller)
.directive('dirTwo', DirTwo);
function Controller() {
var este = this;
this.list = ["number", "text", "text"];
this.change = function() {
this.list = this.list.reverse();
}
}
function DirTwo($compile) {
return {
restrict: 'E',
template: '',
scope: {
type: "="
},
link: function(scope, element) {
var tmplScope;
scope.name = "Pepito";
scope.$watch('type', function(type) {
if (tmplScope) tmplScope.$destroy();
tmplScope = scope.$new();
element.html(getTemplate(type));
$compile(element.contents())(tmplScope);
});
}
}
}
var getTemplate = function(type) {
switch (type) {
case 'number':
return '{{type}}';
break;
default:
return ' ---- '
break;
}
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.1/angular.min.js"></script>
<div class="app" ng-app="my_app" ng-controller="controller as App">
<p>{{App.list}}
<button ng-click="App.change()">Reverse</button>
</p>
<dir-two ng-repeat="item in App.list track by $index" type="item"></dir-two>
</div>
UPDATE NOTE: I've changed the actual compile system to create a new child scope every time it gets compiled and also destroy this child scope when it gets recompiled to prevent adding new watchers when recompiling it without discarding the previous ones. Thanks to #georgeawg who warnned about this issue in the comments.

Angular Directive mouseenter/mouseleave working but not setting to initial state after mouseleave

I have a directive that shows a list of student information on a template and on mouseenter it then shows additional student information. I want to be able to go back to the initial state on mouseleave.
Tried all the resources and not much luck.
html - this is where i'm injecting my directive
<div ng-repeat="student in studentPortfolio">
<portfolio-view student="student"></portfolio-view>
</div>
html directive template
<div class="outer-box">
<img src="{{student.picture}}" alt="{{student.name.first}} {{student.name.last}}" style="width: 200px; height: 200px">
Name: {{student.name.first}} {{student.name.last}}
<br>Bio: {{student.Bio}}
<br>
Skills:
<div ng-repeat="skill in student.skills">
{{skill.title}}
</div>
<br>
</div>
directive
app.directive('portfolioView', function() {
return {
restrict: 'E',
scope: {
student: "="
},
templateUrl: '/html-templates/hoverPortfolio.html',
link: function(scope, elem, attrs) {
//gets the first project and shows it
var project = scope.student.projects;
var firstProject = project[0];
var fp_name = firstProject.name;
var fp_type = firstProject.projectType;
var fp_description = firstProject.description;
//gets the second project and shows it
var secondProject = project[1];
var sp_name = secondProject.name;
var sp_type = secondProject.projectType;
var sp_description = secondProject.description;
//the template that shows the second project
var newHtml =
'<div class="projects outer-box"><div class="firstproject"> Project Name: ' +
fp_name + '<br>Type: ' + fp_type + '<br>Description: ' +
fp_description +
'</div><br><div class="secondproject"> Project Name: ' +
sp_name + '<br>Type: ' + sp_type + '<br>Description: ' +
sp_description +
'</div> </div>';
elem.on('mouseenter', function() {
elem.html(
newHtml
)
});
elem.on('mouseleave', function() {
//return to intial state
});
}
}
});
I didn't have your data, but the ng-show thing works, like in this fiddle.
Here's a simpler variant. If your template includes the parts you wish to show or hide, with an ng-show variable on it, your directive could be fairly simple:
return {
restrict: 'EAC',
replace: true,
template: '<div><div ng-show="show">show</div><div ng-show="!show">hide</div></div>',
link: function (scope, element, attrs, controller) {
scope.show = true;
element.on('mouseenter', function () {
scope.$apply(function () {
scope.show = false;
});
});
element.on('mouseleave', function () {
scope.$apply(function () {
scope.show = true;
});
});
}
};

Categories

Resources