custom $compile directive applies only sometimes in same scope - javascript

thanks for looking. I'll dive right in:
A JSON object has HTML links with ng-click attributes, employing ng-bind-html, with $sce's trustAsHtml to make the HTML safe. I have also used a custom angular-compile directive to compile the angular click listener into the app after the json is loaded in a $q promise.
All of this works as intended, at first glance...
JSON
{
"text" : "Sample of text with <a data-ng-click=\"__services._animation.openModal('g');\">modal trigger</a>?"
}
VIEW
<p data-ng-bind-html="__services.trustAsHTML(__data.steps[step.text])"
data-angular-compile></p>
DIRECTIVE
angular.module('app.directives.AngularCompile', [], ["$compileProvider", function($compileProvider) {
$compileProvider.directive('angularCompile', ["$compile", function($compile) {
return function(scope, element, attrs) {
scope.$watch(
function(scope) {
return scope.$eval(attrs.angularCompile);
},
function(value) {
element.html(value);
$compile(element.contents())(scope);
}
);
};
}])
}]);
The Issue:
Okay, so it works. My app loads, it serves the safe HTML, and my ng-click opens the modal, passing it's params. I see class='ng-binding' on the surrounding p tag, class="ng-scope" on the a tag in the generated html. Hooray!
The next order of business is to write that data in a another model that tracks progress, and run it through the same ng-bind, trustAsHTML, angular-compile treatment in another view. Just copying the data into a sibling object.
Here's where it fails!
<p data-ng-bind-html="__services.trustAsHTML(__state.modal.text)"
data-angular-compile></p>
In the second view, which is a modal in the same scope ($rootScope) on the same page - the bind, and trustAsHTML Angular is applied. But the link is not clickable, and no class="ng-scope" is generated on the a tag.
If fFurther explanation of my set-up might help understand the issue, let me detail it here. All initial app is set up by the concierge and it store most data in $rootScope:
return angular
.module('app', [
'ngResource',
'ngSanitize',
'ui.router',
'oslerApp.controllers.GuideController',
'oslerApp.services.ConciergeService',
'oslerApp.services.DataService',
'oslerApp.services.LocationService',
'oslerApp.services.StatesService',
'oslerApp.directives.AngularCompile',
])
.config(function($stateProvider, $urlRouterProvider) {
// For any unmatched url, redirect to landing
$urlRouterProvider.otherwise("/");
// Now set up the states
$stateProvider
.state('guide', {
url: "/guide",
templateUrl: "views/guide.html",
controller: 'GuideController as guide'
})
.state('contact', {
url: "/contact",
templateUrl: "views/contact.html"
})
})
.run(function(ConciergeService) {
ConciergeService.init();
});
I've spent 2 days refactoring my whole site to see if it was because the modal was in it's own directive, but putting it within the same template and scope didn't seem to help me here.

Lesson: If you have to refactor everything and still make no strides, you're doing it wrong.
To solve this, 2 days of hair pulling later, I made a tiny little service and pass the problem text through that:
compiler: function(element, template, content, scope) {
element = $(element);
template = template[0] + content + template[1];
var linkFn = $compile(template);
content = linkFn(scope);
element.replaceWith(content);
},

Related

Dependency injection in Angular Components like directives

Here's one thing I'm used to do with angular directives
angular.module('app.directives').directive('login', ['$templateCache', function ($templateCache) {
return {
restrict: 'E',
template: $templateCache.get('directives/login/login.html'),
controller: 'LoginController as vm',
scope: true
};
}]);
I've grown very attached to using Template Cache to inject HTML content in my directive's template. Now with Angular 1.5 there's this new thing all the cool kids are using called component() which I'm giving a look to see if it's really good and I'm stuck at this very beginning part: how to inject things in the component itself (not in the controller)?
In this case you can see that I'm injecting into the login directive the $templateCache dependency. How would I rewrite this directive as a component? (keeping in mind my desire to use $templateCache over templateUrl)
Well, In Angular 1.5 components, template property can be a function and this function is injectable (documentation).
So, you can just use something like:
...
template: ['$templateCache', function ($templateCache) {
return $templateCache.get('directives/login/login.html')
}]
...
Few links from google search: one and two.
Hope it will help.
MaKCblMKo 's answer is right, but remember that AngularJS will check the templateCache first before going out to to retrieve the template. Therefore, you don't have to make this call in your directive or component.
angular.module('myApp', [])
.component('myComponent',{
templateUrl: 'yourGulpPattern'
})
.run(function($templateCache) {
$templateCache.put('yourGulpPattern', 'This is the content of the template');
});
https://jsfiddle.net/osbnoebe/6/

Angular directive inside ng-template with modal

I am curious about the way angular works with preloading directives since I have a problem with a directive that resides in a <script> tag and ng-template.
When I pause execution in chrome dev-tools, during the initial document load, I can clearly see that the code inside the directive's controller does not get called if my directive lies in some arbitrary template. Here is the example code, when e.g. myDirective is included in index.html as a part of myModule.js module, also included both in index and in the main app module:
This is some other directive's html containing the problematic myDirective
<script type="text/ng-template" id="callThis">
<myDirective></myDirective>
</script>`
and I call it on click with ngDialog like this
ngDialog.open({
template: 'callThis',
scope: $scope
});
and it can't run the directive since it doesn't have any html to work with (thats the error, about some html element missing).
Finally here is the code for the module which holds myDirective
angular.module('myModule', ['myDirectiveTemplates', 'myDirectiveDirective'])
angular.module('myDirectiveTemplates', []).run(["$templateCache", function($templateCache) {$templateCache.put("myDirectiveTemplate.html","<h1></h1>");}]);
angular.module('myDirectiveDirective', []).directive('myDirective', function ($window, $templateCache) {
return {
restrict: 'E',
template: $templateCache.get('myDirectiveTemplate.html'),
controller: function($scope) {
//some arbitrary code
}
};
})
Interestingly if i put <my-directive></my-directive> right in index.html file it works ok, and the code inside the controller gets loaded on startup. I'm unsure how to solve this.
From what I understand of this problem you need to use the $compile service. There is a tutorial here that might help : $compile
The reason given in the tutorial is:
"The newly minted template has not been endued with AngularJS powers
yet. This is where we use the $compile service..."
And quoted from the Angular Docs:
Compiles a piece of HTML string or DOM into a template and produces a
template function, which can then be used to link scope and the
template together.
Here is a brief code example as per the tutorial :
app.directive('contentItem', function ($compile) {
/* EDITED FOR BREVITY */
var linker = function(scope, element, attrs) {
scope.rootDirectory = 'images/';
element.html(getTemplate(scope.content.content_type)).show();
$compile(element.contents())(scope);
}
/* EDITED FOR BREVITY */
});

$templateCache is undefined In directive though I set `{cache: $templateCache}` in $http call

I have two html pages, snippet1.html & snippet2.html. I want use them inside my directive. Because I'm going to add multiple template by using single directive.
I tried this thing with by adding html templates inside <script> tag & gave type="text/ng-template" to them like Below.
<script type="text/ng-template" id="snippet1.html">
<div>Here is Snippet one</div>
</script>
<script type="text/ng-template" id="snippet2.html">
<div>Here is Snippet two</div>
</script>
And then I use $templateCache.get('snippet1.html'). This implementation is working Fine.
But in my case I need to load them from html itself, so I decided to load template by ajax and make $http cache: $templateCache
Working JSFiddle
Run Block
myApp.run(['$templateCache','$http', function($templateCache, $http){
$http.get('snippet1.html',{ cache : $templateCache });
$http.get('snippet2.html',{ cache : $templateCache });
}]);
But inside my controller $templateCache.get('snippet1.html') is undefined.
My question is, Why it is working while i declared template inside <script>' tag & Why it don't work when I html inside$templateCachewhile making$http` ajax call?
Plunkr With Problem
Can anyone help me out with this issue? Or I'm missing anything in code.
Help would greatly appreciated. Thanks.
This is an interesting issue, and I can offer an interesting workaround and my thoughts on what is going on. I think a better solution may exist, but finding such solutions also proved to be a challenge. Nonetheless, I think the main issue is simply your console.log($templateCache.get('snippet1.html')) is returning undefined because of the race condition with your $http.get's not resolving first.
Examining the api for $templateCache, I couldn't find any sort of helpful way of knowing when templates resolve requested via ajax. To see the simple issue, run this in your directive to see some basic information about what is currently stored in your $templateCache
console.log($templateCache.info())
With the result of
Object {id: "templates", size: 0}
For observing the core of the issue, run the same JS in the directive, but with a timeout as such
setTimeout(function() {
console.log($templateCache.info())
}, 1000);
With the result of
Object {id: "templates", size: 2}
Interesting, so they're in there... but getting a handle on them is now the challenge. I crafted the following workaround to at least give us something for now. Inject $q and $rootScope into your .run function as such
myApp.run(['$templateCache', '$http', '$q', '$rootScope', function($templateCache, $http, $q, $rootScope){
$q.all([
$http.get('snippet1.html',{ cache : $templateCache }),
$http.get('snippet2.html',{ cache : $templateCache })
]).then(function(resp){
$rootScope.templateCache = resp
})
}]
);
Examining this, you'll notice I place an arbitrary var on our $rootScope as such $rootScope.templateCache for the purpose of placing a $watch on it in our directive. Then in our directive, let's call into our $templateCache when we then know there is a value on $rootScope.templateCache, indicating the $q service has resolved our promises as such
link: function(scope, element, attrs) {
scope.$parent.$parent.$watch('templateCache', function(n, o) {
if(n) {
element.append($compile($templateCache.get('snippet1.html')[1])(scope));
}
});
}
And hey look! Our template directive is correctly rendered. The hacky looking scope.$parent.$parent is because in this directive, we have isolated our scope and now need to climb some ladders to get the value defined on $rootScope.
Do I hope we can find a cleaner more consise way? Of course! But, hopefully this identifies why this is happening and a possible approach to get up and running for now. Working plunker provided below.
Plunker Link
Edit
Here is a completely different approach to solve this which involves manual bootstrapping
var providers = {};
var $injector = angular.injector(['ng']);
var myApp = angular.module('myApp', []);
$injector.invoke(function($http, $q, $templateCache, $document) {
$q.all([
$http.get('snippet1.html',{ cache : $templateCache }),
$http.get('snippet2.html',{ cache : $templateCache })
]).then(function(resp){
providers.cacheProvider = $templateCache;
angular.bootstrap($document, ['myApp']);
});
});
myApp
.controller('test',function() {
})
.directive('myTemplate', function ($templateCache, $compile) {
return {
restrict: 'EA',
scope: {
snippets: '='
},
link: function(scope, element, attrs) {
element.append($compile(providers.cacheProvider.get('snippet1.html')[1])(scope));
}
};
});
Updated Plunker
This is the expected behavior. When you include a template inside of a script tag, angular finds it during the bootstrap process and adds it to the cache before any code runs. That's why it is available in your directive.
When you use $templateCache.put() (or use $http.get to retrieve an html file as you specify in your code, angular has to use ajax to resolve the template. While the request is "in flight", the template cache doesn't know anything about it - the file is only added to the template cache after the response is received.
Since your directive runs as part of the first digest cycle (on startup), there will never be any remote files in the cache, so you get the error you see.
The "correct" way to do what you are trying to do is to not use the $templateCache directly, but rather use the $http service to request the remote template. If the original response has returned, $http will just call $templateCache.get for you. If it hasn't, it will return the same promise that the original $http request generated.
Doing it this way, there will be no requirement to use $timeout or $watch. The template will be compiled as soon as it is available using promises.
myApp.controller('test',function(){})
.directive("myTemplate", function ($http, $compile) {
return {
restrict: 'EA',
scope: {
template: '&?myTemplate',
src: '&?'
},
link: function(scope, element, attrs) {
$http.get(scope.template() || scope.src()).then(function(result) {
element.append($compile(result.data)(scope));
});
}
};
});
<my-template src="snippet1.html"></my-template>
or
<div my-template="snippet1.html"></div>
Here is a working Plunk
EDIT: Alternative without $compile and $http
myApp.controller('test',function(){})
.directive("myTemplate", function ($http, $compile) {
return {
restrict: 'EA',
scope: {
snippets: '='
},
template: 'snippet1.html',
link: function(scope, element, attrs) {
}
};
});
As to your final question (why you have to use [1] to get the html - $http service does not store only html in the cache - it stores a data structure that may contain a promise or the element (if loaded from a script tag). Since it knows what it put in, it knows how to get it out. When you short circuit things you're only guessing.
Long story short - don't use $templateCache to resolve templates yourself.
EDIT: Code from $http demonstrating the different types of data that might be stored in the cache.
if (cache) {
cachedResp = cache.get(url);
if (isDefined(cachedResp)) {
if (isPromiseLike(cachedResp)) {
// cached request has already been sent, but there is no response yet
cachedResp.then(resolvePromiseWithResult, resolvePromiseWithResult);
} else {
// serving from cache
if (isArray(cachedResp)) {
resolvePromise(cachedResp[1], cachedResp[0], shallowCopy(cachedResp[2]), cachedResp[3]);
} else {
resolvePromise(cachedResp, 200, {}, 'OK');
}
}
} else {
// put the promise for the non-transformed response into cache as a placeholder
cache.put(url, promise);
}
}

ngBind equivalent of NgModelController (or best practice)

http://plnkr.co/edit/C4mFd5MOLBD2wfm8bMhJ?p=preview
Let's take a simple example and say you want to display the value of a cookie regardless of what it is, but this could be a customer name or whatever you want. There seem to be so many options available: directives, services, directives with services, controllers - and no matter how many docs I review or blog posts I read, I still have some fundamental questions about the appropriate way to access data and then update the scope accordingly.
What's clouding my thought right now is the fact that there doesn't seem to be the equivalent of NgModelController for non ngModel capable DOM elements like span or div or anything besides user input. Basically, seeing how ngModelCtrl is utilized in the link function of a directive seems to make a lot of sense, it doesn't allow you to drown in scope soup and it nicely organizes your thoughts, but how do we achieve this decoupling with ngBind elements?
I think the answer is just 'use services', but perhaps maybe not in all cases is the thing that's gnawing at me. Suppose you want to display a very specific cookie (or a customer name) and you don't know where you want to display it, you could continually inject your custom CookieService where ever you go, but what about a specific directive that cleans things up: <specific-cookie></specific-cookie> Would we just inject our CookieService into that directive, or just access it via $cookies like we've done elsewhere.
Does the answer simply lie in whether or not you'll be accessing more than one cookie in the future? That is, if you only need one <specific-cookie></specific-cookie>, then just use $cookies in you're directive and move on with your life, or it is always appropriate to abstract away this type of call into a service, or am I just being super pedantic about understanding this.
Directive
angular-myapp.js
var app = angular.module('myApp', ['ngCookies']);
app.directive('cookie', ['$cookies', function($cookies) {
return {
scope: true,
controller: function($scope, $element, $attrs) {
$scope.cookie = $cookies[$attrs.cookie];
}
}
}]);
index.html
<div cookie="__utma">Cookie: {{cookie}}</div>
Controller
angular-myapp.js
app.controller('CookieCtrl', function($attrs, $cookies) {
this.value = $cookies[$attrs['getcookie']];
});
index.html
<a ng-controller="CookieCtrl as cookie" getCookie="__utma" href="/{{cookie.value}}">{{cookie.value}}</a>
Service
angular-myapp.js
app.controller('SomeCtrl', function($scope, CookieService) {
$scope.cookie = CookieService.getCookie('__utma');
});
app.service('CookieService', function($cookies) {
var getCookie = function(cookie) {
return $cookies[cookie];
};
return ({ getCookie: getCookie });
});
index.html
<div ng-controller="SomeCtrl">Cookie: {{cookie}}</div>
Directive with service
angular-myapp.js
app.directive('specificCookie', function(CookieService) {
return {
scope: true,
template: 'Cookie: <span ng-bind="cookie"></span>',
controller: function($scope, $element, $attrs) {
$scope.cookie = CookieService.getCookie('__utma');
}
}
});
index.html
<specific-cookie></specific-cookie>
Unless I'm misunderstanding some of your scenarios, the simplest (and proper) way to do this is to create a reusable directive that displays a cookie based on a name passed to it via its attribute.
app.directive('cookie', ['$cookies', function($cookies) {
return {
scope: {},
template: "<span>{{cookie}}</span>",
restrict: "E",
link: function(scope, element, attrs) {
attrs.$observe("name", function(newVal){
scope.cookie = $cookies[newVal];
});
}
};
}]);
The usage would be trivial (and page controller-independent):
<cookie name="__utma"></cookie>
<input ng-model="cookieName" type="text">
<cookie name="{{cookieName}}"></cookie>
the resulting DOM would be:
<span class="ng-binding">137862001.838693016.141754...</span>
<span class="ng-binding">GA1.2.838693016.1417544553</span>

angularjs routing behaviour of content and function calls

I'm new to Angular.js which is why I have a basic question regarding routing. I figured out how to create routes and inject specific .htmls by $routeProvider
var app = angular.module('test', ['ngRoute']);
app.config(function($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'routes/view2.html'
});
});
but what I really don't get is how content or function of view2.html are handled in Angular.
Lets take view2.html. It has a <p> with some text in a specific color. Nothing to special. But also it has a little slideshow which is called by $('slideshow').cycle() function.
All what happens is it displays me the <p> tag in a different color and no slideshow function is called on my rootsite of the app.
Could you give me some approach how to actually solve this?
Thanks
Just load required view and then compile it. During compilation Angular processes all directives in view.
If you want to do this proper(Angular) way you should put such code like $('slideshow').cycle() inside of directive. And then use it like
<div my-slideshow=""></div>
angular.module('myModule', [])
.directive('mySlideshow', [function () {
return {
restrict: 'A',
link: function (scope, element) {
element.cycle();
}
}
}]);
Directives documentation
Much more comprehensive documentation

Categories

Resources