Using ng-show directive to toggle multiple elements? - angularjs

Say I've got a <nav> element with three buttons, and an <article> containing three <section> elements. I want a user to be able to click a button, which toggles the display of all of the <section> elements in such a way that only the one relevant <section> is shown.
I'm new to AngularJS and trying to figure out if theres a more Angularly way of achieving this than by giving the nav elements ng-click attributes and the section elements the ng-show attribute. It feels very imperative.
My HTML would look something like this:
<nav>
<div ng-click="showRedSection()"></div>
<div ng-click="showBlueSection()"></div> //etc.
<nav>
<article>
<section ng-show="redSectionShown">
// content; etc.
</section>
//etc.
</article>
Am I on the right track, or is there an approach that makes this more declarative?

What you are going is correct.
Instead of needing a function to toggle the value on click you could do redSectionShown = !!redSectionShown
Or if you have a lot of sections and other data you want to store create a service to store the visible state of the regions and create directive that can toggle the values or use the values to hide elements.
The second approach reduces your $scope pollution.
.service('ViewableRegions', function() {
var viewablePropertiesMap = {};
this.getValue = function(value) {
return viewablePropertiesMap[value];
}
this.setValue = function(valueToUpdate, value) {
viewablePropertiesMap[valueToUpdate] = value
}
})
Directive to toggle region visability
.directive('regionToggler', function(ViewableRegions) {
return {
restrict: 'A',
compile: function($element, $attrs) {
var directiveName = this.name;
var valueToUpdate = attrs[directiveName];
return function(scope, element, attrs) {
element.on('click', function() {
var currentValue = ViewableRegions.getValue(ViewableRegions);
ViewableRegions.setValue(valueToUpdate, !!currentValue);
scope.$apply();
})
};
}
};
}
);
Directive to display the regions
.directive('regionDisplayer', function(ViewableRegions) {
return {
restrict: 'A',
compile: function($element, $attrs) {
var directiveName = this.name;
var valueToUpdate = attrs[directiveName];
return function(scope, element, attrs) {
scope.$watch(
function() {
return ViewableRegions.getValue(ViewableRegions);
},
function(newVal) {
if (newVal) {
element[0].style.display = "none";
} else {
element[0].style.display = "";
}
}
)
};
}
};
}
);
HTML Uasge
//etc.
<article>
<section region-displayer="redSectionShown">
// content; etc.
</section>
//etc.
</article>

I would actually recommend trying to use ng-hide as opposed to ng-show. These directives complete the same task most of the time,but ng-hide defaults to a hidden state, where you can then setup parameters in which the information will be shown. I ran into a similar problem with an app I was working on in angular, and using ng-hide seemed to help simplify the code in my view.

You could try something like:
<div ng-click="sectionToShow=1"></div>
<div ng-click="sectionToShow=2"></div>
<article>
<section ng-show="sectionToShow==1"></section>
</article>
I haven't tested this, but there's no reason it shouldn't work. That way, you will automatically only ever show the correct section, and all other sections will auto hide themselves.

Related

Angular JS conditional logic to check "class name" exists

Below Angular JS code works fine on Mouseover & Mouseout. Need help regard adding conditional logic on JS code.
If class name "active" exists, img src path have to be in "overImg" even if user mouseover & mouseout. But, present behaviour removes overImg once user mouseout from element. Active state have to be different from the rest of navigation element.
AngularJS:
.directive('eleHoverAction', function() {
return {
link: function (scope, elem, attrs) {
var imgObj = $(elem).find('img');
var upImg = attrs.eleUpImgSrc;
var overImg = attrs.eleOverImgSrc;
elem.bind('mouseover', function () {
$(imgObj).attr("src", overImg);
scope.$apply();
});
elem.bind('mouseout', function() {
$(imgObj).attr("src", upImg);
scope.$apply();
});
}
};
});
HTML:
<li class="menu-item menu-item--category active" ele-hover-action ele-up-img-src="images/test1.png" ele-over-img-src="images/test1-over.png">
<img src="images/test1.png" oversrc="images/test1-over.png" alt=""/><span>Test1</span>
</li>
<li class="menu-item menu-item--category" ele-hover-action ele-up-img-src="images/test2.png" ele-over-img-src="images/test2-over.png">
<img src="images/test2.png" oversrc="images/test2-over.png" alt=""/><span>Test2</span>
</li>
The most obvious method is to add an if statement to your "mouseout" handler that checks if the element hasClass active:
.directive('eleHoverAction', function() {
return {
link: function (scope, elem, attrs) {
var imgObj = elem.find('img');
var upImg = attrs.eleUpImgSrc;
var overImg = attrs.eleOverImgSrc;
elem.bind('mouseover', function () {
imgObj.attr("src", overImg);
scope.$apply();
});
elem.bind('mouseout', function() {
if (!elem.hasClass("active")) {
imgObj.attr("src", upImg);
}
scope.$apply();
});
if (elem.hasClass("active")) {
imgObj.attr("src", overImg);
} else {
imgObj.attr("src", upImg);
}
}
};
});
I went ahead and set the src attribute of the image based on the directive attributes. You could just take that part out if you don't want it. Also, wrapping elem in a jQuery call is redundant because Angular elements are already wrapped in either jQuery (if available when Angular loads) or its own jQLite. Otherwise, you wouldn't be able to call elem.bind.
Try it in a fiddle.

Angular ui-router - expose a portion of the layout to a child state/template

Taking the following mockup screenshot as an example, I need to be able to put some custom actions on the top right of the view. What the user sees there will depend on the current view that's being displayed (e.g. share button).
I'm using ui-router, and nested states. I may be approaching it wrong, so my question is more generic than specific. What is the best way to go about exposing a portion of the layout to a child state that may be a few layers deep?
<div>
<header id="menu">
<a class="fa fa-bars"></a>
<div>Title</div>
<div class="actions">
<!-- I want a child state, possibly multiple levels deep in the
state hierarchy, to be able to insert content here -->
</div>
</header>
<div id="content">
<div ui-view></div>
</div>
</div>
I can see a couple of approaches, pick one that feels the best to you:
1) The actions view could be implemented as a directive that watches $state.current and based on the current state, update its view accordingly. If you want the ability for each individual state to specify their own actions, then perhaps setting the data property when using $stateProvider.state, and using that information to determine what actions there are could work.
2) Implement some kind of cross transclude directive. So something like this (untested and not fully implemented, be sure to test for memory leaks):
angular.module('app',[]).directive('crossTransclude', function(){
return {
require : '^crossTranscludeController',
transclude: true,
link: function($scope, $el, $attr, ctrl, transclude){
ctrl.setTranscludeFn(transclude);
$scope.$on('$destroy', function(){
ctrl.unsetTranscludeFn(transclude);
});
}
};
})
.directive('crossTranscludeController', function(){
return {
controller : function CrossTranscludeController(){
this.transcludeFn = null;
this.transcludeTargetEl = null;
this.setTranscludeFn = function(func){
this.transcludeFn = func;
this._crossTransclude();
}
this.unsetTranscludeFn = function(){
this.transcludeFn = null;
this._crossTransclude();
}
this.registerTranscludeTarget = function($element){
this.transcludeTargetEl = $element;
this._crossTransclude();
}
this.deregisterTranscludeTarget = function(){
this.transcludeTargetEl = null;
this._crossTransclude();
}
this._crossTransclude = function(){
if(this.transcludedClone)
if(this.transcludeFn && this.transcludeTargetEl){
this.transcludeFn(function(clone){
this.transcludeTargetEl.empty();
this.transcludeTargetEl.append(clone);
});
} else {
if(this.transcludeTargetEl){
this.transcludeTargetEl.empty();
}
}
}
}
};
})
.directive('crossTranscludeTarget', function(){
return {
require: '^crossTranscludeController',
link: function($scope, $el, $attr, ctrl){
ctrl.registerTranscludeTarget($el);
$scope.$on('$destroy', function(){
ctrl.deregisterTranscludeTarget();
});
}
};
});
Which is used like:
index.html
<div cross-transclude-controller>
<div cross-transclude-target>
</div>
<div ui-view>
</div>
</div>
subview.html
<div>
<div cross-transclude>
This content should end up in 'cross-transclude-target', bound to the subview's scope
</div>
</div>

How do I use an Angular directive to show a dialog?

Using Angular, I'm trying to create a directive that will be placed on a button that will launch a search dialog. There are multiple instances of the search button, but obviously I only want a single instance of the dialog. The dialog should be built from a template URL and have it's own controller, but when the user selects an item, the directive will be used to set the value.
Any ideas on how to create the dialog with it's own controller from the directive?
Here's what I've go so far (basically just the directive)...
http://plnkr.co/edit/W9CHO7pfIo9d7KDe3wH6?p=preview
Here is the html from the above plkr...
Find
Here is the code from the above plkr...
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
var person = {};
person.name = 'World';
$scope.person = person;
$scope.setPerson = function(newPerson) {
person = newPerson;
$scope.person = person;
}
});
app.directive('myFind', function () {
var $dlg; // holds the reference to the dialog. Only 1 per app.
return {
restrict: 'A',
link: function (scope, el, attrs) {
if (!$dlg) {
//todo: create the dialog from a template.
$dlg = true;
}
el.bind('click', function () {
//todo: use the dialog box to search.
// This is just test data to show what I'm trying to accomplish.
alert('Find Person');
var foundPerson = {};
foundPerson.name = 'Brian';
scope.$apply(function () {
scope[attrs.myFind](foundPerson);
});
});
}
}
})
This is as far as I've gotten. I can't quite figure out how to create the dialog using a template inside the directive so it only occurs once and then assign it a controller. I think I can assign the controller inside the template, but first I need to figure out how to load the template and call our custom jQuery plugin to generate the dialog (we have our own look & feel for dialogs).
So I believe the question is, how do I load a template inside of a directive? However, if there is a different way of thinking about this problem, I would be interested in that as well.
I will show you how to do it using bootstrap-ui. (you can modify it easily, if it does not suit your needs).
Here is a skeleton of the template. You can normally bound to any properties and functions that are on directive's scope:
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
... // e.g. <div class="button" ng-click=cancel()></div>
</div>
<div class="modal-body">
...
</div>
<div class="modal-footer">
...
</div>
</div>
</div>
Here is how to create/declare directive in your module:
.directive("searchDialog", function ($modal) {
return {
controller: SearchDialogCtrl,
scope : {
searchDialog: '=' // here you will set two-way data bind with a property from the parent scope
},
link: function (scope, element, attrs) {
element.on("click", function (event) { // when button is clicked we show the dialog
scope.modalInstance = $modal.open({
templateUrl: 'views/search.dialog.tpl.html',
scope: scope // this will pass the isoleted scope of search-dialog to the angular-ui modal
});
scope.$apply();
});
}
}
});
Then controller may look something like that:
function SearchDialogCtrl(dep1, dep2) {
$scope.cancel = function() {
$scope.modalInstance.close(); // the same instance that was created in element.on('click',...)
}
// you can call it from the template: search.dialog.tpl.html
$scope.someFunction = function () { ... }
// it can bind to it in the search.dialog.tpl.html
$scope.someProperty;
...
// this will be two-way bound with some property from the parent field (look below)
// if you want to perform some action on it just use $scope.$watch
$scope.searchDialog;
}
Then it your mark-up you can just use it like that:
<div class="buttonClass" search-dialog="myFieldFromScope">search</div>
I recommend this plugin:
https://github.com/trees4/ng-modal
Demo here:
https://trees4.github.io/ngModal/demo.html
Create a dialog declaratively; and it works with existing controllers. The content of the dialog can be styled however you like.

Ng-controller on same element as ng-repeat - no two-way-data-binding

I can't get two-way-data-binding to work in an Angular js ng-repeat.
I have an ng-controller specified on the same element that has the ng-repeat specified -
I just learnt that by doing this, I can get a hold of each item that is being iterated over by ng-repeat. Here is the HTML:
<div ng-controller="OtherController">
<div id="hoverMe" ng-controller="ItemController" ng-mouseenter="addCaption()"
ng-mouseleave="saveCaption()" ng-repeat="image in images">
<div class="imgMarker" style="{{image.css}}">
<div ng-show="image.captionExists" class="carousel-caption">
<p class="lead" contenteditable="true">{{image.caption}}</p>
</div>
</div>
</div>
</div>
And here is the ItemController:
function ItemController($scope){
$scope.addCaption = function(){
if($scope.image.captionExists === false){
$scope.image.captionExists = true;
}
}
$scope.saveCaption = function(){
console.log($scope.image.caption);
}
}
And the OtherController:
function OtherController($scope){
$scope.images = ..
}
When I hover the mouse over the #hoverMe-div - the caption-div is added correctly. But when I input some text in the paragraph and then move the mouse away from the #hoveMe-div, the $scope.image-variables caption value is not updated in the saveCaption-method. I understand I'm missing something. But what is it?
You don't need a ng-controller specified on the same element that has the ng-repeat to be able to get each item.
You can get the item like this:
<div ng-repeat="image in images" ng-mouseenter="addCaption(image)" ng-mouseleave="saveCaption(image)" class="image">
And in your controller code:
$scope.addCaption = function (image) {
if(!image.captionExists){
image.captionExists = true;
}
};
To get contenteditable to work you need to use ng-model and a directive that updates the model correctly.
Here is a simple example based on the documentation:
app.directive('contenteditable', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, controller) {
element.on('blur', function() {
scope.$apply(function() {
controller.$setViewValue(element.html());
});
});
controller.$render = function(value) {
element.html(value);
};
}
};
});
Note that the directive probably needs more logic to be able to handle for example line breaks.
Here is a working Plunker: http://plnkr.co/edit/0L3NKS?p=preview
I assume you are editing the content in p contenteditable and are expecting that the model image.caption is update. To make it work you need to setup 2 way binding.
2 way binding is available for element that support ng-model or else data needs to be synced manually. Check the ngModelController documentation and the sample available there. It should serve your purpose.

AngularJS: ng-repeat list is not updated when a model element is spliced from the model array

I have two controllers and share data between them with an app.factory function.
The first controller adds a widget in the model array (pluginsDisplayed) when a link is clicked. The widget is pushed into the array and this change is reflected into the view (that uses ng-repeat to show the array content):
<div ng-repeat="pluginD in pluginsDisplayed">
<div k2plugin pluginname="{{pluginD.name}}" pluginid="{{pluginD.id}}"></div>
</div>
The widget is built upon three directives, k2plugin, remove and resize. The remove directive adds a span to the template of the k2plugin directive. When said span is clicked, the right element into the shared array is deleted with Array.splice(). The shared array is correctly updated, but the change is not reflected in the view. However, when another element is added, after the remove, the view is refreshed correctly and the previously-deleted element is not shown.
What am I getting wrong? Could you explain me why this doesn't work?
Is there a better way to do what I'm trying to do with AngularJS?
This is my index.html:
<!doctype html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.5/angular.min.js">
</script>
<script src="main.js"></script>
</head>
<body>
<div ng-app="livePlugins">
<div ng-controller="pluginlistctrl">
<span>Add one of {{pluginList.length}} plugins</span>
<li ng-repeat="plugin in pluginList">
<span>{{plugin.name}}</span>
</li>
</div>
<div ng-controller="k2ctrl">
<div ng-repeat="pluginD in pluginsDisplayed">
<div k2plugin pluginname="{{pluginD.name}}" pluginid="{{pluginD.id}}"></div>
</div>
</div>
</div>
</body>
</html>
This is my main.js:
var app = angular.module ("livePlugins",[]);
app.factory('Data', function () {
return {pluginsDisplayed: []};
});
app.controller ("pluginlistctrl", function ($scope, Data) {
$scope.pluginList = [{name: "plugin1"}, {name:"plugin2"}, {name:"plugin3"}];
$scope.add = function () {
console.log ("Called add on", this.plugin.name, this.pluginList);
var newPlugin = {};
newPlugin.id = this.plugin.name + '_' + (new Date()).getTime();
newPlugin.name = this.plugin.name;
Data.pluginsDisplayed.push (newPlugin);
}
})
app.controller ("k2ctrl", function ($scope, Data) {
$scope.pluginsDisplayed = Data.pluginsDisplayed;
$scope.remove = function (element) {
console.log ("Called remove on ", this.pluginid, element);
var len = $scope.pluginsDisplayed.length;
var index = -1;
// Find the element in the array
for (var i = 0; i < len; i += 1) {
if ($scope.pluginsDisplayed[i].id === this.pluginid) {
index = i;
break;
}
}
// Remove the element
if (index !== -1) {
console.log ("removing the element from the array, index: ", index);
$scope.pluginsDisplayed.splice(index,1);
}
}
$scope.resize = function () {
console.log ("Called resize on ", this.pluginid);
}
})
app.directive("k2plugin", function () {
return {
restrict: "A",
scope: true,
link: function (scope, elements, attrs) {
console.log ("creating plugin");
// This won't work immediately. Attribute pluginname will be undefined
// as soon as this is called.
scope.pluginname = "Loading...";
scope.pluginid = attrs.pluginid;
// Observe changes to interpolated attribute
attrs.$observe('pluginname', function(value) {
console.log('pluginname has changed value to ' + value);
scope.pluginname = attrs.pluginname;
});
// Observe changes to interpolated attribute
attrs.$observe('pluginid', function(value) {
console.log('pluginid has changed value to ' + value);
scope.pluginid = attrs.pluginid;
});
},
template: "<div>{{pluginname}} <span resize>_</span> <span remove>X</span>" +
"<div>Plugin DIV</div>" +
"</div>",
replace: true
};
});
app.directive("remove", function () {
return function (scope, element, attrs) {
element.bind ("mousedown", function () {
scope.remove(element);
})
};
});
app.directive("resize", function () {
return function (scope, element, attrs) {
element.bind ("mousedown", function () {
scope.resize(element);
})
};
});
Whenever you do some form of operation outside of AngularJS, such as doing an Ajax call with jQuery, or binding an event to an element like you have here you need to let AngularJS know to update itself. Here is the code change you need to do:
app.directive("remove", function () {
return function (scope, element, attrs) {
element.bind ("mousedown", function () {
scope.remove(element);
scope.$apply();
})
};
});
app.directive("resize", function () {
return function (scope, element, attrs) {
element.bind ("mousedown", function () {
scope.resize(element);
scope.$apply();
})
};
});
Here is the documentation on it: https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$apply
If you add a $scope.$apply(); right after $scope.pluginsDisplayed.splice(index,1); then it works.
I am not sure why this is happening, but basically when AngularJS doesn't know that the $scope has changed, it requires to call $apply manually. I am also new to AngularJS so cannot explain this better. I need too look more into it.
I found this awesome article that explains it quite properly.
Note: I think it might be better to use ng-click (docs) rather than binding to "mousedown". I wrote a simple app here (http://avinash.me/losh, source http://github.com/hardfire/losh) based on AngularJS. It is not very clean, but it might be of help.
I had the same issue. The problem was because 'ng-controller' was defined twice (in routing and also in the HTML).
Remove "track by index" from the ng-repeat and it would refresh the DOM
There's an easy way to do that. Very easy. Since I noticed that
$scope.yourModel = [];
removes all $scope.yourModel array list you can do like this
function deleteAnObjectByKey(objects, key) {
var clonedObjects = Object.assign({}, objects);
for (var x in clonedObjects)
if (clonedObjects.hasOwnProperty(x))
if (clonedObjects[x].id == key)
delete clonedObjects[x];
$scope.yourModel = clonedObjects;
}
The $scope.yourModel will be updated with the clonedObjects.
Hope that helps.

Resources