How to recompile an Angular 1.x directive that uses "replace: true" - angularjs

Summary
I am using a third party directive that configures replace: true.
I want to recompile the directive every time the user clicks a button. I've tried several things without luck. For example, I tried wrapping the Cloudinary directive in my own directive, but I can't seem to get it. Any help will be greatly appreciated.
Dependencies
"angular": "1.6.2",
"cloudinary-core": "^2.5.0",
"cloudinary_ng": "^1.1.1",
Controller
$scope.rotate = (leader) => {
leader.cloudinary_angle = {
'0': '90',
'90': '180',
'180': '270',
'270': '0'
}[leader.cloudinary_angle] || '0';
};
View
<div ng-repeat="leader in leaders">
Rotate
<cloudimage leader="leader"></cloudimage>
</div>
Did not work #1
angular.module('app').directive('cloudimage', ($compile) => {
return {
restrict: 'E',
replace: false,
scope: {
leader: '='
},
link: (scope, element) => {
let cloudinaryImage = $compile('<cl-image angle="' + scope.leader.cloudinary_angle + '"' +
' crop="fit"' +
' format="jpg"' +
' height="150"' +
' public-id="' + scope.leader.cloudinary + '"' +
' quality="80"' +
' width="150"' +
'></cl-image>'
)(scope);
element.html(cloudinaryImage[0]);
scope.$watch('scope.leader.cloudinary_angle', (cloudinaryImage) => element.html(cloudinaryImage[0]));
}
};
});
Did not work #2
angular.module('app').directive('cloudimage', ($compile) => {
return {
restrict: 'E',
replace: false,
scope: {
leader: '='
},
template: '<cl-image crop="fit" format="jpg" height="150" angle="{{angle}}" public-id="{{id}}" quality="80" width="150"></cl-image>',
link: (scope, element) => {
scope.angle = scope.leader.cloudinary_angle || 0;
scope.id = scope.leader.cloudinary;
}
};
});
Did not work #3
I can decorate the 3rd party directive to make its replace: false, but that breaks its transclude.
angular.module('app').config(['$provide', function($provide) {
$provide.decorator('clImageDirective', function($delegate) {
var directive = $delegate[0];
directive.replace = false;
return $delegate;
})
}]);

#1 doesn't work because html() expects a string, not DOM element. cloudinaryImage element shouldn't be converted to a string because this will destroy information about the element.
It should be:
scope.$watch('scope.leader.cloudinary_angle', (cloudinaryImage) => {
element.empty().append(cloudinaryImage)
});

Time for dirty hacks... You can recompile anything (including one-time bindings, any directive) using ng-repeat on array with single element:
<body ng-controller="MainCtrl" ng-init="arr = [{}]">
<button ng-click="arr = [{}]">Recompile</button>
<p test ng-repeat="o in arr"></p>
</body>
where test directive is
app.directive('test', function() {
return {
replace: true,
template: '<span>hello</span>',
link: function() {
console.log('I am linked');
}
}
})
P.S. you can do pretty same with ng-if, but then you will require 2 digests.
P.P.S. I agree this is kinda weird.
http://plnkr.co/edit/vlC7TV8eIrW1tLgcvP7Y?p=preview

Related

How to make directive use the controller specified in directive attribute?

So I have a directive:
<directive data="user" templateUrl="./user.html" controller="UserController"></directive>
I want that directive to use the controller specified in "controller" attribute, as you see above.
Is it possible with AngularJS directives? Or should I do it other way, maybe with components?
My code currently looks like this:
app.directive('directive', function() {
var controllerName = "UserController"; // i want that to dynamicaly come from attribute
// check if controller extists:
var services = [];
app['_invokeQueue'].forEach(function(value){
services[value[2][0]] = true;
});
if (!services[controllerName]) controllerName = false;
return {
scope: { 'data' : '=' },
link: function (scope) {
Object.assign(scope, scope.data);
},
templateUrl: function(element, attr) {
return attr.templateurl;
},
controller: controllerName
}
});
You can do following (not exactly what you ask - it creates bunch of nested scopes, but should be sufficient):
.directive('directive', () => {
scope: { 'data' : '=' },
template: (elem, attrs) => {
return '<div ng-controller="' + attrs.controller + ' as vm"><div ng-include="' + attrs.template + '"></div></div>';
}
});
<directive data="user" templateUrl="./user.html" controller="UserController"></directive>
you may use $templateCache directly instead of ng-include
if you need controller/template/... to be dynamic, you need to observe/watch + dom manipulation + recompile stuff
Okay, so after analysing Petr's answer I post the working code using nested divs:
app.directive('directive', function() {
return {
scope: { 'data' : '=' },
link: function (scope) {
// this makes your fields available as {{name}} instead of {{user.name}}:
Object.assign(scope, scope.data);
},
template: function(element, attrs) {
var controllerName = attrs.controller;
var controllerString = controllerName + ' as vm';
// check if controller extists:
var services = [];
app['_invokeQueue'].forEach(function(value){
services[value[2][0]] = true;
})
if (!services[controllerName]) {
return '<div ng-include="\'' + attrs.templateurl + '\'"></div>';
} else {
return '<div ng-controller="' + controllerString + '"><div ng-include="\'' + attrs.templateurl + '\'"></div></div>';
}
}
}
});

AngularJS + SwipeJS not working without setTimeout of 0 milliseconds

I am writing an AngularJS directive that makes it easy for me to create swipeable pages (such as the viewpager on Android). I use SwipeJS as library for this.
This is how I use the directive
<ng-swipe ng-if="model.messages">
<div class="page" ng-repeat="message in model.messages">
{{message.title}}
</div>
</ng-swipe>
And the code of the directive looks like this.
var swipe = angular.module('ngSwipe', []);
swipe.directive('ngSwipe', function() {
return {
restrict: 'EA',
replace: false,
transclude: true,
scope: {},
template:
'<div>' +
' <div id="slider" class="swipe">' +
' <div class="swipe-wrap" ng-transclude></div>' +
' </div>' +
' <div class="pagecontrol">' +
' <div class="pagedot" ng-repeat="p in swipe.pages" ng-click="swipe.switchPage($index)"></div>' +
' </div>' +
'</div>',
link: function($scope, $element, $attrs) {
var $model = $scope.swipe = {
pages: [],
switchPage: function(index) {
$model.swipe.slide(index);
}
}
setTimeout(function() {
$model.swipe = new Swipe(document.getElementById('slider'), {
continuous: false,
callback: function(index, elem) {
$model.currentTab = index;
}
});
for(var i=0; i<$model.swipe.getNumSlides(); i++) {
$model.pages.push(i);
}
$scope.$apply();
}, 0);
}
};
});
First of all, the reason I use ng-if in the ng-swipe directive is because the link method should be invoked after my messages are loaded. The messages are being retrieved from the server and it can take up to 2 seconds before they are retrieved. If I don't wait untill the messages are loaded, the new Swipe() object will be created but it won't find pages so it will not work.
But besides that, you can also see that I have a setTimeout() function of 0 milliseconds. If I don't use that one, it just doesn't render the swipe pages.
I made a JSFiddle for this issue. Thanks in advance!
switchPage: function(index) {
$model.swipe.slide(index);
$scope.$apply();
}

Calling element.replaceWith from inside scope.$on throws TypeError

We have the following directive:
.directive("directiveToggleElement", function(FlagsService) {
return {
restrict: "E",
scope: {},
link: function(scope, element, attrs)
{
scope.showMe = FlagsService.isOn(attrs.toggleKey);
scope.featureText = attrs.featureText;
var htmlTag = '<div class="panel ng-scope" ng-class="{selected: selected}" ng-click="selected = !selected;"></div>';
var htmlComment = '<!-- TogglePlaceholder: ' + attrs.toggleKey +'-->';
element.replaceWith(scope.showMe ? htmlComment + htmlTag : htmlComment);
// Listen for change broadcast from flagsservice to toggle local store
scope.$on('LocalStorage.ToggleChangeEvent.' + attrs.toggleKey, function(event, parameters) {
console.log('stuff changed - ' + parameters.key + ' ' + parameters.newValue);
scope.showMe = parameters.newValue;
element.replaceWith(scope.showMe ? htmlComment + htmlTag : htmlComment);
});
}
}
})
The idea is that based on the value of a feature toggle (or feature flag) the directive will output the comment with the tag, or just the comment.
The initial element.replaceWith works as expected, but the call from inside the scope.$on generates a "TypeError: Cannot call method 'replaceChild' of null". We've inspected element before each call and can't spot an obvious difference.
Can anyone explain why the error would be thrown here, or advise of a potential workaround that will allow us to do the same thing?
We have a working directive that sets the value of ng-show:
.directive("directiveToggle", function(FlagsService) {
return {
restrict: "A",
transclude: true,
scope: {},
link: function(scope, element, attrs)
{
scope.showMe = FlagsService.isOn(attrs.toggleKey);
// Listen for change broadcast from flagsservice to toggle local store
scope.$on('LocalStorage.ToggleChangeEvent.' + attrs.toggleKey, function(event, parameters) {
console.log('stuff changed - ' + parameters.key + ' ' + parameters.newValue);
scope.showMe = parameters.newValue;
});
},
template: '<div class="panel ng-scope" ng-show="showMe" ng-class="{selected: selected}" ng-click="selected = !selected;"><span ng-transclude></span></div>',
replace: true
}
})
But we would prefer to remove the element from the DOM instead of setting display to none.
You could still use the ng-if directive within your custom "directiveToggle" without the need for a controller. To achieve this you need to use the transclude option:
.directive("directiveToggle", function (FlagsService) {
return {
template: '<div ng-transclude ng-if="isEnabled"></div>',
transclude: true,
restrict: 'A',
scope: true,
link: function ($scope, $element, $attrs) {
var feature = $attrs.featureToggle;
$scope.isEnabled = FlagsService.isOn(feature);
$scope.$on('LocalStorage.ToggleChangeEvent.' + feature, function (event, parameters) {
$scope.isEnabled = parameters.newValue;
});
}
}
});
The above is taking the mark-up from inside the "directiveToggle" and then wrapping it inside of the template, where the "ng-transclude" directive marks the insertion point. Also included on the template is an "ng-if" directive that is watching the "isEnabled" member on the current scope for the "directiveTemplate".
One caveat with this approach is that you need the directive to create it's own scope/isolated scope if you have multiple instances of this directive, otherwise the "isEnabled" member will be shared between the directives.

AngularJS scope updated after ng-change

I have a directive that centralises my select HTML and functionality but I have an issue where the ng-model is updating after ng-change happens.
Here's a focused jsfiddle example:
http://jsfiddle.net/U3pVM/1568/
(Code because SO complains otherwise)
HTML:
<div ng-app="myApp">
<div ng-controller="MainCtrl">
<p>fooId is currently : {{fooId}}</p>
<app-drop-down model="fooId" options="fooOptions" opt-value="id" opt-label="label" on-change="dropDownChanged"></app-drop-down>
</div>
</div>
JS:
var app = angular.module('myApp', []);
app.controller('MainCtrl', function ($scope, $log) {
$scope.fooId = -1;
$scope.fooOptions = [{
id: 1,
label: "A"
}, {
id: 2,
label: "B"
}];
$scope.dropDownChanged = function (id) {
$log.info('changed : ' + $scope.fooId + ' but really: ' + id);
};
});
app.directive('appDropDown', function () {
return {
restrict: 'E',
replace: true,
scope: {
model: '=',
options: '=',
onChange: '='
},
template:
'<div><select ng-model="model" ng-options="a[optValue] as a[optLabel] for a in options" ng-change="changed()"></select></div>',
link: function (scope, element, attrs) {
scope.optValue = attrs.optValue;
scope.optLabel = attrs.optLabel;
scope.changed = function () {
scope.onChange(scope.model);
};
}
};
});
The console logs:
changed : -1 but really: 1
changed : 1 but really: 2
When you change the select to A, then to B.
It is updating but after the ng-change is triggered.
Obviously, I can work around this by passing the id (like I do) or using $watch in the controller on the value but this isn't ideal for certain more complex scenarios.
Any ideas?
I know this is a bit after the fact, but I had a similar problem and searching around I found this question as well. As there doesn't seem to be a real answer, I thought to post what I ended up doing as it may help someone else in the future. This seems to work for my case (and your fiddle as well) but as I'm only starting to use AngularJS, I might be doing something against the 'rules' so any specialist, feel free to correct me...
Anyway, here is an updated version of your Fiddle with my changes:
http://jsfiddle.net/5vb5oL7e/1/
And here is the actual code of the directive:
app.directive('appDropDown', function () {
return {
restrict: 'E',
replace: true,
scope: {
model: '=',
options: '=',
onChange: '='
},
template:
'<div><select ng-model="model" ng-options="a[optValue] as a[optLabel] for a in options"></select></div>',
link: function (scope, element, attrs) {
scope.optValue = attrs.optValue;
scope.optLabel = attrs.optLabel;
scope.$watch('model', function(newValue, oldValue)
{
// Execute function on change
if (scope.onChange !== undefined &&
newValue !== undefined && oldValue !== undefined)
{
scope.onChange();
}
});
}
};
});
Basically, what I did was to add a watch inside the link function on the model. Inside this watch I fire the onChange function when it's defined. The added checks for undefined on the old and new value were added to prevent the function to change unneeded on page load.
Hope this helps someone...
Kind regards,
Heino

How to create a directive with a dynamic template in AngularJS?

How can I create a directive with a dynamic template?
'use strict';
app.directive('ngFormField', function($compile) {
return {
transclude: true,
scope: {
label: '#'
},
template: '<label for="user_email">{{label}}</label>',
// append
replace: true,
// attribute restriction
restrict: 'E',
// linking method
link: function($scope, element, attrs) {
switch (attrs['type']) {
case "text":
// append input field to "template"
case "select":
// append select dropdown to "template"
}
}
}
});
<ng-form-field label="First Name" type="text"></ng-form-field>
This is what I have right now, and it is displaying the label correctly. However, I'm not sure on how to append additional HTML to the template. Or combining 2 templates into 1.
i've used the $templateCache to accomplish something similar. i put several ng-templates in a single html file, which i reference using the directive's templateUrl. that ensures the html is available to the template cache. then i can simply select by id to get the ng-template i want.
template.html:
<script type="text/ng-template" id=“foo”>
foo
</script>
<script type="text/ng-template" id=“bar”>
bar
</script>
directive:
myapp.directive(‘foobardirective’, ['$compile', '$templateCache', function ($compile, $templateCache) {
var getTemplate = function(data) {
// use data to determine which template to use
var templateid = 'foo';
var template = $templateCache.get(templateid);
return template;
}
return {
templateUrl: 'views/partials/template.html',
scope: {data: '='},
restrict: 'E',
link: function(scope, element) {
var template = getTemplate(scope.data);
element.html(template);
$compile(element.contents())(scope);
}
};
}]);
Had a similar need. $compile does the job. (Not completely sure if this is "THE" way to do it, still working my way through angular)
http://jsbin.com/ebuhuv/7/edit - my exploration test.
One thing to note (per my example), one of my requirements was that the template would change based on a type attribute once you clicked save, and the templates were very different. So though, you get the data binding, if need a new template in there, you will have to recompile.
You should move your switch into the template by using the 'ng-switch' directive:
module.directive('testForm', function() {
return {
restrict: 'E',
controllerAs: 'form',
controller: function ($scope) {
console.log("Form controller initialization");
var self = this;
this.fields = {};
this.addField = function(field) {
console.log("New field: ", field);
self.fields[field.name] = field;
};
}
}
});
module.directive('formField', function () {
return {
require: "^testForm",
template:
'<div ng-switch="field.fieldType">' +
' <span>{{title}}:</span>' +
' <input' +
' ng-switch-when="text"' +
' name="{{field.name}}"' +
' type="text"' +
' ng-model="field.value"' +
' />' +
' <select' +
' ng-switch-when="select"' +
' name="{{field.name}}"' +
' ng-model="field.value"' +
' ng-options="option for option in options">' +
' <option value=""></option>' +
' </select>' +
'</div>',
restrict: 'E',
replace: true,
scope: {
fieldType: "#",
title: "#",
name: "#",
value: "#",
options: "=",
},
link: function($scope, $element, $attrs, form) {
$scope.field = $scope;
form.addField($scope);
}
};
});
It can be use like this:
<test-form>
<div>
User '{{!form.fields.email.value}}' will be a {{!form.fields.role.value}}
</div>
<form-field title="Email" name="email" field-type="text" value="me#example.com"></form-field>
<form-field title="Role" name="role" field-type="select" options="['Cook', 'Eater']"></form-field>
<form-field title="Sex" name="sex" field-type="select" options="['Awesome', 'So-so', 'awful']"></form-field>
</test-form>
One way is using a template function in your directive:
...
template: function(tElem, tAttrs){
return '<div ng-include="' + tAttrs.template + '" />';
}
...
If you want to use AngularJs Directive with dynamic template, you can use those answers,But here is more professional and legal syntax of it.You can use templateUrl not only with single value.You can use it as a function,which returns a value as url.That function has some arguments,which you can use.
http://www.w3docs.com/snippets/angularjs/dynamically-change-template-url-in-angularjs-directives.html
I managed to deal with this problem. Below is the link :
https://github.com/nakosung/ng-dynamic-template-example
with the specific file being:
https://github.com/nakosung/ng-dynamic-template-example/blob/master/src/main.coffee
dynamicTemplate directive hosts dynamic template which is passed within scope and hosted element acts like other native angular elements.
scope.template = '< div ng-controller="SomeUberCtrl">rocks< /div>'
I have been in the same situation, my complete solution has been posted here
Basically I load a template in the directive in this way
var tpl = '' +
<div ng-if="maxLength"
ng-include="\'length.tpl.html\'">
</div>' +
'<div ng-if="required"
ng-include="\'required.tpl.html\'">
</div>';
then according to the value of maxLength and required I can dynamically load one of the 2 templates, only one of them at a time is shown if necessary.
I heope it helps.

Resources