I have a page with dynamic content. Content depends of a lot of options, so I use manual compiling using $compile. I have a directive like this:
function compileHtml($compile, $timeout) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
scope.$watch(function () {
return scope.$eval(attrs.compileHtml);
}, function (value) {
if (value) {
element.html(value);
$compile(element.contents())(scope);
}
});
}
};
There are a lot of DevExtreme charts in compiling HTML. They are in their own directives.
I need an event after rendering whole page including all charts. I tried to use $timeout(function () {} but it fires before rendering charts. It works with hack like this:
$timeout(function () {
$timeout(function () {
}
}, 100);
But this is not exactly what I want. Could you please suggest something instead?
Related
All,
I have an attribute directive called scrolly. The directive looks like this:
.directive('scrolly', function () {
return {
restrict: 'A',
scope: {
scrollFunction: '&scrolly'
},
link: function (scope, element, attrs) {
var raw = element[0];
scope.$watch(element[0].children.length, function(){
if(raw.clientHeight <= raw.scrollHeight){
scope.scrollFunction();
}
});
element.bind('scroll', function () {
if (raw.scrollTop + raw.offsetHeight >= raw.scrollHeight) {
scope.scrollFunction();
}
});
}
};
})
The objective is to perform an infinite scroll. The scroll part works fine. The part that isn't working is the scope.$watch part. Basically, the objective for the watch portion is to execute the paging function until the element becomes scrollable at which point the scroll binding would take over. Although the purpose of my directive is paging, I do not want to get hung up on that. The root question is how to watch an attribute of an element.
scope.$watch(angular.bind(element, function(){
return element.children.length;
}), function(oldValue, newValue) {
if(raw.clientHeight <= raw.scrollHeight){
scope.scrollFunction();
}
});
In a $watch you can use a function as the watch parameter to watch specific properties on things.
scope.$watch(function() {
return element[0].children.length;
},
function() {
// do your thang on change
})
So here is what my issue is:
i have a directive:
autocompleteDirective.$inject = ['$timeout'];
function autocompleteDirective($timeout) {
return {
restrict: 'E',
scope: {
searchParam: '=ngModel',
suggestions: '=data',
onType: '=onType',
onSelect: '=onSelect',
autocompleteRequired: '='
},
controller: autocompleteController,
link: autocompleteLink,
templateUrl:'modules/components/autocomplete/templates/autocomplete.html'
};
}
my Link function looks like this:
function autocompleteLink(scope, element, attrs) {
$timeout(function() {
scope.initLock = false;
scope.$apply();
}, 250);
.... some other code
}
and my controller (not really relevant) :
autocompleteController.$inject = ['$scope'];
function autocompleteController($scope) {
//scope code
}
in my link function, I have a function that is (at the moment) using setTimeout:
if (attrs.clickActivation) {
element[0].onclick = function() {
if (!scope.searchParam) {
setTimeout(function() {
scope.completing = true;
scope.$apply();
}, 200);
}
};
}
I would like to unit test this certain block of code, but my unit tests fails:
elem.triggerHandler('click');
expect(scope.completing).to.equal(true);
even though, in the coverage report, i can see that the logic does successfully execute when the triggerHandler is clicked.
what i believe the culprit is the timeout.
digging around SO and other websites, i found using $timeout works best due to its exposure to "flush()" method.
my question is, how do I "inject" $timeout to the link function?
the previous examples i have see like injecting $timeout directly to directive, and then nesting the link function() inside the directive declaration:
function directive($timeout){
return {
link: function(scope, attrs) {
$timeout(blah!) //timeout is useable here...
}
}
The above doesn't work for me since i am not creating the function inside the directive function...so based on the model i am using, how can i use $timeout in a link?
I am writing and AngularJS directive for DagreD3. I have some problems with $scope update in Angular. When I update the Model, the Directive does not re-render the graph.
A plunker can be found here.
My directive looks like this:
myApp.directive('acDagre', function() {
function link(scope, element, attrs) {
scope.$watch(scope.graph, function(value) {
alert('update'); //NOT EVEN THIS IS CALLED ON UPDATE
});
var renderer = new dagreD3.Renderer();
renderer.run(scope.graph, d3.select("svg g"));
}
return {
restrict: "A",
link: link
};
The variable $scope.graph is modified in the Controller during runtime like this:
$scope.addNode = function(){
$scope.graph.addNode("kbacon2", { label: "Kevin Bacon the second" });
}
Did I understand something wrong in Angular? Everytime the Variable $scope.graph is changed, i want the graph to update.
You can find more information in the Plunker.
Thank you for very much your help!
The watcher should look either like this:
scope.$watch('graph', function(value) {
console.log('update');
});
Or like this:
scope.$watch(function () { return scope.graph; }, function(value) {
console.log('update');
});
It will not fire when adding nodes however, cause it's comparing by reference.
You can add true as a third parameter to perform a deep watch instead (it will use angular.equals):
scope.$watch('graph', function(value) {
console.log('update');
}, true);
Note that this is more expensive.
Example:
.directive('acDagre', function() {
var renderer = new dagreD3.Renderer();
function link(scope, element, attrs) {
scope.$watch(function () { return scope.graph; }, function(value) {
render();
}, true);
var render = function() {
renderer.run(scope.graph, d3.select("svg g"));
};
}
return {
restrict: "A",
link: link
};
});
Demo: http://plnkr.co/edit/Dn1t3sMH58mDz9HhqYD5?p=preview
If you are just changing the nodes you can define the watchExpression like this instead:
scope.$watch(function () { return scope.graph._nodes; }
Deep watching large objects can have a negative effect on performance. This will of course depend on the size and complexity of the watched object and the application, but it's good to be aware of.
Is it possible to apply two way binding to a <textarea></textarea> that has had TinyMCE applied to it for Rich Text Formatting.
I can't get it to work! I can get TinyMCE to load the content of my model, but when I update the text in TinyMCE, my model does not auto update!
Is there a way?
You can do this by creating your own directive.
What you need to do is to let your directive sync your model when something in the TinyMCE editor changes. I have not used TinyMCE, but Wysihtml5. I think you can remake this to use TinyMCE instead.
angular.module('directives').directive('wysihtml5', ['$timeout',
function ($timeout) {
return {
restrict: 'E',
require: 'ngModel',
template: "<textarea></textarea>", // A template you create as a HTML file (use templateURL) or something else...
link: function ($scope, $element, attrs, ngModel) {
// Find the textarea defined in your Template
var textarea = $element.find("textarea");
// When your model changes from the outside, use ngModel.$render to update the value in the textarea
ngModel.$render = function () {
textarea.val(ngModel.$viewValue);
};
// Create the editor itself, use TinyMCE in your case
var editor = new wysihtml5.Editor(textarea[0],
{
stylesheets: ["/style.css"],
parserRules: wysihtml5ParserRules,
toolbar: true,
autoLink: true,
useLineBreaks: false,
});
// Ensure editor is rendered before binding to the change event
$timeout(function () {
// On every change in the editor, get the value from the editor (textarea in case of Wysihtml5)
// and set your model
editor.on('change', function () {
var newValue = textarea.val();
if (!$scope.$$phase) {
$scope.$apply(function () {
ngModel.$setViewValue(newValue);
});
}
});
}, 500);
}
};
}]);
Then you can use the directive in your html page like this:
<wysihtml5 ng-model="model.text" />
Here's a link if you need more info on creating your own directive: http://docs.angularjs.org/guide/directive
Also compare the render function from the directive above to this render function from angular-ui-tinymce ( https://github.com/angular-ui/ui-tinymce )
ngModel.$render = function() {
if (!tinyInstance) {
tinyInstance = tinymce.get(attrs.id);
}
if (tinyInstance) {
tinyInstance.setContent(ngModel.$viewValue || '');
}
Plnkr: http://plnkr.co/edit/04AFkp?p=preview
However depending on the timing of the loading of your DOM you may need to set the priority on your directive upwards. :-)
Here is my solution using a custom angular directive.
You'll need to use jQuery with angularJS, TinyMCE 4 and their jQuery plugin.
myApp.directive('tinymce', function() {
return {
restrict: 'C',
require: 'ngModel',
link: function(scope, element, attrs, modelCtrl) {
element.tinymce({
setup: function (e) {
e.on("change", function() {
modelCtrl.$setViewValue(element.val());
scope.$apply();
}
}
});
}
}
}
Then in your HTML:
<textarea class="tinymce" ng-model="data"></textarea>
That's it, have fun.
I'm building an application using AngularJS and UniformJS. I'd like to have a reset button on the view that would reset my select's to their default value. If I use uniform.js, it isn't working.
You can examine it here:
http://plnkr.co/edit/QYZRzlRf1qqAYgi8VbO6?p=preview
If you click the reset button continuously, nothing happens.
If you remove the attribute, therefore no longer using uniform.js, everything behaves correctly.
Thanks
UPDATE:
Required the use of timeout.
app.controller('MainCtrl', function($scope, $timeout) {
$scope.reset = function() {
$scope.test = "";
$timeout(jQuery.uniform.update, 0);
};
});
Found it. For the sake of completeness, I'm copying my comment here:
It looks like Uniform is really hacky. It covers up the actual select element, and displays span instead. Angular is working. The actual select element's value is changing, but the span that Uniform displays is not changing.
So you need to tell Uniform that your values have changed with jQuery.uniform.update. Uniform reads the value from the actual element to place in the span, and angular doesn't update the actual element until after the digest loop, so you need to wait a little bit before calling update:
app.controller('MainCtrl', function($scope, $timeout) {
$scope.reset = function() {
$scope.test = "";
$timeout(jQuery.uniform.update, 0);
};
});
Alternatively, you can put this in your directive:
app.directive('applyUniform',function($timeout){
return {
restrict:'A',
require: 'ngModel',
link: function(scope, element, attr, ngModel) {
element.uniform({useID: false});
scope.$watch(function() {return ngModel.$modelValue}, function() {
$timeout(jQuery.uniform.update, 0);
} );
}
};
});
Just a slightly different take on #john-tseng's answer. I didn't want to apply a new attribute to all my check-boxes as we had quite a few in the application already. This also gives you the option to opt out of applying uniform to certain check-boxes by applying the no-uniform attribute.
/*
* Used to make sure that uniform.js works with angular by calling it's update method when the angular model value updates.
*/
app.directive('input', function ($timeout) {
return {
restrict: 'E',
require: '?ngModel',
link: function (scope, element, attr, ngModel) {
if (attr.type === 'checkbox' && attr.ngModel && attr.noUniform === undefined) {
element.uniform({ useID: false });
scope.$watch(function () { return ngModel.$modelValue }, function () {
$timeout(jQuery.uniform.update, 0);
});
}
}
};
});
Please try blow code.
app.directive('applyUniform', function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
if (!element.parents(".checker").length) {
element.show().uniform();
// update selected item check mark
setTimeout(function () { $.uniform.update(); }, 300);
}
}
};
});
<input apply-uniform type="checkbox" ng-checked="vm.Message.Followers.indexOf(item.usrID) > -1" ng-click="vm.toggleSelection(item.usrID)" />