AngularJS self contained directives for designer use - angularjs

I'm working with a web application that allows designers to create pages by writing html in a combination with angularjs directives that are created by myself and other developers. I'm struggling with the best way to populate the directives with data.
Initial attempt was to keep all directives completely self contained. So a product page for example might look like the following (with custom html around all of this - left out for clarity):
<product>
<product-information></product-information>
<product-image></product-image>
<product-quantities></product-quantities>
<product-add-to-cart-button></product-add-to-cart-button>
</product>
If the directives need data (which almost all do) they would use a service to call a web API and get the data they need. There are a few issues that have arisen with this approach.
The directives often need some information from a parent or sibling. In the example below, product-image likely needs the ProductID so it can get the correct image. In this case I have to rely on getting that information from a querystring parameter or store in an angularJS service that is initially populated by the parent directive.
Lots of API calls. With every directive making it's own API calls I'm now ending up with pages that have 15+ API calls to load, and that can be expected to grow over time. Even though a lot of the data may be closely related (even in the same database table). Obviously this is less than ideal.
So I've started changing my approach with the second pass through. Now the directives are set up like a tree structure which each directive expecting it's data requirements to be passed in through an attribute. Here's an example:
<product-image product-url="vm.product.imageUrl" ng-if="vm.product"></product-image>
This solves the problem #2 of too many API and database calls but exposes too many internals to the designer. Now the designer has to know to pass in product-url and must understand that there is a vm.product behind the scenes. He may even need to understand some angularJS (ng-if). I've seen this pattern used a lot even in Angular2 with Inputs. Seems fine for developer usage but not for designer used directives, we want to hide the inner workings and complexity while giving the designer the power of controlling the layout.
Finally, I'm considering using the parent controller to populate everything that might be needed on the page. Then all the child directives will just use a service like they are now but instead of calling an API, the data is already loaded. The directives remain simple and mostly self-contained, but their data load is triggered by a parent. The only issue I have with this is that we may end up loading a lot of data that is unused because of directives not being used by the designer. But I feel like this is a necessary trade off.
Has anyone built something similar, are there any possible approaches I am missing?

You can use a component tree with your service.
Starting with a designer friendly:
<div ng-app="MyApp">
<product id="1">
<product-image></product-image>
</product>
</div>
Something like this sorta works:
angular.module('MyApp', [])
.factory('api', function($q){
return {
loadProduct: function(id){
return $q.when({
id: id,
imageUrl: 'http://i2.cdn.turner.com/cnnnext/dam/assets/160407085910-setsuna-main-overlay-tease.jpg'
})
}
};
})
.component('product', {
transclude: true,
bindings: {
id: '='
},
template: [
'<div ng-transclude></div>'
].join(''),
controller: function(api) {
var self = this;
this.$onInit = function() {
self.data = api.loadProduct(this.id);
};
}
})
.component('productImage', {
require: {
product: '^product'
},
bindings: {
},
template: [
'<pre>{{ $ctrl.url | json }}</pre>'
].join(''),
controller: function() {
var self = this;
this.url = false;
this.$onInit = function() {
this.product.data.then(function(data){
self.url = data.imageUrl;
})
};
}
})
See this CodePen:
http://codepen.io/anon/pen/wGmEKP?editors=1011

Related

Angular controller function outside current controller

I have 4 controllers in my Angular 1.x app:
houseController
apartmentController
addressController
contactInfoController
With addressController & contactInfoController I update the address and contact info of a house or an apartment.
I work in the following way:
<div ng-controller="houseController" ng-init="getHouseInformation()>
{{house.contact_info.email}}
{{house.contact_info.mobile_phone_number}}
<a ng-controller="contactInfoController" ng-click="showContactInfoEdit(house.contact_info.id)">Edit</a>
</div>
When the update of the contact information is successfull within the contactInfoController, I want to update the house information getHouseInformation() in the houseController (read: make a new call to the API to get the updated information).
Since I have more than one (and in the future even more) house/apartment/... controllers, I need to have a solid way on how to 'refresh' the scope of these apartments, houses, ... on the fly.
What would be the best solution in my case?
Edit:
My scopes look like this:
// in houseController
$scope.house = {
id : 1,
title : "House title",
contact_info : {
email: '',
mobile_phone_number : ''
}
}
// in apartmentController
$scope.apartment = {
id : 1,
title : "Apartment title",
contact_info : {
email: '',
mobile_phone_number : ''
}
}
// in contactInfoController
$scope.contact_info : {
email: '',
mobile_phone_number : ''
}
So when updating the contact information scope, I'm not directly changing the house scope... Hope this helps.
I disagree with the other advice you got here - directives help you intelligently manipulate the DOM with your data but not necessarily share it between scopes, which is what you're asking about. As I wrote to you on Slack, what you seem to be looking for is a Service which will contain all your data (either declared in it or linked to an external file or API), and then injected into every controller that needs access to that data. That's one of the main uses for services!
For posterity, here's what I wrote to you on Slack:
"...You’re currently creating unrelated objects that don’t communicate - why should they? They’re in different scopes. You’re actually not getting any use out of Angular and could use vanilla Javascript for that:)
The idea is to use persistent data that is ​shared​ across your web app. $scope.anything will never communicate outside its scope unless you ​bind​ it to something outside the scope, like a service. Whether the service draws data from an external API or really just an object model that lies ​outside​ of Angular, (on your server/file structure, for example), or even defined within the service itself doesn’t matter (although the latter is far from a best practice) - but you need something persistent outside of the local scopes that's shared between your controllers/views.
Then there are a few ways to connect the dots. A very common (and probably the best) design pattern is to create a ​service​ (NOT directive! I don’t know why they gave you that advice) that encapsulates that data. So, for example,
myApp.service(‘dataModel', function(){
$scope.houses = {}; //an object containing ALL the houses, each one containing ALL the apartments, each apt.containing ALL the contact info, etc. etc.
});
Then in your controllers you pass the dataModel service, and then declare and link the local scope 'reference' of the same object to it, for example:
myApp.controller(‘buildingsView’, [dataModel, function(dataModel){
//and link to that service’s object locally:
$scope.houses = dataModel.houses;
}]);
Then, once you affect that model in one view/controller, the data in the other views/controllers will magically change too!
And ​that​ is the angular way:)
Hope this makes sense!
(To clarify, there are two issues here: getting your data INTO angular and then SHARING that same object by injecting the same service to your various controllers. That’s sort of the exact idea of services - they’re singletons, which means only one copy of them exists at any time and if they’re referred to more than once, it’ll always be to the same object, which is why manipulating it in one place will change it in another.)"
Put your data within a $scope variable, and make your controllers watch this varaible from scope. When the event is triggered, you can then do what you want.
$scope.houses = [];
$scope.$watch('houses', function(newValue, oldValue) {
// This is triggered when $scope.houses changes directly
// Add true as param to this function if you want to watch properties nested within the houses object
});
If houses is within a controller, use the following:
(in controller)
var self = this;
self.houses = [];
// This tells angular to watch the controller property houses instead of a scope variable
$scope.$watch(function(){return self.houses}, function(newValue, oldValue) {
// This is triggered when $scope.houses changes directly
// Add true as param to this function if you want to watch properties nested within the houses object
});
I suggest to use directive, then it's easier to exchange data. And that is the reason why directive exists. I try to explain how to build your use case with directives. Assume that you have one controller (houseController) and for every sub requirements you make a directive. One for the contact, one for the appartment and one for the address. Then you define your whole object inside houseController. You pass all necessary data as a parameter to the directive and you can access them from the houseController or from inside the directive, because of the two way data binding. In this way you don't have to exchange something or to call update functions. You adjuste just the object.
your html
<div ng-controller="houseController">
<contact data="house.contact"></contact>
<apartment data="house.apartment"></apartment>
...
</div>
house controller
$scope.house = {
apartment:{
floor: 1,
number:34,
...
},
contact:{
id:2,
email:"peter#test.com",
mobile_phone_number:"123456789",
...
},
...
}
contact directive
.directive('contact', function() {
return {
restrict: 'E',
scope: {
data: '='
},
link: function(scope, element, attrs) {
scope.showContactInfoEdit = function(id){
// do your stuff here
scope.data.name = "Peter";
}
},
templateUrl: 'contact.html'
};
});
contact.html
{{data.email}}
{{data.mobile_phone_number}}
<a ng-click="showContactInfoEdit(data.id)">Edit</a>
I made a plunkr: http://plnkr.co/edit/8tRMtgztaXRC3EKyQhcH?p=preview

Passing data to new page using Onsenui

I am trying to call an API end point once a user clicks a button holding a myNavigator.pushPage() request. However,I can not get the $scope data generated from the $http.get request to be passed to the new page.
If I test using console.log('test'); inside the .success of the $http.get request I successfully get the log info in the console but any data held in $scope.var = 'something'; does not gets passed to the page! Really confused!
$scope.historyDetails = function(id){
var options = {
animation: 'slide',
onTransitionEnd: function() {
$http.get('http://xxx-env.us-east-1.elasticbeanstalk.com/apiget/testresult/testId/'+id).success(function(data) {
$scope.testscore = 'something'; // this is not getting passed to page!
console.log('bahh'); // But I see this in console
});
}
};
myNavigator.pushPage("activity.html", options);
}
Page:
<ons-page ng-controller="HistoryController">
...
<span style="font-size:1.2em">{{testscore}} </span><span style="font-size:0.5em;color:#555"></span>
...
</ons-page>
Yes, that's so because both pages has different controllers, resulting in different scopes. One can not access variables from one scope to another.
Hence one solution in this case can be using rootScope service.
Root Scope is parent scope for all scopes in your angular application.
Hence you can access variable of root scopes from any other scope, provided that you are injecting $rootScope service in that controller.
to know more about rootScope check this link.
Good luck.
Update 1:
check these articles
http://www.dotnet-tricks.com/Tutorial/angularjs/UVDE100914-Understanding-AngularJS-$rootScope-and-$scope.html
https://toddmotto.com/all-about-angulars-emit-broadcast-on-publish-subscribing/
As Yogesh said the reason you're not getting your values is because if you look at $scope.testscore and try to find where is the $scope defined you will see that it's an argument for the controller function (thus it's only for that controller).
However we can see that the controller is attached to the page and you are pushing another page.
So in that case you have several options:
Use the $rootScope service as Yogesh suggested (in that case accept his answer).
Create your own service/factory/etc doing something similar to $rootScope.
(function(){
var historyData = {};
myApp.factory('historyData', function() {
return historyData;
});
})();
Technically you could probably make it more meaningful, but maybe these things are better described in some angular guides.
If you have multiple components sharing the same data then maybe you could just define your controller on a level higher - for example the ons-navigator - that way it will include all the pages. That would be ok only if your app is really small though - it's not recommended for large apps.
If this data is required only in activity.html you could just get it in that page's controller. For example:
myApp.controller('activityController', function($scope, $http) {
$http.get(...).success(function(data) {
$scope.data = data;
});
}
But I guess you would still need to get some id. Anyway it's probably better if you do the request here, now you just need the id, not the data.
You could actually cheat it with the var directive. If you give the activity page <ons-page var="myActivityPage"> then you will be able to access it through the myActivityPage variable.
And the thing you've been searching for - when you do
myNavigator.pushPage("activity.html", options);
actually the options is saved inside the ons-page of activity.html.
So you can do
myNavigator.pushPage("activity.html", {data: {id: 33}, animation: 'slide'});
And in the other controller your id will be myActivityPage.options.data.id.
If you still insist on passing all the data instead of an id - here's a simple example. In the newer versions of the 2.0 beta (I think since beta 6 or 7) all methods pushPage, popPage etc return a promise - which resolve to the ons-page, making things easier.
$scope.historyDetails = function(id){
myNavigator.pushPage("activity.html", {animation: 'slide'}).then(function(page) {
$http.get('...' + id).success(function(data) {
page.options.data = data;
});
});
});
Side note: You may want to close the question which you posted 5 days ago, as it's a duplicate of this one (I must've missed it at that time).

How do I implement the C in CRUD with AngularJS, components, and ngResource?

I'm quite new to Angular, and I'm adapting a simple CRUD app written using standard controllers and ngResource to use the components introduced in 1.5. None of the docs and resources I've found so far discuss how to:
create a new item from scratch
integrate with ngResource
so I'm wondering if anyone can give some pointers on how best to proceed.
My existing app has a simple factory declaring a resource entity, and a single controller that
instantiates a new instance of the resource: $scope.newEntity = new Entity();
populates the $scope with a list of the resources retrieved from the backend: Entity.query(function (data) { $scope.entities = data; });
provides a couple of functions for deleting, updating, and saving the resource to the backend.
In the HTML I have a form that works with $scope.newEntity and the controller saving method to save the new entity to the backend. I also have an ng-repeat that lists the entries stored in $scope.entities, with a couple of additional ng-clicks to perform some editing and deleting.
What I want to do now is implement some inline editing in the list. I know I can do this with my existing approach, but I want to cleanly reuse the form validation functionality I have in the existing entity creation form in the entity editing code, without duplicating. Components seem like a natural fit for that to my (admittedly inexperienced) eyes.
With the component-based approach, I have followed the documentation at https://docs.angularjs.org/guide/component under Example of a component tree, and created an entity-list and entity-detail component. These work okay so far, and I think I can figure out how to wire up the on-delete and on-update events. What I can't figure out is how to approach an on-create event.
Should I use a completely separate controller with my existing simple form to handle the creation event? If so, how can I get the existing list to automatically update? Will that creation event propagate across to the list controller?
Or am I missing something in the existing list controller? Or is the entity creation a special case for the detail controller?
I'm looking specifically for information about how to implement this using Angular components and ngResource, as I'd also like to be ready for Angular 2. Unless components and resources aren't meant to work together please don't post answers about how to achieve this using a completely different approach, or how to reuse HTML code without components. Thanks!
Actually the C in CRUD is realy simple. You were probably expecting an on-create method to be used from your entity-detail. entity-list should take care of the creation of the details however.
Here is the working code
I extended the example from the guide https://docs.angularjs.org/guide/component under Example of a component tree you were reading too and added the create:
(function () {
'use strict';
angular
.module('componentCrud')
.component('heroList', {
templateUrl: "component/hero-list.component.html",
controller : [
HeroListController
]
});
function HeroListController() {
var ctrl = this;
ctrl.list = createHeroes();
ctrl.updateHero = updateHero;
ctrl.deleteHero = deleteHero;
ctrl.createHero = createHero;
function createHero(){
ctrl.list.push({
name : 'Crazy Newling',
location: 'Morgues'
})
}
function updateHero(hero, prop, value) {
hero[prop] = value;
}
function deleteHero(hero) {
var idx = ctrl.list.indexOf(hero);
if (idx >= 0) {
ctrl.list.splice(idx, 1);
}
}
function createHeroes() {
return [{
name : 'Superman',
location: ''
},
{
name : 'Batman',
location: 'Wayne Manor'
}
]
}
}
})();
Then in HTML you just add a create button:
<b>Heroes</b><br>
<hero-detail ng-repeat="hero in $ctrl.list"
hero="hero"
on-delete="$ctrl.deleteHero(hero)"
on-update="$ctrl.updateHero(hero, prop, value)"></hero-detail>
<button ng-click="$ctrl.createHero()">Hire a new Hero</button>
I hope it is going to help you!

Multiple states and urls for the same template

I'm building a web application and I have a screen that consists in five sections, each section represents a level, the areas are the higher level of my tree, when I click in any card of the area, the system should return the skills of that area and so on.
I need to change the url and state according what the user is accessing, for example, if the user access some skill, the url must be
example.com/#/curriculum/skill/<skillId>
and if I access this link it should automatically load the capabilities from this skill and his parent which is area in this case.
I have one controller for area, skill, capability, knowledge and criteria, in each controller I have a action to load the next level of the tree, which looks like that
$scope.loadSkills = function (id) {
Area.loadSkills(...)
$state.go('curriculo.skill', {id: this.current.id}, {nofity: false, reload: false});
}
And these are my states
$stateProvider
.state('curriculum', {
url: '/curriculum',
templateUrl: '/templates/curriculo.html',
})
.state('curriculum.are', {
url: '/area/:id',
template: '',
})
.state('curriculum.skill', {
url: '/skill/:id',
template: '',
})
.state('curriculum.capability', {
url: '/capability/:id',
})
.state('curriculum.knowledge', {
url: '/knowledge/:id',
})
.state('curriculum.criteria', {
url: '/criteria/:id',
});
I'm new in Angular and I not sure about what to do, should I created multiple nested views in this case, and if so, how do I load stuff that I need according the url?
I would suggest to use the capability of multiple named views offered by the ui-router. You can read more about it here. Basically the documentation says the following:
You can name your views so that you can have more than one ui-view per
template.
If you check the example in the documentation, you'll notive that there are similarities between your scenario and the example, because you want to dynamically populate a different views (here named views).
Example
I tried to recreate your scenario in this JSFiddle.
First I created an abstract state which provides the different views like areas, skills etc. This is the template for the abstract state:
<div class="curriculum" ui-view="areas"></div>
<div class="curriculum" ui-view="skills"></div>
Next I created a nested state curriculo.main, which declares the different views (areas, skills etc.) you need. Each view has its own template and controller. Notice that the nested state has a resolve which initially loads the areas from a service called curriculo. If you use resolves remember that the resolve keyword MUST be relative to the state not the views (when using multiple views).
Basically the service is responsible for the business logic, means getting the areas, skills etc. In the JSFiddle I have hard-coded the HTTP results. Replace that with HTTP calls and make use of promises. Since each named view has its own controller we need a mechanism to notify about changes, for example to notify the SkillsController that skills have been loaded. Thus, I created a simple event system (subcribe-notify):
.factory('notifier', function($rootScope) {
return {
subscribe: function(scope, callback, eventName) {
var handler = $rootScope.$on(eventName, callback);
scope.$on('$destroy', handler);
},
notify: function(eventName, data) {
$rootScope.$emit(eventName, data);
}
};
});
The SkillsController can then subscribe to a specific event like so:
notifier.subscribe($scope, function(event, data) {
$scope.skills = data;
}, 'onSkillsLoaded');
The curriculo service calls (at the end of the getSkills()) notifyand provides an event. In this case the same event as you subscribed to in the SkillsController.
notifier.notify('onSkillsLoaded', result);
All in all, that's the magic behind my little example. It's worth mentioning that you need to apply best practices to the code, since this is just to recreate your scenario. For best practices I suggest the Angular Style Guide by John Papa.
Update 1
I updated my example to show you deep linking. I simulate the deep link via
$state.go('.', {area: 2, skill: 5});
This way I can activate a certain state. Now each view has its activate function. Inside this function I do all the work that is neseccary for the initialization, e.g. selecting an area if the query param is set. As you know, you can access the params with the $state service. I had to use a $timeout to delay the init of the areas controller because the subscribe wasn't made yet. Please try to find a better solution to this problem. Maybe you can use promises or register each controller in a service which resolves if all controller have been initialized.
If anything has been selected I also use the go with an additional option to set the notify to false.
$state.go('.', {area: area.id, skill: skillId ? skillId : undefined}, {notify: false});
If notify is set to false it will prevent the controllers from being reinitialized. Thus you can only update the URL and no state change will happen.

How to create generic controllers in AngularJS?

I am looking for advice with regards to creating a generic controller which I can reuse in my application. As an example I have created the following GIST:
https://gist.github.com/heide-de/10832576
but here is the relevant code snippet:
angular.module('genericTestApp', [])
.factory('Names', function() {
return {
names: []
};
})
.controller('IndexController', function(Names) {
Names.names = [
{firstname:'Sarah', surname:'Schmitt'},
{firstname:'Paul', surname:'Wells'},
{firstname:'Felix', surname:'the cat'}
]
})
.controller('SecondIndexController', function(Names) {
Names.names = [
{firstname:'Octo', surname:'Cat'},
{firstname:'Beth', surname:'Appleby'},
{firstname:'Fred', surname:'Bloggs'}
]
})
.controller('TableController', function($scope, Names) {
$scope.names = Names.names;
})
What feels very wrong to me is that the TableController in this example relies on the fact that the injected Names factory needs to have been configured previously in the IndexController.
The next developer that comes along will just inject Names, and will have no idea that it needs configuring prior to that.
Is there a better way of doing this with Angular?
You're right in your approach – Configuring a factory in a controller is always a bad idea. A controller is supposed to contain view specific logic, and in my opinion should even only be included when that particular view is rendered. (that's how I write my apps anyway)
To configure anything when the application loads up, you need run, that should contain all the initial stuff that your app needs to do in the beginning.
This might be a good read.

Resources