I have the following angular code:
application.controller('ImageController', function ImageController($scope, ImageService, ngDialog) {
$scope.open = function (image) {
ngDialog.open({
className: 'modal',
plain: false,
scope: scope,
template: 'image'
});
}
};
On page loading, when the url has the parameters source and key:
http://www.google.pt/?source=1&key=sdfd-sd-sf
I would like to call open and pass an image with:
image.source = 1;
image.key = sdfd-sd-sf;
How can I do this?
UPDATE
I tried to use ngroute:
$routeProvider
.when('/:source?/:key?',
{
controller: "ImageController"
}
)
with the following route:
domain.com/?source=ddf&key=23jf-34j
On ImageController I tried to get the parameters source and key using:
var image = { source: $routeParams.source, key: $routeParams.key };
if (image.source != null && image.key != null) {
open(image);
}
But both source and key are undefined. Any idea why?
If you're using ngRoute, you can inject $routeParams into your controller and simply do:
image.source = $routeParams.source;
image.key = $routeParams.key;
Nice egghead video about it: https://thinkster.io/egghead/routeparams-api/
UPDATE
There's no need to specify query parameter names in when (it's only needed when using paths like domain.com/source/123/key/456), so this is wrong:
.when('/:source?/:key?',
It should be just:
.when('/',
While your URL has the hashbang (or html5mode):
domain.com/#/?source=ddf&key=23jf-34j
then this will work just fine:
var image = { source: $routeParams.source, key: $routeParams.key };
Note that if you're not using ng-view the parameters won't be available due to their async nature, so you need to use this watcher in your controller:
$scope.$on('$routeChangeSuccess', function() {
console.log($routeParams);
});
or, if you inject $route instead of $routeParams, you can use:
$scope.$on('$routeChangeSuccess', function() {
console.log($route.current.params);
});
it will return the same object.
UPDATE 2
After a little research, seems like by far the easiest way to do it is to inject $location service, and simply use:
var params = $location.search();
var image = { source: params.source, key: params.key };
Here is a simple example with html5 mode on (will work with your original URL): http://run.plnkr.co/sElZhTrI4JvGc0if/?source=SomeSrc&key=SomeKey
And the full Plunker: http://plnkr.co/edit/Jxol8e7YaghbNScICHqW
Related
I'm using $resource for my RESTful api's and love the parameterized URL template for example 'api/clients/:clientId'
This works great for CRUD operations. Some of my api's however are just reports or read-only end points without the need for the full RESTful treatment. I felt it was overkill to use $resource for those and instead used a custom data service with $http.
The only drawback is I lose the parameterized URL templates. I would love to define a url like'api/clients/:clientId/orders/:orderId' and just pass { clientId: 1, orderId: 1 }. I realize I can build the url dynamically but was hoping $http supported the parameterized template and I just haven't found it yet.
All the best
UPDATE 7/5
The word I was missing in my searches is 'Interpolate'. More information comes up when I search for 'url interpolation in angular $http'. The short answer looks to be 'No' $http doesn't support url interpolation. There are a few fairly easy ways to accomplish this however.
1. Use $interpolate:
Documentation for $interpolate here
var exp = $interpolate('/api/clients/{{clientId}}/jobs/{{jobId}}', false, null, true);
var url = exp({ clientId: 1, jobId: 1 });
2. Write your own url interpolation function
Ben Nadel has a great post on this exact topic here.
3. Steal the functionality right out of angular-resource
Check out setUrlParams on Route.prototype in angular-resource.js. It is fairly straightforward.
Sample data service using $interpolate
(function () {
'use strict';
var serviceId = 'dataservice.jobsReports';
angular.module('app').factory(serviceId, ['$http', '$interpolate', function ($http, $interpolate) {
var _urlBase = 'http://localhost:59380/api';
var _endPoints = {
getJobsByClient: {
url: 'Clients/{{clientId}}/Jobs',
useUrlInterpolation: true,
interpolateFunc: null
}
};
// Create the interpolate functions when service is instantiated
angular.forEach(_endPoints, function (value, key) {
if (value.useUrlInterpolation) {
value.interpolateFunc = $interpolate(_urlBase + '/' + value.url, false, null, true);
}
});
return {
getJobsByClient: function (clientId) {
var url = _endPoints.getJobsByClient.interpolateFunc({ clientId: clientId });
return $http.get(url);
}
};
}]);
})();
To prevent this being "unanswered" when it has been answered ...
1. Use $interpolate:
Documentation for $interpolate here
var exp = $interpolate('/api/clients/{{clientId}}/jobs/{{jobId}}', false, null, true);
var url = exp({ clientId: 1, jobId: 1 });
2. Write your own url interpolation function
Ben Nadel has a great post on this exact topic here.
3. Steal the functionality right out of angular-resource
Check out setUrlParams on Route.prototype in angular-resource.js. It is fairly straightforward.
Sample data service using $interpolate
(function () {
'use strict';
var serviceId = 'dataservice.jobsReports';
angular.module('app').factory(serviceId, ['$http', '$interpolate', function ($http, $interpolate) {
var _urlBase = 'http://localhost:59380/api';
var _endPoints = {
getJobsByClient: {
url: 'Clients/{{clientId}}/Jobs',
useUrlInterpolation: true,
interpolateFunc: null
}
};
// Create the interpolate functions when service is instantiated
angular.forEach(_endPoints, function (value, key) {
if (value.useUrlInterpolation) {
value.interpolateFunc = $interpolate(_urlBase + '/' + value.url, false, null, true);
}
});
return {
getJobsByClient: function (clientId) {
var url = _endPoints.getJobsByClient.interpolateFunc({ clientId: clientId });
return $http.get(url);
}
};
}]);
})();
For URL templateing, there is a clearly defined recommandation: RFC 6570
You can find one implementation on Github : bramstein/url-template
It is quite simple. Here is an AngularJS service making use of a library implementing RFC 6570 standard:
var app=angular.module('demo',[]);
app.service('UserStore',function () {
var baseUrl=urltemplate.parse('/rest/v1/users{/_id}');
return {
load:function(id){
return $http.get(baseUrl.expand({_id:id}));
},
save:function (profile) {
return baseUrl.expand(profile);
//return $http.post(baseUrl.expand(profile),profile);
},
list:function (id) {
}
}
});
app.controller('demoCtrl',function(UserStore){
this.postUrlOfNewUser=UserStore.save({name:"jhon"});
this.postUrlOfExistingUser=UserStore.save({_id:42,name:"Arthur",accessory:"towel"});
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://cdn.rawgit.com/bramstein/url-template/master/lib/url-template.js"></script>
<div ng-app="demo">
<div ng-controller="demoCtrl as ctrl">
<div>New user POST URL: {{ctrl.postUrlOfNewUser}}</div>
<div>Existing user POST URL: {{ctrl.postUrlOfExistingUser}}</div>
</div>
</div>
<script>
</script>
As you can see, the standard even handle optional PATH component. It make it a breeze !
And you can also use "." or ";" -- for matrix notation and even expand query strings!
i'm trying to update url without params is it possible ? on my app i created state
.state('webPrint', {
url: '/web/print/:lead',
templateUrl: '/application/views/web-print.html'
})
and when i'm redirecting i'm passing an object
var obj = JSON.stringify(lead);
$state.go('webPrint', { lead: obj });
but my url is something should be like
/web to /web/print but it shows /web/print + "stringify object data" so i changed my code to
$state.go('webPrint', { lead: obj }, { location: false, inherit: false });
but now it's not change url at all. i mean /web to /web
how can i avoid stringify string on url and pass direct to new url?
thanks
You are using option location: false which means you are telling not to change the url.
I would say create a service with getter and setter to save and retrieve your data like below
angular.module('app.service').factory('leadService', function(){
var lead = {};
return{
getLead : function(){
return lead;
},
setLead: function(data){
lead = data;
}
}
});
.state('webPrint', {
url: '/web/print',
templateUrl: '/application/views/web-print.html'
})
var obj = JSON.stringify(lead);
leadService.setLead(obj);
$state.go('webPrint');
Then in your controller get the lead Object using
var lead = leadService.getLead();
I am building a small rss reader using Express(ie Jade) and Angular. I have a dropdown menu, where the menu items are populated by a list of items in a model.
Whatever the user chooses as an item, there is a rss url attached to it and it should trigger a factory.
This is the jade part:
div.btn-group
button.btn.btn-info(type='button') {{loadButtonText}}
button.btn.btn-info.dropdown-toggle(data-toggle='dropdown')
span.caret
span.sr-only Toggle Dropdown
ul.dropdown-menu(role='menu')
li(ng-repeat='rss in RSSList')
a(href='#', ng-click="feedSrc='{{rss.url}}';loadFeed($event);") {{rss.Title}}
input.form-control(type='text', autocomplete='off', placeholder="This is where your feed's url will appear" data-ng-model='feedSrc')
This is my angular controller:
var News = angular.module('myApp', []);
News.controller('FeedCtrl', ['$scope','FeedService', function($scope, Feed){
$scope.loadButtonText = 'Choose News Feed';
$scope.RSSList = [
{Title: "CNN", url: 'http://rss.cnn.com/rss/cnn_topstories.rss'},
{Title: "Reuters", url: 'http://feeds.reuters.com/news/usmarkets'}
];
$scope.loadFeed = function (e) {
Feed.parseFeed($scope.feedSrc).then(function (res) {
$scope.loadButtonText=angular.element(e.target).text();
$scope.feeds = res.data.responseData.feed.entries;
}); }}]);
News.factory('FeedService', ['$http', function($http){
return {parseFeed: function (url){
return $http.jsonp('//ajax.googleapis.com/ajax/services/feed/load?v=1.0&num=50&callback=JSON_CALLBACK&q='+encodeURIComponent(url));}}
}]);
It seems feedSrc in ng-click doesn't capture rss.url and can not be passed as argument to the parseFeed function.
I tried to pass rss.url directly into loadFeed, like this ng-click="loadFeed({{rss.url}});" and even ng-click="loadFeed('{{rss.url}}');" I didn't work either.
Simply pass it this way :
ng-click="loadFeed(rss.url)"
No need to use the {{ }} in ng-click
Why not to use just:
Jade:
a(href='#', ng-click="loadFeed(rss.url,$event)") {{rss.Title}}
Controller:
$scope.loadFeed = function (url, e) {
Feed.parseFeed(url).then(function (res) {
$scope.loadButtonText=angular.element(e.target).text();
$scope.feeds = res.data.responseData.feed.entries;
}); }}]);
App design question. I have a project which has a very large number of highly customized inputs. Each input is implemented as a directive (and Angular has made this an absolute joy to develop).
The inputs save their data upon blur, so there's no form to submit. That's been working great.
Each input has an attribute called "saveable" which drives another directive which is shared by all these input types. the Saveable directive uses a $resource to post data back to the API.
My question is, should this logic be in a directive at all? I initially put it there because I thought I would need the saving logic in multiple controllers, but it turns out they're really happening in the same one. Also, I read somewhere (lost the reference) that the directive is a bad place to put API logic.
Additionally, I need to introduce unit testing for this saving logic soon, and testing controllers seems much more straightforward than testing directives.
Thanks in advance; Angular's documentation may be… iffy… but the folks in the community are mega-rad.
[edit] a non-functional, simplified look at what I'm doing:
<input ng-model="question.value" some-input-type-directive saveable ng-blur="saveModel(question)">
.directive('saveable', ['savingService', function(savingService) {
return {
restrict: 'A',
link: function(scope) {
scope.saveModel = function(question) {
savingService.somethingOrOther.save(
{id: question.id, answer: question.value},
function(response, getResponseHeaders) {
// a bunch of post-processing
}
);
}
}
}
}])
No, I don't think the directive should be calling $http. I would create a service (using the factory in Angular) OR (preferably) a model. When it is in a model, I prefer to use the $resource service to define my model "classes". Then, I abstract the $http/REST code into a nice, active model.
The typical answer for this is that you should use a service for this purpose. Here's some general information about this: http://docs.angularjs.org/guide/dev_guide.services.understanding_services
Here is a plunk with code modeled after your own starting example:
Example code:
var app = angular.module('savingServiceDemo', []);
app.service('savingService', function() {
return {
somethingOrOther: {
save: function(obj, callback) {
console.log('Saved:');
console.dir(obj);
callback(obj, {});
}
}
};
});
app.directive('saveable', ['savingService', function(savingService) {
return {
restrict: 'A',
link: function(scope) {
scope.saveModel = function(question) {
savingService.somethingOrOther.save(
{
id: question.id,
answer: question.value
},
function(response, getResponseHeaders) {
// a bunch of post-processing
}
);
}
}
};
}]);
app.controller('questionController', ['$scope', function($scope) {
$scope.question = {
question: 'What kind of AngularJS object should you create to contain data access or network communication logic?',
id: 1,
value: ''
};
}]);
The relevant HTML markup:
<body ng-controller="questionController">
<h3>Question<h3>
<h4>{{question.question}}</h4>
Your answer: <input ng-model="question.value" saveable ng-blur="saveModel(question)" />
</body>
An alternative using only factory and the existing ngResource service:
However, you could also utilize factory and ngResource in a way that would let you reuse some of the common "saving logic", while still giving you the ability to provide variation for distinct types of objects / data that you wish to save or query. And, this way still results in just a single instantiation of the saver for your specific object type.
Example using MongoLab collections
I've done something like this to make it easier to use MongoLab collections.
Here's a plunk.
The gist of the idea is this snippet:
var dbUrl = "https://api.mongolab.com/api/1/databases/YOURDB/collections";
var apiKey = "YOUR API KEY";
var collections = [
"user",
"question",
"like"
];
for(var i = 0; i < collections.length; i++) {
var collectionName = collections[i];
app.factory(collectionName, ['$resource', function($resource) {
var resourceConstructor = createResource($resource, dbUrl, collectionName, apiKey);
var svc = new resourceConstructor();
// modify behavior if you want to override defaults
return svc;
}]);
}
Notes:
dbUrl and apiKey would be, of course, specific to your own MongoLab info
The array in this case is a group of distinct collections that you want individual ngResource-derived instances of
There is a createResource function defined (which you can see in the plunk and in the code below) that actually handles creating a constructor with an ngResource prototype.
If you wanted, you could modify the svc instance to vary its behavior by collection type
When you blur the input field, this will invoke the dummy consoleLog function and just write some debug info to the console for illustration purposes.
This also prints the number of times the createResource function itself was called, as a way to demonstrate that, even though there are actually two controllers, questionController and questionController2 asking for the same injections, the factories get called only 3 times in total.
Note: updateSafe is a function I like to use with MongoLab that allows you to apply a partial update, basically a PATCH. Otherwise, if you only send a few properties, the entire document will get overwritten with ONLY those properties! No good!
Full code:
HTML:
<body>
<div ng-controller="questionController">
<h3>Question<h3>
<h4>{{question.question}}</h4>
Your answer: <input ng-model="question.value" saveable ng-blur="save(question)" />
</div>
<div ng-controller="questionController2">
<h3>Question<h3>
<h4>{{question.question}}</h4>
Your answer: <input ng-model="question.value" saveable ng-blur="save(question)" />
</div>
</body>
JavaScript:
(function() {
var app = angular.module('savingServiceDemo', ['ngResource']);
var numberOfTimesCreateResourceGetsInvokedShouldStopAt3 = 0;
function createResource(resourceService, resourcePath, resourceName, apiKey) {
numberOfTimesCreateResourceGetsInvokedShouldStopAt3++;
var resource = resourceService(resourcePath + '/' + resourceName + '/:id',
{
apiKey: apiKey
},
{
update:
{
method: 'PUT'
}
}
);
resource.prototype.consoleLog = function (val, cb) {
console.log("The numberOfTimesCreateResourceGetsInvokedShouldStopAt3 counter is at: " + numberOfTimesCreateResourceGetsInvokedShouldStopAt3);
console.log('Logging:');
console.log(val);
console.log('this =');
console.log(this);
if (cb) {
cb();
}
};
resource.prototype.update = function (cb) {
return resource.update({
id: this._id.$oid
},
angular.extend({}, this, {
_id: undefined
}), cb);
};
resource.prototype.updateSafe = function (patch, cb) {
resource.get({id:this._id.$oid}, function(obj) {
for(var prop in patch) {
obj[prop] = patch[prop];
}
obj.update(cb);
});
};
resource.prototype.destroy = function (cb) {
return resource.remove({
id: this._id.$oid
}, cb);
};
return resource;
}
var dbUrl = "https://api.mongolab.com/api/1/databases/YOURDB/collections";
var apiKey = "YOUR API KEY";
var collections = [
"user",
"question",
"like"
];
for(var i = 0; i < collections.length; i++) {
var collectionName = collections[i];
app.factory(collectionName, ['$resource', function($resource) {
var resourceConstructor = createResource($resource, dbUrl, collectionName, apiKey);
var svc = new resourceConstructor();
// modify behavior if you want to override defaults
return svc;
}]);
}
app.controller('questionController', ['$scope', 'user', 'question', 'like',
function($scope, user, question, like) {
$scope.question = {
question: 'What kind of AngularJS object should you create to contain data access or network communication logic?',
id: 1,
value: ''
};
$scope.save = function(obj) {
question.consoleLog(obj, function() {
console.log('And, I got called back');
});
};
}]);
app.controller('questionController2', ['$scope', 'user', 'question', 'like',
function($scope, user, question, like) {
$scope.question = {
question: 'What is the coolest JS framework of them all?',
id: 1,
value: ''
};
$scope.save = function(obj) {
question.consoleLog(obj, function() {
console.log('You better have said AngularJS');
});
};
}]);
})();
In general, things related to the UI belong in a directive, things related to the binding of input and output (either from the user or from the server) belong in a controller, and things related to the business/application logic belong in a service (of some variety). I've found this separation leads to very clean code for my part.
I am working with twitter's typeahead.js and I was wondering if it was possible to modify hogan.js to use something other than {{}}?
I am looking at the minified code now and I have no idea what to change for something so simple. Doing a find and replace breaks it.
I am asking this mainly because I'm using Angular JS but twitter's typeahead requires a templating engine, causing hogan and angular's {{}} to clash. An even better solution would be simply modifying Angular JS (I know it's not a templating engine) and ditching Hogan to fit the following criteria:
Any template engine will work with typeahead.js as long as it adheres to the following API:
// engine has a compile function that returns a compiled template
var compiledTemplate = ENGINE.compile(template);
// compiled template has a render function that returns the rendered template
// render function expects the context to be first argument passed to it
var html = compiledTemplate.render(context);
Ignore the documentation on this, just look at the source code:
function compileTemplate(template, engine, valueKey) {
var renderFn, compiledTemplate;
if (utils.isFunction(template)) {
renderFn = template;
} else if (utils.isString(template)) {
compiledTemplate = engine.compile(template);
renderFn = utils.bind(compiledTemplate.render, compiledTemplate);
} else {
renderFn = function(context) {
return "<p>" + context[valueKey] + "</p>";
};
}
return renderFn;
}
It happens you can just pass a function to template, callable with a context object which contains the data you passed in the datum objects at the time of instantiation, so:
$('#economists').typeahead({
name: 'economists',
local: [{
value: 'mises',
type: 'austrian economist',
name: 'Ludwig von Mises'
}, {
value: 'keynes',
type: 'keynesian economist',
name: 'John Maynard Keynes'
}],
template: function (context) {
return '<div>'+context.name+'<span>'+context.type.toUpperCase()+'</span></div>'
}
})
If you want to use Hogan.js with Angular, you can change the delimiters by doing something like:
var text = "my <%example%> template."
Hogan.compile(text, {delimiters: '<% %>'});
It appears that the template engine result that typeahead.js expects is an html string and not the dom element (in dropdown_view.js). So I am not sure there is a good solution for using an angular template. As a test I was able to get it binding the result to an angular template but it has to render to an element and then get the html value from the element after binding with the data. I don't like this approach but I figured someone might find it useful. I think I will go with a template function like in the previous post.
Jade template looks like
.result
p {{datum.tokens}}
p {{datum.value}}
Directive
angular.module('app').directive('typeahead', [
'$rootScope', '$compile', '$templateCache',
function ($rootScope, $compile, $templateCache) {
// get template from cache or you can load it from the server
var template = $templateCache.get('templates/app/typeahead.html');
var compileFn = $compile(template);
var templateFn = function (datum) {
var newScope = $rootScope.$new();
newScope.datum = datum;
var element = compileFn(newScope);
newScope.$apply();
var html = element.html();
newScope.$destroy();
return html;
};
return {
restrict: 'A',
link: function (scope, element, attrs, ctrl) {
element.typeahead({
name: 'server',
remote: '/api/search?q=%QUERY',
template: templateFn
});
element.on('$destroy', function () {
element.typeahead('destroy');
});
element.on('typeahead:selected', function () {
element.typeahead('setQuery', '');
});
}
};
}
]);