Using ng-show to have a button-click display a separate div - javascript

I'm trying to get my head around the way angular does DOM element to DOM element communication:
We're putting together a set of e-learning templates. The first interactive components we've added are a bullet (effectively a button) and a popup. The bullet and the popup can be in separate parts of the page, so they will not be within the bounds of the same directive (although there is an over-arching pageCtrl, so they have that in common).
We've made custom attribute directives to trigger loading in each component type's html template; Here's the directive for eng-bullet:
app.directive('engBullet', function() {
return {
restrict: 'A',
replace: true,
templateUrl: 'components/bullet.html',
link: function(scope, element, attrs) {
// following function referenced in bullet.html
scope.showElementByUniqueName = function (showOnClick) {
// remove 'replaces' element
$('#'+$('#'+showOnClick).attr('data-replaces')).addClass('hidden');
// hide all popups (in case another popup is currently visible)
$('.popup').addClass("hidden");
// show selected popup
$('#'+showOnClick).removeClass("hidden");
}
}
};
});
At the moment the bullet component is given a 'show_on_click' string (from the json file that describes the page content) and this is passed to the bullet template (components/bullet.html):
<div class="button bullet" ng-click="showElementByUniqueName( component.bullet.show_on_click )"><p>{{component.bullet.text}}</p></div>
The 'show_on_click' variable corresponds to the id of the specific popup the bullet should display and this variable is then used in the eng-bullet directive to show the correct popup.
If I instead added an ng-show directive to my popup template, how would I bind that to the corresponding bullet which is on a different part of the page?
I could put the logic in pageCtrl, but then I've been told controllers should be 'lean and dumb'. How would I specify which popup to show, in the angular way?

Related

Angular 1.5 to use transclude or not to use transclude

A question regarding transclude within an angular 1.5.8 component, and it's uses.
Here is an example of some code;
var app = angular.module('app', [])
function AccordionController () {
var self = this;
// add panel
self.addPanel = function(panel) {
// code to add panel
}
self.selectPanel = function() {
//code to select panel
}
}
// register the accordion component
app.component('accordion', {
template: '<!---accordion-template-->',
controller: AccordionController
}
function AccordionPanelController () {
// use parents methods here
var self = this;
// add panel
self.parent.addPanel(self);
// select panel
self.parent.selectPanel(self);
}
// register the accordion-panel component
app.component('accordionPanel', {
// require the parent component
// In this case parent is an instance of accordion component
require: {
'parent': '^accordion',
template: '<!---accrodion-panel-template-->',
controller: AccordionController
}
My question is would it be better to nest all the according panels within the parent using transclude or alternatively pass in a data array to the parent which this loops out the required number of panels based on the array passed inside using a binding.
Thanks
// added
Many thanks for your reply, an example I have of transclude possibly being necessary is in the following bit of code
<modal modal-id="editCompany" title="Edit Company"> <company-form company="$ctrl.company"></company-form> </modal>
Here we have a modal component which may have a variety of other components used within it, on the example above I am adding the company form, but this could we be an contact form. is there an alternative way?
I've worked with angular pretty extensively. Two enterprise tools managing and displaying large amounts of data, dozens of interactive widget modules, all that.
Never, once, have I had anything to do with transclude. At work we are explicitly told not to use it (link functions too). I thought this was a good thing, and the way Angular 2 turned out it seemed that thinking wasn't totally without reason.
I would go with the iteration to lay out the required number of items. At work this wouldn't be a choice because transclude wouldn't be an option.
The thing with using transclude in a component architecture is that it visually breaks the idea of single responsibility and messes with the architecture.
<html>
<navigation></navigation>
<accordion>
<accordion-panel></accordion-panel>
</accordion>
<footer></footer>
</html>
In this example you know your page has a navigation menu, an accordion and a footer. But at the index level (or root component) you don't want to know / see what the accordion contains.
So the accordion-panel component should only appear in its direct parent component.
As for your other question, through the use of require or attributes you pass an array of panels that you iterate using ng-repeat inside the accordion component.
app.component('accordion', {
template: `<accordion-panel ng-repeat="..."></accordion-panel>`,
controller: AccordionController
}

Loading CSS with Angular Stylings

My page loads and applies the CSS defined in one of its include css link.
I also have an angular controller, that does a call to some services to grab some data. This data will also apply css stylings(via angular) depending on the type of data it returns.
The problem is that, because the services has to wait a split second or so for the data in order to manipulate the stylings on the page, there is this appearance of delay being applied to the page stylings.
Page loads applies CSS...waits for data to return from service call, and then applies some more stylings.
Is there a way to just wait for the data to return before any stylings on the page is applied, whether its from a css html link or angular directive, to avoid this delay/loading issue?
You can pre-load data before angular route is resolved.
Example:
$routeProvider
.when('/bar/foo/',
{
templateUrl: '',
controller: Ctrl,
resolve: {
data: ['service', function (service) {
return service.function();
}
],
}
})
Inject 'data' as dependency in your controller. In this case, data is available before page is rendered.
How does your service "apply some more stylings"? Is it by appending a link tag to your DOM?
You could manually store the different styles you get in a temporary variable, and apply them all at once when you know you have them all.
Another nice alternative would be to hide the whole page, for instance with some logo or progress bar, or just a plain white page until all your styles have been applied. In your top controller, put some $scope.pageReady that you will set to true once your services have returned all the CSS. In the mean time, hide the content:
<div ng-show = "pageReady"> ... </div>

AngularJS custom directive loosing root properties

I have created a sidebar custom directive. It's working properly as it loads on where it should. What isn't working properly are the tags. Their supposed behavior is that of a drowpdown, where when clicked they show their inner elements. It works properly when the code is pasted directly but not when the directive is called with the code inside the other html file. I took 2 screenshots to show the difference between using a class="page-sidebar" inside the file that contains the html code of the directive and using it on the "root" file:
It's pretty clear that several properties on the highlighted lines are not being applied on the first one.
Please help as I need this as a "partial" view to be used across several pages.
EDIT: Directive code:
app.directive('sidebar', function () {
return {
restrict: 'E',
templateUrl: "/app/views/sidebar.html"
};
});
EDIT2:
Adding this in the post because it might be confusing from how I explained it:
I see where the confusion might be but they're different things. < sidebar > is a directive created by me. class="page-sidebar" is from the template I'm using and is what formats everything to its place. I tried to insert the class="page-sidebar" into the directive to see if it would work, but they're different things.
EDIT3:
To clear up the confusion, I hope: both pics show the sidebar is working. I know it's an element and as such I'm using < sidebar >, it's working, this is not the problem. The problem is when I use it, the contents such as Dropdowns (as shown in the second pic) don't work when I click them, while when the element contents are simply pasted into the index.html and not in the sidebar.html, it works.
EDIT4:
Found the issue but still no solution. I changed some stuff up and instead of the sidebar it's now on the widgets. Sidebar is now always loaded and it's the page contents which are loaded depending on the URL. This helped me track down the issue:
$(".owl-carousel").owlCarousel({mouseDrag: false, touchDrag: true, slideSpeed: 300, paginationSpeed: 400, singleItem: true, navigation: false,autoPlay: true});
The previous code is in a plugins.js file which is included in the html. For some reason, this line is NOT being run when the page is loaded. When I ran this line in the chrome console, the proper widget appeared.
For some reason, the js contents are not being run when the page loads.
Your main problem is the restrict: 'E',, which is restricting it to elements. This explains why it works for <sidebar>, but not for <div class="sidebar">. If you want to use classes, you need to change it to restrict: 'C'.
Another problem is that when you are trying to use the directive as a class, you are using class="page-sidebar" rather than class="sidebar".
See the docs for directives.
From angular documentation:
The restrict option is typically set to:
'A' - only matches attribute name
'E' - only matches element name
'C' - only matches class name
'M' - only matches comment
These restrictions can all be combined as needed:
'AEC' - matches either attribute or element or class name
The directive definition object for your sidebar quite clearly states that it will treat a DOM node with the tag name sidebar to render the directive template due to the restrict : 'E' property.
So use the directive as an HTML node, and NOT in a class (as it would require the property restrict to be set to C letter).
<sidebar></sidebar>

How can I utilize the capabilities of an AngularJS directive to defer rendering content until a click event?

In our rental application, we make an API call that populates an array, thus triggering an ngRepeat. This creates a list of divs that show basic information about the rental property.
Clicking on a property expands the div and then another API call is made to populate an interior ngRepeat with a list of tenants. Some of these properties have up to 100 tenants listed (past and present are included). The tenant divs themselves are also expandable, and this reveals the majority of the functionality. You can download rental agreements, view history, etc. All of this functionality is a single directive made up of a number of ng-includes.
If you're still following, there's an outside ngRepeat and an interior ngRepeat with a huge directive inside of it.
<div ng-repeat="properties in property_collection track by property.ID>
*code removed*
<div ng-repeat="tenant in property_tenant_collection track by tenant.ID>
*...code here...*
<div tenant-menu></div>
The directive tenant-menu and all of the ngIncludes and watchers that come with it are rendered when you expand the list of properties. They just aren't visible yet. Clicking on the tenant in the list just changes the height of the div, thus revealing the interior menu.
The performance implications of the way this UI was designed are absolutely dire. There's over 15,000 watchers on the page for elements that you can't even see. Taking action on one tenant fires the digest loop for all of them for no reason. It takes under a second to retrieve the necessary data, but almost 20 seconds to render a list of 60 applicants. I've removed the directive completely (which means nothing happens when you click on a tenant) and the loading & rendering time goes from 20 seconds to 2-3 seconds.
I'm not sure how I can achieve this, but is it possible to defer attaching this directive until the tenant is clicked? I don't want clicking on the tenant div to just change the height and reveal what's inside, I want to append the entire directive right then and THEN expand the height. Ideally, when the click event is fired again and the tenant is collapsed I would also be able to destroy any watchers and clean up after myself.
Whew.
Edit: The infamous sliding directive is pasted below. I think it's strange that a click event is being bound inside, but perhaps this is an O.K. code pattern in Angular. I have a hunch that I may be able to leverage the compile, postlink, and prelink functions along with the accepted answer. I inherited a bit of legacy code I think.
angular.module('jnjManagement.directives').directive('slideableTenant', function($compile, tenantService) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
var targetX, contentX;
attrs.expanded = false;
element.bind('click', function() {
if (!targetX) targetX = document.querySelector(attrs.slideToggleTenant);
if (!contentX) contentX = targetX.querySelector('.slideable_tenant_content');
if (!attrs.expanded) {
contentX.style.border = '1px solid rgba(0,0,0,0)';
var y = contentX.clientHeight;
contentX.style.border = 0;
targetX.style.height = 'auto';
} else {
targetX.style.height = '0px';
}
attrs.expanded = !attrs.expanded;
});
}
};
});
Ideally you would have a property directive and a tenant directive and you would indeed only load the content of the expanded element when it was clicked. I would make use of Angular's $compile.
$scope.clickHandler = function(){
$('expanded-element').append($compile('<detail-directive></detail-directive')($scope));
};
Of course you would want to clean out elements when they are collapsed, too, but it sounds like you have a good grasp of performance considerations.

Using Angular, how can I show a DOM element only if its ID matches a scope variable?

I am relatively new to AngularJS.
I have a series of DIVs in a partial view. Each of the DIVs has a unique ID. I want to show / hide these DIVs based on a scope value (that matches one of the unique ID).
I can successfully write out the scope value in the view using something like {{showdivwithid}}
What would be the cleanest way to hide all the sibling divs that dont have an ID of {{showdivwithid}}
I think you are approaching the problem with a jQuery mindset.
Easiest solution is to not use the id of each div and use ngIf.
<div ng-if="showdivwithid==='firstDiv'">content here</div>
<div ng-if="showdivwithid==='secondDiv'">content here</div>
<div ng-if="showdivwithid==='thirdDiv'">content here</div>
If you don't mind the other elements to appear in the DOM, you can replace ng-if with ng-show.
Alternatively use a little directive like this:
app.directive("keepIfId", function(){
return {
restrict: 'A',
transclude: true,
scope: {},
template: '<div ng-transclude></div>',
link: function (scope, element, atts) {
if(atts.id != atts.keepIfId){
element.remove();
}
}
};
});
HTML
<div id="el1" keep-if-id="{{showdivwithid}}">content here</div>
<div id="el2" keep-if-id="{{showdivwithid}}">content here</div>
<div id="el3" keep-if-id="{{showdivwithid}}">content here</div>
First, I want to echo #david004's answer, this is almost certainly not the correct way to solve an AngularJS problem. You can think of it this way: you are trying to make decisions on what to show based on something in the view (the id of an element), rather than the model, as Angular encourages as an MVC framework.
However, if you disagree and believe you have a legitimate use case for this functionality, then there is a way to do this that will work even if you change the id that you wish to view. The limitation with #david004's approach is that unless showdivwithid is set by the time the directive's link function runs, it won't work. And if the property on the scope changes later, the DOM will not update at all correctly.
So here is a similar but different directive approach that will give you conditional hiding of an element based on its id, and will update if the keep-if-id attribute value changes:
app.directive("keepIfId", function(){
return {
restrict: 'A',
transclude: true,
scope: {
keepIfId: '#'
},
template: '<div ng-transclude ng-if="idMatches"></div>',
link: function (scope, element, atts) {
scope.idMatches = false;
scope.$watch('keepIfId', function (id) {
scope.idMatches = atts.id === id;
});
}
};
});
Here is the Plunkr to see it in action.
Update: Why your directives aren't working
As mentioned in the comments on #david004's answer, you are definitely doing things in the wrong way (for AngularJS) by trying to create your article markup in blog.js using jQuery. You should instead be querying for the XML data in BlogController and populating a property on the scope with the results (in JSON/JS format) as an array. Then you use ng-repeat in your markup to repeat the markup for each item in the array.
However, if you must just "get it working", and with full knowledge that you are doing a hacky thing, and that the people who have to maintain your code may hate you for it, then know the following: AngularJS directives do not work until the markup is compiled (using the $compile service).
Compilation happens automatically for you if you use AngularJS the expected, correct way. For example, when using ng-view, after it loads the HTML for the view, it compiles it.
But since you are going "behind Angular's back" and adding DOM without telling it, it has no idea it needs to compile your new markup.
However, you can tell it to do so in your jQuery code (again, if you must).
First, get a reference to the $compile service from the AngularJS dependency injector, $injector:
var $compile = angular.element(document.body).injector().get('$compile');
Next, get the correct scope for the place in the DOM where you are adding these nodes:
var scope = angular.element('.blog-main').scope();
Finally, call $compile for each item, passing in the item markup and the scope:
var compiledNode = $compile(itm)(scope);
This gives you back a compiled node that you should be able to insert into the DOM correctly:
$('.blog-main').append(compiledNode);
Note: I am not 100% sure you can compile before inserting into the DOM like this.
So your final $.each() in blog.js should be something like:
var $compile = angular.element(document.body).injector().get('$compile'),
scope = angular.element('.blog-main').scope();
$.each(items, function(idx, itm) {
var compiledNode = $compile(itm)(scope);
$('.blog-main').append(compiledNode);
compiledNode.readmore();
});

Categories

Resources