angularjs - get ng-repeat element using element.html() inside a jsPanel directive - javascript

I am trying to create a directive which will take the html content and place it n a jsPanel element.
So far I am able to compile the scope and able get elements with the scope. Then i tried to get html tag with ng-repeat, where i got stuck.
Following is the code used to create the directive
mainApp.directive('jspanelcontent', function($compile) {
console.log("loading");
var directive = {};
directive.restrict = 'AE';
directive.compile = function(element, attributes) {
var linkFunction = function($scope, element, attributes) {
console.log(element.html());
var contenthtml = element.html();
contenthtml = angular.element($compile(contenthtml)($scope));
element.html("");
$scope.jspanleinstance = $.jsPanel({
position: {
my: "left-bottom",
at: "left-bottom",
offsetY: 15
},
theme: "rebeccapurple",
contentSize: {
width: 600,
height: 350
},
headerTitle: attributes.panelcaption,
content: contenthtml,
});
// });
}
return linkFunction;
}
return directive;
});
And using like following in html
<div jspanelcontent panelcaption="list">
<div ng-controller="ListController">
{{test}}
<div ng-repeat="user in users">
<span>{{user.id}}</span>
<span>{{user.name}}</span>
<span>{{user.email}}</span>
<br />
</div>
</div>
The console log returns as
Output i am getting (jsPanel)
As you see the "test" variable is properly bind and the ng-repeat element is display as commented, I am not aware why it's returning like that. If anyone can figure out the issue, then i might get it working.
I know i can access the users data as $scope.users inside the directive. The problem here is that user able to use the directive commonly, so i have to assume that i don't the variables in the $scope.
I stuck in this place, and couldn't find any solutions to try. Any suggestions or solutions will be more helpful. :)
NOTE: No syntax errors, outside the directive the data is displaying (Tested)

Related

Does $last mean that ng-repeat elements are already rendered?

The Problem
if(scope.$last)
{
//get container height
}
I've been thinking that it's the proper way of getting height of container which has ng-repeated elements inside. I've reached the conclusion that it's not the proper approach.
Have a look at my directive:
AdminApp.directive("ngExpander", function($rootScope, $timeout){
var GetProperContainerHeight = function(container, content){
var container = $(container);
var content = $(content);
container.height(content.outerHeight());
return container.height();
}
return{
restrict:'A',
link:function(scope, elem, attr){
if(scope.$last){
$timeout(function(){
$rootScope.ContainerHeight = GetProperContainerHeight(attr.container, attr.content);
}, 500);
}
}
}
});
If I hadn't added $timeout, the directive wouldn't have worked properly, because it wouldn't have returned proper container height (some negative value I achieved then).
The Background
Directive works here:
<div class="SwitchContent" data-ng-show="ShowContent" id="testId">
<div class="user-list-element"
data-ng-repeat="user in UserList | filter:userFilter track by $index"
data-ng-click="GetUserDetails(user);"
data-ng-expander
data-container="div.UserPanel"
data-content="div[id=testId]">
<i class="fa fa-user fa-lg padding-r-10"></i> {{ user.name + ' ' + user.surname }}
</div>
</div>
How can I achieve proper results without $timeout?
You cannot do it without timeout, because Angular rendering happens asynchronously after your directive is compiled. But what you don't need is a number 500 which you probably don't like.
Just use the $timeout without second parameter.
$timeout(function(){
$rootScope.ContainerHeight = SetProperContainerHeight(attr.container, attr.content);
});
This puts the task in the browser queue without any delay. You will still have some small time between the moment when browser stopped rendering and your timeout start but this is the closest you can do.
EDIT
Probably the problem is in your scope.$last method. What you can do instead of if(scope.$last){ is watching the HTML content:
link:function(scope, elem, attr){
scope.$watch(function() {
return elem.html();
}, function() {
$timeout(function(){
$rootScope.ContainerHeight = GetProperContainerHeight(attr.container, attr.content);
});
});
}
Although it can look ugly it always works for me. Whenever HTML of the element is changed (and it will be as long as ng-repeat changes the element's inner content) the watcher event happens.
You still need a timeout here because HTML is changed but it is not rendered yet, so you need to put it to asynchronous browser queue with the timeout.
This also has another advantage: current height is always reflected in your rootScope property while your method will only do it once on the directive load.

angular directive: switch between two templates dynamically

I am trying to create a directive named availableTo that can switch between two different templates depending on some message. For example, if the field is an input with the ng-model directive, I would first need to change it to read-only using the <span> tag. So far, my code can switch the view to read-only, but I cannot seem to switch it back to input:
var directive = {
restrict: 'A',
require: '?ngModel',
link: linkerFn,
replace: true
};
function linkerFn(scope, element, attrs, ngModelCtrl) {
var clonedElement = angular.copy(element);
var preOuterHTML = clonedElement[0].outerHTML; //this can save the <input> field html code
scope.$on('mode_changed', function() {
var curUserRole = userservices.getUserRole();
if (attrs.availableTo == curUserRole) {
var e = $compile(preOuterHTML)(scope);
element.replaceWith(e);
} else {
var template = '<span>' + ngModelCtrl.$viewValue + '</span>';
var e = $compile(template)(scope);
element.replaceWith(e);
}
}); //scope.$on
} //linkerFn
For an input field:
<input name="test1" class="form-control" ng-model="name" placeholder="Name 1" available-to="ADMIN"/>
I also noticed that once I change the template in the else block above, the element re-renders, and the preOuterHTML does not contain the original element html any more. This seems to be mission impossible to me, but I would like to hear some expert opinions. Thanks
element.replaceWith(e); Don't do that. In Angular, if you find yourself attempting to modify the DOM directly, you are by definition doing it wrong. You gotta sit back and let Angular do the work.
If you need to replace a directive's entire template, a fairly straightforward approach is to use ng-include with a scope variable containing the desired conditional templateUrl, e.g.
var directive = {
// ...
template: '<div ng-include="myTemplateUrl"></div>',
link: function(scope, el) {
if (/* whatever */) {
scope.myTemplateUrl="templates/foo.html";
} else {
//...etc
}
},
};
(This does add an extra DOM node to the tree, but that's generally harmless.)
It sounds like in your case you may not need to go that far, though; a simple ng-if inside your template is probably enough to swap between your read-only <span> and <input>.

AngularJS ng-switch without extra DOM

I have a custom directive and an object myObj on the current $scope (inside an ng-repeat).
If the object has a type of html, I want to use one template:
<span ng-bind-html="myObj.html"></span>`
Otherwise I want to use a different template:
<span>{{myObj.value}}</span>`
My problem
This is invalid because a custom directive template must contain exactly one root node:
<span ng-if="myObj.type==='html'" ng-bind-html="myObj.html"></span>
<span ng-if="myObj.type!=='html'">{{myObj.value}}</span>
This is invalid because it destroys my page with extra DOM: (wrapping all my spans (there could be thousands) in unnecessary ng-switch nodes...)
<ng-switch on="myObj.type">
<span ng-switch-when="html" ng-bind-html="myObj.html"></span>
<span ng-switch-default>{{myObj.value}}</span>
</ng-switch>
My Question
Is it possible to have a directive pick it's template based on the result of a switch, without creating extra unnecessary DOM? For example, you can specify replace: true when creating a directive - is it possible to similarly have an ng-switch where the result replaces the switch tag itself?
Edit
My Directive:
return {
replace: true,
controller: 'ChunkController',
scope: {
chunk: '=deChunk'
},
templateUrl: de.partial.chunk,
link: function (scope, el, attr, ctrl) {
el.on('keydown', handleKeypress.bind(ctrl));
el.on('click', ctrl.showValue);
}
};
And its usage:
<div class="content" contenteditable="{{node.type!=='static'}}">
<div data-ng-repeat="chunk in node.chunks" data-de-chunk="chunk"></div>
</div>
With the intent that the child <div> will be replaced with the sequence of <span>s from above.
I wouldn't even bother if you are storing the html in a service just check to see if a value for myObj.html exists in the object and if it does compile and bind the html in the linker function instead of using ng-bind-html
something like this maybe:
myapp.directive('something',function($compile){
return {
link: function(scope,elem,attrs) {
var obj = scope.$eval(attrs.something);
if(obj.html) {
var html = angular.element($compile(obj.html)(scope));
elem.append(html);
} else {
//go get the data and set obj.html
}
}
}
});

Getting ng-repeat to work inside of AngularJS's $interpolate service

I am using the AngularJs-UI components for Bootstrap. I would like to insert a filled out template into one of the data elements for the popover feature. This works find for all elements not inside of a ng-repeat. How do I get the ng-repeat elements to work inside of a interpolated template?
I have a plunker at http://plnkr.co/edit/Cuku7qaUTL1lxRkafKzv Its not working because I don't know how to get Angular-UI-bootstrap to in plunker.
<div data-popover="{{createHTML()}}">some content</div>
My local scope has the function createHTML() that looks something like this.
angular.module('myApp', ['ngSanitize'])
.controller("myController", function(myService){
$scope.createHTML = function() {
var thingy = { blah: "foobar", collection: [ "1", "2", "3" ] };
return myService.html_for(thingy);
}
});
And the service is
angular.module('myApp')
.service('myService', function($templateCache, $interpolate, $sanitize, $log) {
"use strict";
function html_for(thingy) {
var template = $templateCache.get('thingyTemplate.html'),
link = $interpolate(template),
html = link(thingy),
unsafe = $sanitize(html);
return unsafe;
}
return {
html_for: html_for
}
});
Templates:
<script type="text/ng-template" id="thingyTemplate.html">
<div>
<div><strong>Blah:</strong> {{blah}}</div>
<div data-ng-repeat="foo in collection"><strong>Collection:</strong> {{foo}}</div>
<div><strong>Collection[0]:</strong> {{collection[0]}}</div>
<div><strong>Collection[1]:</strong> {{collection[1]}}</div>
<div><strong>Collection[2]:</strong> {{collection[2]}}</div>
</div>
</script>
<script type="text/ng-template" id="template/popover/popover.html">
<div class="popover {{placement}}" data-ng-class="{ in: isOpen(), fade: animation() }">
<div class="arrow"></div>
<div class="popover-inner">
<h3 class="popover-title" data-ng-bind="title" data-ng-show="title"></h3>
<div class="popover-content" data-ng-bind-html="content"></div>
</div>
</div>
</script>
$interpolate doesn't handle directives like ngRepeat (
Difference between parse, interpolate and compile ). $interpolate:
Compiles a string with markup into an interpolation function. This
service is used by the HTML $compile service for data binding.
To handle ngRepeat and other directives you want $compile. But for your use case $compile is going to result, unfortunately, in a number of changes because:
It needs a scope to compile against rather than just a context like $interpolate. Moreover it needs the scope thingy is on.
This means we'll need to reference your properties like so {{thingy.blah}} instead of {{blah}} inside your template.
The compile needs to happen when the popup is on the dom.
The popup is only on the dom when it's open.
So we can't just replace $interpolate with $compile inside your service.
One approach is to replace data-ng-bind-html with the following directive that acts like an ng-bind-html that has a built in $compile (clearly you should only use this with html that you know is safe).
.directive('compile', function($compile) {
return function(scope, element, attrs) {
scope.$watch(
function(scope) {
return scope.$eval(attrs.compile);
},
function(value) {
var result = element.html(value);
$compile(element.contents())(scope.$parent.$parent);
}
);
};
});
Used like so (with compile replacing ng-bind-html:
<div class="popover-content" compile="content"></div>
One issue is that we need thingy to be in scope. There's a few of ways of handling that- but for demonstration purposes I've manually gone back up to the scope the popover is called from - which is 2 scopes up thus the scope.$parent.$parent.
Using this compile directive you no longer $interpolate or $sanitizeso the function in your service can shrink down to just returning the appropriate template:
function html_for() {
var template = $templateCache.get('thingyTemplate.html');
return template;
}
demo fiddle

Get original transcluded content within angular directive

My goal is to create an editable directive that allows a user to edit HTML of any element to which the attribute is attached (see Plunker: http://plnkr.co/edit/nIrr9Lu0PZN2PdnhQOC6)
This almost works except I can't get the original raw HTML of the transcluded content to initialize the text area. I can get the text of it from clone.text(), but that's missing the HTML tags like <H1>, <div>, etc. so clicking apply with no edits is not idempotent.
The method clone.html() throws an error, Cannot read property 'childNodes' of undefined
app.directive("editable", function($rootScope) {
return {
restrict: "A",
templateUrl: "mytemplate.html",
transclude: true,
scope: {
content: "=editContent"
},
controller: function($scope, $element, $compile, $transclude, $sce) {
// Initialize the text area with the original transcluded HTML...
$transclude(function(clone, scope) {
// This almost works but strips out tags like <h1>, <div>, etc.
// $scope.editContent = clone.text().trim();
// this works much better per #Emmentaler, tho contains expanded HTML
var html = "";
for (var i=0; i<clone.length; i++) {
html += clone[i].outerHTML||'';}
});
$scope.editContent = html;
$scope.onEdit = function() {
// HACK? Using jQuery to place compiled content
$(".editable-output",$element).html(
// compiling is necessary to render nested directives
$compile($scope.editContent)($rootScope)
);
}
$scope.showEditor = false;
$scope.toggleEditor = function() {
$scope.showEditor = !$scope.showEditor;
}
}
}
});
(This question is essentially a wholesale rewrite of the question and code after an earlier attempt to frame the question, Get original transcluded content in Angular directive)
The $element.innerHTML should contain the original HTML. I am showing that it contains
<div class="editable">
<span class="glyphicon glyphicon-edit" ng-click="toggleEditor()"></span>
<div class="editable-input" ng-show="showEditor">
<b><p>Enter well-formed HTML content:</p></b>
<p>E.g.<code><h1>Hello</h1><p>some text</p><clock></clock></code></p>
<textarea ng-model="editContent"></textarea>
<button class="btn btn-primary" ng-click="onEdit()">apply</button>
</div>
<div class="editable-output" ng-transclude=""></div>
</div>

Categories