Rich tabs and transcluded content - javascript

I currently have this problem: I made 3 directives (check plunkr for a 'reduced' test case, dont mind the closures they come from Typescript) that control tabs, using a controller to keep them grouped, since I can have more than one tabbed content on the current view. The problem happens when the tab itself has some bindings that are located outside of the scope, and when the tab is 'transcluded' in place, the binding never updates because the scope is different.
Here is the plunkr http://plnkr.co/edit/fnw1oV?p=preview and here is the "tab transclude" part that is important
this.link = function (scope, element, attr) {
var clickEvent, el, item;
item = scope.item;
console.log(scope);
el = item.element.filter('.is-tab');
el.addClass('picker-tabs-item');
clickEvent = 'tabs.select(item);';
if (el.is('[ng-click]')) {
clickEvent += el.attr('ng-click') + ';';
}
el.attr('ng-click', clickEvent);
el.attr('ng-class', 'tabs.classes(item)');
item.element = $compile(item.element)(scope);
element.replaceWith(item.element);
};
The current approach feels hacky (keeping the original scope and element in an array). Plus in my app, the data is loaded after the tabs were loaded, so it can't even retain some initial state. The tabs look like this right now:
and the way it should look like (but doesn't work, as you can see clicking one tab select all of them):
A real tab code from my app:
<div class="clearfix" login-space user-space>
<div class="picker clearfix" ng-class="{'to-hide': user.data.get('incomplete') || checkout.checkoutForm.$invalid}">
<div class="pick-title icon icon-pay">Forma de Pagamento</div>
<div class="for-hiding">
<div tabs="pagamento">
<div tab="/templates/tabs/plans/credito" selected="true">
<button class="is-tab clicky" ng-disabled="checkout.disabledTab('credito')" type="button">
Cartão
<span class="pick-pill-indicator-placeholder" ng-bind="checkout.total('credito')"></span>
</button>
</div>
<div tab="/templates/tabs/plans/debito">
<button class="is-tab clicky" ng-disabled="checkout.disabledTab('debito')" type="button">
Débito
<span class="pick-pill-indicator-placeholder" ng-bind="checkout.total('debito')"></span>
</button>
</div>
<div tab="/templates/tabs/plans/boleto">
<button class="is-tab clicky" ng-disabled="checkout.disabledTab('boleto')" type="button">
Boleto
<span class="pick-pill-indicator-placeholder" ng-bind="checkout.total('boleto')"></span>
</button>
</div>
</div>
</div>
</div>
</div>
login-space and user-space are directives just to assign it to the login and user controllers. checkout is the current controller inside ui-view.
$stateProvider.state('planos.checkout', {
url: '/checkout',
templateUrl: '/templates/partials/plans/checkout',
controllerAs: 'checkout',
controller: Controllers.Checkout,
data: {
allowed: false,
enclosed: true
}
});
since the checkout controller must be instantiated only once, I can't reinstantiate it, but still need to access it's functions and bound data.
'/templates/partials/plans/checkout' contains the tab code above (so yes, technically it's in the same scope as checkout controller)

In your plunker, changing your tabs to this:
<span data-subctrl="">{{ subctrl.sum('credito') }}</span>
Showed the sum. I looked at what subctrl was and you have it as a directive, that's why subctrl.sum is not working. Plunker with it working: http://plnkr.co/edit/qwEHMqfzJ6pC79hM8cDj?p=preview
If that's not what's wrong with your application, then please describe it a little more.

Solved this by removing the html content of the tab, and applying a different scope to the inner content, then reattaching to the original element.
this.link = function (scope, element, attr) {
var clickEvent, el, item;
item = scope.item;
console.log(scope);
el = item.element.filter('.is-tab');
var contents = el.html(); //added
el.empty(); // added
el.addClass('picker-tabs-item');
clickEvent = 'tabs.select(item);';
if (el.is('[ng-click]')) {
clickEvent += el.attr('ng-click') + ';';
}
el.attr('ng-click', clickEvent);
el.attr('ng-class', 'tabs.classes(item)');
item.element = $compile(item.element)(scope);
item.element.append($compile('<div>' + contents + '</div>')(item.scope)); //added
element.replaceWith(item.element);
};

Related

How to pass a DOM element to "ng-disabled"

All,
How can I pass the current DOM element to the Angular directive "ng-disabled"?
I do know that messing w/ the DOM in Angular is bad practice. But I can't think of another - simple - way to do this. Here is my problem:
I have a button that updates a scope variable when clicked:
<button ng-click="target_model.display_detail=true">click me</button>
Elsewhere in my template there is code that watches "target_model.display_detail" - when it is true it displays a modal-dialog which includes an Angular directive which gets some data from the server and populates a form which includes another button like the one above.
The data structure that I am working w/ is potentially recursive; there are loads of nested "target_models". So it is possible for a button in a modal-dialog to point a target_model whose form has already been created. In that case, I just want to disable the button. I'd like todo something like:
<button ng-disabled="ancestor_model_exists(the_current_element, target_model.some_unique_id)">click me</button>
Where "ancestor_model_exists" is a function that would check the DOM to see if there is an ancestor element with a particular id. But how do I know which element to start from?
You're approaching DOM manipulations imperatively - the jQuery way, not declaratively - the Angular way.
DOM manipulation is fine... inside directives. You don't do it in controllers, where you presumably defined that function.
When you get a chance, try to get away with 0 calls to $ in a sandbox to force you to learn how to do things the Angular way - not because it's "better" in an absolute way - it's just generally better to first learn the toolkit and recommended approaches before doing it your way, anyway.
This should do what you want, except maybe searching beyond multiple ancestors (but I mention how to do that if you need that):
https://plnkr.co/edit/7O8UDuqsVTlH8r2GoxQu?p=preview
JS
app.directive('ancestorId', function() {
return {
restrict: 'A',
controller: 'AncestorIdController',
require: ['ancestorId'],
link: function(scope, element, attrs, controllers) {
var ancestorIdController = controllers[0];
// If you wanted to use an expression instead of an
// interpolation you could define an isolate scope on this
// directive and $watch it.
attrs.$observe('ancestorId', function(value) {
ancestorIdController.setId(value);
});
}
}
});
app.controller('AncestorIdController', function() {
this.getId = _getId;
this.setId = _setId;
var id;
function _getId() {
return id;
}
function _setId(value) {
id = value;
}
});
app.directive('disableForAncestorId', function() {
return {
restrict: 'A',
require: ['?^ancestorId'],
link: function(scope, element, attrs, controllers) {
var ancestorIdController = controllers[0];
// Check to make sure the ancestorId is a parent.
if (ancestorIdController) {
scope.$watch(function() {
var watch = {
target: ancestorIdController.getId(),
actual: attrs.disableForAncestorId
};
return watch;
}, function(value) {
if (value.target === value.actual) {
element.attr('disabled', 'disabled');
} else {
element.removeAttr('disabled');
}
}, true /* Deep watch */ );
}
}
}
});
HTML
<!-- The simple happy path. -->
<div ancestor-id="A">
<button disable-for-ancestor-id="A">'A' === 'A' ?</button>
</div>
<!-- require will match the 'B' before the 'A' because it's closer.
if you need to match any parent you could use a third coordinating
directive. -->
<div ancestor-id="A">
<div ancestor-id="B">
<button disable-for-ancestor-id="A">'B' === 'A' ?</button>
</div>
</div>
<!-- require lets you freely change the DOM to add extra elements separating
you from what you're looking for.-->
<div ancestor-id="B">
<div>
<div>
<button disable-for-ancestor-id="B">'B' === 'B' ?</button>
</div>
</div>
</div>
<!-- It doesn't blow up if it doesn't find an ancestorId. -->
<div>
<button disable-for-ancestor-id="B">'B' === undefined ?</button>
</div>
<br>
Dynamic AncestorId test (it will be disabled if the text fields are equal):
<br>
Target AncestorId <input ng-model="targetAncestorId">
<br>
Actual Ancestor <input ng-model="actualAncestorId">
<!-- It doesn't blow up if it doesn't find an ancestorId. -->
<div ancestor-id="{{ targetAncestorId }}">
<button disable-for-ancestor-id="{{ actualAncestorId }}">'{{ actualAncestorId }}' === '{{ actualAncestorId }}' ?</button>
</div>
It never fails... posting a question on Stack Overflow always makes me realize the answer just minutes later.
The following code gets me pretty close:
template.html:
<button ng-click="display_if_ancestor_model_exists($event, target_model)">click me</button>
app.js:
$scope.display_if_ancestor_model_exists = function($event, target_model) {
var ancestor_form = $($event.target).closest("#" + target_model.some_unique_id);
if (ancestor_form.length) {
/* don't show the modal-dialog */
display_msg("This form already exists!");
}
else {
target_model.display_detail = true;
}
};
Of course, I would rather the button just be disabled but I can live w/ this solution for now.

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.

Memory leak with angularJS and jQuery / angular bootstrap

I have a ng-repeat list that updates every minute. Its a list of cards that contains stuff like title, description, dates and so on.
In those cards there's also a angular-ui-bootstrap popover which i use to display comments on those cards.
When the list updates, the popover will keep some reference which creates a lot of detached dom elements.
Heres some of the code.
The directive i use.
.directive('mypopover', function ($compile, $templateCache) {
var getTemplate = function (contentType) {
var template = '';
switch (contentType) {
case 'user':
template = $templateCache.get("templateId.html");
break;
}
return template;
}
return {
restrict: "A",
link: function ($scope, element, attrs) {
var popOverContent;
popOverContent = getTemplate("user");
popOverContent = $compile("<span>" + popOverContent + "</span>")($scope);
var options = {
content: popOverContent,
placement: "bottom",
html: true,
trigger: "manual",
selector: '.fa-comment',
date: $scope.date,
animation: true
};
$(element).popover(options).on("mouseenter", function () {
var _this = this;
$(this).popover("show");
$('.popover').linkify();
$(".popover").on("mouseleave", function () {
$(this).popover('destroy');
$('.popover').remove();
});
}).on("mouseleave", function () {
var _this = this;
setTimeout(function () {
if (!$(".popover:hover").length) {
$(this).popover('destroy');
$('.popover').remove();
}
}, 350);
});
var destroy = function () {
$(element).popover('destroy');
}
$scope.$on("$destroy", function () {
destroy();
});
}
}
})
from the html..
The bo-something is the just a one way bind i use instead of the normal double bind from angular
<a bo-href="c.ShortUrl" target="_blank" bindonce ng-repeat="c in cards | filter:searchText | limitTo: limitValue[$index] track by c.Id">
<div class="panel detachable-card">
<div class="panel-body" bo-class="{redLabel: c.RedLabel, orangeLabel: c.OrangeLabel}">
<!-- Comments if any -->
<script type="text/ng-template" id="templateId.html">
<div ng-repeat="comment in c.Comment track by $index">
<strong style="margin-bottom: 20px; color:#bbbbbb; white-space: pre-wrap;">{{c.CommentMember[$index]}}</strong>
<br />
<span style="white-space: pre-wrap;">{{comment}}</span>
<hr />
</div>
</script>
<span bo-if="c.Comment" data-container="body" mypopover style="float:right"><i class="fa fa-comment fa-lg"></i></span>
<!-- Card info -->
<strong style="font-size:12px; color:#999999"><span bo-if="!c.BoardNameOverride" bo-text="c.NameBoard"></span> <span bo-if="c.BoardNameOverride" bo-text="c.BoardNameOverride"></span></strong>
<br />
<strong bo-text="c.Name"></strong>
<br />
<span bo-if="c.Desc" bo-text="c.Desc"><br /></span>
</div>
</div>
</a>
Heres a heap-snapshot of the site after one update.
http://i.stack.imgur.com/V4U1O.png
So Im fairly bad at javascript in general, and i have my doubts about the directive. I would have thought that the .popover('destroy') would remove the reference, but it doesnt seem to..
Any help is greatly appreciated..
Why are you constantly destroying the popup over and over? There is no need to destroy the popup every time the mouse is moved. Just show and hide the popup. It's much nicer on memory than constantly destroying and recreating the popup.
But, what you may not realize is that bootstrap components don't play well with AngularJS. Bootstrap components weren't architected in ways that allow the content within them to be updated easily which poses problems when you use them with AngularJS because the update model is built into the framework. And that's why the AngularUI project rewrote their Javascript components from the ground up in AngularJS so they behave as you would expect. I think you'll find those much easier to use.
http://angular-ui.github.io/bootstrap/
If you are using bootstrap 2.3 AngularUI v0.8 was the last version supporting bootstrap v2.3.

How to add a hover background image style on a div the Angularjs way

I have a background image on a div that I want to have switch on hover. I can't change the class because I'm using a bound property to fill in the information. As far as I know I don't see any way to add hover with styles inside of the html and I found a way to do it in jquery but it just doesn't seem like the angular way.
Method #1: No controller, everything in template.
<div ng-init="bg = ''"
ng-mouseenter="bg = 'http://www.gravatar.com/avatar/b76f6e92d9fc0690e6886f7b9d4f32da?s=100'"
ng-mouseleave="bg = ''"
style="background-image: url({{bg}});">
</div>
Method #2: Using vars to store the values (uses a controller)
<div ng-mouseenter="bg = imageOn"
ng-mouseleave="bg = imageOff"
style="background-image: url({{bg1}});">
</div>
Controller:
function myCtrl($scope){
$scope.bg1 = "" // this is the default image.
$scope.imageOn = "http://www.gravatar.com/avatar/b76f6e92d9fc0690e6886f7b9d4f32da?s=100";
$scope.imageOff = ""; // image that would after after the mouse off.
}
Method #3: Saved the best for last! Using a directive!!
<div hover-bg-image="{{image}}"></div>
Directive (could be improved to revert back to original image if there is one... its basic to show example):
.directive('hoverBgImage',function(){
return {
link: function(scope, elm, attrs){
elm.bind('mouseenter',function(){
this.style.backgroundImage = 'url('+attrs.hoverBgImage+')';
});
elm.bind('mouseleave',function(){
this.style.backgroundImage = '';
})
}
};
});
Controller:
function withDirective($scope){
$scope.image = "http://www.gravatar.com/avatar/b76f6e92d9fc0690e6886f7b9d4f32da?s=100";
}
Note: The items in the controllers could/should/would be set dynamically.
Demos: http://jsfiddle.net/TheSharpieOne/kJgVw/1/

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