I'm trying to do inline editing in a form with an Angular directive, unfortunately I ran into two issues and I can't get around them, so a second opinion will be very much appreciated.
here is the fiddle: http://jsfiddle.net/jorgecas99/bc65N/
Question 1:
As you can see I added a section that is suppose to listen to key strokes (in this case the esc key) and exit the edit mode, unfortunately it is not working. I tried listening for key 13 which is 'enter' and that worked ok, so I don't understand.
Question 2:
I will like to change the second field to a dropdown when you click to edit it without having to create a new directive, is that even possible? I certainly care about number of lines of code so if this can be achieve with one directive, then that would be my preferred option.
Thank you!
for first question, you can see a revised version of your fiddle which incorporate the technique described in
http://css-tricks.com/snippets/javascript/saving-contenteditable-content-changes-as-json-with-ajax/ here http://jsfiddle.net/bonamico/cAHz7/
var myApp = angular.module('myApp', []);
please note that var myApp = was missing, and so the following declaration did not execute
myApp.directive('contenteditable', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
// view -> model
elm.bind('blur', function() {
scope.$apply(function() {
ctrl.$setViewValue(elm.html());
});
});
// model -> view
ctrl.render = function(value) {
elm.html(value);
};
// load init value from DOM
ctrl.$setViewValue(elm.html());
elm.bind('keydown', function(event) {
console.log("keydown " + event.which);
var esc = event.which == 27,
el = event.target;
if (esc) {
console.log("esc");
ctrl.$setViewValue(elm.html());
el.blur();
event.preventDefault();
}
});
}
};
});
See also
http://api.jquery.com/keydown/
For the second question, I would suggest that minimizing numer of lines of code is not normally a main concern, while making code modular and maintainable is. So it would be definitely better to create two directives, and possibly a common javascript function for the commonm parts between the two, if any.
Related
I am working on a problem for accessibility issues in my project. One scenario I am struggling is, we have anchor tag with ng-click event. When someone is hitting tab and reached anchor tag on the page using keyboard, they are hitting enter key, and they think it should do something. But, unfortunately enter key and click events works well with button control but not with anchor. Anyone has any suggestions how to solve this problem?
thanks in advance.
I faced a similar issue, and resolved it be creating a simple directive which would intercept the keydown/keypress events on a link, and then call a function associated with the directive.
Here is the directive:
// Allows for the interception of the enter key, and then calling a passed in function
(function () {
'use strict';
angular
.module('app.directives')
.directive('convEnter', convEnter);
function convEnter() {
var directive = {
restrict: 'A',
link: link
};
return directive;
////////////////////////////////////////////////////////////////////////
function link(scope, element, attrs) {
element.bind('keydown keypress', function (event) {
if (event.which === 13) {
scope.$apply(function () {
scope.$eval(attrs.convEnter, { 'event': event });
});
event.preventDefault();
}
});
}
}
})();
To use it, you would simply apply it to your link, ie:
<a conv-enter="vm.click()" ng-click="vm.click()">Click Link</a>
Of course this is a bit repetitive as you are specifying "vm.click()" twice; you could optimize this if desired by having the directive simply call the "click" function on the element itself, as long as you always wanted the click and the enter key to invoke the same function. (In my scenario, I sometimes needed to distinguish between the two, and hence i allowed passing in a separate function)
We are using bootstrap datepicker for our project needs. What we need to do is that whenever user selects the today's date, date has to be shown appended with "(TODAY)" like "May 12, 2008 (TODAY)" in the textbox.
What can be the best approach here? As we are using this datepicker at multiple places, I think having a general approach like creating a directive would be helpful. Was trying to bind change event with the datepickerPopup directive, but have not been able to make it work.
Here is what I have tried so far:
Have created one decorator. This seems to be working. However one issue, how do I access the parent directive methods in this decorator (such as dateFilter, parseDate here)? (sorry if you find my questions naïve, as I am very new to angularjs). I have attached the code.
app.config(function($provide) {
$provide.decorator('datepickerPopupDirective', function($delegate) {
var directive = $delegate[0],
link = directive.link;
//closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection,
//angular.extend(directive.scope, { 'monthChanged': '&' });
directive.compile = function() {
return function(scope, element, attrs, ngModel) {
link.apply(this, arguments);
ngModel.$render = function() {
var date = ngModel.$viewValue ? dateFilter.apply(ngModel.$viewValue, dateFormat) : '';
var currentDate = new Date();
currentDate.setHours(0,0,0,0);
if(date.getTime() == currentDate.getTime()){
element.val(date + "(TODAY)");
}else{
ngModel.$setViewValue(scope.date);
}
scope.date = parseDate( ngModel.$modelValue );
};
};
};
return $delegate;
});
});
You've got a couple of options. The quick and dirty way would be to extract the component from the third party library into your own directive and template, and modify as needed. The disadvantage to this is that you'll no longer be up to date with the component. Future versions would require you to manually update your directive, which you might not care about... yet.
The second option, is to take advantage of angular's $provide.decorator
This post gives you an initial idea
What are "decorators" and how are they used?
Here's a basic example of decorating a directive definition object
app.directive("foo", function() {
return {
replace: true,
template: '<div>This is foo directive</div>'
};
});
app.config(function($provide) {
$provide.decorator('fooDirective', function($delegate) {
var directive = $delegate[0];
directive.restrict = "AM";
return $delegate;
});
});
In your case, you'll want to override what value is being referenced on the template. You could decorate the entire directive if you want to completely modify it.
I would recommend this as the best approach when looking to tackle modification of a third party library.
Here's an example of decorating a directive to override a scope level function, and use an existing scope variable within the directive while overriding it.
https://jsfiddle.net/wvty8rpc/3/
I have been recently working on dynamically changing constraints on input fields on a form. The aim was to make a back-end-driven form, whereas all fields and their constraints are being sent to us by a server. Whilst I managed to get the constraints to be added/removed as we please by creating a simple directive, it seems like a form controller is not picking up these changes, and so the form is always $valid. Have a look at this jsfiddle, here is the directive:
myApp.directive('uiConstraints', [function(){
function applyConstraints(element, newVal, oldVal){
//remove old constraints
if(oldVal !== undefined && oldVal !== null){
for (var i = 0; i < oldVal.length; i++) {
element.removeAttr(oldVal[i].key);
}
}
//apply new constraints
if(newVal !== undefined && newVal !== null){
for (var i = 0; i < newVal.length; i++) {
var constraint = newVal[i];
element.attr(constraint.key, constraint.value);
}
}
}
function link(scope, element, attrs){
scope.$watch(attrs.uiConstraints, function(newVal, oldVal){
applyConstraints(element, newVal, oldVal);
});
}
return {
restrict : 'A',
link : link
};
}]);
The required behavior is so it works like on the official angularjs plunker. However, it seems the FormController is being created before the directive populates constraints on the input fields, and updating these constraints doesn't update the corresponding values in the FormController.
Does any1 know if I can force the FormController to pickup the changes to constraints made by the directive? And if so, how? I have no idea where to even start... Thanks.
-- EDIT --
I couldn't get plunker to work (show to others my latest changes) so here is jsfiddle of what I have: latest
To more in detail describe the issue:
go to the jsfiddle described
if you remove the initial value from the textbox, it will become red (invalid), however the controller won't pick that up and will still show:
myform.$valid = true
myform.myfield.$valid = true
-- EDIT --
The bounty description doesn't recognize Stack Overflow formatting (like 2 spaces for new line etc) so here it is in more readable form:
Since this is still unsolved and interesting question I decided to start a bounty.
The requirements are:
- works on ancient AngularJS(1.0.3) and newer (if it can't be done on 1.0.3 but someone did it on newer version of angular I will award bounty)
- initially a field has no constraints on it (is not required, max and min not set etc)
- at any time constraints for the field can change (it can become required, or a pattern for the value is set etc), as well as any existing constraints can be removed
- all constraints are stored in a controller in an object or array
- FormController picks up the changes, so that any $scope.FormName.$valid is being changed appropriately when constraints
on any fields in that form change
A good starting point is my jsfiddle.
Thanks for your time and good luck!
Check out this PLUNK
.directive('uiConstraints', ["$compile",
function($compile) {
function applyConstraints(element, newVal, oldVal) {
//apply new constraints
if (newVal !== undefined && newVal !== null) {
for (var i = 0; i < newVal.length; i++) {
var constraint = newVal[i];
element.attr(constraint.key, constraint.value);
}
}
}
return {
restrict: 'A',
terminal: true,
priority: 1000,
require: "^form",
link: function(scope, element, attrs, formController) {
var templateElement;
var previousTemplate;
templateElement = element.clone(); //get the template element and store it
templateElement.removeAttr("ui-constraints");// remove the directive so that the next compile does not run this directive again
previousTemplate = element;
scope.$watch(attrs.uiConstraints, function(newVal, oldVal) {
var clonedTemplate = templateElement.clone();
applyConstraints(clonedTemplate, newVal);
clonedTemplate.insertBefore(previousTemplate);
var control = formController[previousTemplate.attr("name")];
if (control){
formController.$removeControl(control);
}
if (previousTemplate) {
previousTemplate.remove();
}
$compile(clonedTemplate)(scope);
previousTemplate = clonedTemplate;
});
}
};
}]);
The idea here is to set terminal: true and priority: 1000 to let our directive be compiled first and skip all other directives on the same element in order to get the template element. In case you need to understand more, check out my answer: Add directives from directive in AngularJS.
After getting the template element, I remove the ui-constraints directive to avoid this directive getting compiled again and again which would add a $watch to the digest cycle every time we toggle the constraints.
Whenever the constraints change, I use this template element to build a new element containing all the constraints without the ui-constraints directive and compile it. Then I remove the previous element from the DOM and its controller from the Form Controller to avoid leaking and problems caused by previous element's controller existing in the Form Controller.
For starters, AngularJS has ng-required directive that allows you to toggle required validation
<input type="text" data-ng-required="isRequired"
data-ng-model="mydata" name="myfield" />
see the Fiddle:
Fiddle
Making Min and Max logic dynamic is a little more complex. Your current approach JQuery-like in alter elements which is NOT AngularJS strength. The ideal Answer is a directive like ng-minlnegth with more intelligence.
I'll look into that.
Quick and dirty solution:
scope.$watch(attrs.uiConstraints, function(newVal, oldVal){
applyConstraints(element, newVal, oldVal);
if (newVal != oldVal) {
element = $compile(element)(scope);
}
});
I don't like solution and it's not perfect. This is an interesting problem, so I'm still working on finding a more elegant solution.
Here is an example.
What went wrong for you was that you tried to use scope in your apply thing, and you were watching the wrong thing.
I changed your link function:
link : function(scope, element, attrs){
scope.$watch(attrs.constraints, function(newConstraints, oldConstraints){
applyConstraints(newConstraints, oldConstraints, element);
}, true);
}
And your toggle function now does some random attributes and stuff:
$scope.toggleConstraints = function(myconstraints){
var getRandomPropOrValueName = function(){
return Math.random().toString(36).substring(7);
}
$scope.myconstraints = [{key: getRandomPropOrValueName(), value: getRandomPropOrValueName() }];
};
I have looked at a number of answers for binding Angular-JS scope data to Polymer components. (Use Angular-Bind-Polymer Another, And a third). It seems like this shouldn't be this hard, if the polymer components are truly just DOM. (Note that I'm using Chrome beta (36)).
I tried the Angular-Bind-Polymer suggestion, but no luck. My real interest is extending ngModel to work with Polymer so that I can use the Polymer check boxes, radio buttons, etc. For example, I tried getting paper-checkbox to work, so I tried the following, thinking that it should work:
var ngPaper = angular.module('ng-paper', []);
ngPaper.directive('paper-checkbox', function() {
console.log("Processing directive");
return {
restrict: 'E',
require: '?ngModel',
link: function(scope, element, attr, ctrl) {
console.log("Running linker");
element.on('click', function() {
scope.$apply(function() {
ctrl.$setViewValue(element[0].checked);
});
});
ctrl.$render = function() {
element[0].checked = ctrl.$viewValue;
};
ctrl.$isEmpty = function(value) {
return value != true;
};
ctrl.$formatters.push(function(value) {
return value === true;
});
ctrl.$parsers.push(function(value) {
return value ? true : false;
});
}
};
});
But no.
I then tried using angular-bind-polymer to bind the checked value on the paper-checkbox to a model attribute but didn't have any success.
I feel like if I could figure out how to get one of the form control elements to work, the others should fall quickly in line. Does anyone have a better idea on how to do this or an explanation as to why the directive I wrote isn't getting picked up and applied to the paper-checkbox?
I made this generic work-around that I use to watch changes on check-boxes and most Polymer elements from AngularJS, it's really useful while you find a more proper way, I hope it helps you.
You can also use it to manipulate Polymer Elements (E.g. Toggle).
in your HTML:
<paper-radio-group selected="firstOption">
<paper-radio-button label="First Option" id="firstOption" ng-click="change()"></paper-radio-button>
<paper-radio-button label="Second Option" id="secondOption" ng-click="change()"></paper-radio-button>
</paper-radio-group>
In the corresponding AngularJS controller, requites $scope.
var firstOption= document.querySelector('paper-radio-button[id="firstOption"]');
var secondOption= document.querySelector('paper-radio-button[id="secondOption"]');
console.log(firstOption);
console.log(secondOption);
$scope.change = function()
{
console.log(firstOption);
console.log(secondOption);
}
This way every time the user changes the selection, AngularJS will get notified so it can query the changes, you can scope the data you get back to something more specific, this is particularly useful to toggle Polymer Elements from AngularJS.
Let me know if this works for you, happy coding!
See bottom of question for an improved solution to this problem
I have been trying for some time now to get a directive for the pagedown working. This is the exact same editor used by stackoverflow. Stackoverflow make this code available here:
https://code.google.com/p/pagedown/
There are some versions out there on the internet but none work well. What I need is one that will appear with all the editor buttons just like stackoverflow both when coded inline and also when it's inline as part of an ngRepeat.
I would like to make this directive work when it's coded inline and also inside an ng-repeat using Angular version 1.2.7. What's needed is that when the model data changes the directive needs to update the pagedown views to show the new question and answers. When the user changes the pagedown edit area the directive needs to be able to update the model. When the user clicks [save] the model data needs to be saved to the database (or at least to another object to confirm it worked).
The directive needs to be able to respond to changes in the model and also save it's raw data to the model on keyup or when the 'change' button is pressed in the editing paned. Here is what I have so far. Note that this version does not have the $wmdInput.on('change' but it's a start for what is needed.
Most important I would like to have this working with version 1.2.7 of Angular and jQuery 2.0.3 Please note that I found differences with my non-working code between versions 1.2.2 and 1.2.7. I think it's best if any solution works for the latest (1.2.7) release.
Update
I now this directive which is simpler and solves some recent problems
I had with the content not showing. I would highly recommend using
this directive which is based on the answer accepted plus a few
improvements: https://github.com/kennyki/angular-pagedown
Here is a working link:
http://cssdeck.com/labs/qebukp9k
UPDATE
I made some optimizations.
I use ngModel.$formatters ! no need for another $watch.
I use $timeout and then scope.$apply to avoid $digest in progress errors.
Angular.js & Performance
If you hit performance maybe your application is using too many $watch / $on.
In my experience, using 3rd-party libraries can cause all sort of non efficient / memory leaking behavior, mostly because it was not implemented with angular / SPA in mind.
I was able to do some smart integration for some libraries but some just don't fit well to angular's world.
If your application must show 1000+ questions you should probably start with writing your custom repeater, and prefer dynamic DOM insertions.
Angular.js will not perform well with tons of data bindings unless you are willing to write some smart lower level stuff (It's actually fun when you know how!).
Again, prefer pagination! As Misko Hevery says: "You can't really show more than about 2000 pieces of information to a human on a single page. Anything more than that is really bad UI, and humans can't process this anyway".
Read this: How does data binding work in AngularJS?
I'm more than happy to help you, but First let me show the code (contact me)..
Solution:
var app = angular.module('App', []);
app.directive('pagedownAdmin', function ($compile, $timeout) {
var nextId = 0;
var converter = Markdown.getSanitizingConverter();
converter.hooks.chain("preBlockGamut", function (text, rbg) {
return text.replace(/^ {0,3}""" *\n((?:.*?\n)+?) {0,3}""" *$/gm, function (whole, inner) {
return "<blockquote>" + rbg(inner) + "</blockquote>\n";
});
});
return {
require: 'ngModel',
replace: true,
template: '<div class="pagedown-bootstrap-editor"></div>',
link: function (scope, iElement, attrs, ngModel) {
var editorUniqueId;
if (attrs.id == null) {
editorUniqueId = nextId++;
} else {
editorUniqueId = attrs.id;
}
var newElement = $compile(
'<div>' +
'<div class="wmd-panel">' +
'<div id="wmd-button-bar-' + editorUniqueId + '"></div>' +
'<textarea class="wmd-input" id="wmd-input-' + editorUniqueId + '">' +
'</textarea>' +
'</div>' +
'<div id="wmd-preview-' + editorUniqueId + '" class="pagedown-preview wmd-panel wmd-preview"></div>' +
'</div>')(scope);
iElement.html(newElement);
var help = function () {
alert("There is no help");
}
var editor = new Markdown.Editor(converter, "-" + editorUniqueId, {
handler: help
});
var $wmdInput = iElement.find('#wmd-input-' + editorUniqueId);
var init = false;
editor.hooks.chain("onPreviewRefresh", function () {
var val = $wmdInput.val();
if (init && val !== ngModel.$modelValue ) {
$timeout(function(){
scope.$apply(function(){
ngModel.$setViewValue(val);
ngModel.$render();
});
});
}
});
ngModel.$formatters.push(function(value){
init = true;
$wmdInput.val(value);
editor.refreshPreview();
return value;
});
editor.run();
}
}
});
You can change this:
scope.$watch(attrs.ngModel, function () {
var val = scope.$eval(attrs.ngModel);
For this:
scope.$watch(attrs.ngModel, function(newValue, oldValue) {
var val = newValue;
});
Additionally can try commenting this code out:
if (val !== undefined) {
$wmdInput.val(val);
...
}
I think it may be associated with the odd behavior.
It might not be the answer, but all the problem occurs when you start using Markdown.Editor which does not gives you a lot of benefits.
Of course, you need to use it for markdown editor beginners, but when use markdown, they are already not beginners anyway(I might be wrong).
What I approached to this problem was to make fully working version of this without using editor.
It has preview also.
It's also very simple.
https://github.com/allenhwkim/wiki
---- edit ----
removed
---- edit ----
removed
---- edit ----
To provide a fully working editor, after few hours of trial and asking questions, the following is the simplest I can get.
This does require any $watch nor $formatters. It just wraps the given element with all attributes given by the textarea.
http://plnkr.co/edit/jeZ5EdLwOfwo6HzcTAOR?p=preview
app.directive('pagedownEditor', function($compile, $timeout) {
var num=0;
return {
priority: 1001, //higher than ng-repeat, 1000
link: function(scope, el, attrs) {
var uniqNum = scope.$index || num++;
var wmdPanel = document.createElement('div');
wmdPanel.className = "wmd-panel";
var wmdButtonBar = document.createElement('div');
wmdButtonBar.id = 'wmd-button-bar-'+uniqNum;
wmdPanel.appendChild(wmdButtonBar);
el.wrap(wmdPanel); // el is ng-repeat comment, it takes tim
var converter = Markdown.getSanitizingConverter();
var editor = new Markdown.Editor(converter, "-"+uniqNum);
$timeout(function() {
wmdPanel.querySelector('textarea').id = 'wmd-input-'+uniqNum;
wmdPanel.querySelector('textarea').className += ' wmd-input';
wmdPanel.insertAdjacentHTML('afterend', '<div id="wmd-preview-'+uniqNum+'" '
+'class="pagedown-preview wmd-panel wmd-preview">');
editor.run()
}, 50);
}
};
Demo: http://plnkr.co/edit/FyywJS?p=preview
Summary
I removed keyup and added a hook on onPreviewRefresh to ensure clicking on toolbar will properly update ng-model.
Functions on $rootScope will demonstrate the ability to update ng-model from outside of pagedown.
save functionality purely depends on your choice, since you can access ng-model anywhere now.