Angularjs share to social networks - angularjs
I'm using angularjs and wanted to add share buttons to my pages for different social networking sites. So far I've found Angular-socialshare and Socialitejs but don't know which one to choose or if other really good ones exist, that I don't even know of.
Here is an alternative to angular-socialshare (has share counter) that is currently the most popular/active on GitHub:
http://github.com/esvit/angular-social
Here is a simplified share button (no counter). This directive uses Font Awesome as a dependency.
http://github.com/tinacious/angular-easy-social-share
A note about metadata scraping and single page applications
Keep in mind that social network crawlers, which scrape HTML metadata for rich snippets (like OpenGraph links to images and descriptions), do not evaluate JavaScript. This can render certain meta tags in the header as {{bracketed}} expressions in a share box rather than dynamically loaded content... Assuming you have metadata that is loaded dynamically with AngularJS.
Check out this article about sharing rich snippets with Angular should you have that requirement: http://www.michaelbromley.co.uk/blog/171/enable-rich-social-sharing-in-your-angularjs-app
there is very easy solution that works fine with me,
you can rename you index.html to index.php
and include a php page that have a code to parse URL and based on the URL Parameters you can add your Title and description and image for twitter and facebook
here is the code of php page included in my index
<?
function curPageURL() {
$pageURL = 'http';
if ($_SERVER["HTTPS"] == "on") {$pageURL .= "s";}
$pageURL .= "://";
if ($_SERVER["SERVER_PORT"] != "80") {
$pageURL .= $_SERVER["SERVER_NAME"].":".$_SERVER["SERVER_PORT"].$_SERVER["REQUEST_URI"];
} else {
$pageURL .= $_SERVER["SERVER_NAME"].$_SERVER["REQUEST_URI"];
}
return $pageURL;
}
$parse = parse_url(curPageURL());
$urlArray = explode("/",$parse['path']);
$urlPage = $urlArray[2];
if($urlPage == "profile"){
$image = "profile-img.jpg";
$title = "Title of the page ";
$desc = "Description of Profile the page ";
$websiteUrl = "Http://example.com";
}elseif($urlPage == "news"){
$title = "Title of the News page ";
$desc = "Description of the page ";
$websiteUrl = "Http://example.com";
}
?>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title><? echo $title; ?></title>
<meta property="twitter:card" content="summary" />
<meta property="twitter:site" content="<? echo $websiteUrl; ?>" />
<meta property="twitter:title" content="<? echo $title; ?>" />
<meta property="twitter:description" content="<? echo $description; ?>" />
<meta property="twitter:image" content="<? echo $image; ?>" />
<meta property="twitter:url" content="<? echo curPageURL(); ?>" />
<meta property="og:title" content="<? echo $title; ?>" />
<meta property="og:description" content="<? echo $description; ?>" />
<meta property="og:image" content="<? echo $image; ?>" />
<meta property="og:type" content="article" />
<meta property="og:site_name" content="<? echo $website_name; ?>" />
<meta property="og:url" content="<? echo curPageURL(); ?>" />
Considering sinisterOrange's answer I'd like to add that there's some vital pieces of information about Michael Bromley's elegant solution.
In order to have multiple conditions for redirecting your urls into a script that will render the meta-tags correctly you must apply the condition multiple times for each rule.
And finally you'd have to make a negative condition for your app access.
<ifModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP_USER_AGENT} (facebookexternalhit/[0-9]|Twitterbot|Pinterest|Google.*snippet)
RewriteRule ^post/([A-Za-z0-9-]+)/([0-9-]+)$ http://www.example.com/api/renderMetaTags/post/$2 [P,L]
RewriteCond %{HTTP_USER_AGENT} (facebookexternalhit/[0-9]|Twitterbot|Pinterest|Google.*snippet)
RewriteRule ^location/([A-Za-z0-9-]+)/([0-9-]+)$ www.example.com/api/renderMetaTags/location/$2 [P,L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !index
#this condition for crawlers not to interfere with normal access
RewriteCond %{HTTP_USER_AGENT} !(facebookexternalhit/[0-9]|Twitterbot|Pinterest|Google.*snippet)
#this rule for html5 mode and friendly urls without the #
RewriteRule ^(.*)$ /#/$1 [L]
</ifModule>
That'd be it. I know it's a little bit off-topic but I lost valuable time arriving at this solution.
Consider this solution as it worked for me using mvc 5 with angular js
In Index page copy this code
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.12/angular.min.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
<script src='test.js'></script>
<script src="http://platform.twitter.com/widgets.js"></script>
</head>
<body ng-app='testing'>
<div ng-controller='temp'>
<div facebook class="facebookShare" data-title='{{title}}' data-picture-url='http://i.imgur.com/WACv9Cl.jpg' data-url='{{url}}' data-shares='shares' data-callback='callback'>{{ shares }} </div>
<br />
<a twitter data-count='horizontal' data-url='{{url}}' data-size="medium" data-text='{{text}}'></a>
<br /><br />
<div class="linkedinShare" linkedin data-url='{{url}}' data-title='{{title}}' data-summary="{{text}}" data-shares='linkedinshares'>{{linkedinshares}}</div>
<br /><br />
<div gplus data-size="tall" data-annotation="bubble" data-href='{{url}}' data-action='share'></div>
</div>
</body>
</html>
In script.js write this below mentioned code
angular.module('testing', ['djds4rce.angular-socialshare'])
.run(function ($FB) {
$FB.init('Facebook App Id');
});
angular.module('testing').controller('temp', function ($scope, $timeout) {
$timeout(function () {
$scope.url = 'http://google.com';
$scope.text = 'testing share';
$scope.title = 'title1'
}, 1000)
$timeout(function () {
$scope.url = 'https://www.youtube.com/watch?v=wxkdilIURrU';
$scope.text = 'testing second share';
$scope.title = 'title2';
}, 1000)
$scope.callback = function (response) {
console.log(response);
}
});
This code is for the directive
angular.module('djds4rce.angular-socialshare', [])
.factory('$FB', ['$window', function ($window) {
return {
init: function (fbId) {
if (fbId) {
this.fbId = fbId;
$window.fbAsyncInit = function () {
FB.init({
appId: fbId,
channelUrl: 'app/channel.html',
status: true,
xfbml: true
});
};
(function (d) {
var js,
id = 'facebook-jssdk',
ref = d.getElementsByTagName('script')[0];
if (d.getElementById(id)) {
return;
}
js = d.createElement('script');
js.id = id;
js.async = true;
js.src = "//connect.facebook.net/en_US/all.js";
ref.parentNode.insertBefore(js, ref);
}(document));
} else {
throw ("FB App Id Cannot be blank");
}
}
};
}]).directive('facebook', ['$http', function ($http) {
return {
scope: {
callback: '=',
shares: '='
},
transclude: true,
template: '<div class="facebookButton">' +
'<div class="pluginButton">' +
'<div class="pluginButtonContainer">' +
'<div class="pluginButtonImage">' +
'<button type="button">' +
'<i class="pluginButtonIcon img sp_plugin-button-2x sx_plugin-button-2x_favblue"></i>' +
'</button>' +
'</div>' +
'<span class="pluginButtonLabel">Share</span>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="facebookCount">' +
'<div class="pluginCountButton pluginCountNum">' +
'<span ng-transclude></span>' +
'</div>' +
'<div class="pluginCountButtonNub"><s></s><i></i></div>' +
'</div>',
link: function (scope, element, attr) {
attr.$observe('url', function () {
if (attr.shares && attr.url) {
$http.get('https://api.facebook.com/method/links.getStats?urls=' + attr.url + '&format=json').success(function (res) {
var count = res[0] ? res[0].total_count.toString() : 0;
var decimal = '';
if (count.length > 6) {
if (count.slice(-6, -5) != "0") {
decimal = '.' + count.slice(-6, -5);
}
count = count.slice(0, -6);
count = count + decimal + 'M';
} else if (count.length > 3) {
if (count.slice(-3, -2) != "0") {
decimal = '.' + count.slice(-3, -2);
}
count = count.slice(0, -3);
count = count + decimal + 'k';
}
scope.shares = count;
}).error(function () {
scope.shares = 0;
});
}
element.unbind();
element.bind('click', function (e) {
FB.ui({
method: 'share',
href: attr.url
}, function (response) {
if (scope.callback !== undefined && typeof scope.callback === "function") {
scope.callback(response);
}
});
e.preventDefault();
});
});
}
};
}]).directive('facebookFeedShare', ['$http', function ($http) {
return {
scope: {
callback: '=',
shares: '='
},
transclude: true,
template: '<div class="facebookButton">' +
'<div class="pluginButton">' +
'<div class="pluginButtonContainer">' +
'<div class="pluginButtonImage">' +
'<button type="button">' +
'<i class="pluginButtonIcon img sp_plugin-button-2x sx_plugin-button-2x_favblue"></i>' +
'</button>' +
'</div>' +
'<span class="pluginButtonLabel">Share</span>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="facebookCount">' +
'<div class="pluginCountButton pluginCountNum">' +
'<span ng-transclude></span>' +
'</div>' +
'<div class="pluginCountButtonNub"><s></s><i></i></div>' +
'</div>',
link: function (scope, element, attr) {
attr.$observe('url', function () {
if (attr.shares && attr.url) {
$http.get('https://api.facebook.com/method/links.getStats?urls=' + attr.url + '&format=json').success(function (res) {
var count = res[0] ? res[0].total_count.toString() : 0;
var decimal = '';
if (count.length > 6) {
if (count.slice(-6, -5) != "0") {
decimal = '.' + count.slice(-6, -5);
}
count = count.slice(0, -6);
count = count + decimal + 'M';
} else if (count.length > 3) {
if (count.slice(-3, -2) != "0") {
decimal = '.' + count.slice(-3, -2);
}
count = count.slice(0, -3);
count = count + decimal + 'k';
}
scope.shares = count;
}).error(function () {
scope.shares = 0;
});
}
element.unbind();
element.bind('click', function (e) {
FB.ui({
method: 'feed',
link: attr.url,
picture: attr.picture,
name: attr.name,
caption: attr.caption,
description: attr.description
}, function (response) {
if (scope.callback !== undefined && typeof scope.callback === "function") {
scope.callback(response);
}
});
e.preventDefault();
});
});
}
};
}]).directive('twitter', ['$timeout', function ($timeout) {
return {
link: function (scope, element, attr) {
var renderTwitterButton = debounce(function () {
if (attr.url) {
$timeout(function () {
element[0].innerHTML = '';
twttr.widgets.createShareButton(
attr.url,
element[0],
function () { }, {
count: attr.count,
text: attr.text,
via: attr.via,
size: attr.size
}
);
});
}
}, 75);
attr.$observe('url', renderTwitterButton);
attr.$observe('text', renderTwitterButton);
}
};
}]).directive('linkedin', ['$timeout', '$http', '$window', function ($timeout, $http, $window) {
return {
scope: {
shares: '='
},
transclude: true,
template: '<div class="linkedinButton">' +
'<div class="pluginButton">' +
'<div class="pluginButtonContainer">' +
'<div class="pluginButtonImage">in' +
'</div>' +
'<span class="pluginButtonLabel"><span>Share</span></span>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="linkedinCount">' +
'<div class="pluginCountButton">' +
'<div class="pluginCountButtonRight">' +
'<div class="pluginCountButtonLeft">' +
'<span ng-transclude></span>' +
'</div>' +
'</div>' +
'</div>' +
'</div>',
link: function (scope, element, attr) {
var renderLinkedinButton = debounce(function () {
if (attr.shares && attr.url) {
$http.jsonp('https://www.linkedin.com/countserv/count/share?url=' + attr.url + '&callback=JSON_CALLBACK&format=jsonp').success(function (res) {
scope.shares = res.count.toLocaleString();
}).error(function () {
scope.shares = 0;
});
}
$timeout(function () {
element.unbind();
element.bind('click', function () {
var url = encodeURIComponent(attr.url).replace(/'/g, "%27").replace(/"/g, "%22")
$window.open("//www.linkedin.com/shareArticle?mini=true&url=" + url + "&title=" + attr.title + "&summary=" + attr.summary);
});
});
}, 100);
attr.$observe('url', renderLinkedinButton);
attr.$observe('title', renderLinkedinButton);
attr.$observe('summary', renderLinkedinButton);
}
};
}]).directive('gplus', [function () {
return {
link: function (scope, element, attr) {
var googleShare = debounce(function () {
if (typeof gapi == "undefined") {
(function () {
var po = document.createElement('script');
po.type = 'text/javascript';
po.async = true;
po.src = 'https://apis.google.com/js/platform.js';
po.onload = renderGoogleButton;
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(po, s);
})();
} else {
renderGoogleButton();
}
}, 100);
//voodo magic
var renderGoogleButton = (function (ele, attr) {
return function () {
var googleButton = document.createElement('div');
var id = attr.id || randomString(5);
attr.id = id;
googleButton.setAttribute('id', id);
element.innerHTML = '';
element.append(googleButton);
if (attr.class && attr.class.indexOf('g-plusone') != -1) {
window.gapi.plusone.render(id, attr);
} else {
window.gapi.plus.render(id, attr);
}
}
}(element, attr));
attr.$observe('href', googleShare);
}
};
}]);
function debounce(func, wait, immediate) {
var timeout;
return function () {
var context = this,
args = arguments;
var later = function () {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
function randomString(len, an) {
an = an && an.toLowerCase();
var str = "",
i = 0,
min = an == "a" ? 10 : 0,
max = an == "n" ? 10 : 62;
for (; i++ < len;) {
var r = Math.random() * (max - min) + min << 0;
str += String.fromCharCode(r += r > 9 ? r < 36 ? 55 : 61 : 48);
}
return str;
}
Related
How to show angular bootstrap popover on custom event?
I want to display bootstrap popover on custom event (after loading data from server). I don't understand why this code don't work. JSFiddle And second question, how to trigger events if I set replace = true? Because of IE8 doesn't support custom tags and throws exception. <div ng-app="tcApp"> <manual></manual> </div> var tcApp = angular.module('tcApp', [ 'ui.bootstrap' ]); tcApp.config(function ($tooltipProvider) { $tooltipProvider.setTriggers({ 'openAfterLoad': 'closeOnClick' }); }); tcApp.directive('manual', [ '$timeout', '$interval', function($timeout, $interval) { return { restrict: 'E', scope: {}, template: '<a ' + ' href="#" ' + ' ng-if="isManualVisible" ' + ' popover-append-to-body="true" ' + ' popover-placement="bottom" ' + ' popover-trigger="openAfterLoad" ' + ' popover-title="{{title}}" ' + ' popover="{{content}}" ' + ' tooltip="{{title}}" ' + ' >' + ' <span class="{{textClass}}">' + ' <span ng-show="!loading" class="glyphicon glyphicon-question-sign"></span>' + ' <span ng-show="loading" class="glyphicon glyphicon-refresh"></span>' + ' </span>' + '</a>', replace: false, // true for IE8 only link: function (scope, element, attr) { scope.isManualVisible = true; scope.title = 'First title value'; scope.content = 'First content value'; scope.textClass = 'text-info'; scope.opened = false; scope.loading = false; scope.loaded = false; scope.loadedTitle = false; scope.loadedContent = false; scope.checkLoadState = function () { if (scope.loadedTitle && scope.loadedContent) { scope.loaded = true; if (scope.loading) scope.loading = false; element.triggerHandler('openAfterLoad'); } }; scope.$watch('loadedTitle', scope.checkLoadState); scope.$watch('loadedContent', scope.checkLoadState); element.on('openAfterLoad', function(event) { console.log('openAfterLoad'); scope.opened = true; }); element.on('closeOnClick', function(event) { console.log('closeOnClick'); scope.opened = false; }); element.on('click', function(event) { console.log('click'); if (scope.loaded) { if (scope.opened) element.triggerHandler('closeOnClick'); else element.triggerHandler('openAfterLoad'); } else { scope.loadData(); } }); scope.loadData = function() { if (!scope.loaded && !scope.loading) { scope.loading = true; $interval(function() { scope.title = 'New title value'; scope.loadedTitle = true; }, 5000, 1); $interval(function() { scope.content = 'New content value'; scope.loadedContent = true; }, 5000, 1); } }; } }; }]);
Ok, I'm found solution. First of all, mixing of replace=true and ng-if on root template element is a bad idea. Next, I separated angular on/broadcast events and native js events. Native events still required for popover. JSFiddle <div ng-app="tcApp"> <manual></manual> </div> var tcApp = angular.module('tcApp', [ 'ui.bootstrap' ]); tcApp.config(function ($tooltipProvider) { $tooltipProvider.setTriggers({ 'openAfterLoad': 'closeOnClick' }); }); tcApp.directive('manual', [ '$timeout', '$interval', function($timeout, $interval) { return { restrict: 'E', scope: {}, template: '<a ' + ' href="#" ' + ' ng-show="isManualVisible" ' + ' ng-click="clickHandler()" ' + ' popover-append-to-body="true" ' + ' popover-placement="bottom" ' + ' popover-trigger="openAfterLoad" ' + ' popover-title="{{title}}" ' + ' popover="{{content}}" ' + ' tooltip="{{title}}" ' + ' >' + ' <span class="{{textClass}}">' + ' <span ng-show="!loading" class="glyphicon glyphicon-question-sign"></span>' + ' <span ng-show="loading" class="glyphicon glyphicon-refresh"></span>' + ' </span>' + '</a>', replace: true, // true for IE8 only link: function (scope, element, attr) { scope.isManualVisible = true; scope.title = 'First title value'; scope.content = 'First content value'; scope.textClass = 'text-info'; scope.opened = false; scope.loading = false; scope.loaded = false; scope.loadedTitle = false; scope.loadedContent = false; scope.checkLoadState = function () { if (scope.loadedTitle && scope.loadedContent) { scope.loaded = true; if (scope.loading) scope.loading = false; scope.$broadcast('openAfterLoad'); } }; scope.$watch('loadedTitle', scope.checkLoadState); scope.$watch('loadedContent', scope.checkLoadState); scope.$on('openAfterLoad', function(event) { console.log('openAfterLoad'); scope.opened = true; $timeout(function() { // trigger popover element.triggerHandler('openAfterLoad'); }); }); scope.$on('closeOnClick', function(event) { console.log('closeOnClick'); scope.opened = false; $timeout(function() { // trigger popover element.triggerHandler('closeOnClick'); }); }); scope.clickHandler = function(event) { console.log('click'); if (scope.loaded) { if (scope.opened) scope.$broadcast('closeOnClick'); else scope.$broadcast('openAfterLoad'); } else { scope.loadData(); } }; scope.loadData = function() { if (!scope.loaded && !scope.loading) { scope.loading = true; $interval(function() { scope.title = 'New title value'; scope.loadedTitle = true; }, 2000, 1); $interval(function() { scope.content = 'New content value'; scope.loadedContent = true; }, 3000, 1); } }; } }; }]);
Is it okay to have $compile inside the link part of a directive?
I am using AngularJS v1.4.1 and I have the following directive. It was I thought working but now for some reason when my page loads the directive gets called twice. I checked everything I could but I cannot see anything different from before to now except the directive no longer works. Specifically what happens is it appears to be getting called twice. app.directive('pagedownAdmin', ['$compile','$timeout', function ($compile, $timeout) { var nextId = 0; var converter = Markdown.getSanitizingConverter(); converter.hooks.chain("preBlockGamut", function (text, rbg) { return text.replace(/^ {0,3}""" *\n((?:.*?\n)+?) {0,3}""" *$/gm, function (whole, inner) { return "<blockquote>" + rbg(inner) + "</blockquote>\n"; }); }); return { require: 'ngModel', replace: true, scope: { modal: '=modal' }, template: '<div class="pagedown-bootstrap-editor"></div>', link: function (scope, iElement, attrs: any, ngModel) { var editorUniqueId; if (attrs.id == null) { editorUniqueId = nextId++; } else { editorUniqueId = attrs.id; } scope.showPagedownButtons = function () { document.getElementById("wmd-button-bar-" + editorUniqueId).style.display = 'block'; }; var newElement = $compile( '<div>' + '<div class="wmd-panel">' + '<div data-ng-hide="modal.wmdPreview == true" id="wmd-button-bar-' + editorUniqueId + '" style="display:none;"></div>' + '<textarea ng-click="showPagedownButtons()" data-ng-hide="modal.wmdPreview == true" class="wmd-input" id="wmd-input-' + editorUniqueId + '">' + '</textarea>' + '</div>' + '<div data-ng-show="modal.wmdPreview == true" id="wmd-preview-' + editorUniqueId + '" class="pagedownPreview wmd-panel wmd-preview"></div>' + '</div>')(scope); // iElement.html(newElement); iElement.append(newElement); var hide = function () { document.getElementById("wmd-button-bar-" + editorUniqueId).style.display = 'none'; } var editor = new Markdown.Editor(converter, "-" + editorUniqueId, { handler: hide }); // var $wmdInput = iElement.find('#wmd-input-' + editorUniqueId); var $wmdInput = angular.element(document.getElementById("wmd-input-" + editorUniqueId)); var init = false; editor.hooks.chain("onPreviewRefresh", function () { var val = $wmdInput.val(); if (init && val !== ngModel.$modelValue) { $timeout(function () { scope.$apply(function () { ngModel.$setViewValue(val); ngModel.$render(); }); }); } }); ngModel.$formatters.push(function (value) { init = true; $wmdInput.val(value); editor.refreshPreview(); return value; }); editor.run(); } } }]); Here is the code that calls the directive: <textarea data-pagedown-admin data-modal="cos" id="contentText" name="contentText" ng-minlength="5" ng-model="cos.content.text" ng-required="true"></textarea> When I debug the directive by putting a breakpoint on "var editorUniqueId" then I see it goes there twice. Does anyone have any ideas what might be happening?
I have done this in the past without issue: var newElement = '<div>' + // ... more HTML ... '</div>'; iElement.append( $compile(newElement)(scope) );
Creating PDF and HTML output using PhantomJS and evaluate Angular directives to be included both in HTMl and PDF output
I am trying to append a angular directive inside HTML so that PhantomJS creates the HTML and PDF output. The snippet is below: Code where I am trying to append Angular directive is below: function drawWidget(widgets) { for(var i = 0; i < widgets.length; i++) { var iDiv = document.createElement('div'); var att = document.createAttribute("data-ss-colspan"); iDiv.className = "widget_box"; iDiv.id = "graphId" + i; iDiv.innerHTML = '<div class="col-md-12 col-sm-12 head_categories">\ <span>' + widgets[i]["widgetTitle"] + '</span>\ <span class="pull-right menu_categori"></span>\ </div>\ <div style="clear:both;"></div>'; att.value = widgets[i]["widgetSize"].toString(); // Set the value of the class attribute iDiv.setAttributeNode(att); document.getElementById('widget_container').appendChild(iDiv); //var chartDiv = document.createElement('editor'); document.getElementById(iDiv.id).append(angular.element('#widget_container'). scope().getChartElement(widgets, i)); } } My Angular directive seems to be as shown below: angular.module('app',[]).directive("editor", function(){ return { require: "?ngModel", scope: true, controller: 'ChartController', template: "<input ng-model='value' ng-change='onChange()'>", link: function(scope, element, attrs, ngModel) { if (!ngModel) return; scope.onChange = function(){ ngModel.$setViewValue(scope.value); }; ngModel.$render = function(){ scope.value = ngModel.$modelValue; }; } }; }); The get ChartElement code is below: (function(){ 'use strict' angular.module('app').controller('ChartController', chartController); chartController.$inject = ['$scope', '$compile']; function chartController($scope, $compile) { $scope.getChartElement = function(widgets, i) { $scope.id = "graphId" + i; $scope.data = widgets[i]["chart_data"]["data"]; /*var elementString ='<line-graph graph-id="id" graph-data="data" ' + ' width="' + widgets[i]["chart_data"]["divWidth"] + '" height="' + widgets[i]["chart_height"] + '" colors="' + widgets[i]["chart_data"]["colors"] + '" interval="' + widgets[i]["chart_data"]["interval"] + '"></line-graph>';*/ var elementString ='<editor ng-model="abc" minlength="3"></editor>'; console.log("Element String : " + elementString); return elementString; }; }; })();
Using JSPlumb in an angular.js directive
I'm trying to use jsPlumb along with AngularJS and have run into some problems. I started with the jsplumb-ng example from github and attempted to modify the jsPlumb parameters to mimic the statemachine demo from the jsPlumb site. The jsplumb-ng approach creates a jsPlumbCanvas directive and it contains jsPlumbNodes. As a starting point I tried to place some static nodes from the statemachine demo and then add in two nodes controlled by the angular controller. The demo nodes are placed on the canvas correctly and connections are created. The angular nodes are created and jsPlumb has trouble in the makeTarget method and prints messages to the console. TypeError: Cannot set property '_jsPlumbTarget' of undefined {stack: (...), message: "Cannot set property '_jsPlumbTarget' of undefined"} I would greatly appreciate any advice to get moving forward again! Apologize up front because I'm dancing around the fact that I do not have enough reputation to post images of the problem. i.imgur.com/Ao4qzm9.png i.imgur.com/OMS0FGE.png Plunk index.html <!DOCTYPE html> <html ng-app="plunker"> <head> <meta charset="utf-8" /> <title>AngularJS Plunker</title> <link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet"> <link href="https://code.jquery.com/ui/1.11.3/themes/smoothness/jquery-ui.css" rel="stylesheet"> <link data-require="bootstrap#3.3.1" data-semver="3.3.1" rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css" /> <script>document.write('<base href="' + document.location + '" />');</script> <link rel="stylesheet" href="https://jsplumbtoolkit.com/css/jsplumb.css" /> <link rel="stylesheet" href="style.css" /> <script data-require="angular.js#1.3.x" src="https://code.angularjs.org/1.3.14/angular.js" data-semver="1.3.14"></script> <script data-require="jquery#*" data-semver="2.1.3" src="http://code.jquery.com/jquery-2.1.3.min.js"></script> <script src="https://code.jquery.com/ui/1.11.3/jquery-ui.js"></script> <script data-require="bootstrap#3.3.1" data-semver="3.3.1" src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script> <script data-require="jsplumb#*" data-semver="1.7.3" src="https://jsplumbtoolkit.com/js/jquery.jsPlumb-1.7.3-min.js"></script> <script src="http://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.12.0.min.js"></script> <script src="app.js"></script> </head> <body ng-controller="DiagramEditorCtrl"> <div id="diagramView" style="overflow: hidden;" ng-cloak> <js-plumb-canvas on-connection="onConnection1" x="posx" y="posy" zoom="zoomlevel" ng-drop="true" ng-drop-success="onDropComplete1($data,$event)" > <div ng-repeat="state in stateObjects track by state.uuid"> <js-plumb-node state-object="state" style="positon: relative;"></js-plumb-node> </div> <js-plumb-connection ng-click="setActiveConnection($index)" ng-repeat="connection in stateConnections" ng-model="connection"> </js-plumb-connection> </js-plumb-canvas> </div> </body> </html> app.js var app = angular.module('plunker', []); app.controller('DiagramEditorCtrl', ['$scope', function($scope) { $scope.zoomlevel = 85; $scope.posx = 14; $scope.posy = 14; $scope.stateObjects = [{ 'name': 'jsPlumb', 'uuid': '123', 'style': '', 'x': 300, 'y': 150 }, { 'name': 'Brewer', 'uuid': '124', 'style': '', 'x': 45, 'y': 175 } ]; // Defaults the connections between objects. Will come from a service. $scope.stateConnections = [ // { // targetUUID: 'ds1', // sourceUUID: 'sn1' // } ]; var instance = jsPlumb.instance; $scope.onConnection = function(instance, connection, targetUUID, sourceUUID) { $scope.stateConnections.push({ 'targetUUID': targetUUID, 'sourceUUID': sourceUUID, 'conn': connection }); $scope.$apply(); }; } ]) .directive('jsPlumbCanvas', function() { var jsPlumbZoomCanvas = function(instance, zoom, el, transformOrigin) { transformOrigin = transformOrigin || [0, 0]; var p = ['webkit', 'moz', 'ms', 'o'], s = 'scale(' + zoom + ')', oString = (transformOrigin[0] * 100) + '% ' + (transformOrigin[1] * 100) + '%'; for (var i = 0; i < p.length; i++) { el.style[p[i] + 'Transform'] = s; el.style[p[i] + 'TransformOrigin'] = oString; } el.style.transform = s; el.style.transformOrigin = oString; instance.setZoom(zoom); }; var def = { restrict: 'E', scope: { onConnection: '=onConnection', zoom: '=', x: '=', y: '=' }, controller: function($scope) { this.scope = $scope; }, transclude: true, templateUrl: 'workspace.html', link: function(scope, element, attr) { angular.element(document).ready(function () { var instance = jsPlumb.getInstance({ Endpoint : ['Dot', {radius:2}], HoverPaintStyle : {strokeStyle:'#1e8151', lineWidth:2 }, ConnectionOverlays : [ [ 'Arrow', { location:1, id:'arrow', length:14, foldback:0.8 } ], [ 'Label', { label:'Connecting...', id:'label', cssClass:'aLabel' }] ], Container:'workspace-container' }); scope.jsPlumbInstance = instance; /* * Standard JSPlumb Statemachine example that works fine. */ var windows = jsPlumb.getSelector('.workspace-container .node'); instance.draggable(windows); instance.bind('click', function(c) { instance.detach(c); }); instance.bind('connection', function(info) { info.connection.getOverlay('label').setLabel(info.connection.id); }); instance.doWhileSuspended(function() { var isFilterSupported = instance.isDragFilterSupported(); if (isFilterSupported) { instance.makeSource(windows, { filter:'.ep', anchor:'Continuous', connector:[ 'StateMachine', { curviness:20 } ], connectorStyle:{ strokeStyle:'#5c96bc', lineWidth:2, outlineColor:'transparent', outlineWidth:4 }, maxConnections:5, onMaxConnections:function(info, e) { alert('Maximum connections (' + info.maxConnections + ') reached'); } }); } else { var eps = jsPlumb.getSelector('.ep'); for (var i = 0; i < eps.length; i++) { var e = eps[i], p = e.parentNode; instance.makeSource(e, { parent:p, anchor:'Continuous', connector:[ 'StateMachine', { curviness:20 } ], connectorStyle:{ strokeStyle:'#5c96bc',lineWidth:2, outlineColor:'transparent', outlineWidth:4 }, maxConnections:5, onMaxConnections:function(info, e) { alert('Maximum connections (' + info.maxConnections + ') reached'); } }); } } }); instance.makeTarget(windows, { dropOptions:{ hoverClass:'dragHover' }, anchor:'Continuous', allowLoopback:true }); instance.connect({ source:'opened', target:'phone1' }); instance.connect({ source:'phone1', target:'phone1' }); jsPlumb.fire('jsPlumbDemoLoaded', instance); /* * End standard JSPlumb statemachine example */ }); } }; return def; }) .directive('jsPlumbNode', function() { var def = { restrict: 'E', require: '^jsPlumbCanvas', scope: { node: '=stateObject' }, // At one point, I tried to have a real uuid assigned as the div#id, but // JSPlumb seemed to see {{node.uuid}} // id="{{node.uuid}}" template: '<div class="node" ' + ' ng-style="{ \'left\':node.x, \'top\':node.y }" >' + ' <div id="ds1-nh" class="node-header" ></div>' + ' <div id="ds1-nb" class="node-body">' + '{{node.name}}' + ' </div>' + ' <div id="ds1-nt" class="node-tools">' + ' <div id="ds1-nt-o" class="node-tool-options">' + ' <span ng-click="configure($index)" class="glyphicon glyphicon-cog"></span>' + ' </div>' + ' <div id="ds1-nt-d" class="node-tool-delete">' + ' <span ng-click="trash($index)" class="glyphicon glyphicon-trash"></span>' + ' </div>' + ' </div>' + '</div>', link: function($scope, element, attrs, jsPlumbCanvas) { // removed dependence on uuid4 for this plunker. $scope.node.uuid = '1234-123-1233'; angular.element(document).ready(function () { var instance = jsPlumbCanvas.scope.jsPlumbInstance; function displayMaxConnectionError(info, e) { alert('Maximum connections (' + info.maxConnections + ') reached'); } var nodeEl = element.find('.node' ); instance.draggable( nodeEl, { grid: [20, 20], drag: function(event, ui) { var posX, posY; if( typeof event.pos !== 'undefined') { posX = event.pos[0]; posY = event.pos[1]; } if (typeof ui !== 'undefined') { posX = ui.position.left; posY = ui.position.top; } $scope.node.x = posX; $scope.node.y = posY; $scope.$apply(); } }); // suspend drawing and initialise. instance.doWhileSuspended(function() { var eps = element.find('.node-header'); //console.log(eps); for (var i = 0; i < eps.length; i++) { var e = eps[i], p = e.parentNode; console.log( 'e: %o', e ); console.log( 'e.parent: %o', p ); instance.makeSource(e, { parent: p, anchor: 'Continuous', connector: ['StateMachine', { curviness: 20 }], connectorStyle: { strokeStyle: '#5c96bc', lineWidth: 2, outlineColor: 'transparent', outlineWidth: 4 }, maxConnections: 5, onMaxConnections: displayMaxConnectionError }); } instance.makeTarget(nodeEl, { dropOptions: { hoverClass: 'dragHover' }, anchor: 'Continuous', allowLoopback: true }); }); }); } }; return def; }) .directive('jsPlumbConnection', function($timeout) { var def = { restrict: 'E', require: '^jsPlumbCanvas', scope: { ngClick: '&ngClick', ngModel: '=ngModel' }, link: function($scope, element, attrs, jsPlumbCanvas) { var instance = jsPlumbCanvas.scope.jsPlumbInstance; $timeout(function() { if (typeof $scope.ngModel.conn === 'undefined') { $scope.ngModel.conn = instance.connect({ uuids: [ $scope.ngModel.targetUUID, $scope.ngModel.sourceUUID ], overlays: [ ['Label', { label: '', id: 'label' }] ], editable: true }); } var connection = $scope.ngModel.conn; connection.bind('mouseenter', function(conn, originalEvent) { $scope.ngModel.mouseover = true; $scope.$apply(); }); connection.bind('mouseleave', function(conn, originalEvent) { $scope.ngModel.mouseover = false; $scope.$apply(); }); }, 1300); $scope.$on('$destroy', function() { instance.detach($scope.ngModel.conn); }); } }; return def; }) ; I have also looked at the mrquincle/jsplumb-example from github. It works fine until I tried to load a more recent copy of jsPlumb, jquery, jquery-ui and angular. The issue with the updated libraries is that the connections are not attached between the two nodes. i.imgur.com/VLtjWFy.png Plunk I am not tied to either the jsplumb-ng or the jsplumb-example approach. I'm just trying to get one to work. Thanks ahead of time!
AngularJS Editable Table
I want to create a editable table in AngularJs. Same date I am using for another list and details view in same page. My query is when I click the edit link in the list details view (Selected View Section in the page) part and modify the values in the textbox it is not changing in the table view section until I click the save button, but when I click the edit in the table section (Editable Table section in the page) part and modify the value in the textbox is it changing in the Selected View section also. But, I want the change the all the values after I click the save link. Please fine the sample code and advise me. <!doctype html> <html lang="en" ng-app="myApp"> <head> <meta charset="UTF-8"> <title>Example - example-example53-production</title> <script src="js/angular.min.js"></script> </head> <body ng-controller="MainCtrl"> <h1>Editable Table</h1> <table id="searchObjResults"> <tr><th ng-click="sort('name')">Name</th><th ng-click="sort('phone')">Phone</th></tr> <tr><td><input ng-model="search.name"></td><td><input ng-model="search.phone"></td></tr> <tr ng-repeat="friendObj in users | orderBy:orderProp:direction| filter:search:strict" ng-class-odd="'odd'"> <td click-to-edit="friendObj" type="tbl"></td> </tr> </table> <h1>Selected View</h1> <ul> <li ng-repeat="user in users" ng-class="{active: checkActive(user)}" ng:click="select(user)">{{user.name}}</li> </ul> <p>selected: {{selectedUser.phone}}</p> <p click-to-edit="selectedUser.name"></p> <p click-to-edit="selectedUser.phone"></p> <script> var myApp = angular.module('myApp', []); //myApp.by.id('setbtn')element('h1').addClass('active'); myApp.controller('MainCtrl', ['$scope','$filter', function ($scope,$filter) { $scope.users = [{name:'John', phone:'555-1276'}, {name:'John', phone:'555-1278'}, {name:'Mary', phone:'800-BIG-MARY'}, {name:'Mike', phone:'555-4321'}, {name:'Adam', phone:'555-5678'}, {name:'Julie', phone:'555-8765'}, {name:'Juliette', phone:'555-5678'}]; //setting for order $scope.users = $filter('orderBy')($scope.users, 'name'); //to set the defalult search //$scope.search = { //phone : "555-1278" //}; //sorting $scope.direction = false; $scope.orderProp = "name"; $scope.sort = function(column) { if ($scope.orderProp === column) { $scope.direction = !$scope.direction; } else { $scope.orderProp = column; $scope.direction = false; } }; //selected when list click $scope.select = function(user) { $scope.selectedUser = user; }; //applying the selected Class $scope.checkActive = function(user) { return $scope.selectedUser == user; }; //set the first item as selected //$scope.select($scope.users[0]); $scope.selectedUser = $scope.users[0]; }]); myApp.directive("clickToEdit", function() { var editorTemplate = '<td class="click-to-edit">' + '<div ng-hide="view.editorEnabled">' + '{{value.name}} ' + '{{value.phone}} ' + 'Edit' + '</div>' + '<div ng-show="view.editorEnabled">' + '<input ng-model="view.editableValue.name">' + '<input ng-model="view.editableValue.phone">' + 'Save' + ' or ' + 'cancel.' + '</div>' + '</td>'; var editorTemplate1 = '<div class="click-to-edit">' + '<div ng-hide="view.editorEnabled">' + '{{value}} ' + 'Edit' + '</div>' + '<div ng-show="view.editorEnabled">' + '<input ng-model="view.editableValue">' + 'Save' + ' or ' + 'cancel.' + '</div>' + '</div>'; return { restrict: "A", replace: true, //template: editorTemplate, template: function(element, attrs) { if(attrs.type=='tbl'){ return editorTemplate; } else{ return editorTemplate1; } }, scope: { value: "=clickToEdit", }, link: function(scope, element, attrs) { //alert(element); }, controller: function($scope) { $scope.view = { editableValue: $scope.value, editorEnabled: false }; $scope.enableEditor = function() { $scope.view.editorEnabled = true; $scope.view.editableValue = $scope.value; }; $scope.disableEditor = function() { $scope.view.editorEnabled = false; }; $scope.save = function() { $scope.value = $scope.view.editableValue; $scope.disableEditor(); }; } }; }); </script> <style> .active{color:green} </style> </body> </html>
When you click edit, you could do a clone of your model and edit that. Then when you click save, update (or replace) the original model. Discard the changes if they do not click save. There is a cloneDeep function in the underscore library that you could use.