angular custom wysi directive with ngModel - angularjs

i am using the wysiwyg redactor in my angular project with the following directive i extended.
why does the element not display anything at focusIn nor does it update after focusOut, it's probably because of ngModel.$render().
how can i register the last event ngModel.$render() if it is only valid after focusIn was executed?
do the events have to be re-registered on focusIn/Out because i replace elements?
// focusin/out event for replacing div
function focusIn(e) {
//wysi redactor template
var tmpl = '<div ng-model=model validation="{{::scope.column.validation}}" placeholder="{{::scope.column.placeholder}}" class="wysi f12 form-control" contenteditable redactor';
if (element.hasClass('one-row'))
tmpl += ' ="{deniedTags: [\'br\'],enterKey: false,pastePlainText: true,linebreaks: true}" ng-class="one-row"></div>';
else
tmpl += '></div>';
var tmp = $compile(tmpl)(scope);
//var tmp = angular.element(tmpl);
// Resume the compilation phase after setting ngModel
element.replaceWith(tmp);
// put in timeout to avoid $digest collision. call render() to
// set the initial value.
$timeout(function() {
editor = element.redactor(options);
element.on('focusout', focusOut);
ngModel.$render();
//element.on('remove', function () {
// //console.log('redactor remove ' + scope.column);
// element.off('remove');
// element.redactor('core.destroy');
// //todo aow: maybe save value?
//});
}, 150);
}
//destroy redactor when losing focus
function focusOut(e) {
//for html render in read-only div
scope.readonlyContent = scope.column.content;
//destroy redactor
element.redactor('core.destroy');
//replace redactor with read-only div
element.replaceWith(this.template);
//$compile(element)(scope);
element.on('click', focusIn);
console.log('after settemplate');
}
ngModel.$render = function() {
if(angular.isDefined(editor)) {
$timeout(function() {
console.log('redactor render ' + scope.column);
//var ed = element.find('.wysi');
element.redactor('code.set', ngModel.$viewValue || '');
element.redactor('placeholder.toggle');
scope.redactorLoaded = true;
});
}
};
i am doing this so complicated because ng-model doesn't suppurt the proper rendering of html, it has to be in ng-bind-html to render properly so i have to use 2 different divs
please check out my plunker version.

i have solved it by putting the ngModel.$render() function in a separate directive as it originally was. my mistake

Related

angularjs textarea with colors (with html5 contenteditable)

I'm trying to create an editor which does "syntax highlighting",
it is rather simple:
yellow -> <span style="color:yellow">yellow</span>
I'm also using <code contenteditable> html5 tag to replace <textarea>, and have color output.
I started from angularjs documentation, and created the following simple directive. It does work, except it do not update the contenteditable area with the generated html.
If I use a element.html(htmlTrusted) instead of ngModel.$setViewValue(htmlTrusted), everything works, except the cursor jumps to the beginning at each keypress.
directive:
app.directive("contenteditable", function($sce) {
return {
restrict: "A", // only activate on element attribute
require: "?ngModel", // get ng-model, if not provided in html, then null
link: function(scope, element, attrs, ngModel) {
if (!ngModel) {return;} // do nothing if no ng-model
element.on('blur keyup change', function() {
console.log('app.directive->contenteditable->link->element.on()');
//runs at each event inside <div contenteditable>
scope.$evalAsync(read);
});
function read() {
console.log('app.directive->contenteditable->link->read()');
var html = element.html();
// When we clear the content editable the browser leaves a <br> behind
// If strip-br attribute is provided then we strip this out
if ( attrs.stripBr && html == '<br>' ) {
html = '';
}
html = html.replace(/</, '<');
html = html.replace(/>/, '>');
html = html.replace(/<span\ style=\"color:\w+\">(.*?)<\/span>/g, "$1");
html = html.replace('yellow', '<span style="color:yellow">yellow</span>');
html = html.replace('green', '<span style="color:green">green</span>');
html = html.replace('purple', '<span style="color:purple">purple</span>');
html = html.replace('blue', '<span style="color:yellow">blue</span>');
console.log('read()-> html:', html);
var htmlTrusted = $sce.trustAsHtml(html);
ngModel.$setViewValue(htmlTrusted);
}
read(); // INITIALIZATION, run read() when initializing
}
};
});
html:
<body ng-app="MyApp">
<code contenteditable
name="myWidget" ng-model="userContent"
strip-br="true"
required>This <span style="color:purple">text is purple.</span> Change me!</code>
<hr>
<pre>{{userContent}}</pre>
</body>
plunkr: demo (type yellow, green or blue into the change me input area)
I tried scope.$apply(), ngModel.$render() but has no effect. I must miss something really obvious...
The links I already read through:
others' plunker demo 1
others' plunker demo 2
angularjs documentation's example
$sce.trustAsHtml stackoverflow question
setViewValue stackoverflow question
setViewValue not updating stackoverflow question
Any help is much appreciated. Please see the plunker demo above.
After almost a year, I finally settled to Codemirror, and I was never happier.
I'm doing side-by-side markdown source editing with live update (with syntax highlighting, so even a bit more advanced than stackoverflow's editing page.)
I created a simple codeEditor angular directive, which requires codeMirror, and uses it.
For completeness, here is the component sourcecode:
$ cat components/codeEditor/code-editor.html
<div class="code-editor"></div>
$ cat codeEditor.js
'use strict';
angular.module('myApp')
.directive('codeEditor', function($timeout, TextUtils){
return {
restrict: 'E',
replace: true,
require: '?ngModel',
transclude: true,
scope: {
syntax: '#',
theme: '#'
},
templateUrl: 'components/codeEditor/code-editor.html',
link: function(scope, element, attrs, ngModelCtrl, transclude){
// Initialize Codemirror
var option = {
mode: scope.syntax || 'xml',
theme: scope.theme || 'default',
lineNumbers: true
};
if (option.mode === 'xml') {
option.htmlMode = true;
}
scope.$on('toedit', function () { //event
//This is required to correctly refresh the codemirror view.
// otherwise the view stuck with 'Both <code...empty.' initial text.
$timeout(function() {
editor.refresh();
});
});
// Require CodeMirror
if (angular.isUndefined(window.CodeMirror)) {
throw new Error('codeEditor.js needs CodeMirror to work... (o rly?)');
}
var editor = window.CodeMirror(element[0], option);
// Handle setting the editor when the model changes if ngModel exists
if(ngModelCtrl) {
// Timeout is required here to give ngModel a chance to setup. This prevents
// a value of undefined getting passed as the view is rendered for the first
// time, which causes CodeMirror to throw an error.
$timeout(function(){
ngModelCtrl.$render = function() {
if (!!ngModelCtrl.$viewValue) {
// overwrite <code-editor>SOMETHING</code-editor>
// if the $scope.content.code (ngModelCtrl.$viewValue) is not empty.
editor.setValue(ngModelCtrl.$viewValue); //THIRD happening
}
};
ngModelCtrl.$render();
});
}
transclude(scope, function(clonedEl){
var initialText = clonedEl.text();
if (!!initialText) {
initialText = TextUtils.normalizeWhitespace(initialText);
} else {
initialText = 'Both <code-editor> tag and $scope.content.code is empty.';
}
editor.setValue(initialText); // FIRST happening
// Handle setting the model if ngModel exists
if(ngModelCtrl){
// Wrap these initial setting calls in a $timeout to give angular a chance
// to setup the view and set any initial model values that may be set in the view
$timeout(function(){
// Populate the initial ng-model if it exists and is empty.
// Prioritize the value in ngModel.
if(initialText && !ngModelCtrl.$viewValue){
ngModelCtrl.$setViewValue(initialText); //SECOND happening
}
// Whenever the editor emits any change events, update the value
// of the model.
editor.on('change', function(){
ngModelCtrl.$setViewValue(editor.getValue());
});
});
}
});
// Clean up the CodeMirror change event whenever the directive is destroyed
scope.$on('$destroy', function(){
editor.off('change');
});
}
};
});
There is also inside the components/codeEditor/vendor directory the full codemirror sourcecode.
I can highly recommend codeMirror. It is a rocksolid component, works in
every browser combination (firefox, firefox for android, chromium).

Programmatically changed value not detected by AngularJS

This is only my 2nd AngularJS project. I'm trying to add new functionality to an existing jQuery app rather than starting over.
The jQuery portion populates a div with several image thumbnails. The purpose of the new AngularJS features is to allow the user to edit the information for the photos - caption, photographer credit, and such. Clicking a thumbnail should trigger the Angular app to retrieve its info from the database for edit. My problem is, the image URL is never picked up by Angular.
The ng-app tag is placed on the page's body element, so everything should be visible to Angular. I have tried $scope.$watch and $scope.$apply with no success. The URL of the clicked image does appear in expected locations, but the Angular model element "imgurl" never seems to get updated despite using ng-model="imgurl" on the text input field.
I have looked at similar questions such as these:
I can't detect programmatically value change in angularjs
AngularJS : ng-model binding not updating when changed with jQuery
My existing jQuery, trying to push data into Angular
function populateImageForm(imageURL) {
jQuery("#imageurltest").html("clicked image is " + imageURL); //show the URL of the clicked image
//updates the image, but Angular doesn't see change: jQuery("#working-image").attr("src", imageURL);
jQuery("#working-image").attr("ng-src", imageURL);
jQuery("#wiurl").val(imageURL);
}
HTML elements using Angular:
<img id="working-image" class="center-block img-responsive" ng-src={{imgurl}} imageonload />
<input id="wiurl" type="text" size="65" ng-model="imgurl" />
Angular controller code:
app.controller("imageInfoControl", function($scope, $http) {
$scope.imgurl = "http://example.org/placeholder.jpg";
$http.get("ng-get-image.php", {params:{"url": $scope.imgurl}} ).success(function(response){
console.log (response);
$scope.message = response.title;
});
$scope.$watch('imgurl',
function(newVal, oldVal) {
console.log('imgurl programmatically changed');
});
$scope.$watch(function() {
return $scope.message
}, function(newVal, oldVal) {
if(newVal !== null) {
console.log('message programmatically changed to ' + $scope.message);
}
});
});
app.directive('imageonload', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
element.bind('load', function() {
console.log (element);
console.log (attrs);
//doesn't seem to do anything: scope.$apply(attrs.imageonload); https://stackoverflow.com/questions/17884399/image-loaded-event-in-for-ng-src-in-angularjs
scope.$digest;
scope.message = attrs.src;
console.log(scope.message);
//alert('image is loaded');
});
element.bind('error', function(){
alert('image could not be loaded');
});
}
};
});
Sample Link
Make it as separate js file and call it in index under angular and jquery script
(function($, ng) {
var $val = $.fn.val; // save original jQuery function
// override jQuery function
$.fn.val = function (value) {
// if getter, just return original
if (!arguments.length) {
return $val.call(this);
}
// get result of original function
var result = $val.call(this, value);
// trigger angular input (this[0] is the DOM object)
ng.element(this[0]).triggerHandler('input');
// return the original result
return result;
}
})(window.jQuery, window.angular);
You need to use model as a property of an object.
Please try ng-model="data.imgurl".
Of course, you need to declare data object in the scope.
$scope.data = {}:
$scope.data.imgurl = "something.jpg";
Like this
Thank you

Changing Angular Carousel Position with Directive->controller change not working

I am trying to create some event triggers to change the position of an Angular Carousel (Github repo).
See the plunker for a working version
The expected behavior is that you click the text that says "Go to position: 0" and the Carousel will change index position to 0.
This is accomplished using two-way binding on the Carousel: rn-carousel-index="carouselIndex"
This scope value is passed to a directive by setting: carousel-index="carouselIndex"
A controller method to modify carouselIndex is also passed to the directive with: index-model="indexModel(value)"
The directive takes the scopes and has a function bound to the text that should change the Carousel position:
app.directive('timeline', function () {
return {
replace:true,
scope:{
indexModel:'&',
carouselIndex:'='
},
link:function (scope, element, attrs) {
var valueto = 0;
var textElement = d3.select(".timeLine").text('Go to position: ' + valueto)
textElement
.on('click',clickMe)
function clickMe(d){
console.log('click');
console.log("but index is: " + scope.carouselIndex);
scope.carouselIndex = valueto;
// scope.$apply(carouselIndex) = valueto;
scope.indexModel({value: valueto});
//alert(d);
}
}
}
})
As you can see, when you click, the watcher on $carouselIndex does not always change. Moreover, and more importantly, the Carousel behavior to change position is not working.
$scope.$watch('carouselIndex', function(a,b) {
console.log('change detected');
console.log('Index really is: ' + $scope.carouselIndex);
})
Adding a digest() $apply() function solved the problem.
$scope.indexModel = function(slide) {
$scope.$apply(function() {
console.log('change slide ' + slide);
var changeSlide = parseInt(slide);
$scope.carouselIndex = changeSlide;
console.log('Index should be: ' + $scope.carouselIndex);
)}
};
For the problem with the watcher on $scope.carouselIndex, try what the Docs say
Note: If the indicators don't seem to update with the slides, try binding to an object param i.e. carousel.index, set in the controller like $scope.carousel.index = 0
https://github.com/revolunet/angular-carousel

AngularJS - preloader for ngRepeat

I'm using the infinite scroll technique in conjunction with ng-repeat. I want to display a preloader up until the directive has finished adding the items to the DOM. What would be the simplest way of achieving that?
Try this live DEMO I set up for reference.
It depends on your infinite scroll implementation. And for best answer you should set up a plunker or jsbin.
But what about just setting a loader and using ng-if directive to only show it while the item container is empty ?
Imagine we have a template like
<div id="data-container" when-scrolled="loadMore()">
<img ng-if="!items.length" ng-src="http://placehold.it/100x395&text=Loading">
<div class="item" ng-repeat="item in items">{{item.id}}</div>
<img ng-if="items.length && busy" ng-src="http://placehold.it/85x50&text=Loading">
</div>
Here when-scrolled is our infinite-scroll directive which just monitors the scroll position and calls the supplied handler when it is time to load more items.
app.directive('whenScrolled', function() {
return function(scope, element, attr) {
var containerNode = element[0];
element.bind('scroll', function() {
if (containerNode.scrollTop + containerNode.offsetHeight >= containerNode.scrollHeight) {
scope.$apply(attr.whenScrolled);
}
});
};
});
Handler is called when the scroll hits the bottom of the content area.
loadMore() method in the controller could be defined like this:
$scope.items = [];
$scope.busy = false;
$scope.loadMore = function() {
if (true === $scope.busy) {
return;
}
$scope.busy = true;
$timeout(function() {
var currentLength = $scope.items.length;
for (var i = currentLength; i < currentLength + 10; i++) {
$scope.items.push({id: i + 1});
}
$scope.busy = false;
}, 350); // Imitating the long remote request.
};
We first initialize the $scope.items and while it's length is 0 the loading image is shown in the template as it is shown while "!items.length" is true. When the first items are added to the collection preloader gets hidden.
Then there's a local loading image which could be replaced with a spinner or whatever you like. It is shown then the $scope.busy var is set to true and is hidden when it's false. We change the $scope.busy value at the start and end of the aync request. Timeouts are used here for simple of async request demo.

In an AngularJS directive, how do I set a parent controller's property?

Here's a jsFiddle that shows what I'm trying to do: http://jsfiddle.net/P3c7c
I'm using the Google Places AutoComplete widget to obtain lat/long coordinates, which I then wish to use in a subsequent search function. It seemed that the proper way to implement this, considering the need to add an event listener to an element, was to use a directive, and to attach the listener using the directive's linking function. However, inside of this listener, I need it to set the location property of the SearchForm controller, which is its parent. And I have not figured out how to make that connection. Here's the relevant chunk of code:
/* Controllers */
function SearchForm($scope){
$scope.location = ''; // <-- this is the prop I wish to update from within the directive
$scope.doSearch = function(){
if($scope.location === ''){
alert('Directive did not update the location property in parent controller.');
} else {
alert('Yay. Location: ' + $scope.location);
}
};
}
/* Directives */
angular.module('OtdDirectives', []).
directive('googlePlaces', function(){
return {
restrict:'E',
replace:true,
transclude:true,
scope: {location:'=location'}, // <--prob something wrong here? i tried #/& too, no luck
template: '<input id="google_places_ac" name="google_places_ac" type="text" class="input-block-level"/>',
link: function($scope, elm, attrs, ctrl){
var autocomplete = new google.maps.places.Autocomplete($("#google_places_ac")[0], {});
google.maps.event.addListener(autocomplete, 'place_changed', function() {
var place = autocomplete.getPlace();
// THIS IS THE STRING I WANT TO SAVE TO THE PARENT CONTROLLER
var location = place.geometry.location.lat() + ',' + place.geometry.location.lng();
// THIS IS NOT DOING WHAT I EXPECT IT TO DO:
$scope.location = location;
});
}
}
});
Thanks in advance.
Two minor corrections and it should work:
<google-places location="location"></google-places>
and when you set location inside your directive you also need to do $scope.$apply()
$scope.$apply(function() {
$scope.location = location;
});
You have to do $apply() because the event happens outside of angular digest loop, so you have to let angular know that something has changed inside the scope and it needs to "digest" it's bi-directional bindings and other internal async stuff.
Also, I don't think you need transclude:true.
http://jsfiddle.net/P3c7c/1/

Resources