Dynamically manipulating DOM element in AngularJS - angularjs

I have a requirement to render all the posts from my database by using AngularJS. I need to provide Edit functionality for each post that is visible to the user. Currently, I am doing this by using a 'edit-post' directive. Here is the linking function for that.
link: function ($scope, element, attrs) {
element.bind('click', function () {
var divId = $scope.$parent.post.meta.id + "Data";
var html = $compile("<div class='editTextAreaDiv' id='" + divId + "'>" +
"<textarea class='editTextArea' id='editBox' rows='3' ng-model='editedPostText' name='editedPostText'>" + $scope.$parent.post.meta.data + "</textarea><br />" +
"<span class='pull-right'>" +
"<input class='btn' type='button' value='Save' ng-click='saveEditedPost(\"" + divId + "\")'/>" +
"<input class='btn' type='button' value='Cancel' ng-click='cancelEdit(\"" + divId + "\")'/>" +
"</span>" +
"</div>")($scope);
$("#" + divId).html(html);
});
}
I am manipulating DOM by dynamically adding a textarea and 2 buttons.
My question is whether this approach of dynamically manipulating DOM elements is preferable in Angular world. Or should I go for some other approach (like using ng-show/ng-hide directives to show/hide the textarea and 2 buttons).
Note: I preferred not to use ng-show/ng-hide since I didn't want to introduce an extra textarea and 2 buttons for every post.
Please guide me regarding this.

I think you should use Directives.
putting your HTML inside the javascript is bad, and breaks the idea behind angular which aims to separate the logic from the view, and keep your objects lossly-coupled.
see similar question here: HTML template in AngularJS like KnockoutJS

Related

Using angularjs dropDown directive multiple times in a page, How to close directive when click of another and getting values of that particular

I'm very much new to angular. Making a dropDown directive for selecting projects on each and every entries on listings of my website. Well I've just started.
app.directive('dropDown', function () {
return {
restrict: 'AE',
scope: {},
template: '<div class="btn-group">\n' +
' <button type="button" class="btn btn-sm btn-custom btn-default" ng-click="btnClick()"> Hey'+
' </button>' +
' <div ng-show="divFlag">' +
' Directive Content' +
' </div> ' +
' </div>'link: function ($scope, elem, attr) {
$scope.divFlag = false;
$scope.btnClick = function () {
$scope.divFlag = !$scope.divFlag;
}
}
};
});
with this, when I click on a button, it will show the <div> but when clicking on another, first one won't get collapse still second shows. Though my website will contain many like this but I've used just two for now.
<drop-down></drop-down>
<drop-down></drop-down>
I want them to expand only one at the time. in a click of another, expanded one must be collapsed. Please help me guys. This is quit awkward to me.
They are under new and isolate scope which means each directive works on its own.
You need to share something between the directives to control when to show and hide.
You need to write a parent directive that controls what is shown and hidden. Child directive doesn't take on the responsbility of hiding/showing its siblings.
1) look into directive inheritance
2) create a parent directive with show/hide functions which are accessed via child
3) on click parent directive sets some property on that child directive to show/hide then calls oppoosite on other directives (that weren't clicked, some kind of a loop to set all hidden and one to show)
I wont write code since you're learning but if you can't figure it out create a plunker and I will update it with working code.
the same can be done without parent directive and by simply using a controller with a function show/hide that loops through child properties and sets show/hide.

How to use ng-transclude to extend md-autocomplete? [Illegal use of ngTransclude directive in the template!]

I'm trying to extend md-autocomplete by wrapping it in my own directive like this:
//..other directive code
template: '<div><md-autocomplete ng-transclude=""></md-autocomplete></div>';
transclude: true;
So hopefully, I can use this HTML:
<my-custom-autocomplete>
<span>{{item.name}}</span>
</my-custom-autocomplete>
So what I'm basically after is that when <my-custom-autocomplete> gets rendered the final code becomes something like this:
<my-custom-autocomplete><div><md-autocomplete><span>{{item.name}}</span></md-autocomplete></div></my-custom-autocomplete>
i.e. the html code inside <my-custom-autocomplete> will get transcluded in between the <md-autocomplete> directive.
Unfortunately this does not work and I keep getting this error:
angular.js:13550 Error: [ngTransclude:orphan] Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found. Element: <md-autocomplete ng-transclude="" tabindex="-1" class="ng-isolate-scope">
My questions are:
What am I doing wrong?
How can I include the HTML from my custom directive and place it between <md-autocomplete> before rendering?
I ran into this same problem, and after a lot of tinkering, I have found a working solution. The md-autocomplete uses template manipulation instead of ng-transclusion to set the inner content. No matter where you put your transclusion tags, autocomplete will strip them off as it completely rewrites your html into the auto-complete html.
The solution turned out to be not relying on transclusion, but instead using the same type of template manipulation that Angular itself uses.
Here is a fiddle of a working solution: http://jsfiddle.net/Pluisjen/h3n1ch0f/
And here is the crux to having the option to overriding the md-item-template:
template: function(elem, attr) {
getItemTemplateHtml = function() { return elem.find('md-item-template').html() || 'Default by directive' }
return '<md-autocomplete layout-margin' +
'md-selected-item="first"' +
'md-items="i in firstOptions"' +
'md-min-length="0"' +
'md-item-text="i"' +
'ng-transclude' +
'placeholder="First">' +
'<md-item-template>' + getItemTemplateHtml() + '</md-item-template>' +
'</md-autocomplete>'; },

Angular: add ng-click inside directive

In my directive i using appendTo and ng-click inside it:
$("<div>" + "<a href='' ng-click='addIt()' + user + "</a></div>").appendTo '.user-class'
And ng-click doesn't work. Should i use $compile? How to use it in that case?
this code is a part of directive that make dropdown menu from json. There is no option to move this to directive template, so need to find solution how to make 'ng-click' works here.
Like Matthew, I am most curious as to which part(s) cannot be moved out into an external template / internal template function. It doesn't make much sense to me to have to re $compile a directive only to add a ng-click handler.
The senseful way would be to add the ng-click directive to a template. Either an external one, or a function returning a string value in the directive definition object.
Running $compile after the link step has finished is not a good idea performance wise, seeing as how it recompiles the entire DOM element your directive is attached to. Avoid doing so for as long as you possibly can.
.directive('myDir', function () {
return {
template: function (/*element, attrs*/) {
return '<div>{{::user}}</div>';
},
link: function (scope, el, attrs) {
scope.user = 'John';
}
};
});
This would result in the following DOM structure:
<div>
John
</div>
jsbin
Yes you will need $compile , inject $compile in directive and you can do it like this :
var link = $compile("<div>" + "<a href='' ng-click='addIt()'" + user + "</a></div>")(scope);
link.appendto('.user-class');

Angularjs - Generate a secure read only view or editable view [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 4 years ago.
Improve this question
I've been searching but i might have been looking for the wrong terms.
Anyway, what i want to do is generate different elements based on if the user is authorized or not (read-only mode).
For example, if the user is not authorize i want to generate a label with the data. If instead the user is authorized i want to generate a textbox / textarea etc.. that they can edit. Are there any built in directives for this type of generation or do i need to create a custom directive?
Looking at angulars site, http://docs.angularjs.org/api/ng/directive/ngReadonly
It almost has the functionality except that i want to switch the textbox to a label instead.
A note on security, the users should not be able to change an attribute in the html to activate the control.
You could use ng-if
i.e.
<div>
<input type="text" ng-if='user.admin' ng-model="myModel" />
<span ng-if='!user.admin'>{{ myModel }}</span>
</div>
It requires a bit more html. If there is another way I would love to hear about it because I use ng-if in my companies role-based order system I wrote.
Edit
Okay, so I wrote a quick little Directive that still needs some more work.
The HTML (Please note: there are no {{ curly braces }} for scope objects):
<auth-input can-edit="admin" obj="input" model="authTest" obj-class="form-control" type="text">
</auth-input>
can-edit - admin (boolean) is coming from the scope, i.e. $scope.admin=true; Using {{ admin }} will throw an error.
obj - The type of object you want if admin = true (currently only supports input and textarea).
model - reference to the ng-model you want to bind to.
obj-class - The class attribute, in case you are using a framework like bootstrap, foundation, etc.
Current Input element supported attributes (more need to be supported):
type
id
Directive:
angular.module('authInputs', []).directive('authInput', function ($compile) {
var inputTemplate = '';
var textTemplate = '';
var textareaTemplate = '';
var getTemplate = function (objType, editable) {
var template = '';
switch (objType) {
case 'input':
template = (editable) ? inputTemplate : textTemplate;
break;
case 'textarea':
template = (editable) ? textareaTemplate : textTemplate;
break;
}
return template;
};
var setupTemplates = function (attrs) {
// Need to build in more flexibility for other input tags, and make this part better.
// Will eventually have an array of accepted attribute names and if attrs contains a matching key
// then inject it by building the input element in the for (var k in attrs) loop.
// Also need to do the same for other element types.
inputTemplate = (attrs.id) ? '<input type="' + attrs.type + '" id="' + attrs.id + '" ng-model="model" class="' + attrs.objClass + '" />' : '<input type="' + attrs.type + '" ng-model="model" class="' + attrs.objClass + '" />';
textareaTemplate = (attrs.id) ? '<textarea ng-model="model" id="' + attrs.id + '" class="' + attrs.objClass + '">{{ model }}</textarea>' : '<textarea ng-model="model" class="' + attrs.objClass + '">{{ model }}</textarea>';
textTemplate = '{{ model }}';
};
return {
restrict: "E",
replace: true,
link: function (scope, element, attrs) {
setupTemplates(attrs);
scope.$watch('watch', function () {
element.html(getTemplate(attrs.obj, Boolean(scope.$parent[attrs.canEdit])));
$compile(element.contents())(scope);
});
},
scope: {
model: '=model',
watch: '=canEdit'
}
};
});
Then just inject it as you would other modules:
var app = angular.module('app', ['authInputs']);
Feel free to make it better, I have a lot more to do with it but little time at the moment. I will try to spend some time on it soon and update this answer and my fiddle link.
Demo with binding and toggle
The AngularFire Seed uses NG-Show-Auth (located here: https://github.com/firebase/angularFire-seed/blob/master/app/index.html) to determine visibility of elementes, not sure if this is what you're looking for.
Once you decide to use that you could maybe use a (my brain is slipping on the name at the moment) but a custom HTML element I.E.
<name></name>
and then in the text for the custom element, it will contain both the ng-show-auth true and false cases.
<input ng-show-auth=true></input>
<p ng-show-auth=false>{{text}}</p>
This will allow you to read the code a little easier (when you're reviewing it) and keep that particular implementation (sort of) out of the actual HTML.

Google pagedown AngularJS directive

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.

Resources