I am trying to create a directive named availableTo that can switch between two different templates depending on some message. For example, if the field is an input with the ng-model directive, I would first need to change it to read-only using the <span> tag. So far, my code can switch the view to read-only, but I cannot seem to switch it back to input:
var directive = {
restrict: 'A',
require: '?ngModel',
link: linkerFn,
replace: true
};
function linkerFn(scope, element, attrs, ngModelCtrl) {
var clonedElement = angular.copy(element);
var preOuterHTML = clonedElement[0].outerHTML; //this can save the <input> field html code
scope.$on('mode_changed', function() {
var curUserRole = userservices.getUserRole();
if (attrs.availableTo == curUserRole) {
var e = $compile(preOuterHTML)(scope);
element.replaceWith(e);
} else {
var template = '<span>' + ngModelCtrl.$viewValue + '</span>';
var e = $compile(template)(scope);
element.replaceWith(e);
}
}); //scope.$on
} //linkerFn
For an input field:
<input name="test1" class="form-control" ng-model="name" placeholder="Name 1" available-to="ADMIN"/>
I also noticed that once I change the template in the else block above, the element re-renders, and the preOuterHTML does not contain the original element html any more. This seems to be mission impossible to me, but I would like to hear some expert opinions. Thanks
element.replaceWith(e); Don't do that. In Angular, if you find yourself attempting to modify the DOM directly, you are by definition doing it wrong. You gotta sit back and let Angular do the work.
If you need to replace a directive's entire template, a fairly straightforward approach is to use ng-include with a scope variable containing the desired conditional templateUrl, e.g.
var directive = {
// ...
template: '<div ng-include="myTemplateUrl"></div>',
link: function(scope, el) {
if (/* whatever */) {
scope.myTemplateUrl="templates/foo.html";
} else {
//...etc
}
},
};
(This does add an extra DOM node to the tree, but that's generally harmless.)
It sounds like in your case you may not need to go that far, though; a simple ng-if inside your template is probably enough to swap between your read-only <span> and <input>.
Related
I'm trying to validate dynamically generated inputs, but I do not know how to do this.
When I add the div that triggers the directive and inserts the inputs dynamically the div adds the 'has-error' class but does not apply the input style, anyone knows the best way to do this that I'm trying to do?
Here is the markup:
<div ng-if="conditionItem.field.id"
ng-class="{true: 'has-error'}[conditionItem.field.hasError]"
dynamic
input-router
source="conditionItem.field"
ng-click="FieldConditionsCtrl.valueTest(conditionItem.field.hasError)"
ng-required="true"
ng-model="conditionItem.situation[$index]">
</div>
Here is the directive how generate the inputs:
(function() {
'use strict';
angular
.module('applicationInputs', ['rzModule', 'focus-if', 'ui.utils.masks'])
.directive('inputRouter', inputRouter);
/** #ngInject */
function inputRouter($compile){
return {
restrict: 'EA',
scope: {
ngModel: '=',
source: '=',
placeholder: '#',
tabIndex: '='
},
link: function(scope, element, attrs) {
var canvas = angular.element(element[0]);
scope.source.editable = angular.isUndefined(scope.source.editable) ? true : scope.source.editable === true;
//used by setting to override selected field
if (angular.isDefined(attrs.dynamic)) {
scope.$watch('source', function () {
var html = '<' + scope.source.type + 'input></' + scope.source.type + 'input>';
canvas.children().detach();
canvas.append($compile(html)(scope));
});
} else {
var html = '<' + scope.source.type + 'input></' + scope.source.type + 'input>';
canvas.append($compile(html)(scope));
}
}
}
}
})();
Here is my style:
.has-error {
border-color: red
}
Try border: 1px solid red; instead of just setting border-color. Border width is 0 by default, so just setting a color isn't enough
Just a couple nitpicky-subjective style comments also:
element is already a jqLite/jquery element, so no need to call angular.element
the scope.source.editable === true assignment can be shortened to !!scope.source.editable if you really want it to be a boolean.
While clever, this sort of jquery style element building is generally a code smell in angular js. If you really want to go this route, I would build self-contained directives for your inputs and use inputRouter's template to choose. It's easier to understand, so your future self will thank you
{true: 'has-error'}[conditionItem.field.hasError] took me a minute. Just write it as {conditionItem.field.hasError: 'has-error'}
I'm a big fan of these style guides for AngularJS:
John Papa - ES5, but good advice
Todd Motto - ES6+
There's overlap, but take what you like from each.
I set a progress in my app
I want to controll The progress in angular's directive
but how can I change data-value and data-total in directive's link func?
app.html
<div class="ui indicating small progress" data-value="39" data-total="50" plan-progress>
<div class="bar">
<div class="progress"></div>
</div>
</div>
In this html, I want change data-value and data-total
I try this:
app.js
todoApp.directive('planProgress', function() {
return {
link: function(scope, elem, attrs) {
attrs.value = 10
attrs.total = 20
elem.progress();
}
};
});
But it doesn't work
so I want to know how to change it in my directive?
Use attrs.$set() in your link function and recompile the element. Also, don't forget to inject the $compile service to your directive.
In your html you've added the directive as an attribute but didn't mention it in the restrict value in your directive definition. You need to mention it in directive definition.
See the code bellow:
todoApp.directive('planProgress', function($compile) {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
attrs.$set('value', 10);
attrs.$set('total', 20);
$compile(elem)(scope);
}
};
});
Simply use :
attrs["data-value"] = 10;
attrs["data-total"] = 20;
You don't want to use attrs.data-total = 20 because the - will force a subtraction.
It's always legal in javascript to use x[keyName] instead of x.keyName, and you must use this second notation when keyName is a strange key such as **$^ùjsls* or data-value. A more useful case is when the key is a variable.
On last thing : as you do, you will always rewrite the coder's inputs. It may have sense, but it's not very elegant.
I have a custom directive and an object myObj on the current $scope (inside an ng-repeat).
If the object has a type of html, I want to use one template:
<span ng-bind-html="myObj.html"></span>`
Otherwise I want to use a different template:
<span>{{myObj.value}}</span>`
My problem
This is invalid because a custom directive template must contain exactly one root node:
<span ng-if="myObj.type==='html'" ng-bind-html="myObj.html"></span>
<span ng-if="myObj.type!=='html'">{{myObj.value}}</span>
This is invalid because it destroys my page with extra DOM: (wrapping all my spans (there could be thousands) in unnecessary ng-switch nodes...)
<ng-switch on="myObj.type">
<span ng-switch-when="html" ng-bind-html="myObj.html"></span>
<span ng-switch-default>{{myObj.value}}</span>
</ng-switch>
My Question
Is it possible to have a directive pick it's template based on the result of a switch, without creating extra unnecessary DOM? For example, you can specify replace: true when creating a directive - is it possible to similarly have an ng-switch where the result replaces the switch tag itself?
Edit
My Directive:
return {
replace: true,
controller: 'ChunkController',
scope: {
chunk: '=deChunk'
},
templateUrl: de.partial.chunk,
link: function (scope, el, attr, ctrl) {
el.on('keydown', handleKeypress.bind(ctrl));
el.on('click', ctrl.showValue);
}
};
And its usage:
<div class="content" contenteditable="{{node.type!=='static'}}">
<div data-ng-repeat="chunk in node.chunks" data-de-chunk="chunk"></div>
</div>
With the intent that the child <div> will be replaced with the sequence of <span>s from above.
I wouldn't even bother if you are storing the html in a service just check to see if a value for myObj.html exists in the object and if it does compile and bind the html in the linker function instead of using ng-bind-html
something like this maybe:
myapp.directive('something',function($compile){
return {
link: function(scope,elem,attrs) {
var obj = scope.$eval(attrs.something);
if(obj.html) {
var html = angular.element($compile(obj.html)(scope));
elem.append(html);
} else {
//go get the data and set obj.html
}
}
}
});
Here is the little app in which you can add items, edit them and delete - all described in controller, based on angular 2 way data-bind.
http://cssdeck.com/labs/schedule-of-the-red-hood
Everytime the element is added, in the calednar-bar programmatically should be inserted an element, described in directive's template:
template: '<div ng-repeat="event in events" class="event">' +
'<h3 ng-model="event.Name">{{event.Name}}</h3>' +
'<span ng-model="event.StartTime; event.EndTime" class="time">{{event.StartTime}}-{{event.EndTime}}</span>' +
'</div>'
Though I can't get it how to link the scope from controller, bind the element insertion from the controller:
$scope.addEvent = function(event, attrs) {
$scope.events.push(event);
$scope.event = {};
var eventGrid = angular.element(document.createElement('eventGrid')),
el = $compile(eventGrid)($scope);
angular.element(document.body).append(eventGrid);
$scope.insertHere = el;
}
as I understand now, my code creates an element in the DOM, but doesn't use template from directive... How can I do it? Is the chosen structure of code appropriate to this goal?
You need to be more data driven, forget about adding things to the dom. E.g.
$scope.addEvent = function(event, attrs) {
$scope.events.push(event);
$scope.event = {};
/*var eventGrid = angular.element(document.createElement('eventGrid')),
el = $compile(eventGrid)($scope);
angular.element(document.body).append(eventGrid);
$scope.insertHere = el;*/
}
//add an order by to your ng-repeat
<li ng-repeat="event in events|orderBy:StartTime">
Instead of Saving StartTime as a string save it as a number, and use a filter to convert from seconds to your friendly HH:MM:AM|PM.
The conversion from string to seconds is the hardest part, but it is the right way to do it.
I'm trying make a directive out of a great jquery zooming/panning library. How can I access the elements in my template to initialize the plugin?
The directive looks like this as of now:
directive('zui', [function () {
return {
restrict: 'E',
scope: { URL: "#"},
template: '<div id="zui" ><div id="viewport" ><img ng-src="{{imageURL}}"></div></div>',
link: function (scope, element, attrs) {
scope.imageURL = URL;
var zui = new ZUI53.Viewport( document.getElementById('zui') );
zui.addSurface( new ZUI53.Surfaces.CSS( document.getElementById('viewport') ) );
var pan_tool = new ZUI53.Tools.Pan(zui);
zui.toolset.add( pan_tool );
pan_tool.attach()
}
};
}]);
Clearly document.getByID() is not the best way to accomplish this. What is a better solution? Thanks a lot.
Getting to the element using document.getElementById('zui') is indeed not nice. If you ever had to have two instances of the zui directive, it would break.
Don't use id on the root element. Use a marker class
such as class="zui" or a data attribute such as data-zui.
Using jquery, you can get to your element using find like
this: element.find('.zui')[0];.