Angular.js updating SVG templates in directives - javascript

A while ago I asked about "Angular.js rendering SVG templates in directives", where I was replacing the DOM nodes that angular makes when rendering templates, with SVG nodes. I got a response that answered it for me, but I realized that I lost all the databindings from angular.
See Plunkr (click update): http://plnkr.co/edit/HjOpqc?p=preview
How do I replace these DOM nodes with SVG nodes, and leave my angular bindings intact? I tried using $compile to make it work (as I've done with regular html), but its just not working.
code:
var svgNS = 'http://www.w3.org/2000/svg';
app.directive('path', ngSvg('path'));
app.directive('g', ngSvg('g'));
function ngSvg(type) {
return function($timeout, $compile) {
return {
restrict: 'E',
link: function(scope, el, attr) {
//skip nodes if they are already svg
if (el[0].namespaceURI === svgNS) {
return;
}
// I would expect the chunk of code below to work,
// but it does not with ng-repeat
// var newAttr = {};
// _.each(el[0].attributes, function(at) {
// newAttr[at.nodeName] = at.value;
// });
// var path = makeNode(type, el, newAttr);
// var parent = path.cloneNode(true);
// $compile(parent)(scope);
// var children = el.children();
// $(parent).append(children);
// $timeout(function() {
// el.replaceWith(parent);
// })
// this works for rendering, but does not update the svg elements
// when update is clicked
$timeout(function() {
var newAttr = {};
_.each(el[0].attributes, function(at) {
newAttr[at.nodeName] = at.value;
});
var path = makeNode(type, el, newAttr);
var parent = path.cloneNode(true);
var children = el.children();
$(parent).append(children);
el.replaceWith(parent);
});
}
}
}
}
/* Create a shape node with the given settings. */
function makeNode(name, element, settings) {
// var ns = 'http://www.w3.org/2000/svg';
var node = document.createElementNS(svgNS, name);
for (var attribute in settings) {
var value = settings[attribute];
if (value !== null && value !== null && !attribute.match(/\$/) &&
(typeof value !== 'string' || value !== '')) {
node.setAttribute(attribute, value);
}
}
return node;
}

This issue is solved in Angular 1.3 and here is an implementation of some custom svg directives with the behaviors you would expect from an Angular directive. There is now a special requirement is on the directive declaration as follows templateNamespace: 'svg'
Also notice I am overriding some reserved attributes, for example, x and height in regards to <rect/>. To retain more control over these you can leverage ng-attr as such '<rect ng-attr-width="{{ ngWidth }}" />
JSFiddle Link
Here is a custom <rect/> and <circle/>
app.directive('ngRect', [function () {
return {
templateNamespace: 'svg',
replace: true,
template: '<rect ng-attr-width="{{ ngWidth }}" ng-attr-height="{{ ngHeight }}" ng-attr-x="{{ ngX }}" ng-attr-y="{{ ngY }}" ng-click="ngRectClick()"/>',
scope: {
'ngHeight': '=',
'ngWidth': '='
},
link: function (scope, elem, attrs) {
scope.ngRectClick = function() {
console.log(elem);
}
}
}
}]);
app.directive('ngCircle', [function () {
return {
templateNamespace: 'svg',
replace: true,
template: '<circle ng-attr-cx="{{ ngCx }}" ng-attr-cy="{{ ngCy }}" ng-attr-r="{{ ngR }}" ng-attr-fill="{{ ngFill }}" ng-click="ngCircleClick()"/>',
scope: {
'ngCx': '=',
'ngCy': '=',
'ngR': '=',
'ngFill': '='
},
link: function (scope, elem, attrs) {
scope.ngCircleClick = function() {
console.log(elem);
}
}
}
}]);

Related

How to watch ng-html-bind from a directive?

I'm trying to watch the content of ng-html-bind and modify the div content to auto link all hyperlinks in the div since the original content will not have hyperlink html.
Here is the plunker
Here is the directive
app.directive('autolink', ['$compile', '$timeout', function ($compile, $timeout) {
return {
restrict: 'EA',
replace: true,
link: function (scope, element, attrs) {
$timeout(function () {
var text = element[0].innerHTML;
var linkTypes = ["http://", "https://"];
linkTypes.forEach(function (linkType) {
var startSpace = 0;
while (startSpace >= 0) {
text = element[0].innerHTML;
var locationOfHttp = text.indexOf(linkType, startSpace);
if (locationOfHttp < 0) break;
var locationOfSpace = text.indexOf(" ", locationOfHttp);
var textAfter = "";
if (locationOfSpace < 0) {
locationOfSpace = text.length;
} else {
textAfter = text.substring(locationOfSpace, text.length);
}
var linkUrl = text.substring(locationOfHttp, locationOfSpace);
var htmlText = text.substring(0, locationOfHttp) + '' + linkUrl + '' + textAfter;
element[0].innerHTML = htmlText;
startSpace = (text.substring(0, locationOfHttp) + '' + linkUrl + '').length - 1;
}
});
scope.$apply();
console.log("autolink");
}, 1);
},
};
}]);
My directive is working when the page loads but not when I click on the change URL, div is not auto linking. How do I watch for the change and run the directive on change ?
So you can use scope.$watch() to watch for the change on a scope variable, run it through your link creating function, and then add it back in to the element.
Here is a fork of your plunk that does just that.
I changed ng-bind-html to be autolink by way of using an isolate scope (Directive isolate scope), which allows your new text with the urls in it to be passed to the directive, where the scope.$watch takes over. By making the isolate scope variable the same as the directive name, you can use it both to invoke the directive and pass a variable into it.
The new html:
<div autolink="parseResult(details)"></div>
Here is the code for the directive below:
app.directive('autolink', ['$compile', '$timeout', function ($compile, $timeout) {
return {
restrict: 'EA',
replace: false,
// isolate scope below the html attribute
// unlinked-text is automatically translated
// to the scope variable unlinkedText by angular.
scope: {
autolink: '='
},
// added a template that uses ng-bind-html with
// your new, link-ified text
template: '<span ng-bind-html="text"></span>',
link: function (scope, element, attrs) {
scope.text = scope.autolink;
function addLinks(str) {
var text = str;
console.log(text.match(/https?:\/\/\w*/));
var links_parsed = text
.replace(/https?:\/\/[\w\.\/]*/g,
function(substr) {
return '' + substr + '';
});
return links_parsed;
}
// Still using timeout for initial run of addLinks
$timeout(function() {
scope.text = addLinks(scope.text);
},0)
// scope watches autolink variable
scope.$watch('autolink', function(newVal, oldVal) {
if(newVal !== oldVal) { // if variable has changed...
scope.text = addLinks(newVal); // ...runs addLinks() again
}
} );
}
};
}]);

Use ngModel setViewValue from controller

I bind an array of values to a number of input elements. There are directives on the input elements, that set $parsers, $formatters and $validators. The controller should not care about the pipeline from viewValue to modelValue.
The view:
<ul>
<li ng-repeat="value in main.values">
<input ng-model="value.v" twice /> {{value.v}}
</li>
</ul>
Controller / Directive:
function MainController($scope) {
this.values = [
{v: 1}, {v: 2}, {v: 3}
];
}
function twice() {
return {
require: 'ngModel',
link: function(scope, elem, attr, ngModel) {
ngModel.$formatters.push(function(x) { return 2 * x });
ngModel.$parsers.push(function(x) { return 0.5 * x });
}
}
}
I want to implement a copy & paste feature. The values in all the input elements should be overwritten from clipboard data. Therefore the controller implements a function which parses the clipboard data and sets the value for each input element. The values from clipboard are view values. Since the controller has no idea how to calculate model values from these view values, it has to use the '$parsers' pipeline from 'ngModelController'. How can I implement MainController.paste() to set the view value on each input element?
Edit
I currently solved the actual problem (see comments) with a directive on the list element. http://plnkr.co/edit/9c2q2X?p=preview
function pasteValues() {
return {
link: function(scope, elem, attr, ngModel) {
elem.on('paste', function($event) {
var data = $event.clipboardData || window.clipboardData;
var text = data.getData('Text');
var values = text.split(' ');
var inputs = elem.find('input');
if (values.length === inputs.length) {
for(var i = 0, e = values.length; i != e; ++i) {
var input = inputs[i];
var ngModel = angular.element(input).controller('ngModel');
ngModel.$setViewValue(values[i]);
input.value = values[i];
}
$event.preventDefault();
}
})
}
}
}
I found two possible solutions (while writing the question:-)). http://plnkr.co/edit/ZNfYKTvSf6coGsohRlot?p=preview
1
The first is not really the angular way, because the controller has to know about DOM structure. But it is straighforward and doesn't need additional bindings and watches. To set the view value it uses the angular.element.controller() method to retrieve the ngModelController for each input element.
function MainController($scope) {
this.paste = function() {
var value = this.pasteValue;
var inputs = angular.element(document.getElementById('values')).find('input');
angular.forEach(inputs, function(input) {
var ngModel = angular.element(input).controller('ngModel');
ngModel.$setViewValue(value);
input.value = value;
});
};
}
2
The second solution is more the angular way and uses an addtional directive that whatches on paste data.
function setView() {
return {
require: 'ngModel',
scope: {
setView : '='
},
link: function(scope, elem, attr, ngModel) {
scope.$watch('setView', function(newValue) {
if (angular.isDefined(newValue)) {
elem.val(newValue);
ngModel.$setViewValue(newValue);
}
})
}
}
}
function MainController($scope) {
this.paste = function() {
var value = this.pasteValue;
this.values.forEach(function(v) { v.i = value });
};
}
The view:
<ul>
<li ng-repeat="value in main.values">
<input ng-model="value.v" twice set-view="value.i"/> {{value.i}}({{value.v}})
</li>
</ul>

Why is Custom Directive conflicting with ui-sref?

Here is the directive:
directive('cgHasPermissions', ['$animate', '$rootScope', 'PermissionService', 'PERMISSION', '$compile', function ($animate, $rootScope, PermissionService, PERMISSION, $compile) {
return {
multiElement: true,
transclude: 'element',
restrict: 'A',
$$tlb: true,
link: function ($scope, $element, $attr, ctrl, $transclude) {
var block, childScope, previousElements;
var unregister = $scope.$watch('user', function (newValue, oldValue) {
var value = $attr.cgHasPermissions;
var needsPermissions = value || ""
if($rootScope.user && $rootScope.permissions) {
unregister()
}
needsPermissions = value.replace(/\s+/g,"").split(",");
needsPermissions = _.map(needsPermissions, function(perm){
return PERMISSION[perm];
})
var user = $rootScope.user;
if(!needsPermissions || PermissionService.hasPermissions(needsPermissions)){
if (!childScope) {
$transclude (function (clone, newScope) {
childScope = newScope;
clone [clone.length++] = document.createComment (' end cgHasPermissions: ' + $attr.cgHasPermissions + ' ');
block = {
clone: clone
}
$animate.enter(clone, $element.parent (), $element);
});
}
} else {
if (previousElements) {
previousElements.remove();
previousElements = null;
}
if (childScope) {
childScope.$destroy();
childScope = null;
}
if (block) {
previousElements = getBlockNodes (block.clone);
$animate.leave (previousElements).then(function () {
previousElements = null;
});
block = null;
}
}
})
}
};
}])
Here is the HTML
<a ui-sref="addStudentForm" cg-has-permissions="canAddNewStudent,canViewStudent">+</a>
There is no problem in the logic of how the permissions work. I have verified that. whenever I remove the directive form HTML, ui-sref works just fine but when I add my directive, the ui-sref probably doesn't get executed and href attributed is not added at all.
I tried an ng-click, which also doesn't work.
What is in this directive that is not letting other directives get executed ?
What I have done is, take the source code of ng-if and create my own directive with an extra condition of permission check but ng-if does work with other elements what's wrong here ?

Object html tag data attribute not working with angularjs binding in IE11

I got any application where I need to display file from urls I got in database. Now this file can be an image and it can be a pdf. So I need to set some binding dynamically. I looked on internet and object tag looked promising but it is not working in IE11. It is working fine in Chrome and Firefox. SO that is why I am asking here for help.
I have created a directive just to make sure If we have to do any dom manipulation. Here goes my directive code.
mainApp.directive("displayFile", function () {
return {
restrict: 'AE', // only activate on element attribute
scope: {
displayFile: "=",
fileType:"="
},
link: function (scope, elem, attrs) {
scope.filePath = "";
var element = angular.element(elem);
// observe the other value and re-validate on change
scope.$watch('displayFile', function (val) {
if (val !== "") {
scope.filePath = val;
scope.type="application/"+ fileType;
//element.attr("data", scope.filePath)
}
});
},
template: '<object data="{{filePath}}" type="{{type}}">'
}
});
My html for directive
<div data-display-pdf="fileUrl" file-type="type"></div>
Attaching an image also for IE and Chrome/FF output
Above image is a comparison between IE and FF
Final cut of directive which is working on IE11, Chrome and Firefox
use it like
<div data-display-file="fileObject"></div>
where fileObject is like
$scope.fileObject = {
fileUrl: "",
type: ""
}
mainApp.directive("displayFile", function () {
var updateElem = function (element) {
return function (displayFile) {
element.empty();
var objectElem = {}
if (displayFile && displayFile.type !== "") {
if (displayFile.type === "pdf") {
objectElem = angular.element(document.createElement("object"));
objectElem.attr("data", displayFile.fileUrl);
objectElem.attr("type", "application/pdf");
}
else {
objectElem = angular.element(document.createElement("img"));
objectElem.attr("src", displayFile.fileUrl);
}
}
element.append(objectElem);
};
};
return {
restrict: "EA",
scope: {
displayFile: "="
},
link: function (scope, element) {
scope.$watch("displayFile", updateElem (element));
}
};
});

What's the best way to implement a single tri-state checkbox within angularjs?

Is there a simple way to place a single tri-state checkbox on a web-page and bind it to a boolean model so the latter can take true, false or null values?
The closest solution I found so far is http://jsfiddle.net/HB7LU/454/ but it has a flaw when setting up an initial view state (as there is no way to get a model value during first rendering). Any other suggestions deal with multiple child checkboxes and solves the problem by watching on them.
http://jsfiddle.net/xJhEG/ I made it in a commercial project. Tristates are true, false, null (not "unknown")
.directive('indeterminate', [function() {
return {
require: '?ngModel',
link: function(scope, el, attrs, ctrl) {
var truthy = true;
var falsy = false;
var nully = null;
ctrl.$formatters = [];
ctrl.$parsers = [];
ctrl.$render = function() {
var d = ctrl.$viewValue;
el.data('checked', d);
switch(d){
case truthy:
el.prop('indeterminate', false);
el.prop('checked', true);
break;
case falsy:
el.prop('indeterminate', false);
el.prop('checked', false);
break;
default:
el.prop('indeterminate', true);
}
};
el.bind('click', function() {
var d;
switch(el.data('checked')){
case falsy:
d = truthy;
break;
case truthy:
d = nully;
break;
default:
d = falsy;
}
ctrl.$setViewValue(d);
scope.$apply(ctrl.$render);
});
}
};
}])
Here my Fiddle, starting from TruongSinh and changing
http://jsfiddle.net/xJhEG/25/
without
var truthy = true;
var falsy = false;
var nully = null;
You have take advantage of the indeterminate state of <input type="checkbox">.
MDN web docs:
There exists an indeterminate state of checkboxes, one in which it is not checked or unchecked, but undetermined. This is set using the HTMLInputElement object's indeterminate property via JavaScript (it cannot be set using an HTML attribute).
PLUNKER: TRISTATE DIRECTIVE
HTML
<label>
<input type="checkbox" ng-model="state" indeterminate /> {{state}}
</label>
DIRECTIVE
app.directive('indeterminate', function() {
return {
restrict: 'A',
scope: {
model: '=ngModel'
},
link: function(scope, el, attrs, ctrl) {
var states = [true, false, undefined];
var index = states.indexOf(scope.model);
setIndeterminate();
el.bind('click', function() {
scope.model = states[++index % 3];
setIndeterminate();
});
function setIndeterminate() {
scope.$applyAsync(function() {
el[0].indeterminate = (scope.model === undefined);
});
}
}
};
});
I've created directive, which you can use.
Three-state checkbox AngularJS Directive on GitHub
There is also a post, how it was built: Creating Angular Directive "Three-state checkbox
You can try a DEMO
And the directive looks like that:
angular.module("threeStateCheckbox", [])
.directive("threeStateCheckbox", ['$compile', function($compile){
return {
restrict: "A",
transclude: true,
require: 'ngModel',
link: function(scope, element, attrs, ngModel){
var states = [true, false, null];
var classNames = ["checked", "unchecked", "clear"];
scope.click = function(){
var st;
states.map(function(val, i){
if(ngModel.$modelValue === val){
st = states[(i+1)%3];
}
});
ngModel.$setViewValue(st);
ngModel.$render();
};
scope.tscClassName = function(){
var className;
states.map(function(val, i){
if(ngModel.$modelValue=== val){
className = classNames[i];
}
});
return className;
};
element.attr("class", "tri-sta-che ");
element.attr("ng-click", "click()");
element.attr("ng-class", "tscClassName()");
element.removeAttr("three-state-checkbox");
$compile(element)(scope);
}
};
}]);
Because all previous answers don't work since AngularJS 1.7 (the checkbox model only allows boolean values now and everything else gets converted to boolean), I now found a solution that works until v1.8:
Thanks to #The.Bear for the base.
To use it, simply include <tristate label="someOptionalText" ng-model="yourVariableToBindTo" /> and the corresponding directive.
var app = angular.module('tristatedemo', []);
app.controller('MyCtrl', function() {
this.state = null; //initial state
});
app.directive('tristate', function() {
return {
restrict: 'E',
scope: {
model: '=ngModel',
label: '#'
},
template: '<input type="checkbox" /> {{label}}',
link: function(scope, el, attrs, ctrl) {
var states = [true, false, null];
var index = states.indexOf(scope.model);
setIndeterminate();
el.bind('click', function() {
scope.model = states[++index % 3];
setIndeterminate();
});
function setIndeterminate() {
scope.$applyAsync(function() {
var cb = el.find('input')[0];
cb.checked = scope.model;
cb.indeterminate = (scope.model === null);
});
}
},
};
});
<!DOCTYPE html>
<html ng-app="tristatedemo">
<head>
<script data-require="angular.js#1.8.x" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.8.2/angular.min.js" data-semver="1.8.2"></script>
</head>
<body ng-controller="MyCtrl as ctrl">
{{(ctrl.state === null) ? "null" : ctrl.state}}<br> <!-- only for demo -->
<tristate label="abc" ng-model="ctrl.state" />
</body>
</html>

Categories