AngularJS: Sharing scope between multiple instances of same directive - angularjs

I'm writing an app that uses the same table with the same data in multiple places. I created a custom directive that allows me to reuse this table. Unfortunately, if I edit the table in one instance, the other instance does not refresh. How do I link these two so that any edits I make to one show up in the other?

It sounds like you've mostly figured it out, the hard part is getting your data into a shape where the videos and photos can be shared by the slide show. I recommend doing this in a shared data access object returned by a separate factory in Angular, rather than directly in a scope. I've got a sample in Plunkr if it helps.
The sample has a directives that binds to shared data, retrieved from a factory as an object injected into two separate scopes. In your case, you would have to add methods to retrieve data from the server, and shape it for display.
testApp.factory("News", [function () {
var news = {
"stories": [
{"date": new Date("2015-03-01"), "title": "Stuff happened"},
{"date": new Date("2015-02-28"), "title": "Bad weather coming"},
{"date": new Date("2015-02-27"), "title": "Dog bites man"}
],
"addStory": function (title) {
var story = {
"date": new Date(),
"title": title
};
news.stories.push(story);
}
};
return news;
}]);
Both controllers reference the same factory for the data:
testApp.controller("FirstController",
["$scope", "News", function ($scope, news) {
$scope.news = news;
}]);
testApp.controller("SecondController",
["$scope", "News", function ($scope, news) {
$scope.news = news;
}]);
Views then pass the data into to the news list directive, which both shares the data and keeps the directive relatively dumb.
<div ng-controller="FirstController">
<news-list news="news" title="'First List'"></news-list>
</div>
<div ng-controller="SecondController">
<news-list news="news" title="'Second List'"></news-list>
</div>
The news-list directive is just dumb formatting in this example:
testApp.directive("newsList",
function() {
var directive = {
"restrict": "E",
"replace": false,
"templateUrl": "news-list.html",
"scope": {
"news": "=news",
"title": "=title"
}
};
return directive;
});
View template:
<div class="news-list">
<p>{{title}}</p>
<ul>
<li ng-repeat="story in news.stories | orderBy:'date':true">{{story.date | date:'short'}}: {{story.title}}</li>
</ul>
<form>
<input type="text" id="newTitle" ng-model="newTitle" />
<button ng-click="news.addStory(newTitle)">Add</button>
</form>
</div>

Related

How to submit selected checkboxes and group name as an objects within an array

I need to pass selected checkbox object data alone into array on form submit.
service returning json data:
[
{
"groupName" : "A",
"groups": [
"Painting",
"coloring"
]
},
{
"groupName" : "B",
"groups": [
"drawing",
"writing"
]
}
]
service expected format when user selected couple of check boxes and submit form:
{
"groups": [
{
"category": "A",
"subCategory": "coloring"
},
{
"category": "B",
"subCategory": "writing"
}
]
}
My controller:
<div ng-controller="groupCtrl">
<form class="form" name="form" role="form" ng-submit="groupData()" autocomplete="off" novalidate>
<div ng-repeat="groups in groupsList">
<p class="category">{{groups.groupName}}</p>
<p ng-repeat="group in groups.groups" >
<input type="checkbox" id="group" name="{{group}}" class="group" value="{{group}}" ng-model="groups"> {{group}}
</p>
</div>
<button type="submit" class="button">Save</button>
</form>
Controller:
angular.module('myApp')
.controller("groupCtrl", function($rootScope, $scope, $state, $http) {
$http.get('group.json').success(function(groupsList) {
$scope.groupsList = groupsList;
});
$scope.groups = {
}
$scope.groupData = function () {
$http.post('<service endpoint>', $scope.groups, {
})
.success(function(){
console.log("success");
})
.error(function(){
console.log("failed.");
});
}
});
I am new to angular. Looking for help on how to construct array object in controller and update array object on user select/un-select check boxes.
First I would put api logic into a service like this:
angular.module('myApp', [])
//service to handle api communication
.service('apiService', ['$http', function($http) {
return {
getGroups: function() {
return $http.get('http://superapi.com');
}
}
}])
Then you need to call the api service from the controller and build up your view model:
api.getGroups().then(function(res) {
//JSON.parse will not be needed if your API returns a JSON body with application/json set as content-type
$scope.groupsList = JSON.parse(res.data.data);
//Build up the view model.
$scope.groupsList.forEach(function(g) {
$scope.selectedGroups.groups.push({
category: g.groupName,
subCategory: []
});
});
},
function(error) {
//handle API errors gracefully here.
});
Then you can use ng-model on the form components to populate it with data
<form class="form" name="form" role="form" ng-submit="groupData()" autocomplete="off" novalidate>
<div ng-repeat="groups in groupsList track by $index">
<p class="category">{{groups.groupName}}</p>
<p ng-repeat="group in groups.groups track by $index">
<input ng-model="selectedGroups.groups[$parent.$index].subCategory" type="checkbox" id="group" name="{{group}}" class="group" ng-true-value="'{{group}}'" ng-init="checked=''" ng-false-value="''"> {{group}}
</p>
</div>
<button type="submit" class="button">Save</button>
</form>
Since your api response model has a double nested hierarchy, we need two nested ng-repeat loops. We can then leverage the fact that ng-repeat creates a child scope, and the parent scope is accessible by using $parent. That way we can use that information to bind to the correct part of the view model with ng-model.
ng-model="selectedGroups.groups[$parent.$index].subCategory"
$parent.$index is the first ng-repeat groups in groupsList
If you want to be able to select more than one subcategory, you can just bind nd-model to each subcategory's index in an array like this:
ng-model="selectedGroups.groups[$parent.$index].subCategory[$index]"
This will use the index of the subCategory to bind to a specific position in an array.
here is a working example of that :
https://jsfiddle.net/zzq16t8u/4/
Here is a working JSFiddle for you:
https://jsfiddle.net/zzq16t8u/3/
Now when you want to map to the request model specified by the system, you will need to handle this in the function handled by the form submit. You could do something clever with ng-true-value, but then you would still need to do a JSON.parse on your data, as it can only work on strings. And I my opinion handling the mapping to the request model is better done once you prepare to make the request.
$scope.groupData = function() {
// map data to the request model specified
var requestModel = {
groups: []
};
$scope.selectedGroups.forEach(function(group) {
group.subCategory.forEach(function(subCat) {
requestModel.groups.push({
category: group.category,
subCategory: subCat
});
});
});
Here is a working JSFiddle of everything, note that I have simplified the view model here, since we no longer bind directly to the request model.
https://jsfiddle.net/zzq16t8u/15/
Please note that the JSON.parse is only necessary here, as I was unable to figure out how to get the JSFiddle echo service to return json as real json, and not a string, and the api service in the example uses this echo service to mock a real http response, so that will need to be replace by a real http call - as I showed in this answer.
Hope it helps.

AngularJS - What is the right way for this?

I created a page, in which you can view lots of details on a user, in panels.
Every panel contains some information, received from a service, and sometimes has actions on it.
I have about 10 panels, and the viewer decides what he wants to see.
Is it right to make each of those panels a separate controller,
OR only panels with actions need to be in a separate controller, OR all the panels in one controller?
If every panel is a controller, and my code is this:
div class="panel panel-default">
<div class="panel-heading">Panel Heading</div>
<div class="panel-body" ng-controller="historyPanel" ng-include="'panels/history.html'"></div>
</div>
Is there a way for me to know if a panel is empty?
For example, I have an history panel, that I wanna show only when there is history to show, and I don't wanna show "No History", just wanna hide that panel.
But I do want to keep the panel code outside of the history.html view.
You should consider creating a directive for this. You can either create a single directive that contains all 10 panels, and provide an attribute to specify which panel to show, or you could create 10 different directives.
Something like:
<my-panel panel="history" ng-show="userPanel.history.display"></my-panel>
<my-panel panel="settings" ng-show="userPanel.settings.display"></my-panel>
<my-panel panel="friends" ng-show="userPanel.friends.display"></my-panel>
<my-panel panel="music" ng-show="userPanel.music.display"></my-panel>
Then your scope controller might have something like:
app.controller('MyController', function($scope) {
$scope.userPanel = {
history: { display: true },
settings: { display: true },
friends: { display: true },
music: { display: false }
}
});
This way, you could load data from a service which has the user preferences for which panels are displayed.
Try something like this, try not to inject $scope at all cost. :)
function PanelCtrl() {
'use strict';
var ctrl = this;
ctrl.panels = [{
"id": 0,
"name": "Lucia Oconnor",
"history": "my history"
}, {
"id": 1,
"name": "Stevenson Mcintosh",
"history": "my history"
}, {
"id": 2,
"name": "Spears Sims",
"history": "my history"
}];
}
function panel() {
'use strict';
return {
restrict: 'E',
scope: {},
bindToController: {
//Depends what youd like to do with the PanelCtrl
//'&': use a function on that ctrl
//'=': two way data binding
},
controller: 'PanelCtrl as ctrl',
templateUrl: './templates/panel.ng.html'
};
}
angular
.module('app', [])
.controller('PanelCtrl', PanelCtrl)
.directive('panel', panel);
This would be that template:
//also if static content might want to use--one time bindings.
// that would be ::panel.name
<div class="panel panel-default" id="panel_{{panel.id}}">
<div class="panel-heading">{{Panel.name}}</div>
<div class="panel-body">{{Panel.history}}</div>
</div>
This would be your html:
//be sure to include track by, major perf gains.
<div class="container" ng-repeat="panel in ctrl.panels track by panel.id">
<panel bind="here" ng-if="ctrl.history.length"></panel>
</div>

Only show content associated with user

I have a service that does a request,
.factory('movieService', ['$http', function($http) {
return {
loadMovies: function() {
return $http.get('/movies_users.json');
}
};
}])
This is the JSON output and is the result of 2 tables being joined. A user table and a movie table. As you can see the users are associated with 1 or more movies.
[
{"id":1,
"email":"peter#peter.nl",
"movies":[
{
"id":4,
"title":"Creed",
movie_id":"312221"
},
{
"id":5,
"title":"Star Wars: Episode VII - The Force Awakens",
"movie_id":"140607"
}
]
},
{"id":2,
"email":"jan#jan.com",
"movies":[
{
"id":4,
"title":"Creed",
movie_id":"312221"
}
]
}
]
I then have this function in my controller,
movieService.loadMovies().then(function(response) {
$scope.movies = response.data;
});
This stores the data from the service into the movie scope.
If I do,
"ng-repeat" => "movie in movies"
The ng-repeat shows all the movies added by all the users. How would I only show the movies associated with the current user in a view?
This seems to be what you want - let me know if it helps.
Note that you have an extra " on movie_id" that is certainly not helping
angular.module('myApp', [])
.controller('ctrl', function($scope) {
$scope.users = [
{"id":1,
"email":"peter#peter.nl",
"movies":[
{
"id":4,
"title":"Creed",
movie_id:"312221"
},
{
"id":5,
"title":"Star Wars: Episode VII - The Force Awakens",
"movie_id":"140607"
}
]
},
{"id":2,
"email":"jan#jan.com",
"movies":[
{
"id":4,
"title":"Creed",
movie_id:"312221"
}
]
}
];
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp" ng-controller="ctrl">
<div ng-repeat="user in users">
{{user.email}}
<div ng-repeat="movie in user.movies">
{{movie.title}}
</div>
</div>
<h2>Movies for user 2</h2>
<div ng-repeat="movie in users[1].movies">
{{movie.title}}
</div>
</div>
Found in a quick google search:
How do I filter an array with AngularJS and use a property of the filtered object as the ng-model attribute?
<div ng-repeat="movie in movies | filter {email:'user#email.com'}">
Edit: it looks like you actually have a list of users, so you could do a filter on an ngrepeat on users, then you'd have a nested ngrepeat on movies (unfiltered because they're attached to a user). But depending on how your app actually needs to work you could just filter the data when it comes back and throw out the users you don't need. Which introduces the question, do you have access to the server side code? If so you could filter there and not return data that is associated with a different user in the first place

AngularJS: one or many directives?

My problem: I have product object like this: {id: 1, category: 'Television', price: '$2000',...}, then I create product directive. User can buy a product by using product scope function buy(quantity). But I have many products, create scope with this function for every product might be waste of memory? Should I create additional directive, productBuyer has method: buy(product, quantity), then product directive require: '^productBuyer' will be put within it? Which design is better when application scales? Or is there any else way better?
More: I don't put buy to factory because product has to display error message if buy fail (for many reasons: out-of-date product, product-warehouse is empty, don't deliver to user's location...), this process method is passed to product directive.
I would restrict the use of directives to self-contained and reusable bits of functionality. In your example, put common functionality in the directive, but keep functionality related to the broader app in the view's controller - not in the directive.
app.js
angular.module("myApp", []).
.controller("shoppingCtrl", function($scope, productSvc){
productSvc.getProducts()
.then(function(products){
$scope.products = products;
});
$scope.buyProduct = function(product){
productSvc.buy(product)
.then(function(){ /* do something */ });
}
})
.directive("product", function(){
return {
restrict: "EA",
scope: {
product: "=",
onBuy: "&"
},
templateUrl: "productDirective.html",
controller: function($scope){
$scope.clickOnBuy = function(){
$scope.onBuy();
}
}
};
});
productDirective.html
<div>
<div>{{product.title}}</div>
<div>{{product.price}}</div>
<button ng-click="clickOnBuy()">Buy</button>
</div>
index.html
Finally, in your HTML you can do:
<div ng-controller="shoppingCtrl">
<div ng-repeat="item in products" product="item" on-buy="buyProduct(item)"></div>
<hr/>
</div>

Angular not dynamically updating deferred HTML

I'm trying to build a dynamic site using Angular. I'm trying to simulate delays in loading HTML by using setTimeout with $q.defer. It works if I don't have the timeout, but as soon as I add the timeout the data isn't loaded. It does get populated if I click between different views, so I know it's executing. But Angular doesn't seem to be aware that it's finally available.
I've got an HTML file, with the following:
<div ng-controller="MyCtrl">
<div id='my-content' ng-view></div>
<div id='footer'>
footer here
View 1 View 2
</div>
</div>
Here's view1.html:
<div ng-include="'teasers.html'"></div>
Here's teasers.html:
<div class='column content' ng-repeat="teaser in data.teasers">
<div class='button type-overlay' ng-class="{{ 'button-' + ($index + 1) }}">
<div class='teaser-text'>
<img class='button-background' ng-src='{{ teaser.img_src }}'>
<span class='teaser-text'>{{ teaser.name }}</span>
</div>
</div>
</div>
Here's my app.js:
var app = angular.module('app', [], function($routeProvider, $locationProvider) {
$locationProvider.hashPrefix("!");
$routeProvider.when('/', {
templateUrl: 'view1.html'
});
$routeProvider.when('/view2', {
templateUrl: 'view2.html'
});
});
app.factory('data', function($http, $q) {
return {
fetchTeasers: function () {
var deferred = $q.defer();
setTimeout(function() {
deferred.resolve([
{
"name": "Teaser 1",
"img_src": "SRC"
},
{
"name": "Teaser 2",
"img_src": "SRC"
},
{
"name": "Teaser 3",
"img_src": "SRC"
},
{
"name": "Teaser 4",
"img_src": "SRC"
}
]);
}, 5000);
return deferred.promise;
}
}
});
And here's the controller.js:
function MyCtrl($scope, data) {
$scope.data = {};
$scope.data.teasers = data.fetchTeasers();
}
What do I need to do to get deferred working in Angular?
Instead of window.setTimeout use angular timeout method: $timeout. Be sure to load it via DI.
$timeout(function(){
// all your magic goes here
});
By the way, you could use setTimeout, but then you would need do call $scope.$apply() manually. In fact this is what angular does internally.
$scope.$apply(function(){ // all your magic goes here })
But again, as I said, just use $timeout.
PS. The reason view gets updated after user interaction is the fact that it invokes dirty checking, unfortunately a little bit too late in this case. Using $apply or $timeout will ensure that angular gets notified about the changes when needed.
PPS. Although I've posted an answer to this particular problem, I should mention that you in this case you could use $resource, which is a angular service supporting RESTful APIs with promises. It uses an implementation of deferreds based on $q as well. There's no need to reinvent the wheel, unless you have any good reasons for it.

Resources