Object oriented approach with AngularJS - javascript

It seems that Angular does not provide a built-in solution to define class instances with properties and methods and that it's up the developer to build this.
What is the best practice to do this in your opinion?
How to you link this with the backend?
Some of the tips I have gathered use factory services and named functions.
Sources :
Tuto 1
Tuto 2
Thanks for your insights

I think that the closest structure to an Object it's probably a factory, for several reasons:
Basic Syntax:
.factory('myFactory', function (anInjectable) {
// This can be seen as a private function, since cannot
// be accessed from outside of the factory
var privateFunction = function (data) {
// do something
return data
}
// Here you can have some logic that will be run when
// you instantiate the factory
var somethingUseful = anInjectable.get()
var newThing = privateFunction(somethingUseful)
// Here starts your public APIs (public methods)
return {
iAmTrue: function () {
return true
},
iAmFalse: function () {
return false
},
iAmConfused: function () {
return null
}
}
})
And then you can use it like a standard Object:
var obj = new myFactory()
// This will of course print 'true'
console.log( obj.iAmTrue() )
Hope this helps, I perfectly know that the first impact with angular modules can be pretty intense...

You would use an angular service.
All angular services are singletons and can be injected into any controller.
Ideally you would keep only binding/actions on html in your controller and the rest of the logic would be in your service.
Hope this helps.

I got idea by evaluating this library : https://github.com/FacultyCreative/ngActiveResource
However this library assumes strict rest so I it wasn't work for me. What did work for is this:
I created base Model
var app = angular.module('app', []);
app .factory('Model', function(){
var _cache = {}; // holding existing instances
function Model() {
var _primaryKey = 'ID',
_this = this;
_this.new = function(data) {
// Here is factory for creating instances or
// extending existing ones with data provided
}
}
return Model;
});
Than I took simple function extensions "inherits"
Function.prototype.inherits = function (base) {
var _constructor;
_constructor = this;
return _constructor = base.apply(_constructor);
};
and now I cam creating my models like this
app.factory('Blog', [
'Model',
'$http',
function(Model, $http) {
function Blog() {
// my custom properties and computations goes here
Object.defineProperty(this, 'MyComputed' , {
get: function() { return this.Prop1 + this.Prop2 }
});
}
// Set blog to inherits model
Blog.inherits(Model);
// My crud operations
Blog.get = function(id) {
return $http.get('/some/url', {params: {id:id}}).then(function(response) {
return Blog.new(response.data);
});
}
return Blog;
}
]);
Finally, using it in controller
app.controller('MyCtrl', [
'$scope', 'Blog',
function($scope, Blog) {
Blog.get(...).then(function(blog) {
$scope.blog = blog;
});
}
])
Now, there is much more in our Model and extensions but this would be a main principle. I am not claiming this is best approach but I am working pretty big app and it really works great for me.
NOTE: Please note that I typed this code here and could be some errors but main principle is here.

As my question does not really reflect the issue I was facing, I'll just post my approach for the sake of it :
As Domokun put it, rule of thumb is to decouple front and back. But as I am only building a prototype and managing both ends, I would like to keep things in only one place and let the rest of the application use the central information as a service.
What I want to do here is to build a form through ng-repeat containing the model fields and most importantly how to display information in the form (e.g. 'Last name' instead of 'lastname')
So as I started working around with mongoose models here's what I have managed to do :
Firstly, it is possible to pass the mongoose schema of a model from node side to angular side with an app.get request with the following response :
res.send(mongoose.model('resources').schema.paths);
this spitts out an object containing all fields of the 'resources' collection. On top of that I included some additional information in the model like this :
var resourceSchema = new Schema({
_id: { type: Number },
firstname: { type: String, display:'First name' },
lastname: { type: String, display:'Last name' }
});
mongoose.model('resources', resourceSchema);
So basically I can retrieve this symmetrically on angular side and I have all I need to map the fields and display them nicely. It seems I can also describe the validation but I'm not there yet.
Any constructive feedback on this approach (whether it is valid or totally heretic) is appreciated.

Related

Angular/ Javascript module similar to gson in Java?

I was working on the Android and found Gson as a handy utility to convert JSON objects into Java objects. Since I am a big fan of Object Oriented architecture, I am trying to optimize angular code using OO architecture. Every object is mapped to a factory. I am wondering if there is any plugin in javascript or angular that can convert JSON to Angular Objects. e.g. If I have a card factory in Angular
app.factory('Card', [function() {
function Card(cardData) {
if (cardData) {
this.setData(cardData);
}else{
this.new();
}
};
Card.prototype = {
new: function(){
var cardData = {
title: 'Add your recommendations',
}
this.setData(cardData);
}
};
return Card;
}]);
and I am getting JSON data like this
{card: {title: 'demo_title'}}
it should map it automatically like GSON does. I can create a new module to do that, just wondering if someone already did that.
You don't need to create something to work like GSON because JSON has native support on javascript via JSON.parse("{\"card\": {\"title\": 'demo_title'}}" which will produce a javascript object like {card: {title: 'demo_title'}}. In addition, the angularjs' $http service, parses your JSON internally, so you don't even have to think about it.
However, what I think you are looking for, is an approach for using data from the server and instantiate a class from the parsed JSON. A regular approach for that, is using a class which in its constructor you provide the raw object and merge it into its properties. It's convenient to use angular.extend() but you can do it manually if you prefer.
It'd be something like this:
function Card(data) {
angular.extend(this, data);
}
var myCard = new Card({ title: 'demo_title' });
Furthermore, if you want to keep a more reliable model, you can declare a class to work as the object to hold the data, and add methods to retrieve and post data to the server.
angular.module('app')
.factory('Card', ['$http', function CardFactory($http) {
var Card = function (data) {
angular.extend(this, data);
}
Card.get = function (id) {
return $http.get('https://my.api/cards/' + id).then(function(response) {
return new Card(response.data.card);
});
};
Card.all = function () {
return $http.get('https://my.api/cards').then(function (res) {
return res.data.cards.map(function (x) {
return new Card(x);
});
});
};
Card.prototype.create = function () {
var card = this;
return $http.post('https://my.api/cards', card).then(function(response) {
card.id = response.data.card.id;
return card;
});
}
return Card;
}]);
So that you can use it like this:
angular.module('app')
.controller('CardCtrl', ['Card', function CardsCtrl(Card) {
// get a card
this.card = Card.get(1);
// create a new card
this.newCard = function newCard() {
var card = new Card({ title: 'new_card' });
card.create();
};
});
Ref.: Recommended way of getting data from the server

How to extend/overwrite default options in Angular Material $mdDialog.show?

TL;DR : I need a way to overwrite default options provided my Angular Material (especially on Material Dialog) using providers (like any other angular modules - a random example).
I have been looking for a way to customize defaults options Angular Material Modal but without any usable result.
Like I have used on other plugins/modules this way could be achieved using a provider. Having a look in the core of the material (1.0.8) I was trying to set options using setDefaults method like this (let say I just want to disable backdrop for moment):
app.config(['$mdDialogProvider', function($mdDialogProvider){
console.log($mdDialogProvider);
// ^ $get/addMethod/addPreset/setDefaults
var defaults = {
options: function(){
return {
hasBackdrop: false
}
}
}
$mdDialogProvider.setDefaults(defaults);
}]);
Right now when I am checking the options on onComplete callback :
So as you can see the hasBackdrop option is updated, but the modal is not working anymore so I think I am missing something.
Do you have any idea how the angular defaults could be extended in a proper way?
Thanks
UPDATE : Options object without having .setDefaults active (de initial state)
Note : I have copied from their core transformTemplate and added in my defaults object, but the result is the same. I can see the DOM updated, console has no errors, but the modal is not visible.
When you want to update an existing functionality from a third party library, you should try to use decorator pattern and decorate the service method.
Angular provides a neat way of doing this using decorators on providers while configuring the app: https://docs.angularjs.org/api/auto/service/$provide
$provide.decorator
$provide.decorator(name, decorator);
Register a service decorator with the $injector. A service decorator intercepts the creation of a service, allowing it to override or modify the behavior of the service. The object returned by the decorator may be the original service, or a new service object which replaces or wraps and delegates to the original service.
You can write a decorator for $mdDialogProvider to extend the functionality of the .show method and pass it the extended options object like shown below:
.config(function ($provide) {
// Decorate the $mdDialog service using $provide.decorator
$provide.decorator("$mdDialog", function ($delegate) {
// Get a handle of the show method
var methodHandle = $delegate.show;
function decorateDialogShow () {
var args = angular.extend({}, arguments[0], { hasBackdrop: false })
return methodHandle(args);
}
$delegate.show = decorateDialogShow;
return $delegate;
});
});
I have created a codepen with a working example with { hasBackdrop: false } so that backdrop is not shown on calling $mdDialog.show(): http://codepen.io/addi90/pen/RaXqRx
Please find the codepen with the demo here: http://codepen.io/shershen08/pen/vGoQZd?editors=1010
This how service will look:
var dialogFactory = function($mdDialog) {
var options = {};
return {
create: function(conf) {
var preset = $mdDialog.alert()._options; //get defaults
var newOptions = angular.extend(preset, conf, options);//extend with yours
$mdDialog.show(newOptions);
},
//toggle various props
setProp: function(prop, val) {
options[prop] = val;
}
};
};
and in the controller you can use it like this:
$scope.toggleBackdrop = function() {
$scope.backdrop = !$scope.backdrop;
//here we change the state of the service internal var
dialogService.setProp('hasBackdrop', $scope.backdrop);
};
$scope.showDialogViaService = function(ev) {
//here we fill in the needed params of the modal and pass to the service
var obj = {
'title': 'title',
'content': 'content',
'ok':'Ok!'
};
dialogService.create(obj);
}

AngularJS - Using Model in Controller causing Model to update

I have an Angular application where in I'm pulling from a model some data which is saved on the load of the app. For simplicity sake, I've explicitly defined the data which is being pulled.
The issue I have is that in one of my controllers I am running a function on load of the controller which modifies the data pulled from the model. The point is that I want that extra data for that page which is using that controller only. I don't want that data to be saved back into the model (which is what's happening).
My model:
'use strict';
(function () {
var PotsMod = function ($log, _) {
return {
pots: [
{"comp" : "comp1"},
{"comp" : "comp2"}
],
getPots: function () {
return this.pots;
},
};
};
angular
.module('picksApp.models')
.factory('PotsMod', PotsMod);
})();
My controller:
(function () {
function AdmCtrl($log, $routeParams, PotsMod) {
var vm = this;
vm.pots = PotsMod.getPots();
vm.init = function() {
// populate pot.competition
_.forEach(vm.pots, function(pot) {
pot.comp = "test";
});
console.log(PotsMod.getPots());
}
vm.init();
}
angular
.module('picksApp.controllers')
.controller('AdmCtrl', AdmCtrl);
})();
The final line in vm.init(), PotsMod.getPots(), returns to me the updated model, with the values of "comp" as test.
So I tried this instead - I put the debug line under vm.pots like so:
var vm = this;
vm.pots = PotsMod.getPots();
console.log(vm.pots);
vm.init = function() {....
This also returns to me the array where the object values are test...
So I tried one final thing and added an extra debug line in the vm.init() function too:
var vm = this;
vm.pots = PotsMod.getPots();
console.log(vm.pots);
vm.init = function() {
// populate pot.competition
_.forEach(vm.pots, function(pot) {
console.log(pot.comp);
pot.comp = "test";
});
console.log(PotsMod.getPots());
}
vm.init();
The result of this confuses me... The output in the console reads:
[{"comp":"test"},{"comp","test"}]
comp1
comp2
[{"comp":"test"},{"comp","test"}]
I must be missing something here because I don't understand how it can be defining a variable using a model's value, printing that variable with the updated values, then using the old values and printing them, then printing the updated values again from the model (even though nothing in this code touches the model).
Any help would be brilliant please, I see to be making a fundamental mistake somewhere. Thank you.
You're referencing the service's pots object in your controller, so your controller code is also modifying the service's code.
I created a Plunker to demonstrate how angular.copy() creates a deep copy of your service's 'pots', and thus your controller's model is no longer referencing the original.
In your case, all you need to change is vm.pots = angular.copy(getPots());
http://plnkr.co/edit/jg5mWIWds1KMJd51e3o5?p=preview

Jasmine test for javascript getter not working

I'm writing some test for for an angularjs factory and some of the expectations are not working and I really don't know why.
This is my factory (part of it).
'use strict';
angular.module('myAppMod')
.factory('Person', function(BaseModel) {
return BaseModel.extend({
get fullname() {
var name = [];
if (this.first_name) {
name.push(this.first_name);
}
if (this.person_extra && this.person_extra.middle_name) {
name.push(this.person_extra.middle_name);
}
if (this.last_name) {
name.push(this.last_name);
}
return name.join(' ');
}
});
});
and Jasmine tests:
var p;
beforeEach(function() {
p = new Person({
first_name: 'first_name',
person_extra: {
middle_name: 'middle_name',
media_item_id: null
},
last_name: 'last_name',
security_level: 'security_level'
}, true);
});
it("has a fullname", function() {
expect(p.fullname).toEqual('first_name middle_name last_name');
});
p.fullnameis returning ""(empty string) and in the factory, console.log(this.first_name), is undefined.
Any help is really appreciated.
Thank you in advance
EDIT: After further investigation, I have changed my answer.
It is not working because you are using the getter shorthand (get fnName() { }) through the extend method. The getter's this is the anonymous object itself and does not inherit the methods and properties of the Backbone model, whereas the this in function properties do. I have made a codepen that illustrate your problem.
That is, if this is your Model
var Model = BaseModel.extend({
get isBackboneModelThroughGetter() {
return !!this.get;
},
isBackboneModel: function() {
return !!this.get;
},
});
Then an instance of Model will make this test pass:
it('should make you wonder', function() {
var model = new Model();
expect(model.isBackboneModel()).toBe(true);
expect(model.isBackboneModelThroughGetter).not.toBe(true);
});
Thus, to make your Person factory work, you will need:
To replace every property access by the proper Backbone call: this.get('propertyName') instead of this.propertyName
Replace all getters by function properties: full_name : function() { /*...*/ } instead of get full_name() { /* ... */ }
Replace calls to model.full_name by model.full_name();
I assume that you're using the built-in angular.extend. angular.extend does not copy getters and setters. There's been an open issue on GitHub on this specific subject since the 12th of August 2014.
As for why it still isn't implemented:
Angular exposes some of the helper functions that it uses internally. This is the case for extend, copy and many others. There are other libraries that specialize in these functions, keep their
focus is there and can do a better job.
It is not in the best interest of most users to make these helper functions big nor slow, as these are used internally and any change in that direction can have a direct impact in download size and performance. At the same time, apps that need the most accurate version, should be better served with other libraries.
There are many ways to solve this issue. decaf.js provides an example implementation that should work for most cases. GitHub is probably a better environment to dive into their code, but it comes down to this:
function extend (me) {
var args = Array.prototype.slice.call(arguments, 1);
decaf.each(args, function (o) {
for (var key in o) {
if (o.hasOwnProperty(key)) {
var desc = Object.getOwnPropertyDescriptor(o, key);
var g = desc.get;
var s = desc.set;
if (g || s) {
Object.defineProperty(me, key, { get: g, set: s, enumerable: true });
} else {
me[key] = o[key];
}
}
}
});
return me;
}

How to create an empty $resource when using the factory pattern

I am using angular $resource in a factory pattern, where it is injected, so I only have to create the templates once and in one place. This works.
I can not seem to find any documentation on how to create a new resource object. This creates conditional branching when it is time to save, as I do not have an object to call $save() on.
For example: imagine I have this resource:
myService.factory('myWidget', [ '$resource',
function($resource){
return $resource('/my/widget/:id', {
[...]
So my controller can easily get access to myWidget thusly:
function( $scope, ... myWidget ) {
$scope.widget = myWidget.get({id: 'myId'});
$scope.save = function() {
$scope.widget.$save(); // plus progress dialog error handling etc
};
}
Which is very clean and awesome and as it should be. However, if I want to create a new one, I need conditional code both on create and save.
function( $scope, ... myWidget, mode ) {
if (mode === 'create') {
$scope.widget = {
id: 'myNewId',
property: <lots and lots of properties>
};
}
else {
$scope.widget = myWidget.get({id: 'myId'});
}
$scope.save = function() {
$scope.widget.$save(); // Not a $resource; $save() does not exist
};
}
Obviously, I can put conditional code in save(), or I can pass the widget as a parameter, as myWidget.save($scope.widget), but that seems lame. Is there no easy way to simply create a new $resource from the factory? IE:
if (mode === 'create') {
$scope.widget = myWidget.new({
id: 'myNewId',
property: <lots and lots of properties>
});
}
This would be functionally equivalent to :
if (mode === 'create') {
$scope.widget = $resource('/my/widget/:id');
But obviously without duplicating the resource code in the factory.
Surely there is some easy syntax for doing this. Yes?
I am using AngularJS 1.3. I really hope this is a stupid question as it seems like something that is an obvious use case. Even Backbone has a way to create a new REST-backed object with default values. :) Thanks much.
(Maybe the question should be "how do I create an object using the angularjs factory, as if I were the injector?")
You need to be creating a new widget:
if (mode === 'create') {
$scope.widget = new myWidget();
$scope.widget = {
id: 'myNewId',
property: <lots and lots of properties>
};
}

Categories

Resources