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

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.

Related

ngTagsInput with autocomplete in a form and add to db

I've been trying to use ngTagsInput with its Autocomplete feature (mbenford.github.io/ngTagsInput/demos) in a html form to submit recipes in Mongodb.
Basically, I'm using ngTagsInput with Autocomplete to query my ingredients db and display ingredients tags in the 'Ingredients in the recipe'.
It works fine, up until I save the recipe, and the ingredients are not saved.
I know where the problem is but I haven't found a solution yet.
Here is the 'ingredients' field of my add recipe page without ngTagsInput, just a normal text field:
<div class="form-group">
<label for="ingredients">List of ingredients</label>
<input type="text" class="form-control" id="ingredients" ng-model="form.ingredients">
</div>
And here is the 'ingredients' field using ngTagsInput (Working fine, but not saving):
<div class="form-group" ng-controller="recipeApiController">
<tags-input for="Ingredients" id="Ingredients" ng-model="tags" display-property="ingredientID" placeholder="Commence à taper le nom d'un ingrédient..." add-from-autocomplete-only="true">
<auto-complete source="loadTags($query)"></auto-complete>
</tags-input>
</div>
Because I'm replacing ng-model="form.ingredients" with ng-model="tags" required to use ngTagsInput, those ingredient tags are not saved when clicking my "Add recipe" button.
Here is the "save to db" part of my recipeApiController, used on the "add recipe" form page:
$scope.addToDatabase = function(){
RecipeApi.Recipe.save({}, $scope.form,
function(data){
$scope.recipe.push(data);
},
function(err){
bootbox.alert('Error: ' + err);
});
}
Do you have any idea how I could fix that, and save those tags?
Thanks in advance guys. I didn't want this post to be too long but if you need more info, code, I'll be super reactive to provide it. This would help me greatly.
I found a solution in another post:
https://stackoverflow.com/a/38383917/6858949
Basically, I couldn't get those tags to save because ng-model didn't work inside the <tags-input> tags. I therefore used this guy's directive to change the <tags-input> to an attribute:
<div elem-as-attr="tags-input"></div>
Here's the directive code:
app.directive('elemAsAttr', function($compile) {
return {
restrict: 'A',
require: '?ngModel',
replace: true,
scope: true,
compile: function(tElement, tAttrs) {
return function($scope) {
var attrs = tElement[0].attributes;
var attrsText = '';
for (var i=0; i < attrs.length; i++) {
var attr = attrs.item(i);
if (attr.nodeName === "elem-as-attr") {
continue;
}
attrsText += " " + attr.nodeName + "='" + attr.nodeValue + "'";
}
var hasModel = $(tElement)[0].hasAttribute("ng-model");
var innerHtml = $(tElement)[0].innerHTML;
var html = '<' + tAttrs.elemAsAttr + attrsText + '>' + innerHtml + '</' + tAttrs.elemAsAttr + '>';
var e = hasModel ? $compile(html)($scope) : html;
$(tElement).replaceWith(e);
};
}
}
});
I don't think this is optimal, but with my current knowledge of code, I'm thankful I found this solution. ✌🏼
EDIT:
I am now using ui-select: https://github.com/angular-ui/ui-select
And definitely recommend it
EDIT:
I put the code in the code box

<option> ele dynamically added in directive's link function not firing parent select's ngChange

I need to add <option> elements dynamically based on certain property values of the source data populating the dropdown list. Sometimes the child elements need to instead be <optgroup> elements due to the crazy way marketing wants data displayed. Due to the requirements, I cannot do simple statements with ng-options nor can I do a parent <select> with a child element combined with ng-repeat. I run into various issues in my directive trying to use ng-switch or ng-if to get the desired result.
Because of those issues I decided to put the entire select element as the template in my directive. Then in the link function I would parse out all the incoming data and append the correctly formatted <option> or <optgroup> elements to the parent <select> defined in the template. I started with the scaled down code in my directive below and was going to expand upon it with my conditional logic when I noticed changing the selected option in the UI was not firing the parent select's ng-change function in my controller. The ng-change does fire when I let Angular add all the options itself with ng-options or when I use child option element with ng-repeat. However, breaking it out the way shown below with a minimal parent defined in the template and then dynamically adding child elements in link does not work.
app.directive('fullSelect', function ($compile) {
return {
restrict: 'E',
scope: { datarepo: "=datarepo" },
replace: true,
template: "<select class='col-xs-8' id='gridStyle' " +
'ng-model="vm.gridStyle" ng-change="vm.gridStyleUpdated()"></select>',
link: function (scope, element, attrs) {
angular.forEach(scope.datarepo, function (value, key) {
var opt = angular.element('<option value="' + value.value + '">' + value.label + '</option>');
element.append($compile(opt)(scope));
});
}
}
});
I additionally tried adding an ng-click to each of the newly added <option> elements during the forEach loop, but even those do not fire. I assume this is all some sort of scope/visibility issue.
Thanks in advance for any guidance.
please try this:
link: function (scope, element, attrs) {
scope.vm = {};
scope.vm.gridStyleUpdated = function () {
console.log("changed");
}
angular.forEach(scope.datarepo, function (value, key) {
var opt = angular.element('<option value="' + value.value + '">' + value.label + '</option>');
element.append($compile(opt)(scope));
});
}
I finally got what I wanted by breaking up my directive a little and putting a big chunk of it back into the html view. Now the events are correctly firing and I also get additional benefit of nested option and optgroup elements to any level which is something that started this whole cycle. Here is what the view html looks like now:
<fieldset id="fs_gridStyle">
<label class="col-xs-4" for="gridStyle">Grid Style *</label>
<select class="col-xs-8" id="gridStyle"
ng-model="vm.gridStyle"
ng-change="vm.gridStyleUpdated()"
full-select datarepo="vm.gridStyles">
</select>
</fieldset>
And here is what the directive looks like:
app.directive('fullSelect', function ($compile) {
return {
restrict: 'A',
scope: { datarepo: "=datarepo" },
replace: true,
link: function (scope, element, attrs) {
angular.forEach(scope.datarepo, function (value, key) {
var opt;
var display = "";
for (var idx = 0; idx < value.level; idx++) {
display += " ";
}
display += value.label;
if (value.type === "Option") {
opt = angular.element('<option value="' + value.value + '">' + display + '</option>');
}
else {
opt = angular.element('<optgroup label="' + display + '"></optgroup>');
}
element.append($compile(opt)(scope));
});
}
}
});
Now I just need to make sure it performs properly, but so far it looks good. It gives me my desired end result where my incoming object array contains values, labels and misc info such as a level property to indicate where that item should be indented within the dropdown list, etc. I can now have any number of option and optgroup children embedded within the dropdown list indented as needed. You cannot nest multiple optgroup elements, but I visually handle that with the level property which adds spaces to the text.

sanitize text input from textangular

I'm working with textangular as a rich text solution for a project I am working on.
It is required to sanitize this input because we only allow certain html tags.
Since this is not a default possibility of textangular I created a directive that wraps around the textangular directive. I can successfully sanitize the input text, but the view is never updated, and I have ran out of ideas how to achieve this
directive:
.directive('chTextAngular', function(){
return {
restrict: 'E',
require: 'ngModel',
template: function(elements, attributes) {
return '<div text-angular ng-model="' + attributes.ngModel + '" id="' + attributes.id + '"></div>';
},
link: function(scope, element, attributes, ngModel) {
scope.$watch(attributes.ngModel, function(n, o) {
if(n !== undefined) {
// Replace all the divs with <br> tags
var sanitized = ngModel.$viewValue.replace(/<div>/gm, '<br>').replace(/<\/div>/gm, ' ');
sanitized = sanitized.replace(/<p>/gm, '').replace(/<\/p>/gm, '<br><br>');
sanitized = $.htmlClean(sanitized, {
allowedTags: ["strong", "em", "ul", "ol", "li", "a", "b", "i", "br"],
allowedAttributes: ["a", "href"]
});
console.log(sanitized);
ngModel.$setViewValue(sanitized);
}
});
}
}
});
The log prints out the model and it shows that it is actually change. I just can not figure out how to update the actual textarea in textangular.
Can anyone help me with this, or put me in the right direction?
Ok, first question I have to ask is; do you really need the view to update or can it be fine as it is if the model is the correct data.
Second, you should probably be registering your function via ngModel.$parsers.push(function(n,o){...}) to avoid extra watchers.
The reason it's not updating is that we have a catch in textAngular to prevent the model from updating the view while someone is typing. If you do this while the field is focussed then you get an issue with the cursor moving back to the start/end of the text field. If you do want to update the view from the model then register an blur event handler (element.on('blur', ...);) and call scope.updateTaBindtaTextElement() may work depending on which scope your directive attaches to. If that function doesn't work you'll have to add a name attribute to your textAngular element and then use the textAngularManager.refreshEditor(name); function.

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.

Dynamically manipulating DOM element in 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

Resources