AngularJS communication between components through a template (with a promise) - javascript

I'm working on an app that has actor listings. There are tags and actors with a many to many relationships on the backend.
I have 2 components and I want to pass and unresolved promise between them through a template.
This is the actor-tag-detail component:
angular.module('actorTagDetail').component('actorTagDetail', {
templateUrl: 'static/partials/actor-tag-detail/actor-tag-detail.template.html',
controller: ['$routeParams', 'ActorTag',
function ActorTagDetailController($routeParams, ActorTag) {
var self = this;
self.actorTag = ActorTag.get({actorTagId: $routeParams.actorTagId})
}
]
});
The actorTag returns a JSON for the ActorTag object from the backend that looks like this:
{
"id": 37,
"name": "Oscar Winner",
"actors": [
664,
668,
670,
673,
678,
696
] }
I also have an actor-list component that is responsible for retrieving an actor list from the backend:
angular.module('actorList').component('actorList', {
templateUrl: 'static/partials/actor-list/actor-list.template.html',
bindings: {
pk: '='
},
controller: ['Actor',
function ActorListController(Actor) {
var self = this;
alert(self.pk)
self.actors = Actor.query();
self.orderProp = 'name';
}
]
});
In the template for actor-tag-details I'm trying to pass an attribute to the actor-list component.
actor-tag-detail.template.html:
<actor-list pk="$ctrl.actorTag.actors"></actor-list>
What I'm trying to achieve is to have access to the retrieved JSON data in the actor-list component. However I'm getting an 'unresolved object error' instead of the desired result when I try to alert(self.pk) in actor-list.component.This is happening because when alert(self.pk) runs, the data is not yet retrieved from the backend.
I do not know how to remedy the situation.
Is there a way to specify in the receiving component to await execution until the promise is resolved and the data received from the backend?
Or, am I'm doing it completely wrong and there is a better way to pass this information between components?

Use a watcher to listen the object changes:
function ActorListController($scope, Actor) {
var self = this;
$scope.$watch(function() {
return self.pk;
}, function(newValue, oldValue) {
if(angular.isObject(newValue) {
//alert(self.pk)
self.actors = Actor.query();
}
}
}

Related

How to properly resolve multiple factories using Angularjs' UI.Router

Using ui.router this successfully directs me to the proper view and properly retrieves the data from my factory:
.state('calendar', {
url: '/calendar',
templateUrl: 'templates/calendar.html',
controller: 'calendarCtrl',
resolve: {
workoutData: ['WorkoutData', function(WorkoutData){ // USING A FACTORY (workoutDataFct.js)
return WorkoutData.get();
}]
}
})
but I now want to pull in data from another factory but I can't seem to just add a new factory like this:
resolve: {
workoutData: ['WorkoutData', function(WorkoutData){
return WorkoutData.get();
}],
exercises: ['Exercises', function(Exercises){ // USING FACTORY (exercisesFct.js)
return Exercises.get();
}]
}
My factory, exerciseFct.js, is included in my index.html just like workoutDataFct.js and I included the exercises the dependency in my controller calendarCtrl just like I added the workoutData dependency. (am I forgetting to do something?)
I don't get and console errors and but I am not routed to the calendar view. This makes me think that the resolve: is failing. How do I fix this?
To make things clear here are the available options in your case.
Option 1:
Wait for all promises to finish with $q.all.
resolve: {
delayedData: function($q, WorkoutData, Exercises) {
var WorkoutData = WorkoutData.get();
var Exercises = Exercises.get();
WorkoutData.$promise.then(function(response) {console.log('Resource 1 data loaded!')});
Exercises.$promise.then(function(response) {console.log('Resource 2 data loaded!')});
return $q.all([WorkoutData.$promise, Exercises.$promise]);
}
}
Working example can be found here.
Option 2:
As #ExplosionPills stated use object with resolve.
resolve: {
WorkoutData: ["WorkoutData", function (Gists) {
var WorkoutData = WorkoutData.get();
WorkoutData.$promise.then(function(response) {console.log('Resource 1 data loaded!')});
}],
Exercises: ["Exercises", function (Meta) {
var Exercises = Exercises.get();
Exercises.$promise.then(function(response) {console.log('Resource 2 data loaded!')});
}]
}
Working example can be found here.
Thanks #Explosion Pills for your inputs.

Ember Understand execution flow between route/controller

I have a "box" route/controller as below;
export default Ember.Controller.extend({
initialized: false,
type: 'P',
status: 'done',
layouts: null,
toggleFltr: null,
gridVals: Ember.computed.alias('model.gridParas'),
gridParas: Ember.computed('myServerPars', function() {
this.set('gridVals.serverParas', this.get('myServerPars'));
this.filterCols();
if (!this.get('initialized')) {
this.toggleProperty('initialized');
} else {
Ember.run.scheduleOnce('afterRender', this, this.refreshBox);
}
return this.get('gridVals');
}),
filterCols: function()
{
this.set('gridVals.layout', this.get('layouts')[this.get('type')]);
},
myServerPars: function() {
// Code to set serverParas
return serverParas;
}.property('type', 'status', 'toggleFltr'),
refreshBox: function(){
// Code to trigger refresh grid
}
});
My route looks like;
export default Ember.Route.extend({
selectedRows: '',
selectedCount: 0,
rawResponse: {},
model: function() {
var compObj = {};
compObj.gridParas = this.get('gridParas');
return compObj;
},
activate: function() {
var self = this;
self.layouts = {};
var someData = {attr1:"I"};
var promise = this.doPost(someData, '/myService1', false); // Sync request (Is there some way I can make this work using "async")
promise.then(function(response) {
// Code to use response & set self.layouts
self.controllerFor(self.routeName).set('layouts', self.layouts);
});
},
gridParas: function() {
var self = this;
var returnObj = {};
returnObj.url = '/myService2';
returnObj.beforeLoadComplete = function(records) {
// Code to use response & set records
return records;
};
return returnObj;
}.property(),
actions: {
}
});
My template looks like
{{my-grid params=this.gridParas elementId='myGrid'}}
My doPost method looks like below;
doPost: function(postData, requestUrl, isAsync){
requestUrl = this.getURL(requestUrl);
isAsync = (isAsync == undefined) ? true : isAsync;
var promise = new Ember.RSVP.Promise(function(resolve, reject) {
return $.ajax({
// settings
}).success(resolve).error(reject);
});
return promise;
}
Given the above setup, I wanted to understand the flow/sequence of execution (i.e. for the different hooks).
I was trying to debug and it kept hopping from one class to another.
Also, 2 specific questions;
I was expecting the "activate" hook to be fired initially, but found out that is not the case. It first executes the "gridParas" hook
i.e. before the "activate" hook. Is it because of "gridParas"
specified in the template ?
When I do this.doPost() for /myService1, it has to be a "sync" request, else the flow of execution changes and I get an error.
Actually I want the code inside filterCols() controller i.e.
this.set('gridVals.layout', this.get('layouts')[this.get('type')]) to
be executed only after the response has been received from
/myService1. However, as of now, I have to use a "sync" request to do
that, otherwise with "async", the execution moves to filterCols() and
since I do not have the response yet, it throws an error.
Just to add, I am using Ember v 2.0
activate() on the route is triggered after the beforeModel, model and afterModel hooks... because those 3 hooks are considered the "validation phase" (which determines if the route will resolve at all). To be clear, this route hook has nothing to do with using gridParas in your template... it has everything to do with callling get('gridParas') within your model hook.
It is not clear to me where doPost() is connected to the rest of your code... however because it is returning a promise object you can tack on a then() which will allow you to essentially wait for the promise response and then use it in the rest of your code.
Simple Example:
this.doPost().then((theResponse) => {
this.doSomethingWith(theResponse);
});
If you can simplify your question to be more clear and concise, i may be able to provide more info
Generally at this level you should explain what you want to archive, and not just ask how it works, because I think you fight a lot against the framework!
But I take this out of your comment.
First, you don't need your doPost method! jQuerys $.ajax returns a thenable, that can be resolved to a Promise with Ember.RSVP.resolve!
Next: If you want to fetch data before actually rendering anything you should do this in the model hook!
I'm not sure if you want to fetch /service1, and then with the response you build a request to /service2, or if you can fetch both services independently and then show your data (your grid?) with the data of both services. So here are both ways:
If you can fetch both services independently do this in your routes model hook:
return Ember.RSVP.hash({
service1: Ember.RSVP.resolve($.ajax(/*your request to /service1 with all data and params, may use query-params!*/).then(data => {
return data; // extract the data you need, may transform the response, etc.
},
service2: Ember.RSVP.resolve($.ajax(/*your request to /service2 with all data and params, may use query-params!*/).then(data => {
return data; // extract the data you need, may transform the response, etc.
},
});
If you need the response of /service1 to fetch /service2 just do this in your model hook:
return Ember.RSVP.resolve($.ajax(/*/service1*/)).then(service1 => {
return Ember.RSVP.resolve($.ajax(/*/service2*/)).then(service2 => {
return {
service1,
service2
}; // this object will then be available as `model` on your controller
});
});
If this does not help you (and I really think this should fix your problems) please describe your Problem.

Resolve 2 arrays before anything on the page with a promise

I have 2 arrays, sports and leagues and I want those arrays to become resolve before anything on the page, I will paste all of my code regarding those arrays I just mentioned. I need to do this due to an issue I am having with the Angular filters.
this is my html
<div ng-repeat="sport in sportsFilter = (sports | filter:query)">
<div ng-if="sport.leagues.length">
<!--first array-->
{{sport.name}}
</div>
<div ng-repeat="league in sport.leagues">
<!--second array-->
{{league.name}}
</div>
</div>
controller
.controller('SportsController', function($scope, $state, AuthFactory,
SportsFactory, Sports) {
$scope.sports = [];
$scope.sportPromise = Sports;
AuthFactory.getCustomer().then(function(customer) {
$scope.customer = customer;
SportsFactory.getSportsWithLeagues(customer).then(function(sports) {
$ionicLoading.hide();
if (sports.length) {
$scope.sportPromise = Sports;
$scope.sports = sports;
}else {
AuthFactory.logout();
}
}, function(err) {
console.log(err);
});
}
});
$scope.isSportShown = function(sport) {
return $scope.shownSport === sport;
};
});
and here de app.js so far, I thought with this I was resolving the arrays, actually the must important is the array named leagues, but still is giving me troubles
.state('app.sports', {
url:'/sports',
views:{
menuContent:{
templateUrl:'templates/sportsList.html',
controller:'SportsController',
resolve: {
Sports: function(SportsFactory, AuthFactory, $q) {
var defer = $q.defer();
AuthFactory.getCustomer().then(function(customer) {
SportsFactory.getSportsWithLeagues(customer).then(function(sports) {
var sportLeagues = _.pluck(sports, 'leagues'),
leaguesProperties = _.chain(sportLeagues).flatten().pluck('name').value();
console.log(leaguesProperties);
defer.resolve(leaguesProperties);
});
});
return defer.promise;
}
}
}
}
})
UPDATE:
the page is loading and I my filter is getting the array leagues empty, so is not searching thru to it, so I need that array to load first than the filters.
Here's how this could work at a high-level, including avoiding some mistakes you are making.
Mistakes:
You don't need to use $q.defer when the API you are using is already returning a promise. Just return that promise. What you are doing is called an deferred anti-pattern.
resolve is used when you need to do something (like authentication) before you are hitting a particular state. You are under-using the resolve by not resolving the customer, and instead doing this in the controller.
resolve property of $stateProvider can accept other resolves as parameters.
With these out of the way, here's how it could work:
.state('app.sports', {
resolve: {
customer: function(AuthFactory){
return AuthFactory.getCustomer();
},
sports: function(SportsFactory, customer){
return SportsFactory.getSportsWithLeagues(customer);
},
leagues: function(sports){
var leagueProperties;
// obtain leagueProperties from sports - whatever you do there.
return leagueProperties;
}
}
});
Then, in the controller you no longer need AuthFactory - you already have customer:
.controller('SportsController', function($scope, customer, sports, leagues){
$scope.sports = sports;
})
As per request in the comments of New Dev's answer the same only using array notation so that the code remains minifiable:
.state('app.sports', {
resolve: {
customer: ['AuthFactory', function(AuthFactory){
return AuthFactory.getCustomer();
}],
sports: ['SportsFactory', 'customer', function(SportsFactory, customer){
return SportsFactory.getSportsWithLeagues(customer);
}],
leagues: ['sports', function(sports){
var leagueProperties;
// obtain leagueProperties from sports - whatever you do there.
return leagueProperties;
}]
}
});
A little explanation to go with that, when you minify this:
function (AuthFactory) {
return AuthFactory.getCustomer();
}
You get something like this:
function (_1) {
return _1.getCustomer();
}
Now it will try to inject _1 which is not defined so the code will fail. Now when you minify this:
['AuthFactory', function(AuthFactory){
return AuthFactory.getCustomer();
}]
You'll get this:
['AuthFactory', function(_1){
return _1.getCustomer();
}]
And that will keep working because now _1 is assigned to AuthFactory because angular injects the first parameter in the function with the first string in the array.
Reference: https://docs.angularjs.org/guide/di (see: inline array notation)

How do you search and return a specific index of a JSON resource in AngularJS?

I have a json file of events setup like this:
{
2: {
sched_conf_id: "38",
title: "Coffee Break",
},
3: {
sched_conf_id: "39",
title: "registration",
},
}
I setup and angular factory like this:
.factory('eventFactory', ['$resource',
function($resource) {
return {
query: function(event_id) {
return $resource('/assets/events.json', {}, {
query: { method: 'GET', params: {id:event_id}, isArray: false }
}).query();
}
}
}
])
and lastly I have my angular controller which calls the query method from the factory with the id being the id from the url:
.controller('eventCtrl', function($scope, $routeParams, eventFactory) {
var eventId = $routeParams.event_id;
$scope.eventData = eventFactory.query(eventId);
})
The return data seems to be just the entire events.json file rather than just the specific id I want to query for in the parameters. In the params I am trying id but that is obviously not correct. Using this setup how can I return just the data from event_id: 2?
Assuming your production scheme is going to be fetching just a static file that doesn't do anything with arguments passed in, you need to extract the record you need after it's returned from the server.
Basically, you're requesting something like http://yourhost.com/assets/events.json?event_id=3 and if it's just a static file, the server can't do anything with that parameter. In practice, I would think you'd actually be requesting a response from a web service that can handle that, in which case your client-side code would probably work as-is.
In this specific case, however, I would think that you could handle this with an interceptor. Something like this:
.factory('eventFactory', ['$resource',
function($resource) {
return {
query: function(event_id) {
return $resource('/assets/events.json', {}, {
query: { method: 'GET', isArray: false,
interceptor: {response:
function(data){ var d = JSON.parse(data);
return d[event_id];
}
}
}
});
}
}
}
])
I don't have an environment set up to test this at the moment, but I think it should work. I have a couple places in my own code where I do something similar, for other reasons.

Is it possible to use angularjs cached resource method in a filter?

I have a property in the scope that has an id of external object, also I have a filter that expands this id into a full object like this:
{{ typeId | expandType }}
Filter:
.filter('expandType', ['TypeService', function (tsvc) {
return function (id) {
return tsvc.types.get({ id: id });
}
}])
where tsvc.types.get() is normal resource get method with added cache option.
.factory('TypeService', ['$resource', function ($resource) {
var typeResource = $resource('/api/types/:id', { id: '#id' }, {
get: { method: 'GET', cache: true, params: { id: '#id' } }
});
return {
types: typeResource
}
}])
As I understand angular runs additional digest after the fist one just to make sure that nothing changed. But apparently on the next digest the filter is returning a different object and I get the infdig error (digest is executed in infinite loop).
I hoped that if the resource is cached it will return the same object from cache all the time. I can confirm that there is only one trip to server while executing get() so the cache is working.
What can I do to make it work and use the filter to expand ids to full objects?
Although possible, it is usually not a good idea to bind promises to the view. In your case, filters are reevaluated on every digest, and quoting from https://docs.angularjs.org/api/ng/service/$http:
When the cache is enabled, $http stores the response from the server in the specified cache. The next time the same request is made, the response is served from the cache without sending a request to the server.
Note that even if the response is served from cache, delivery of the data is asynchronous in the same way that real requests are.
To clarify, ngResource uses $http internally.
You can still use the filter calling it from your controller:
app.filter('expandType', function ($http) {
return function (id) {
return $http.get('data.json');
};
});
app.controller('MainCtrl', function ($scope, expandTypeFilter) {
var typeId = 'hello';
expandTypeFilter(typeId).success(function (data) {
$scope.expandedTypeId = data[typeId];
});
});
Plunker: http://plnkr.co/edit/BPS9IY?p=preview.
With this approach, if the only reason you were caching the response was to avoid repeated calls to the server, you can now stop caching it so that it gets fresh data later on, but that depends on your needs, of course.
I really wanted to use a filter because it was used all over the app and I didn't want to clutter my controllers. At this point the solution I came out with looks as follows:
.filter('expandType', ['TypeService', function (tsvc) {
var cache = {};
return function (id) {
if (!id) {
return '';
}
var type = cache[id];
if (!type) {
tsvc.types.get({ id: id }).$promise.then(function (data) {
cache[id] = data;
});
cache[id] = {}
return cache[id];
}
else {
return type;
}
}
}])

Categories

Resources