On my blog, I want to be able to have post-specific interactive demos. So each post has both its content and the example demo, which is HTML to be rendered to the page.
So far, no problem. I created a render_html directive:
angular.module("RenderHtml", []).directive "renderHtml", ->
restrict: "A"
scope:
renderHtml: "#"
link: (scope, element, attrs) ->
scope.$watch "renderHtml", (newVal) ->
element.html(newVal)
And I call it like this:
<div class='example' renderHtml='{{post.example}}'></div>
The issue is, I'd like that HTML to have embedded, executed Angular.
So the rendered example HTML would look something like this:
<div ng-controller='SpecificExampleCtrl' ng-init='initFunc()'>
<a ng-click='someFunc()'>Etc</a>
</div>
And when the page was rendered, the SpecificExampleCtrl would be loaded, its init function run, and that ng-click run when that link was clicked.
(I've resigned myself to, if I even manage to get this to work, having to save the ng_controller in the app, but if anyone can think of a way to have that saved in the DB as well, I'd be ecstatic.)
So, at any rate, my problem seems to differ from [AngularJS: callback after render (work with DOM after render) one) and others.
And to clarify what I've been able to get done -- the HTML is rendered as HTML, but none of its Angular is run, even though the Controller being called does exist in my app.
EDIT IN RESPONSE TO SUGGESTION
angular.module("RenderHtml", []).directive ($compile) "renderHtml", ->
restrict: "A"
scope:
renderHtml: "#"
link: (scope, element, attrs) ->
scope.$watch "renderHtml", (newVal) ->
element.html(newVal)
$compile(eval(element))
(The above doesn't work as I've written it. It renders the HTML, but doesn't evaluate the angular at all.)
EDIT It looks like I should be using $eval instead of the vanilla eval, but when I try to inject that into the directive or call it without injecting, the site errors, and when I inject and use $parse, which looks like it does similar things, nothing in the entire angular template renders, and I get no errors.
ANSWER
This ended up working:
angular.module("RenderHtml", []).directive "renderHtml", ($compile) ->
restrict: "A"
scope:
renderHtml: "#"
link: (scope, element, attrs) ->
scope.$watch "renderHtml", (newVal) ->
linkFunc = $compile(newVal)
element.html(linkFunc(scope))
Compiling html returns a function that an argument of the scope.
You can use 'eval' to execute javascript you get from the database to add the controller. Not angular's $eval which evaluates according to a scope, but the vanilla javascript eval which will compile your code. This is not very secure, if there's any chance of user input into the js you probably don't want to do it since it'll be executed in the context of the user on your site. The f at the end of the string returns the function as an object as the result of eval().
eval(response.controllerJavascript);
Then you need to $compile your html. I based my fiddle on this example. Finally you use $injector to call your controller function on your scope.
Directive:
module.directive('compile', function($compile, $injector) {
var obj = {
scope: true, // child scope
link: function(scope, element, attrs) {
// 1st function returns value, if changed call 2nd
scope.$watch(
function(scope) {
return scope.$eval(attrs.compile);
},
function(value) {
element.html(value);
$compile(element.contents())(scope);
}
);
scope.$watch(function(scope) {
return scope.$eval(attrs.compileCode);
}, function(value) {
// get 'function' object
var controller = eval(value);
if (typeof(controller) == "function") {
// invoke controller on our child scope
$injector.invoke(controller, this, { $scope: scope });
}
});
}
};
return obj;
});
HTML:
<div ng-app="TestApp" ng-controller="Ctrl" id="divCtrl">
<label>Name:</label>
<input ng-model="name"> <br/>
<label>Html:</label>
<textarea ng-model="html"></textarea> <br/>
<label>Js:</label> <textarea ng-model="js"></textarea> <br/>
<div compile="html" compile-code="js">Hi {{name}}</div>
<input type="button" value="Simulate AJAX" ng-click="simulateAjax()">
</div>
Controller:
module.controller("Ctrl", function($scope) {
$scope.name = 'Angular';
var code = 'var f = function($scope) { $scope.name = "Ctrl2"; }\r\nf';
$scope.simulateAjax = function() {
$scope.html = '<div>Hello {{name}}</div>';
$scope.js = code;
code = 'var f = function($scope) { $scope.name = "Ctrl2-next"; }\r\nf';
};
});
Related
I'm in need of building a transformation directive that transforms custom directives into html.
Input like: <link text="hello world"></link>
should output to: <a class="someclass" ng-click="linkClicked('hello world')"></a>
linkClicked should be called on the parent controller of the directive.
It would have been very easy if I was the one responsible for the html holding the 'link' directive (using isolated scope), but I'm not. It's an as-is input and I have to figure a way to still do it.
There are countless examples on how to do similar bindings using the default scope of the directive, but I'm writing my controllers using John Papa's recommendations with controllerAs, but don't want to create another instance on the controller in the directive.
This is what I have reached so far:
(function () {
'use strict';
angular
.module('app')
.directive('link', link);
link.$inject = ['$compile'];
function link($compile) {
return {
restrict: 'E',
replace: true,
template: '<a class="someclass"></a>',
terminal: true,
priority: 1000,
link: function (scope, element, attributes) {
element.removeAttr('link'); // Remove the attribute to avoid indefinite loop.
element.attr('ng-click', 'linkClicked(\'' + attributes.text + '\')');
$compile(element)(scope);
},
};
}
})();
$scope.linkClicked = function(text){...} in the parent controller works.
element.attr('ng-click', 'abc.linkClicked(..)') in the directive (where the parent's controllerAs is abc) - also works.
The problem is I don't know which controller will use my directive and can't hard-code the 'abc' name in it.
What do you suggest I should be doing?
It's difficult to understand from your question all the constraints that you are facing, but if the only HTML you get is:
<link text="some text">
and you need to generate a call to some function, then the function must either be:
assumed by the directive, or
conveyed to the directive
#1 is problematic because the user of the directive now needs to understand its internals. Still, it's possible if you assume that a function name is linkClicked (or whatever you want to call it), and the user of your directive would have to take special care to alias the function he really needs (could be done with "controllerAs" as well):
<div ng-controller="FooCtrl as foo" ng-init="linkClicked = foo.actualFunctionOfFoo">
...
<link text="some text">
...
</div>
app.directive("link", function($compile){
return {
transclude: "element", // remove the entire element
link: function(scope, element, attrs, ctrl){
var template = '<a class="someclass" ng-click="linkClicked(\'' +
attrs.text +
'\')">link</a>';
$compile(template)(scope, function(clone){
element.after(clone);
});
}
};
});
Demo
#2 is typically achieved via attributes, which isn't possible in your case. But you could also create a sort of "proxy" directive - let's call it onLinkClick - that could execute whatever expression you need:
<div ng-controller="FooCtrl as foo"
on-link-click="foo.actualFunctionOfFoo($data)">
...
<link text="some text">
...
</div>
The link directive now needs to require: "onLinkClick":
app.directive("link", function($compile){
return {
transclude: "element", // remove the entire element
scope: true,
require: "?^onLinkClick",
link: function(scope, element, attrs, ctrl){
if (!ctrl) return;
var template = '<a class="someclass" ng-click="localClick()">link</a>';
scope.localClick = function(){
ctrl.externalFn(attrs.text);
};
$compile(template)(scope, function(clone){
element.after(clone);
});
}
};
});
app.directive("onLinkClick", function($parse){
return {
restrict: "A",
controller: function($scope, $attrs){
var ctrl = this;
var expr = $parse($attrs.onLinkClick);
ctrl.externalFn = function(data){
expr($scope, {$data: data});
};
},
};
});
Demo
Notice that having a link directive would also execute on <link> inside <head>. So, make attempts to detect it and skip everything. For the demo purposes, I used a directive called blink to avoid this issue.
I have a directive which needs to plug in an additional directive depending
on some model value. I am doing that in the [edit] pre-link phase of the directive.
The parent directive sets up a host of
methods and data which the child uses. Therefore I have child directive with
scope:true.
On the outer (or, parent) directive there is a click handler method. When I
click this, it gets fired twice. I want to know why and how. Presently the
only way I know how to stop that is by calling event::stopImmediatePropagation()
but I have a suspicion I am doing something wrong here.
template usage
<dashboard-box data-box-type="column with no heading">
<div class="card">
<div
ng-click="show($event)"
class="box-title item item-divider ng-binding is-shown"
ng-class="{'is-shown':showMe}">
<span class="box-title-string ng-binding">A/R V1</span>
</div>
<div class="box-content item item-text-wrap" ng-show="showMe">
<!-- plug appropriate dashbox here - in dashboard-box compile -->
<dashbox-column-with-no-heading>
<div>
SOME DATA...
</div>
</dashbox-column-with-no-heading>
</div>
</div>
</dashboard-box>
In the directive for dashboard-box:
scope: {
boxType: "#"
},
pre: function preLink(scope, iElement, iAttrs, controller) {
// some crazy hardcoding - because find by tagname...replaceWith throws in ionic
var parent_div = angular.element(angular.element(iElement.children()[0]).children()[1]);
if (!parent_div) {
return; // programmer error in template??
}
var html_tag_name = d_table_type_to_directive_name.xl[iAttrs.boxType];
parent_div.append(angular.element( "<" + html_tag_name + ">" ));
$compile(iElement.contents())(scope); // angular idiom
}
In the controller for dashboard-box:
$scope.show = function($e){
$log.log("dashboard box show(), was=", $scope.showMe, $e);
if ($e) $e.stopImmediatePropagation(); // <<<<<<<<<<< without this, double-hits!!
$scope.showMe = ! $scope.showMe;
// etc
};
In the directive for dashbox-column-with-row-heading:
restrict: "E",
scope: true,
templateUrl: "dashbox-column-with-row-heading.tpl.html"
controller: function(){
// specialized UI for this directive
}
I am using ionicframework rc-1.0.0 and angularjs 1.3.13.
What is happening here is that you are double-compiling/linking the ng-click directive: 1) first time Angular does that in its compilation phase - it goes over the DOM and compiles directives, first dashboardBox, then its children, together with ngClick), and 2) - when you compile with $compile(element.contents())(scope).
Here's a reduced example - demo - that reproduces your problem:
<foo>
<button ng-click="doFoo()">do foo</button>
</foo>
and the foo directive is:
.directive("foo", function($compile) {
return {
scope: true,
link: {
pre: function(scope, element, attrs, ctrls, transclude) {
$compile(element.contents())(scope); // second compilation
scope.doFoo = function() {
console.log("foo called"); // will be called twice
};
}
}
};
});
What you need to do instead is to transclude the content. With transclusion, Angular compiles the content at the time that it compiles the directive, and makes the content available via the transclude function.
So, instead of using $compile again, just use the already-compiled - but not yet linked (until you tell it what it should be linked to) - contents of the directive.
With the foo example below, this would look like so:
.directive("foo", function($compile) {
return {
scope: true,
transclude: true,
link: {
pre: function(scope, element, attrs, ctrls, transclude) {
transclude(scope, function(clone, transcludedScope){
var newEl = createNewElementDynamically();
$compile(newEl)(transcludedScope); // compile just the newly added content
// clone is the compiled-and-now-linked content of your directive's element
element.append(newEl);
element.append(clone);
});
scope.doFoo = function() {
console.log("foo called");
};
}
}
};
});
My goal is to pass the projectName model from my MainController to my custom contenteditable directive.
I have the following controller:
app.controller("MainController", function($scope){
$scope.projectName = "Hot Air Balloon";
});
Here is how I'm calling the directive:
<div class="column" ng-controller="MainController">
<h2 contenteditable name="myWidget" ng-model="projectName" strip-br="true"></h2>
<p>{{projectName}}</p>
</div>
I've gotten the contenteditable directive working by following this tutorial: https://docs.angularjs.org/api/ng/type/ngModel.NgModelController
If I understand the docs correctly then Angular is not going to use the model I want it to. Instead its going to create a new model w/ scope local to the contenteditable directive. I know that I can add an isolate scope to the directive but I don't know how to use the model passed to the isolate scope within the link function.
I have tried something like the following which didn't work ...
<h2 contenteditable item="projectName"></h2>
--- directive code ---
scope: {
item: '=item'
}
link: function(){
...
item.$setViewValue(...)
...
}
--- my original directive call --
<div class="column" ng-controller="MainController">
<h2 contenteditable name="myWidget" ng-model="projectName" strip-br="true"></h2>
<p>{{projectName}}</p>
</div>
--- my original controller and directive ---
app.controller("MainController", function($scope){
$scope.projectName = "LifeSeeds";
});
app.directive('contenteditable', function(){
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModel){
console.log(ngModel);
if(!ngModel) return;
console.log(ngModel.$viewValue);
ngModel.$render = function(){
element.html(ngModel.$viewValue || '');
};
element.on('blur keyup change', function(){
scope.$apply(read);
});
read();
function read(){
var html = element.html();
if(attrs.stripBr && html == '<br>'){
html = '';
}
ngModel.$setViewValue(html);
}
}
};
});
You can use ng-model with your own directive. To make sure it is included, you can use the attribute require like this:
app.directive("myDirective", function(){
return {
require:"ngModel",
link: function(scope, element, attr, ngModel){
console.log(ngModel);
}
}
});
Then, you can code whatever behavior your want of ng-model within your directive.
Working solution: http://plnkr.co/edit/Lu1ZG9Lpx2sl8CYe8FCx?p=preview
I mentioned that I tried using an isolate scope in my original post.
<h2 contenteditable item="projectName"></h2>
This was actually the correct approach so ignore my full original example using the model argument of the directive's link function.
link: function(scope, element, attrs, ngModel)
The reason the isolate scope approach did not work was because $scope.projectName stored a primitive instead of an object. I did not understand some javascript basics. Mainly, I did not know that primitive types were passed to functions by value.
Primitives are passed by value in javascript. Consequently, changes made to primitive values within a function do NOT change the value of the variable passed to the function.
function changeX(x){
x = 5;
}
x = 4;
changeX(x);
console.log(x) // will log 4 ... Not 5
However, objects passed to functions in javascript are passed by reference so modifications to them within the function WILL be made to the variable passed into the function.
My problem was in how I declared the scope within the MainController.
I had:
$scope.projectName = "LifeSeeds";
That's a primitive. When I passed projectName to the directive I was passing a primitive.
<h2 contenteditable item="projectName"></h2>
Thus, changes to the editable element were being made to the value within the directive but not to the value stored in the MainController's scope. The solution is to store the value within an object in the MainController's scope.
// correct
$scope.project = {
html: "Editable Content"
};
// wrong
$scope.projectName = "Editable Content"
I found this Angular Directive online to add a twitter share button. It all seems staright forward but I can't work out what the attrs.$observe is actually doing.
I have looked in the docs but can't see $observe referenced anywhere.
The directive just seems to add the href which would come from the controller so can anyone explain what the rest of the code is doing?
module.directive('shareTwitter', ['$window', function($window) {
return {
restrict: 'A',
link: function($scope, element, attrs) {
$scope.share = function() {
var href = 'https://twitter.com/share';
$scope.url = attrs.shareUrl || $window.location.href;
$scope.text = attrs.shareText || false;
href += '?url=' + encodeURIComponent($scope.url);
if($scope.text) {
href += '&text=' + encodeURIComponent($scope.text);
}
element.attr('href', href);
}
$scope.share();
attrs.$observe('shareUrl', function() {
$scope.share();
});
attrs.$observe('shareText', function() {
$scope.share();
});
}
}
}]);
Twitter
In short:
Everytime 'shareTwitterUrl' or 'shareTwitterText' changes, it will call the share function.
From another stackoverflow answer: (https://stackoverflow.com/a/14907826/2874153)
$observe() is a method on the Attributes object, and as such, it can
only be used to observe/watch the value change of a DOM attribute. It
is only used/called inside directives. Use $observe when you need to
observe/watch a DOM attribute that contains interpolation (i.e.,
{{}}'s). E.g., attr1="Name: {{name}}", then in a directive:
attrs.$observe('attr1', ...). (If you try scope.$watch(attrs.attr1,
...) it won't work because of the {{}}s -- you'll get undefined.) Use
$watch for everything else.
From Angular docs: (http://docs.angularjs.org/api/ng/type/$compile.directive.Attributes)
$compile.directive.Attributes#$observe(key, fn);
Observes an interpolated attribute.
The observer function will be invoked once during the next $digest fol
lowing compilation. The observer is then invoked whenever the interpolated value changes.
<input type="text" ng-model="value" >
<p sr = "_{{value}}_">sr </p>
.directive('sr',function(){
return {
link: function(element, $scope, attrs){
attrs.$observe('sr', function() {
console.log('change observe')
});
}
};
})
Consider the following directive: (Live Demo)
app.directive('spinner', function() {
return {
restrict: 'A',
scope: {
spinner: '=',
doIt: "&doIt"
},
link: function(scope, element, attrs) {
var spinnerButton = angular.element("<button class='btn disabled'><i class='icon-refresh icon-spin'></i> Doing...</button>");
element.after(spinnerButton);
scope.$watch('spinner', function(showSpinner) {
spinnerButton.toggle(showSpinner);
element.toggle(!showSpinner);
});
}
};
});
which is used like this:
<button ng-click="doIt()" spinner="spinIt">Spin It</button>
When spinner's value (i.e. the value of $scope.spinIt in this example) is true, the element should be hidden and spinnerButton should appear instead. When spinner's value is false, the element should be visible and spinnerButton should be hidden.
The problem here is that doIt() is not in the isolated scope, thus not called on click.
What would be the "Angular way" to implement this directive?
My suggestion is to look at what's going on with these spinners. Be a little more API focused.
Relevant part follows. We use a regular callback to indicate when we're done, so the spinner knows to reset the state of the button.
function SpinDemoCtrl($scope, $timeout, $q) {
$scope.spinIt = false;
$scope.longCycle = function(complete) {
$timeout(function() {
complete();
}, 3000);
};
$scope.shortCycle = function(complete) {
$timeout(function() {
complete();
}, 1000);
};
}
app.directive('spinnerClick', function() {
return {
restrict: 'A',
scope: {
spinnerClick: "=",
},
link: function(scope, element, attrs) {
var spinnerButton = angular.element("<button class='btn disabled'><i class='icon-refresh icon-spin'></i> Doing...</button>").hide();
element.after(spinnerButton);
element.click(function() {
spinnerButton.show();
element.hide();
scope.spinnerClick(function() {
spinnerButton.hide();
element.show();
});
});
}
};
});
Here's one that expects use of $q. It'll work better with Angular-style asynchronous operations, and eliminates the callback functions by instead having the spinner reset on fulfilment of the promise.
Here is the polished version of the directive I ended up with (based on Yuki's suggestion), in case it helps someone: (CoffeeScript)
app.directive 'spinnerClick', ->
restrict: 'A'
link: (scope, element, attrs) ->
originalHTML = element.html()
spinnerHTML = "<i class='icon-refresh icon-spin'></i> #{attrs.spinnerText}"
element.click ->
return if element.is('.disabled')
element.html(spinnerHTML).addClass('disabled')
scope.$apply(attrs.spinnerClick).then ->
element.html(originalHTML).removeClass('disabled')
Use it like so:
<button class="btn btn-primary" spinner-click="createNewTask()"
spinner-text="Creating...">
Create
</button>
Controller's code:
TasksNewCtrl = ($scope, $location, $q, Task) ->
$scope.createNewTask = ->
deferred = $q.defer()
Task.save $scope.task, ->
$location.path "/tasks"
, (error) ->
// Handle errors here and then:
deferred.resolve()
deferred.promise
Yes, it will call doIt in your isolated scope.
You can use $parent.doIt in that case
<button ng-click="$parent.doIt()" spinner="spinIt">Spin It</button>
From the AngularJS Documentation (http://docs.angularjs.org/guide/directive):
& or &attr - provides a way to execute an expression in the context of the parent scope. If no attr name is specified then the attribute name is assumed to be the same as the local name. Given and widget definition of scope: { localFn:'&myAttr' }, then isolate scope property localFn will point to a function wrapper for the count = count + value expression. Often it's desirable to pass data from the isolated scope via an expression and to the parent scope, this can be done by passing a map of local variable names and values into the expression wrapper fn. For example, if the expression is increment(amount) then we can specify the amount value by calling the localFn as localFn({amount: 22}).
so inlclude doIt: "&doIt" in your scope declaration, then you can use doIt as a function in your isolated scope.
I'm confused as to why you are not packaging up everything in the directive as if it's a self-contained module. That's at least what I would do. In other words, you have the click-handler in the HTML, some behavior in the directive and some behavior in the external controller. This makes your code much less portable and more decentralized.
Anyway, you may have reasons for this that are not shared, but my suggestion would be to put all the "Spin It" related stuff in the spinner directive. This means the click-handler, the doIt() function and template stuff all within the link function.
That way there's no need to worry about sharing scope and code entanglement. Or, am I just missing something?
I don't know about the 'angular' way of doing things, but i suggest not using an isolated scope but instead just creating a child scope. You then do attrs.$observe to get any properties you need for your directive.
I.E. :
app.directive('spinner', function() {
return {
restrict: 'A',
scope: true, //Create child scope not isolated scope
link: function(scope, element, attrs) {
var spinnerButton = angular.element("<button class='btn disabled'><i class='icon-refresh icon-spin'></i> Doing...</button>");
element.after(spinnerButton);
//Using attrs.$observe
attrs.$observe('spinner', function(showSpinner) {
spinnerButton.toggle(showSpinner);
element.toggle(!showSpinner);
});
}
};
});
I find this way is better than using '$parent' to escape the isolated scope in other directives (eg ngClick or ngModel) as the end user of your directive does not need to know whether or not using your directive requires them to use '$parent' or not on core angularjs directives.
Using CoffeeScript and a FontAwesome icon.
No need to manually specify the spinner-text
It will just add the spinner content left of the text while loading
We must use finally instead of then for the promise otherwise the spinner will stay there on failure?
I must use $compile because the contents of the button is dynamically compiled as I am using https://github.com/angular-translate/angular-translate
app.directive 'spinnerClick', ["$compile", ($compile) ->
restrict: 'A'
link: (scope, element, attrs) ->
originalHTML = element.html()
spinnerHTML = "<i class='fa fa-refresh fa-spin'></i> "
element.click ->
return if element.is('.disabled')
element.html(spinnerHTML + originalHTML).addClass('disabled')
$compile(element.contents())(scope)
scope.$apply(attrs.spinnerClick).finally ->
element.html(originalHTML).removeClass('disabled')
$compile(element.contents())(scope)
]