$setViewValue with object copy performance - angularjs

I have a directive for a toggle button derived from the btnRadio of Angular UI. Instead of just using a simple model with toggled values I have modified it to accept a model which is a "global" object used by all the directive instances - I pass the key to the directive in order to extract the particular data. In my directive I search through this object and handle it from there. All works great but I have a performance concern. In the directive I have to make a copy of the model (per Angular docs) and reapply that to my view using ngModelController.$setViewValue. This model can contain 100's of entries. And this directive can appear a few dozen times. Is there a potential for some memory leak if I create copies of the this large object in every directive?
Here's the directive:
app.directive('btnChoice', function () {
return {
require: ['btnChoice', 'ngModel'],
controller: 'ButtonsController',
link: function (scope, element, attrs, ctrls) {
var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
function getChoice() {
var val = scope.$eval(attrs.btnValue);
console.log(val);
return (angular.isDefined(val) && ngModelCtrl.$modelValue && ngModelCtrl.$modelValue[val]) ? ngModelCtrl.$modelValue[val] : false;
}
//model -> UI
ngModelCtrl.$render = function () {
element.toggleClass(buttonsCtrl.activeClass, angular.equals(getChoice(), attrs.btnChoice));
};
//ui->model
element.bind(buttonsCtrl.toggleEvent, function () {
if (!element.hasClass(buttonsCtrl.activeClass)) {
scope.$apply(function () {
var ev = attrs.btnEvent;
var c = attrs.btnChoice;
var v = scope.$eval(attrs.btnValue);
scope.$emit(ev, { model: v, value: c });
ngModelCtrl.$modelValue[v] = c;
// The line below worries me
ngModelCtrl.$setViewValue(angular.copy(ngModelCtrl.$modelValue););
ngModelCtrl.$render();
});
}
});
}
};
});
Here's the view:
<div class="btn-group">
<label class="btn btn-primary" ng-model="$attendances" btn-event="attend" btn-choice="yes" btn-value="item.id">Yup</label>
<label class="btn btn-primary" ng-model="$attendances" btn-event="attend" btn-choice="maybe" btn-value="item.id">Maybe</label>
<label class="btn btn-primary" ng-model="$attendances" btn-event="attend" btn-choice="no" btn-value="item.id">Nope</label>
</div>
The model looks like:
var model = {
'7612tas337213': 'yes',
'7q23423h1237s': 'maybe
}

Related

popover functionality lost once another popover is accessed

I have multiple popovers on a page. some have are multiples of the same popover (directive/controller) with different ng-model passed. Others are completely different. when I open are p1 it displays and functions as it should. once I open p2, p2 operates as expected and p1 displays the correct information but can not use any of the controls in p1. when I open p3, p3 operates as expected and p1 and p2 displays the correct information but can not use any of the controls in p1,p2.
The controls are a mixture of toggle buttons, sliders, drop downs.
button that displays the popover
<button href="#" spacingpopover
type="button"
class="btn btn-success btn-spacing"
data-toggle="popover"
title="Padding & Margins"
id="button-spacing-popover-{{$id}}"
ng-model="model.value.spacing"
style="width:100%"
class-id="button-spacing"
class-name="button">
Padding & Margins
</button>
directive
app.directive('colorpopover', function ($compile, $templateCache, $q, $http) {
var getTemplate = function () {
var def = $q.defer();
var template = '';
template = $templateCache.get('color.html');
if (typeof template === "undefined") {
$http.get("/App_Plugins/IBD.ButtonStyles/Popovers/IBD.Color.Popover.html").then(function (data) {
$templateCache.put("color.html", data);
def.resolve(data);
});
} else {
def.resolve(template);
}
return def.promise;
}
return {
restrict: "A",
require: 'ngModel', // ensures the model is passed in
scope: { model: '=ngModel' }, //ties the ng-model to the scope of the popover
controller: 'ColorPopoverCtrl',
link: function (scope, element, attrs, model) {
getTemplate().then(function (popOverContent) {
// Make sure to remove any popover before hand (please confirm the method)
scope.colorPalette = scope.$parent.colorPalette;
scope.units = scope.$parent.units;
scope.classId = attrs.classId;
scope.className = attrs.className;
var compileContent = $compile(popOverContent.data)(scope);
var options = {
placement: "left",
html: true,
content: compileContent,
container: '.ibd-cms'
};
$(element).popover(options);
..........
}
controller
app.controller('ColorPopoverCtrl', function ($scope) {
$scope.adjustOpacity = function (node) {
node.opacity = node.opacity;
node.rgba.a = node.opacity;
console.log($scope)
console.log($scope.model.value)
}
.............
}
Followed this example https://embed.plnkr.co/plunk/MkgGyB
looks like the key is having the controller inside the directive.

Create Hoverable popover using angular-ui-bootstrap

I have the following code for creating a popover in my template file:
<span class="icon-globe visibility"
id="visibilityFor{{post.metaData.assetId}}"
popover="{{post.visibilityListStr}}"
popover-placement="right"
popover-trigger="mouseenter"
popover-popup-delay="50"
visibility>
</span>
I have a few clickable links on the popover. But the problem is I'm not able to hover on the popover created. I referred to the link http://jsfiddle.net/xZxkq/
and tried to create a directive viz. 'visibility' for this purpose.
Here is the code:
myAppModule.directive("visibility", function ($timeout,$rootScope) {
return {
controller: function ($scope, $element) {
$scope.attachEvents = function (element) {
$('.popover').on('mouseenter', function () {
$rootScope.insidePopover = true;
});
$('.popover').on('mouseleave', function () {
$rootScope.insidePopover = false;
$(element).popover('hide');
});
}
},
link: function (scope, element, attrs) {
$rootScope.insidePopover = false;
element.bind('mouseenter', function (e) {
$timeout(function () {
if (!$rootScope.insidePopover) {
element.popover('show');
attachEvents(element);
}
}, 200);
});
element.bind('mouseout', function (e) {
$timeout(function () {
if (!$rootScope.insidePopover) {
element.popover('show');
attachEvents(element);
}
}, 200);
});
}
}
});
But I get an exception for 'element.popover' since it is undefined. Please point as to what I'm doing wrong and how can I show/hide the angular ui popover from the directive. I am using angular ui bootstrap JS file.
I have solved it in a very cleaned way and thought to share it:
.popover is being created not as a child of the uib-popover
so the idea is to wrap uib-popover with a parent and to control show&hide on hovering the parent.
.popover and uib-popover are children of this parent
so just left to set popover-trigger=none and you have what you are wishing for.
I created a plunk example:
<span ng-init="popoverOpened=false" ng-mouseover="popoverOpened=true" ng-mouseleave="popoverOpened=false">
<button class="btn btn-default" uib-popover-html="htmlPopover"
popover-trigger="none" popover-placement="bottom-left" popover-is-open="popoverOpened" >
<span>hover me</span>
</button>
</span>
enjoy.
I don't know if this is relevant to the OP anymore, but I've had the same problem and fortunately I managed to solve it.
Undefined error
First thing first, the undefined error you are getting might be (at least in my case) because you are using the development version of ui-bootstrap. In my case I got this error when trying to bind element.popover. After adding the minified version of the library the error went away.
Keep the popover open when hovering over it
To do this I have created a custom directive that makes use of the popover from the ui-bootstrap library.
Directive
app.directive('hoverPopover', function ($compile, $templateCache, $timeout, $rootScope) {
var getTemplate = function (contentType) {
return $templateCache.get('popoverTemplate.html');
};
return {
restrict: 'A',
link: function (scope, element, attrs) {
var content = getTemplate();
$rootScope.insidePopover = false;
$(element).popover({
content: content,
placement: 'top',
html: true
});
$(element).bind('mouseenter', function (e) {
$timeout(function () {
if (!$rootScope.insidePopover) {
$(element).popover('show');
scope.attachEvents(element);
}
}, 200);
});
$(element).bind('mouseleave', function (e) {
$timeout(function () {
if (!$rootScope.insidePopover)
$(element).popover('hide');
}, 400);
});
},
controller: function ($scope, $element) {
$scope.attachEvents = function (element) {
$('.popover').on('mouseenter', function () {
$rootScope.insidePopover = true;
});
$('.popover').on('mouseleave', function () {
$rootScope.insidePopover = false;
$(element).popover('hide');
});
}
}
};
});
This directive also accepts a custom template for the popover, so you are not limited to just title and some text in it. You can create your own html template and feed it to the control.
Usage
<a href="#" hover-popover>Click here</a>
Hopes this helps someone else in the future :)
Edit
As requested, here is a Fiddle link. It lacks the styling, but it should demonstrate the way it works.
There I spend 1 day and finally get solution.
<button uib-popover="{{dynamicPopover.content}}"
popover-trigger="outsideClick" popover-is-open="popoverIsOpen"
ng-mouseenter="popoverIsOpen = !popoverIsOpen"
popover-title="{{dynamicPopover.title}}" type="button" class="btn btn-default">Dynamic Popover</button>
Please check
Plunkeer Link
Check only Dynamic Popover button code
Thanks,
I think Cosmin has the hoverable popover right, but it does seem to be using the Twitter Bootstrap popover method. The idea is to have this hoverable popover implemented only with AngularJS and one of the Bootstrap wrappers for AngularJS, which are UI Bootstrap or AngularStrap.
So I have put together an implementation which uses only AngularStrap:
myApp.directive('hoverablePopover', function ($rootScope, $timeout, $popover) {
return {
restrict: "A",
link: function (scope, element, attrs) {
element.bind('mouseenter', function (e) {
$timeout(function () {
if (!scope.insidePopover) {
scope.popover.show();
scope.attachEventsToPopoverContent();
}
}, 200);
});
element.bind('mouseout', function (e) {
$timeout(function () {
if (!scope.insidePopover) {
scope.popover.hide();
}
}, 400);
});
},
controller: function ($scope, $element, $attrs) {
//The $attrs will server as the options to the $popover.
//We also need to pass the scope so that scope expressions are supported in the popover attributes
//like title and content.
$attrs.scope = $scope;
var popover = $popover($element, $attrs);
$scope.popover = popover;
$scope.insidePopover = false;
$scope.attachEventsToPopoverContent = function () {
$($scope.popover.$element).on('mouseenter', function () {
$scope.insidePopover = true;
});
$($scope.popover.$element).on('mouseleave', function () {
$scope.insidePopover = false;
$scope.popover.hide();
});
};
}
};
});
When you have a popover element, you need to take into account that you have the element that triggers the popover and you also have the element with the actual popover content.
The idea is to keep the popover open when you mouse over the element with the actual popover content. In the case of my directive, the link function takes care of the element that triggers the popover and attaches the mouseenter/mouseout event handlers.
The controller takes care of setting the scope and the popover itself via the AngularStrap $popover service. The controller adds the popover object returned by the AngularStrap service on the scope so that it is available in the link function. It also adds a method attachEventsToPopoverContent, which attaches the mouseenter/mouseout events to the element with the popover content.
The usage of this directive is like this:
<a title="Popover Title" data-placement="left" data-trigger="manual" data-content="{{someScopeObject}}" content-template="idOfTemplateInTemplateCache" hoverablePopover="">
You have to put the trigger in single quotes, because, reasons:
<button uib-popover="I appeared on mouse enter!" popover-trigger="'mouseenter'" type="button" class="btn btn-default">Mouseenter</button>
demo:
https://jsbin.com/fuwarekeza/1/edit?html,output
directive:
myAppModule.directive('popoverHoverable', ['$timeout', '$document', function ($timeout, $document) {
return {
restrict: 'A',
scope: {
popoverHoverable: '=',
popoverIsOpen: '='
},
link: function(scope, element, attrs) {
scope.insidePopover = false;
scope.$watch('insidePopover', function (insidePopover) {
togglePopover(insidePopover);
})
scope.$watch('popoverIsOpen', function (popoverIsOpen) {
scope.insidePopover = popoverIsOpen;
})
function togglePopover (isInsidePopover) {
$timeout.cancel(togglePopover.$timer);
togglePopover.$timer = $timeout(function () {
if (isInsidePopover) {
showPopover();
} else {
hidePopover();
}
}, 100)
}
function showPopover () {
if (scope.popoverIsOpen) {
return;
}
$(element[0]).click();
}
function hidePopover () {
scope.popoverIsOpen = false;
}
$(document).bind('mouseover', function (e) {
var target = e.target;
if (inside(target)) {
scope.insidePopover = true;
scope.$digest();
}
})
$(document).bind('mouseout', function (e) {
var target = e.target;
if (inside(target)) {
scope.insidePopover = false;
scope.$digest();
}
})
scope.$on('$destroy', function () {
$(document).unbind('mouseenter');
$(document).unbind('mouseout');
})
function inside (target) {
return insideTrigger(target) || insidePopover(target);
}
function insideTrigger (target) {
return element[0].contains(target);
}
function insidePopover (target) {
var isIn = false;
var popovers = $('.popover-inner');
for (var i = 0, len = popovers.length; i < len; i++) {
if (popovers[i].contains(target)) {
isIn = true;
break;
}
}
return isIn;
}
}
}
}]);
html:
<span class="icon-globe visibility"
id="visibilityFor{{post.metaData.assetId}}"
popover="{{post.visibilityListStr}}"
popover-is-open="{{post.$open}}"
popover-trigger="click"
popover-hoverable="true"
visibility>
</span>
html
<span class="icon-globe" id="visibilityFor" popover="hello how are you"
popover-placement="right" popover-trigger="mouseenter"
popover-popup-delay="50" viz>
</span>
directive
myAppModule.directive('viz', function ($rootScope,$timeout){
return{
restrict:"A",
link: function (scope, element, attrs) {
$rootScope.insidePopover = false;
element.bind('mouseenter', function (e) {
$timeout(function () {
if (!$rootScope.insidePopover) {
element.popover('show');
// attachEvents(element);
}
}, 200);
});
element.bind('mouseout', function (e) {
$timeout(function () {
if (!$rootScope.insidePopover) {
element.popover('show');
// attachEvents(element);
}
}, 200);
});
}
}
});
Note : - Don't forget to include angular-strap after jQuery.js & angular.js
This feature was added in Angular UI Bootstrap 0.14.0 and is documented here. Disable the triggers and use the popover-is-open property to manually dictate the opened/closed state.
What I did that gets my by in 0.13.X is to set the element to be hoverable to a <button> and then set the popover-trigger="focus". Then style the button how you wish, and focus the button by clicking it. You can hover in the popover and click a link, all I need to do.
Easiest way to have a mouse-event using uib-popover
Look at the below working example !
You need not have a uib-tabset, I faced an issue with uib-tabset and so added that example.
<uib-tabset>
<uib-tab>
<uib-tab-heading>
Tab 1
</uib-tab-heading>
<div>
<span ng-mouseover="popoverIsOpen = true"
ng-mouseleave="popoverIsOpen = false">
<button uib-popover-template="'includeFile.html'"
popover-trigger="outsideClick"
popover-is-open="popoverIsOpen"
popover-placement="right"
type="button" class="btn btn-default">
Dynamic Popover
</button>
</span>
</div>
<p> tab 1</p>
</uib-tab>
<uib-tab>
<uib-tab-heading>
Tab 2
</uib-tab-heading>
<p> tab 2</p>
</uib-tab>
</uib-tabset>
Template: includeFile.html
<div>
<span>This is for tesitng</span>
<strong> www.google.com</strong>
</div>
I needed to do this as well. I have a checkbox in a table cell that can have 3 possible states: Enabled, Disabled, or Special case. The UI spec I have asked for a popover over the box that shows either of those statuses, or for the special case a sentence with a link.
I tried several of these solutions and one of them worked for me, and they all added extra code. After some playing around, I determined I could just add the "popover-popup-close-delay" attribute with a dynamic value. So this works for me:
<td uib-popover-html="getPopoverTxt()" popover-popup-close-delay="{{ele.isspecial ? 2000 : 300}}" popover-popup-delay="300" popover-append-to-body="true" popover-placement="top" popover-trigger="mouseenter">
<input id="issynced{{ele.id}}" name="isChecked" type="checkbox" data-ng-checked="ele.ischecked" data-ng-model="ele.ischecked">
<label for="issynced{{ele.id}}"></label>
</td>
Some context: My table is looping over an array of data objects, so ele is a single object. The getPopoverTxt() is just a simple method in my controller that returns one of the 3 labels I want to show ("Enabled", "Disabled", or "Special Text with HTML"). Its not necessary here, but the takeaway is to get the HTML to work, you have to wrap the string value in $sce.trustAsHtml(), like:
var specialText = $sce.trustAsHtml('Text with a link to contact support');
The rest is all the usual popover and form input settings we normally use. The "popover-popup-close-delay" is the key.

Angular : how to re-render compiled template after model update?

I am working on an angular form builder which generate a json.
Everything works fine except one thing.
You can find an example here : http://jsfiddle.net/dJRS5/8/
HTML :
<div ng-app='app'>
<div class='formBuilderWrapper' id='builderDiv' ng-controller="FormBuilderCtrl" >
<div class='configArea' data-ng-controller="elementDrag">
<h2>drag/drop</h2>
<form name="form" novalidate class='editBloc'>
<div data-ng-repeat="field in fields" class='inputEdit'>
<data-ng-switch on="field.type">
<div class='labelOrder' ng-class='{column : !$last}' drag="$index" dragStyle="columnDrag" drop="$index" dropStyle="columnDrop">{{field.type}}
</div>
<label for="{{field.name}}" data-ng-bind-html-unsafe="field.caption"></label>
<input data-ng-switch-when="Text" type="text" placeholder="{{field.placeholder}}" data-ng-model="field.value" />
<p data-ng-switch-when="Text/paragraph" data-ng-model="field.value" data-ng-bind-html-unsafe="field.paragraph"></p>
<span data-ng-switch-when="Yes/no question">
<p data-ng-bind-html-unsafe="field.yesNoQuestion"></p>
<input type='radio' name="yesNoQuestion" id="yesNoQuestion_yes" value="yesNoQuestion_yes" />
<label for="yesNoQuestion_yes">Oui</label>
<input type='radio' name="yesNoQuestion" id="yesNoQuestion_no" value="yesNoQuestion_no"/>
<label for="yesNoQuestion_no">Non</label>
</span>
<p data-ng-switch-when="Submit button" class='submit' data-ng-model="field.value">
<input value="{{field.name}}" type="submit">
</p>
</data-ng-switch>
</div>
</form>
</div>
<div id='previewArea' data-ng-controller="formWriterCtrl">
<h2>preview</h2>
<div data-ng-repeat="item in fields" content="item" class='templating-html'></div>
</div>
</div>
</div>
The JS :
var app = angular.module('app', []);
app.controller('FormBuilderCtrl', ['$scope', function ($scope){
$scope.fields = [{"type":"Text/paragraph","paragraph":"hello1"},{"type":"Yes/no question","yesNoQuestion":"following items must be hidden","yes":"yes","no":"no"},{"type":"Text/paragraph","paragraph":"hello2"},{"type":"Submit button","name":"last item"}] ;
}]);
app.controller('elementDrag', ["$scope", "$rootScope", function($scope, $rootScope, $compile) {
$rootScope.$on('dropEvent', function(evt, dragged, dropped) {
if($scope.fields[dropped].type == 'submitButton' || $scope.fields[dragged].type == 'submitButton'){
return;
}
var tempElement = $scope.fields[dragged];
$scope.fields[dragged] = $scope.fields[dropped];
$scope.fields[dropped] = tempElement;
$scope.$apply();
});
}]);
app.directive("drag", ["$rootScope", function($rootScope) {
function dragStart(evt, element, dragStyle) {
if(element.hasClass('column')){
element.addClass(dragStyle);
evt.dataTransfer.setData("id", evt.target.id);
evt.dataTransfer.effectAllowed = 'move';
}
};
function dragEnd(evt, element, dragStyle) {
element.removeClass(dragStyle);
};
return {
restrict: 'A',
link: function(scope, element, attrs) {
if(scope.$last === false){
attrs.$set('draggable', 'true');
scope.dragStyle = attrs["dragstyle"];
element.bind('dragstart', function(evt) {
$rootScope.draggedElement = scope[attrs["drag"]];
dragStart(evt, element, scope.dragStyle);
});
element.bind('dragend', function(evt) {
dragEnd(evt, element, scope.dragStyle);
});
}
}
}
}]);
app.directive("drop", ['$rootScope', function($rootScope) {
function dragEnter(evt, element, dropStyle) {
element.addClass(dropStyle);
evt.preventDefault();
};
function dragLeave(evt, element, dropStyle) {
element.removeClass(dropStyle);
};
function dragOver(evt) {
evt.preventDefault();
};
function drop(evt, element, dropStyle) {
evt.preventDefault();
element.removeClass(dropStyle);
};
return {
restrict: 'A',
link: function(scope, element, attrs) {
if(scope.$last === false){
scope.dropStyle = attrs["dropstyle"];
element.bind('dragenter', function(evt) {
dragEnter(evt, element, scope.dropStyle);
});
element.bind('dragleave', function(evt) {
dragLeave(evt, element, scope.dropStyle);
});
element.bind('dragover', dragOver);
element.bind('drop', function(evt) {
drop(evt, element, scope.dropStyle);
var dropData = scope[attrs["drop"]];
$rootScope.$broadcast('dropEvent', $rootScope.draggedElement, dropData);
});
}
}
}
}]);
app.controller('formWriterCtrl', ['$scope', function ($scope){
}]);
app.directive('templatingHtml', function ($compile) {
var previousElement;
var previousIndex;
var i=0;
var inputs = {};
var paragraphTemplate = '<p data-ng-bind-html-unsafe="content.paragraph"></p>';
var noYesQuestionTemplate = '<p data-ng-bind-html-unsafe="content.yesNoQuestion"></p><input id="a__index__yes" type="radio" name="a__index__"><label for="a__index__yes" />{{content.yes}}</label><input id="a__index__no" class="no" type="radio" name="a__index__" /><label for="a__index__no">{{content.no}}</label>';
var submitTemplate = '<p class="submit"><input value="{{content.name}}" type="submit" /></p>';
var getTemplate = function(contentType, contentReplace, contentRequired) {
var template = '';
switch(contentType) {
case 'Text/paragraph':
template = paragraphTemplate;
break;
case 'Yes/no question':
template = noYesQuestionTemplate;
break;
case 'Submit button':
template = submitTemplate;
break;
}
template = template.replace(/__index__/g, i);
return template;
}
var linker = function(scope, element, attrs) {
i++;
elementTemplate = getTemplate(scope.content.type);
element.html(elementTemplate);
if(previousElement == 'Yes/no question'){
element.children().addClass('hidden');
element.children().addClass('noYes'+previousIndex);
}
if(scope.content.type == 'Yes/no question'){
previousElement = scope.content.type;
previousIndex = i;
}
$compile(element.contents())(scope);
}
return {
restrict: "C",
link: linker,
scope:{
content:'='
}
};
});
On the example there are 2 areas :
- the first one does a ngRepeat on Json and allow to reorder items with drag and drop
- the second area also does a ngRepeat, it is a preview templated by a directive using compile function. Some elements are hidden if they are after what I called "Yes/no question"
Here is an example of Json generated by the form builder :
$scope.fields =
[{"type":"Text/paragraph","paragraph":"hello1"},{"type":"Yes/no question","yesNoQuestion":"following items must be hidden","yes":"yes","no":"no"},
{"type":"Text/paragraph","paragraph":"hello2"},{"type":"Submit button","name":"last item"}] ;
When the page load everything is ok, Hello1 is visible and Hello2 is hidden.
But when I drop Hello1 after "Yes/no question", dom elements are reorganised but Hello1 is not hidden.
I think it comes from $compile but I don't know how to resolve it.
Could you help me with this please?
Thank you
I only see you setting the 'hidden' class on the element based on that rule (after a yes/no) in the link function. That's only called once for the DOM element - when it's first created. Updating the data model doesn't re-create the element, it updates it in place. You would need a mechanism that does re-create it if you wanted to do it this way.
I see three ways you can do this:
In your linker function, listen for the same dropEvent that you listen for above. This is more efficient than you'd think (it's very fast) and you can re-evaluate whether to apply this hidden class or not.
Use something like ngIf or literally re-creating it in your collection to force the element to be recreated entirely. This is not as efficient, but sometimes is still desirable for various reasons.
If your use case is actually this simple (if this wasn't a redux of something more complicated you're trying to do) you could use CSS to do something like this. A simple rule like
.yes-no-question + .text-paragraph { display: none; }
using a sibling target could handle this directly without as much work. This is much more limited in what it can do, obviously, but it's the most efficient option if it covers what you need.

Angular.js: choosing a pre-compiled template depending on a condition

[disclaimer: I've just a couple of weeks of angular behind me]
In the angular app I'm trying to write, I need to display some information and let the user edit it provided they activated a switch. The corresponding HTML is:
<span ng-hide="editing" class="uneditable-input" ng:bind='value'>
</span>
<input ng-show="editing" type="text" name="desc" ng:model='value' value={{value}}>
where editing is a boolean (set by a switch) and value the model.
I figured this is the kind of situation directives are designed for and I've been trying to implement one. The idea is to precompile the <span> and the <input> elements, then choose which one to display depending on the value of the editing boolean. Here's what I have so far:
angular.module('mod', [])
.controller('BaseController',
function ($scope) {
$scope.value = 0;
$scope.editing = true;
})
.directive('toggleEdit',
function($compile) {
var compiler = function(scope, element, attrs) {
scope.$watch("editflag", function(){
var content = '';
if (scope.editflag) {
var options='type="' + (attrs.type || "text")+'"';
if (attrs.min) options += ' min='+attrs.min;
options += ' ng:model="' + attrs.ngModel
+'" value={{' + attrs.ngModel +'}}';
content = '<input '+ options +'></input>';
} else {
content = '<span class="uneditable-input" ng:bind="'+attrs.ngModel+'"></span>';
};
console.log("compile.editing:" + scope.editflag);
console.log("compile.attrs:" + angular.toJson(attrs));
console.log("compile.content:" + content);
})
};
return {
require:'ngModel',
restrict: 'E',
replace: true,
transclude: true,
scope: {
editflag:'='
},
link: compiler
}
});
(the whole html+js is available here).
Right now, the directive doesn't do anything but output some message on the console. How do I replace a <toggle-edit ...> element of my html with the content I define in the directive? If I understood the doc correctly, I should compile the content before linking it: that'd be the preLink method of the directive's compile, right ? But how do I implement it in practice ?
Bonus question: I'd like to be able to use this <toggle-edit> element with some options, such as:
<toggle-edit type="text" ...></toggle-edit>
<toggle-edit type="number" min=0 max=1 step=0.01></toggle-edit>
I could add tests on the presence of the various options (like I did for min in the example above), but I wondered whether there was a smarter way, like putting all the attrs but the ngModel and the editflag at once when defining the template ?
Thanks for any insight.
Here is a tutorial by John Lindquist that shows how to do what you want. http://www.youtube.com/watch?v=nKJDHnXaKTY
Here is his code:
angular.module('myApp', [])
.directive('jlMarkdown', function () {
var converter = new Showdown.converter();
var editTemplate = '<textarea ng-show="isEditMode" ng-dblclick="switchToPreview()" rows="10" cols="10" ng-model="markdown"></textarea>';
var previewTemplate = '<div ng-hide="isEditMode" ng-dblclick="switchToEdit()">Preview</div>';
return{
restrict:'E',
scope:{},
compile:function (tElement, tAttrs, transclude) {
var markdown = tElement.text();
tElement.html(editTemplate);
var previewElement = angular.element(previewTemplate);
tElement.append(previewElement);
return function (scope, element, attrs) {
scope.isEditMode = true;
scope.markdown = markdown;
scope.switchToPreview = function () {
var makeHtml = converter.makeHtml(scope.markdown);
previewElement.html(makeHtml);
scope.isEditMode = false;
}
scope.switchToEdit = function () {
scope.isEditMode = true;
}
}
}
}
});
You can see it working here: http://jsfiddle.net/moderndegree/cRXr6/

ng-model for `<input type="file"/>` (with directive DEMO)

I tried to use ng-model on input tag with type file:
<input type="file" ng-model="vm.uploadme" />
But after selecting a file, in controller, $scope.vm.uploadme is still undefined.
How do I get the selected file in my controller?
I created a workaround with directive:
.directive("fileread", [function () {
return {
scope: {
fileread: "="
},
link: function (scope, element, attributes) {
element.bind("change", function (changeEvent) {
var reader = new FileReader();
reader.onload = function (loadEvent) {
scope.$apply(function () {
scope.fileread = loadEvent.target.result;
});
}
reader.readAsDataURL(changeEvent.target.files[0]);
});
}
}
}]);
And the input tag becomes:
<input type="file" fileread="vm.uploadme" />
Or if just the file definition is needed:
.directive("fileread", [function () {
return {
scope: {
fileread: "="
},
link: function (scope, element, attributes) {
element.bind("change", function (changeEvent) {
scope.$apply(function () {
scope.fileread = changeEvent.target.files[0];
// or all selected files:
// scope.fileread = changeEvent.target.files;
});
});
}
}
}]);
I use this directive:
angular.module('appFilereader', []).directive('appFilereader', function($q) {
var slice = Array.prototype.slice;
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, element, attrs, ngModel) {
if (!ngModel) return;
ngModel.$render = function() {};
element.bind('change', function(e) {
var element = e.target;
$q.all(slice.call(element.files, 0).map(readFile))
.then(function(values) {
if (element.multiple) ngModel.$setViewValue(values);
else ngModel.$setViewValue(values.length ? values[0] : null);
});
function readFile(file) {
var deferred = $q.defer();
var reader = new FileReader();
reader.onload = function(e) {
deferred.resolve(e.target.result);
};
reader.onerror = function(e) {
deferred.reject(e);
};
reader.readAsDataURL(file);
return deferred.promise;
}
}); //change
} //link
}; //return
});
and invoke it like this:
<input type="file" ng-model="editItem._attachments_uri.image" accept="image/*" app-filereader />
The property (editItem.editItem._attachments_uri.image) will be populated with the contents of the file you select as a data-uri (!).
Please do note that this script will not upload anything. It will only populate your model with the contents of your file encoded ad a data-uri (base64).
Check out a working demo here:
http://plnkr.co/CMiHKv2BEidM9SShm9Vv
How to enable <input type="file"> to work with ng-model
Working Demo of Directive that Works with ng-model
The core ng-model directive does not work with <input type="file"> out of the box.
This custom directive enables ng-model and has the added benefit of enabling the ng-change, ng-required, and ng-form directives to work with <input type="file">.
angular.module("app",[]);
angular.module("app").directive("selectNgFiles", function() {
return {
require: "ngModel",
link: function postLink(scope,elem,attrs,ngModel) {
elem.on("change", function(e) {
var files = elem[0].files;
ngModel.$setViewValue(files);
})
}
}
});
<script src="//unpkg.com/angular/angular.js"></script>
<body ng-app="app">
<h1>AngularJS Input `type=file` Demo</h1>
<input type="file" select-ng-files ng-model="fileArray" multiple>
<code><table ng-show="fileArray.length">
<tr><td>Name</td><td>Date</td><td>Size</td><td>Type</td><tr>
<tr ng-repeat="file in fileArray">
<td>{{file.name}}</td>
<td>{{file.lastModified | date : 'MMMdd,yyyy'}}</td>
<td>{{file.size}}</td>
<td>{{file.type}}</td>
</tr>
</table></code>
</body>
This is an addendum to #endy-tjahjono's solution.
I ended up not being able to get the value of uploadme from the scope. Even though uploadme in the HTML was visibly updated by the directive, I could still not access its value by $scope.uploadme. I was able to set its value from the scope, though. Mysterious, right..?
As it turned out, a child scope was created by the directive, and the child scope had its own uploadme.
The solution was to use an object rather than a primitive to hold the value of uploadme.
In the controller I have:
$scope.uploadme = {};
$scope.uploadme.src = "";
and in the HTML:
<input type="file" fileread="uploadme.src"/>
<input type="text" ng-model="uploadme.src"/>
There are no changes to the directive.
Now, it all works like expected. I can grab the value of uploadme.src from my controller using $scope.uploadme.
I create a directive and registered on bower.
This lib will help you modeling input file, not only return file data but also file dataurl or base 64.
{
"lastModified": 1438583972000,
"lastModifiedDate": "2015-08-03T06:39:32.000Z",
"name": "gitignore_global.txt",
"size": 236,
"type": "text/plain",
"data": "data:text/plain;base64,DQojaWdub3JlIHRodW1ibmFpbHMgY3JlYXRlZCBieSB3aW5kb3dz…xoDQoqLmJhaw0KKi5jYWNoZQ0KKi5pbGsNCioubG9nDQoqLmRsbA0KKi5saWINCiouc2JyDQo="
}
https://github.com/mistralworks/ng-file-model/
This is a slightly modified version that lets you specify the name of the attribute in the scope, just as you would do with ng-model, usage:
<myUpload key="file"></myUpload>
Directive:
.directive('myUpload', function() {
return {
link: function postLink(scope, element, attrs) {
element.find("input").bind("change", function(changeEvent) {
var reader = new FileReader();
reader.onload = function(loadEvent) {
scope.$apply(function() {
scope[attrs.key] = loadEvent.target.result;
});
}
if (typeof(changeEvent.target.files[0]) === 'object') {
reader.readAsDataURL(changeEvent.target.files[0]);
};
});
},
controller: 'FileUploadCtrl',
template:
'<span class="btn btn-success fileinput-button">' +
'<i class="glyphicon glyphicon-plus"></i>' +
'<span>Replace Image</span>' +
'<input type="file" accept="image/*" name="files[]" multiple="">' +
'</span>',
restrict: 'E'
};
});
For multiple files input using lodash or underscore:
.directive("fileread", [function () {
return {
scope: {
fileread: "="
},
link: function (scope, element, attributes) {
element.bind("change", function (changeEvent) {
return _.map(changeEvent.target.files, function(file){
scope.fileread = [];
var reader = new FileReader();
reader.onload = function (loadEvent) {
scope.$apply(function () {
scope.fileread.push(loadEvent.target.result);
});
}
reader.readAsDataURL(file);
});
});
}
}
}]);
function filesModelDirective(){
return {
controller: function($parse, $element, $attrs, $scope){
var exp = $parse($attrs.filesModel);
$element.on('change', function(){
exp.assign($scope, this.files[0]);
$scope.$apply();
});
}
};
}
app.directive('filesModel', filesModelDirective);
I had to do same on multiple input, so i updated #Endy Tjahjono method.
It returns an array containing all readed files.
.directive("fileread", function () {
return {
scope: {
fileread: "="
},
link: function (scope, element, attributes) {
element.bind("change", function (changeEvent) {
var readers = [] ,
files = changeEvent.target.files ,
datas = [] ;
for ( var i = 0 ; i < files.length ; i++ ) {
readers[ i ] = new FileReader();
readers[ i ].onload = function (loadEvent) {
datas.push( loadEvent.target.result );
if ( datas.length === files.length ){
scope.$apply(function () {
scope.fileread = datas;
});
}
}
readers[ i ].readAsDataURL( files[i] );
}
});
}
}
});
I had to modify Endy's directive so that I can get Last Modified, lastModifiedDate, name, size, type, and data as well as be able to get an array of files. For those of you that needed these extra features, here you go.
UPDATE:
I found a bug where if you select the file(s) and then go to select again but cancel instead, the files are never deselected like it appears. So I updated my code to fix that.
.directive("fileread", function () {
return {
scope: {
fileread: "="
},
link: function (scope, element, attributes) {
element.bind("change", function (changeEvent) {
var readers = [] ,
files = changeEvent.target.files ,
datas = [] ;
if(!files.length){
scope.$apply(function () {
scope.fileread = [];
});
return;
}
for ( var i = 0 ; i < files.length ; i++ ) {
readers[ i ] = new FileReader();
readers[ i ].index = i;
readers[ i ].onload = function (loadEvent) {
var index = loadEvent.target.index;
datas.push({
lastModified: files[index].lastModified,
lastModifiedDate: files[index].lastModifiedDate,
name: files[index].name,
size: files[index].size,
type: files[index].type,
data: loadEvent.target.result
});
if ( datas.length === files.length ){
scope.$apply(function () {
scope.fileread = datas;
});
}
};
readers[ i ].readAsDataURL( files[i] );
}
});
}
}
});
If you want something a little more elegant/integrated, you can use a decorator to extend the input directive with support for type=file. The main caveat to keep in mind is that this method will not work in IE9 since IE9 didn't implement the File API. Using JavaScript to upload binary data regardless of type via XHR is simply not possible natively in IE9 or earlier (use of ActiveXObject to access the local filesystem doesn't count as using ActiveX is just asking for security troubles).
This exact method also requires AngularJS 1.4.x or later, but you may be able to adapt this to use $provide.decorator rather than angular.Module.decorator - I wrote this gist to demonstrate how to do it while conforming to John Papa's AngularJS style guide:
(function() {
'use strict';
/**
* #ngdoc input
* #name input[file]
*
* #description
* Adds very basic support for ngModel to `input[type=file]` fields.
*
* Requires AngularJS 1.4.x or later. Does not support Internet Explorer 9 - the browser's
* implementation of `HTMLInputElement` must have a `files` property for file inputs.
*
* #param {string} ngModel
* Assignable AngularJS expression to data-bind to. The data-bound object will be an instance
* of {#link https://developer.mozilla.org/en-US/docs/Web/API/FileList `FileList`}.
* #param {string=} name Property name of the form under which the control is published.
* #param {string=} ngChange
* AngularJS expression to be executed when input changes due to user interaction with the
* input element.
*/
angular
.module('yourModuleNameHere')
.decorator('inputDirective', myInputFileDecorator);
myInputFileDecorator.$inject = ['$delegate', '$browser', '$sniffer', '$filter', '$parse'];
function myInputFileDecorator($delegate, $browser, $sniffer, $filter, $parse) {
var inputDirective = $delegate[0],
preLink = inputDirective.link.pre;
inputDirective.link.pre = function (scope, element, attr, ctrl) {
if (ctrl[0]) {
if (angular.lowercase(attr.type) === 'file') {
fileInputType(
scope, element, attr, ctrl[0], $sniffer, $browser, $filter, $parse);
} else {
preLink.apply(this, arguments);
}
}
};
return $delegate;
}
function fileInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) {
element.on('change', function (ev) {
if (angular.isDefined(element[0].files)) {
ctrl.$setViewValue(element[0].files, ev && ev.type);
}
})
ctrl.$isEmpty = function (value) {
return !value || value.length === 0;
};
}
})();
Why wasn't this done in the first place? AngularJS support is intended to reach only as far back as IE9. If you disagree with this decision and think they should have just put this in anyway, then jump the wagon to Angular 2+ because better modern support is literally why Angular 2 exists.
The issue is (as was mentioned before) that without the file api
support doing this properly is unfeasible for the core given our
baseline being IE9 and polyfilling this stuff is out of the question
for core.
Additionally trying to handle this input in a way that is not
cross-browser compatible only makes it harder for 3rd party solutions,
which now have to fight/disable/workaround the core solution.
...
I'm going to close this just as we closed #1236. Angular 2 is being
build to support modern browsers and with that file support will
easily available.
Alternatively you could get the input and set the onchange function:
<input type="file" id="myFileInput" />
document.getElementById("myFileInput").onchange = function (event) {
console.log(event.target.files);
};
Try this,this is working for me in angular JS
let fileToUpload = `${documentLocation}/${documentType}.pdf`;
let absoluteFilePath = path.resolve(__dirname, fileToUpload);
console.log(`Uploading document ${absoluteFilePath}`);
element.all(by.css("input[type='file']")).sendKeys(absoluteFilePath);

Resources