Nested Angular Controllers and Views management - javascript

I guess it is best to describe it with a picture. I have an angular app and here is a simple view.
Obvious explanation: list shows all the entities, if you click on an entity you can edit it in the form that is hidden by default and similar action applies to adding a new entity.
the issue
I know it is basic example so here the solution might be an overkill but I want to separate the logic of 'Add new entity', 'Edit entity' and 'Entities list'. I thought I could implement it like this:
<div ng-include="'userAddForm.html'"
ng-show="???"
ng-controller="AddUser as add">
</div>
<div ng-include="'userEditForm.html'"
ng-show="???"
ng-controller="AddEdit as edit">
</div>
<div class="panel panel-default">
... list managed by the current controller
</div>
What I miss
I have a difficulty in sharing a state of the hidden parts. For example some boolean flag. For instance:
Click on the entity shows the edit form
Save/Cancel in the edit form hides the part
Then, I think the first step is the responsibility of list-controller, but save/cancel part goes to edit-controller. It would be only possible to share the value with a service included in both - but that does not seem reasonable either.
I think there is some simple solution I can not see and I am open for any advice. Thanks!

If your goal is a simple solution with just a boolean being toggled in the model, you can use child controllers like this:
http://plnkr.co/edit/P1ncToJwqvxt9F9MTF5E?p=preview
The child controllers will inherit the scope of the parent controller and can directly edit the values. I have the edit child controller filtering for editMode==true, so when the parent changes that value, the child controller automatically shows the item. All changes are updated live and the child controller simply toggles the editMode property to remove it from the editing area.
Similar logic is used for the add child controller.
The views look like this:
index.html
<div ng-controller="myCtrl">
<div ng-controller="addCtrl" ng-include="'userAddForm.html'">
</div>
<div ng-controller="editCtrl" ng-include="'userEditForm.html'">
</div>
<h1>Listing</h1>
<ul>
<li ng-repeat="item in items | filter:{addMode:false}">
{{item.id}}
{{item.name}}
<button ng-click="startEditing(item)">[ edit ]</button>
</li>
</ul>
<button ng-click="startAdding()">[ add ]</button>
<div>Debug:<br>{{items}}</div>
</div>
userAddForm.html
<ul>
<li ng-repeat="item in items | filter:{addMode:true}">
<input type="text" ng-model="item.id">
<input type="text" ng-model="item.name">
<button ng-click="add(item)">[ add ]</button>
<button ng-click="cancel(item)">[ cancel ]</button>
</li>
</ul>
userEditForm.html
<ul>
<li ng-repeat="item in items | filter:{editMode:true}">
<input type="text" ng-model="item.id">
<input type="text" ng-model="item.name">
<button ng-click="save(item)">[ save ]</button>
</li>
</ul>
And the controllers look like this:
angular.module('myApp.controllers',[])
.controller('addCtrl', function($scope) {
$scope.add = function(item) {
item.addMode = false;
}
$scope.cancel = function(item) {
$scope.items.pop(item);
}
})
.controller('editCtrl', function($scope) {
$scope.save = function(item) {
item.editMode = false;
}
})
.controller('myCtrl', function($scope) {
$scope.items = [
{name:'aap', id:"1", editMode:false, addMode:false},
{name:'noot', id:"2", editMode:false, addMode:false},
{name:'mies', id:"3", editMode:false, addMode:false},
{name:'zus', id:"4", editMode:false, addMode:false}
];
$scope.startAdding = function(){
$scope.items.push({addMode:true});
};
$scope.startEditing = function(item){
item.editMode = true;
};
});

You can achieve this using Angular state routing.In which you will create state (different views) like -
header
addEntity
editEntity
listEntity
refer https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views

Sharing state can be implemented by creating a service which is than injected to all interested párties (controllers), service can hold data which controllers can be bound to and display in template. Services in Angular JS are singletons so all the controllers are accesing and mutating shared state.

Related

Ng-click changing key,value pair

I have a ng-repeat like this:
<div ng-app="myApp" ng-controller="Ctrl">
{{ctrlTest}}<hr/>
<div ng-repeat="elements in filter">
<div>
<li ng-repeat="(key,value) in filter.producers" ng-show="value">
{{key}}<a ng-click="filter.producers.key=false"> X</a>
</li>
</div>
{{filter.producers}}
</div>
angular.module('myApp', [])
.controller('Ctrl', function($scope) {
$scope.ctrlTest = "Filters";
$scope.filter = {"producers": {"Ford":true,"Honda":true,"Ferrari":true}}
});
I am trying to make a ng-click that would set each label to false when clicking in a link, but I haven't achieved to do it properly as the key values are not fixed (they should be treated as variables).
So far I have tried it his way.
http://jsfiddle.net/Joe82/wjz8270z/5/
Thanks in advance!
Ps: I cannot change the json structure.
You just need to access the element of object by its key, to ensure that there references would not get lost & binding will work
<li ng-repeat="(key,value) in filter.producers" ng-show="value">
{{key}}<a ng-click="filter.producers[key]=false"> X</a>
</li>
Forked Fiddle
You also call a function and set value false
HTML
<li ng-repeat="(key,value) in filter.producers" ng-show="value">{{key}} {{value}}<a ng-click="setValue(key)"> X</a>
JS
$scope.setValue = function(key){
$scope.filter.producers[key.toString()] = false;
}
see this link http://jsfiddle.net/wjz8270z/8/

Angular1.3: Access functions from standard controllers, inside a view which uses controllerAs?

I have a simple controller defined in my main app.js file, which controls opening/closing of my navbar and is visible on all other views of my app:
app.js:
.controller('mainController', ['$scope', function($scope){
$scope.menuActive = false;
$scope.toggleMenu = function(){
$scope.menuActive = !$scope.menuActive;
}
}]);
index.html:
<nav class="side-menu" ng-class="{ 'side-menu-open' : menuActive }">
<ul>
<li>LINK1</li>
<li>LINK2</li>
</ul>
</nav>
<!--Other views::.....-->
<div ui-view></div>
All my other views(which use controllerAs), have a button with an ng-click which I am using to access the above $scope.toggleMenu() function and ng-class, but this does not work, and I don't get any errors either:
view1.html :
<span class="btn btn-default"
ng-click="toggleMenu()">
MENU
</span>
View1.js:
angular
.module('myApp.view1', [])
.controller('View1Ctrl', [
function(){
................
}
]);
Also, the reason I have done it this way again is because my navbar is persistent throughout my app. Does this go against best practices by any chance?
If you are using the .. controller as .. syntax, make sure that you are using it for all controllers. Don't be selective about it.
Next, when using the syntax, you need not inject the $scope object. You need to instead use the this variable and attach any properties or functions that you would normally associate with the $scope object with the this object instead.
Thus,
$scope.toggleMenu = function(){
$scope.menuActive = !$scope.menuActive;
}
becomes
this.toggleMenu = function(){
this.menuActive = !this.menuActive;
}
Finally in your view, be sure to associate each expression with a controller.
<div ng-controller="mainController as main">
<nav class="side-menu" ng-class="{ 'side-menu-open' : main.menuActive }">
<ul>
<li>LINK1</li>
<li>LINK2</li>
</ul>
</nav>
<div ui-view>
<!-- Assuming that the following gets compiled to ui-view -->
<span class="btn btn-default" ng-click="main.toggleMenu()">
MENU
</span>
</div>
</div>
You can get some further hints on using the controller as syntax here

Pass parameter to Angular ng-include

I am trying to display a binary tree of elements, which I go through recursively with ng-include.
What is the difference between ng-init="item = item.left" and ng-repeat="item in item.left" ?
In this example it behaves exactly the same, but I use similiar code in a project and there it behaves differently. I suppose it's because of Angular scopes.
Maybe I shouldn't use ng-if, please explain me how to do it better.
The pane.html is:
<div ng-if="!isArray(item.left)">
<div ng-repeat="item in [item.left]" ng-include="'Views/pane.html'">
</div>
</div>
<div ng-if="isArray(item.left)">
{{item.left[0]}}
</div>
<div ng-if="!isArray(item.right)">
<div ng-repeat="item in [item.right]" ng-include="'Views/pane.html'">
</div>
</div>
<div ng-if="isArray(item.right)">
{{item.right[0]}}
</div>
<div ng-if="!isArray(item.left)">
<div ng-init = "item = item.left" ng-include="'Views/pane.html'">
</div>
</div>
<div ng-if="isArray(item.left)">
{{item.left[0]}}
</div>
<div ng-if="!isArray(item.right)">
<div ng-init="item = item.right" ng-include="'Views/pane.html'">
</div>
</div>
<div ng-if="isArray(item.right)">
{{item.right[0]}}
</div>
The controller is:
var app = angular.module('mycontrollers', []);
app.controller('MainCtrl', function ($scope) {
$scope.tree = {
left: {
left: ["leftleft"],
right: {
left: ["leftrightleft"],
right: ["leftrightright"]
}
},
right: {
left: ["rightleft"],
right: ["rightright"]
}
};
$scope.isArray = function (item) {
return Array.isArray(item);
}
});
EDIT:
First I run into the problem that the directive ng-repeat has a greater priority than ng-if. I tried to combine them, which failed. IMO it's strange that ng-repeat dominates ng-if.
It's a little hacky, but I am passing variables to an ng-include with an ng-repeat of an array containing a JSON object :
<div ng-repeat="pass in [{'text':'hello'}]" ng-include="'includepage.html'"></div>
In your include page you can access your variable like this:
<p>{{pass.text}}</p>
Pass parameter to Angular ng-include
You don't need that. all ng-include's sources have the same controller. So each view sees the same data.
What is the difference between ng-init="item = item.left" and ng-repeat="item in item.left"
[1]
ng-init="item = item.left" means - creating new instance named item that equals to item.left. In other words you achieve the same by writing in controller:
$scope.item = $scope.item.left
[2]
ng-repeat="item in item.left" means create list of scopes based on item.left array. You should know that each item in ng-repeat has its private scope
I am trying to display a binary tree of elements, which I go through recursively with ng-include.
I posted in the past answer how to display tree by using ng-include.
It might helpful: how-do-display-a-collapsible-tree
The main part here that you create Node with id wrapped by <scipt> tag and use ng-repeat:
<script type="text/ng-template" id="tree_item_renderer">
<ul class="some" ng-show="data.show">
<li ng-repeat="data in data.nodes" class="parent_li" ng-include="'tree_item_renderer'" tree-node></li>
</ul>
</script>
<ul>
<li ng-repeat="data in displayTree" ng-include="'tree_item_renderer'"></li>
Making a generic directive instead of ng-include is a cleaner solution:
Angular passing scope to ng-include
I am using ng-include with ng-repeat of an array containing string. If you want to send multple data so please see Junus Ergin answer.
See my code Snippet:
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="">
<div ng-repeat="name in ['Sanjib Pradhan']" ng-include="'your_template.html'"></div>
<div ng-repeat="name in ['Chinmay Sahu']" ng-include="'your_template.html'"></div>
<script type="text/ng-template" id="your_template.html">
{{name}}
</script>
</div>

AngularJS - change parent scope from directive?

I'm building my first Angular app, but am having a bit of trouble getting something to work. I have a video container that will be hidden until $scope.video.show = true; I'm trying to set this value when I click on a link. I'm trying to make that happen in a directive. Any help would be appreciated.
html:
<div ng-controller="AppCtrl">
<div ng-cloak
ng-class="{'show':video.show, 'hide':!video.show}">
// youtube iframe content, for example
</div>
<div>
<ul>
<li>
<h3>Video Headline 1</h3>
<button type="button"
video-show
data-video-id="jR4lLJu_-wE">PLAY NOW 〉</button>
</li>
<li>
<h3>Video Headline 2</h3>
<button type="button"
video-show
data-video-id="sd0f9as8df7">PLAY NOW 〉</button>
</li>
</ul>
</div>
</div>
javascript:
var thisViewModel = angular.module("savings-video", [])
.controller('SavingsVideoController', function($scope) {
$scope.video = {
show : false,
videoId : ""
};
};
thisViewModel.directive("videoShow", function(){
return{
restrict: 'A',
link: function(scope , element){
element.bind("click", function(e){
var $this = angular.element(element);
$this.closest('li').siblings().addClass('hide'); // hide the other one
$this.closest('li').removeClass('hide'); // keep me open
scope.video.show = true; // doesn't work.
// what is the best way to do this?
});
}
}
});
I see a few things you can improve.
Checkout ngShow/ngHide and ngIf; they'll give you toggle-ability more easily than trying to do it from scratch.
Think in angular. Rather than trying to use logic to modify the DOM on your own, simply setup your rules using angular directives, and let the framework do the rest for you.
For example, it seems like this is more what you want.
<div ng-controller="AppCtrl">
<div ng-cloak ng-show='video.show">
// youtube iframe content, for example
</div>
<div>
<ul ng-switch="video.videoId">
<my-video my-video-id="jR4ABCD" my-headline="Video Headline 1" ng-switch-when="myVideoId" my-video-manager="video" />
<my-video my-video-id="al1jd89" my-headline="Video Headline 2" ng-switch-when="myVideoId" my-video-manager="video"/>
</ul>
</div>
</div>
What I changed is making your iframe show conditionally with ngShow, and using ngSwitch to control which video appears (the appearing video is based on the $scope's video.videoId). Then, I turned your <li>s into a directive called my-video, which ends up looking like this
thisViewModel.directive("my-video", function(){
return{
restrict: 'E',
replace: true,
scope: {
myVideoId = "=",
myHeadline = "=",
myVideoManager = "="
},
template = '<li><h3>{{myHeadline}}</h3><button type="button" ng-click="play()">PLAY NOW 〉</button></li>',
link: function(scope , element){
scope.play = function(){
myVideoManager.show = true;
/*whatever you want here, using scope.myVideoId*/
}
}
}
});
This directive does exactly what your old HTML did, but brings it into the angular framework so you can access the properties you're looking for. By using the raw angular directives, I eliminate the need for any manual UI logic; I don't need to access element at all anymore, and both my HTML and JavaScript are cleaner. There's certainly room for improvement here, even, but I would say that this is closer to the right track.
It takes practice to get more familiar with, but following the guidelines in the SO link above will help.
EDIT
Sorry, think I missed a requirement the first time around. If you want both videos to show when none are selected, don't use ng-switch; just set up some manual ng-shows.
<div>
<ul>
<my-video my-video-id="jR4ABCD" my-headline="Video Headline 1" ng-show="myVideoId == video.videoId" my-video-manager="video" />
<my-video my-video-id="al1jd89" my-headline="Video Headline 2" ng-show="myVideoId == video.videoId" my-video-manager="video"/>
</ul>
</div>
Since ng-switch is really just a shortcut for ng-show anyways, it amounts to the same thing; the logic just got moved into the ng-show attribute instead.
Also, if you have an array of videos, checkout out ng-repeat; it will let you repeat your video tag multiple times automatically, instead of by hand.
<ul>
<my-video ng-repeat='aVideo in myVideoArray' my-video-id='aVideo.videoId' my-headline...(and so on)>
</ul>
Well your controller names don't match up. Try changing AppCtrl to SavingsVideoController.
You only need a very simple solution.
HTML
<div ng-controller="AppCtrl">
<div ng-cloak ng-show="view.show">
<!-- Use ng-show is more convenient -->
</div>
<div>
<ul>
<li>
<h3>Video Headline 1</h3>
<button type="button"
ng-click="view.show = true"
data-video-id="jR4lLJu_-wE">PLAY NOW 〉</button>
<!-- You don't need an extra directive to change view.show -->
</li>
<li>
<h3>Video Headline 2</h3>
<button type="button"
ng-click="view.show = true"
data-video-id="sd0f9as8df7">PLAY NOW 〉</button>
</li>
</ul>
</div>
</div>
JS
var thisViewModel = angular.module("savings-video", [])
.controller('SavingsVideoController', function($scope) {
$scope.video = {
show : false,
videoId : ""
};
};
// No need to create another directive

supplying AngularJS controller data

Assuming a controller is there to manipulate some data on the scope, what is the best practice to supply the controller with that data?
For example, if I have a list of items I might want a ListController to manipulate the list, and an ItemController to manipulate an individual item. But how do I give each ItemController an item to use?
In the case below, how would doSomethingToItem access the item. When nested in the ngRepeat the item is $scope.item, but in the selection view the item we want to manipulate is $scope.selection.
angular.module('MyApp', [])
.controller('ListController', function($scope, $http) {
$scope.list = $http.get(...);
$scope.selection = null;
})
.controller('ItemController', function($scope) {
$scope.doSomethingToItem = function() {
...
};
})
<div ng-controller="ListController">
<div ng-repeat="item in list" ng-click="selection = item">
<div ng-controller="ItemController">
<button ng-click="doSomethingToItem()">Do Something</button>
</div>
</div>
<div ng-show="selection"
<div ng-controller="ItemController">
<button ng-click="doSomethingToItem()">Do Something</button>
</div>
</div>
</div>
Isn't this a common structure, or is my thinking backwards?
You should understand that Angular would create n+1 ItemController in your case. N for items and 1 for the selection section.
To make passing the object that needs to be worked on easier you can change the method signature to
doSomethingToItem(item) and in html do
<button ng-click="doSomethingToItem(item)">Do Something</button> at both places.
Or else for the repeat case the item variable contains your object that you can access in ItemController
selection variable contains the reference to the select controller, which can be reference from the instance of the controller defined under selection section.
Update: The expression in ng-repeat and selection would differ
<button ng-click="doSomethingToItem(item)">Do Something</button>
and
<div ng-show="selection"
<div ng-controller="ItemController">
<button ng-click="doSomethingToItem(selection)">Do Something</button>
</div>
</div>
You could pass the item data model like this:
<div ng-init="instance = item" ng-controller="ItemController">
</div>
"instance" will be a reference to list array data model item in "ListController".
And you could access its property in your ItemController function closure:
.controller("ItemController", function($scope){
$scope.instance={};
$scope.doSomething = function(){
console.log($scope.instance.name);
}
$scope.$watch('instance',function(){
console.log("iitem changed");
},true);
});
I'm not quite sure what feature do you want to achieve in your "selection" implementation.
I think you want to implement a selected list and list item will be added to it when user clicked the list item. You could try to create a "selected list" model to control the selected list view if you want to add the selected item to a list.
ListController.js
.controller("ListController", function($scope){
$scope.selectedList = [];
$scope.addItem = function(item){
$scope.selectedList.push(item);
}
});
HTML
<div ng-repeat="selected in selectedList">
<div ng-init="instance = selected" ng-controller="ItemController">
<span>{{instance.name}}</span>
<button ng-click="doSomething()">selectedAction</button>
</div>
</div>
I wrote a simple multi-selected list example as following:
HTML
<!doctype html>
<html lang="en" ng-app="myApp">
<head>
<meta charset="UTF-8">
<title>Nested controller</title>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.min.js"></script>
<script src="js/nestedController.js"></script>
</head>
<body>
<div ng-controller="parentCtrl">
<h2>List</h2>
<div ng-repeat="item in list">
<input type="checkbox" ng-model="item.selected" ng-checked="item.selected"/>
<div ng-init="instance = item" ng-controller="childCtrl">
<span>{{instance.name}}</span>
<button ng-click="doSomething()">doSomething</button>
</div>
</div>
<h2>Selected</h2>
<div ng-repeat="selected in selectedList">
<div ng-init="instance = selected" ng-controller="childCtrl">
<span>{{instance.name}}</span>
<button ng-click="selectedAction()">selectedAction</button>
</div>
</div>
</div>
JS
angular.module("myApp",[])
.controller("parentCtrl",function($scope){
//test data
$scope.list = [{name:'item1',age:'12',selected:false},{name:'item2',age:'18',selected:false}];
//use model to control selected list view
$scope.selectedList = [];
//refresh the selected list model when the list checked stauts has been updated
$scope.$watch('list',function(){
console.log("parent controller detected change");
$scope.selectedList = [];
$scope.list.forEach(function(elem,index,array){
if(elem.selected===true){
$scope.selectedList.push(elem);
}
});
},true);
})
.controller("childCtrl",function($scope){
$scope.instance={}
$scope.doSomething = function(){
alert("I'm the item: "+$scope.instance.name);
}
$scope.selectedAction = function(){
alert("I'm the selected item: "+$scope.instance.name);
}
//could register a watcher to monitor the model status
$scope.$watch('instance',function(){
console.log("child controller detected change");
},true);
});
Here is the jsFiddle demo
Hope this is helpful.

Categories

Resources