I am attempting to build a proof of concept application using AngularJS and Jaydata. I am loosely following the MVC pattern on the AngularJS home page (http://angularjs.org/) under "Wire up a Backend". Instead of Firebase, I'm using WebSQL via Jaydata providers.
I have a WebSQL database called InspecTechDB with two tables, Organizations and Customers. There is a parent/child relationship between the two on OrganizationID. This is from my model:
$data.Entity.extend('Customer', {
'CustomerID': { 'key': true, 'type': 'Edm.Guid', 'nullable': false },
'OrganizationID': { 'type': 'Edm.Guid', 'nullable': false, 'required': true },
'CustomerName': { 'type': 'Edm.String' },
'Organization': { 'type': 'Organization', 'inverseProperty': '$$unbound' }
});
$data.Entity.extend('Organization', {
'OrganizationID': { 'key': true, 'type': 'Edm.Guid', 'nullable': false, 'required': true },
'OrganizationName': { 'type': 'Edm.String' },
'Customers': { 'type': 'Array', 'elementType': 'Customer', 'inverseProperty': '$$unbound' }
});
$data.EntityContext.extend('InspecTechDB', {
'Customers': { type: $data.EntitySet, elementType: Customer },
'Organizations': { type: $data.EntitySet, elementType: Organization }
});
I have 3 template views: OrganizationIndex.html, CustomerIndex.html, and CustomerEdit.html. The CustomerEdit.html is the one I'm having issues with:
<form name="myForm">
<div class="form-group">
<label>OrganizationID</label>
<input type="text" class="form-control" placeholder="OrganizationID" name="OrganizationID" ng-model="customer.OrganizationID" required>
<span ng-show="myForm.name.$error.required" class="help-inline">
Required
</span>
</div>
<div class="form-group" ng-class="{error: myForm.name.$invalid}">
<label>Name</label>
<input type="text" class="form-control" name="CustomerName" ng-model="customer.CustomerName" required>
<span ng-show="myForm.name.$error.required" class="help-inline">
Required
</span>
</div>
</form>
I've included my entire js file here:
var app = angular.module('AngularJaydataApp', ['ngRoute', 'jaydata']);
app.config(function ($routeProvider) {
$routeProvider
.when('/', {
controller: 'OrganizationIndex',
templateUrl: 'OrganizationIndex.html'
})
.when('/CustomerIndex/:id', {
controller: 'CustomerIndex',
templateUrl: 'CustomerIndex.html'
})
.when('/CustomerEdit/:id', {
controller: 'CustomerEdit',
templateUrl: 'CustomerEdit.html'
})
.otherwise({
redirectTo: '/'
});
});
var localDB = new InspecTechDB({
name: 'local',
databaseName: 'InspecTech'
});
app.controller('OrganizationIndex', function ($scope, $data){
//wait until the localDB is ready, then get the Organizations
$.when(localDB.onReady())
.then(function () {
$scope.inspectechdb = localDB;
$scope.organizations = localDB.Organizations.toLiveArray();
});
});
app.controller('CustomerIndex', function ($scope, $data, $routeParams) {
$.when(localDB.onReady())
.then(function () {
$scope.inspectechdb = localDB;
$scope.Customers = $scope.inspectechdb
.Customers
.filter('OrganizationID', '==', $routeParams.id)
.toLiveArray();
});
});
app.controller('CustomerEdit', function ($scope, $data, $routeParams) {
var customerID = $routeParams.id;
$.when(localDB.onReady())
.then(function () {
$scope.inspectechdb = localDB;
$scope.inspectechdb.Customers.single(function (customer) {
return customer.CustomerID == this.Id;
},
{Id: customerID},
function (customer) {
$scope.customer = customer;
console.dir(customer);
});
});
console.log('this entry s/b after the customer console entry');
});
I can successfully navigate to each of the views and populate the OrganziationList.html template from my database as shown in the above code. I've set the Organization list up so that when I click the Organization entry on my view then the CustomerIndex.html view is loaded and bound to my customer list. This works fine. I've also set it up so that when I click a customer entry on the CustomerIndex view then the CustomerEdit.html view is loaded and here is where I get lost. The view appears just fine, but the form is not bound when the view is loaded, and I understand why (I think). It seems to be b/c angular is binding the form before my $scope.customer is populated. Evidence of this is the console log:
this entry s/b after the customer console entry bind.js:68
Customer
My question is this: Why do the OrganzationList and CustomerList views populate correctly and the CustomerEdit form does not and what can be done about it?
UPDATE
For anyone interested, I made it work by modifying the CustomerEdit controller per the accepted answer:
app.controller('CustomerEdit', function ($scope, $data, $routeParams) {
var customerID = $routeParams.id;
$.when(localDB.onReady())
.then(function () {
$scope.inspectechdb = localDB;
$scope.inspectechdb.Customers.single(function (customer) {
return customer.CustomerID == this.Id;
},
{ Id: customerID },
function (customer) {
$scope.$apply(function () {
$scope.customer = customer;
});
});
});
});
Angular has no idea that it should update the form. When you call tolivearray jaydata manages this for you. Calling single does not.
One way of solving it is that you call apply yourself when you updated the scope.
A better way would be to pass the entity instead of loading it, since it's already loaded.
Related
I went through hundreds of pages for several days without success and here is my problem.
I use the MEAN stack and at this point I have a simple form that works very well to save a "name" field to a MongoDB collection. Now, I would like, client-side, add an image upload and, on form submit, store the image on my server and finally save the "name" field and the image path to the MongoDB collection.
AngularJS side, I tried using ng-file-upload with multer server-side. I have done well to operate for the upload of the file but only that. But after hundreds of tests, I despair. Here is an extract of my original code without file upload.
Server side
sections.server.model
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
var SectionSchema = new Schema({
name: {
type: String,
default: '',
trim: true,
required: true,
unique: true
},
image: {
type: String,
default: ''
}
});
mongoose.model('Section', SectionSchema);
sections.server.controller
exports.create = function (req, res) {
var section = new Section(req.body);
section.save(function (err) {
if (err) {
return res.status(400).send({
message: getErrorMessage(err)
});
} else {
res.json(section);
}
});
};
sections.server.routes
var sections = require('../../app/controllers/sections.server.controller');
module.exports = function (app) {
app.route('/api/sections')
.post(sections.create);
};
Client side
sections.client.module
'use strict';
var sections = angular.module('sections', []);
sections.client.controller
'use strict';
angular.module('sections')
.controller('SectionsController',
['$scope', '$routeParams', '$location', 'Sections'
function ($scope, $routeParams, $location, Sections) {
$scope.create = function () {
var section = new Sections({
name: this.name
});
section.$save(function (response) {
$location.path('sections/' + response._id);
}, function (errorResponse) {
$scope.error = errorResponse.data.message;
});
};
}]);
sections.client.routes
angular.module('sections').config(['$routeProvider', function ($routeProvider) {
$routeProvider
.when('/sections', {
controller: 'SectionsController',
templateUrl: 'sections/views/list-sections.client.view.html'
})
.when('/sections/create', {
controller: 'SectionsController',
templateUrl: 'sections/views/create-section.client.view.html'
})
.otherwise({
redirectTo: '/'
});
}]);
sections.client.service
'use strict';
angular.module('sections').factory('Sections', ['$resource', function ($resource) {
return $resource('api/sections/:sectionId', {
sectionId: '#_id'
}, {
update: {
method: 'PUT'
}
});
}]);
create-section.client.view
<section>
<h1>New Article</h1>
<form data-ng-submit="create()" novalidate>
<div>
<label for="name">Nom du rayon</label>
<div>
<input type="text" data-ng-model="name" id="name" placeholder="Name" required>
</div>
</div>
<div>
<input type="submit">
</div>
<div data-ng-show="error"><strong data-ng-bind="error"></strong></div>
</form>
</section>
Now, from this can anyone help me to add the image upload in the form and then save the field name and the image path in MongoDB.
Note that I want to reuse the upload mecanism in other forms of my app.
I had the idea of switching a generic middleware function in the road-side server wich call multer and return the image path to my sections.create function for MongoDB storing, something like that :
module.exports = function (app) {
app.route('/api/sections')
.post(uploads.upload, sections.create);
But I've never managed to pass the file in the POST from AngularJS request.
Thank you so much for all your ideas, your help and possibly an example of code that works.
I'm having an issue trying to pass a parameter object to a state using stage.go().
Here is my state definition:
.state('drillhole.ddhinttype', {
url: '/ddhinttype',
templateUrl: VIRTUAL_DIR_PATH + '/App/Views/drillholemanager/drillhole/tabddhinttype.html?v=' + fileVer,
controller: 'DrillHoleDdhIntTypeController',
params: { name: null, description: null }
})
And here is my controller:
try {
angular.module('centric.drillhole.manager');
} catch (e) {
angular.module('centric.drillhole.manager', ['app.config', 'ui.router', 'kendo.directives', 'ui.bootstrap', 'ngCookies', 'centric.common', 'centric.notification', 'pascalprecht.translate', 'centric.security', 'centric.app.settings']);
}
angular.module('centric.drillhole.manager').controller('DrillHoleDdhIntTypeController', ['$scope', 'CentricUIHelper', 'NumberHelper', 'DrillHoleManagerService', 'app.config', '$stateParams',
function ($scope, uihelper, numberHelper, service, appconfig, $stateParams) {
$scope.loading = false;
$scope.isbusy = function () {
return $scope.loading || $scope.$parent.loading;
}
var load = function () {
var hello = $stateParams.name;
var hello2 = $stateParams.description;
};
load();
}]);
And I'm calling the state like so:
$state.go('drillhole.ddhinttype', { name: tab.params.name, description: tab.params.description });
In my controller the name and description properties are always null.
Not sure what I'm missing here. Any ideas?
If you put the params in your url you will be able to access it in controller using $stateParams
.state('drillhole.ddhinttype', {
url: '/ddhinttype/:name/:description',
templateUrl: VIRTUAL_DIR_PATH + '/App/Views/drillholemanager/drillhole/tabddhinttype.html?v=' + fileVer,
controller: 'DrillHoleDdhIntTypeController',
params: { name: null, description: null }
})
You can read more about url routing here: https://github.com/angular-ui/ui-router/wiki/url-routing
Try this in the state definition:
params: { name: undefined, description: undefined }
or this:
params: ['name', 'description']
I feel like I should post the final result. I have decided to pass the parameter in the URL so that I can re-use the same controller for several tabs which each have the same functionality but against different tables in the DB.
Here is the part of my base controller which creates the tabs (CoreLogController.js):
service.getDrillHoleIntervalTypes()
.success(function (res) {
$scope.data.drillHoleIntervalTypes = res;
for (var i = 0; i < $scope.data.drillHoleIntervalTypes.length; i++) {
// add the tab and set it as active if we're in the correct $state
$scope.dynamictabs.push({ heading: $scope.data.drillHoleIntervalTypes[i].Name, route: 'drillhole.ddhinttype', params: { ddhinttype: $scope.data.drillHoleIntervalTypes[i].Name }, active: ($scope.$state.params.ddhinttype == $scope.data.drillHoleIntervalTypes[i].Name) });
}
})
.error(function (error) {
uihelper.showError(error);
});
And here is the relevant HTML portion where the tabs are shown (corelog.html):
<tabset>
<tab ng-repeat="t in statictabs" heading="{{t.heading}}" ui-sref="{{t.route}}" active="t.active"></tab>
<tab ng-repeat="t in dynamictabs" heading="{{t.heading}}" ui-sref="drillhole.ddhinttype({ddhinttype: '{{t.params.ddhinttype}}'})" active="t.active"></tab>
</tabset>
And here is where I define the state (app.js):
.state('drillhole.ddhinttype', {
url: '/ddhinttype/{ddhinttype}',
templateUrl: VIRTUAL_DIR_PATH + '/App/Views/drillholemanager/drillhole/tabddhinttype.html?v=' + fileVer,
controller: 'DrillHoleDdhIntTypeController',
params: { ddhinttype: null }
})
I now get access to the ddhinttype variable on each instance of the controller (DrillHoleDdhIntTypeController.js) which tells it which table to perform operations against.
Since ddhinttype is also contained the URL the user can create a bookmark which will bring them right back to the same tab even though they are dynamically generated.
I've been creating a number of small directives and using hard-coded arrays for testing while I build out functionality. Now that I've got some of that done, I went back and created a service to load the data from a website via JSON; it returns a promise and when it's successful I update the property my template is based off of. Of course, as soon as I did that my directive stopped rendering correctly.
What is the preferred way of binding my directive to asynchronously loaded data so that when the data finally comes back my directive renders?
I'm using Angular 1.4.7.
Here's a simple example of my hard-coded version.
angular
.module('app', []);
angular.module('app').controller('test', function(){
var vm = this;
vm.inv = 'B';
vm.displayInv = function () {
alert('inv:' + vm.inv);
};
});
angular.module('app')
.directive('inventorytest', function () {
return {
restrict: 'E',
template: '<select ng-model="ctrl.selectedOption" ng-options="inv.code as inv.desc for inv in ctrl.invTypes"></select>{{ctrl.sample}}. Selected: {{ctrl.selectedOption}}',
scope: { selectedOption: '='},
bindToController: true,
controller: function () {
this.invTypes = [
{ code: 'A', desc: 'Alpha' },
{ code: 'B', desc: 'Bravo' },
{ code: 'C', desc: 'Charlie' },
];
this.sample = 'Hello';
},
controllerAs: 'ctrl'
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular.js"></script>
<div ng-app="app" ng-controller="test as vm">
<inventorytest selected-option='vm.inv'></inventorytest>
<br/>
Controller: {{vm.inv}}
</div>
My service is essentially just a thin wrapper around an $http call, ex:
return $http({ method: 'GET', url: 'https://myurl.com/getinfo' });
And I had tried modifying my code to do something like:
this.invTypes = [ { code: '', desc: '' }];
ctrService.getInfo()
.then(function successCallback(response) {
this.invTypes = response.data;
}, function errorCallback(response) {
// display error
});
Like I said, that doesn't work since it seems Angular isn't watching this property.
Within the callback this has a different context and isn't what you want it to be.
You need to save a reference to the controller this and use that within any callbacks
// store reference to `this`
var vm = this;
vm.invTypes = [ { code: '', desc: '' }];
ctrService.getInfo()
.then(function successCallback(response) {
// must use reference to maintain context
vm.invTypes = response.data;
}, function errorCallback(response) {
// display error
});
I am building a web application for our customer support. One of the needs is to be able to keep multiple tickets opened at the same time.
I was able to do the first part easily using a tabulation system and UI-Router.
However, with my current implementation, each time I change active tab, the previously-current tab is unloaded, and the now-current tab is loaded (because it was unloaded with a previous tab change).
This is not at all the expected behavior. I've already spent a couple of days trying to find a way to achieve this, without any luck.
The closest thing I was able to do is to use the multiple views system from UI-Router, but I need multiple instance of the same view to keep in memory (if multiple tickets are opened, they all are on the same view, with the same controller, but a different scope)
Here's my current implementation:
supportApp.js:
var app = angular.module("supportApp", ["ui.router", "ui.bootstrap"]);
app.config(function($stateProvider, $urlRouterProvider, $httpProvider){
$urlRouterProvider.otherwise("/");
$stateProvider
.decorator('d', function(state, parent){
state.templateUrl = generateTemplateUrl(state.self.templateUrl);
return state;
})
.state("main", {
abtract: true,
templateUrl: "main.html",
controller: "mainController"
})
.state("main.inbox", {
url: "/",
templateUrl: "inbox.html",
controller: "inboxController"
})
.state('main.viewTicket', {
url: '/ticket/{id:int}',
templateUrl: "viewTicket.html",
controller: "ticketController"
})
;
});
mainController.js: (handles other stuff, minimal code here)
app.controller("mainController", function($rootScope, $http, $scope, $state, $interval){
// Tabs system
$scope.tabs = [
{ heading: "Tickets", route:"main.inbox", active:false, params:{} }
];
var addTabDefault = {
heading: '',
route: null,
active: false,
params: null,
closeable: false
};
$rootScope.addTab = function(options){
if(!options.hasOwnProperty('route') || !options.route)
{
throw "Route is required";
}
var tabAlreadyAdded = false;
for(var i in $scope.tabs)
{
var tab = $scope.tabs[i];
if(tab.route == options.route && angular.equals(tab.params, options.params))
{
tabAlreadyAdded = true;
break;
}
}
if(!tabAlreadyAdded)
{
$scope.tabs.push($.extend({}, addTabDefault, options));
}
if(options.hasOwnProperty('active') && options.active === true)
{
$state.go(options.route, options.hasOwnProperty('params')?options.params:null);
}
};
$scope.removeTab = function($event, tab){
$event.preventDefault();
if($scope.active(tab.route, tab.params))
{
$scope.go($scope.tabs[0].route, $scope.tabs[0].params);
}
$scope.tabs.splice($scope.tabs.indexOf(tab), 1);
};
$scope.go = function(route, params){
$state.go(route, params);
};
$scope.active = function(route, params){
return $state.is(route, params);
};
$scope.$on("$stateChangeSuccess", function() {
$scope.tabs.forEach(function(tab) {
tab.active = $scope.active(tab.route, tab.params);
});
});
});
main.html:
<div class="container-fluid" id="sav-container">
<div class="row-fluid">
<div class="col-lg-2">
<form role="form" id="searchForm" action="#">
<div class="form-group has-feedback">
<input class="form-control" type="search" />
<span class="glyphicon glyphicon-search form-control-feedback"></span>
</div>
</form>
</div>
<div class="col-lg-10" id="support_main_menu">
<ul class="nav nav-tabs">
<li ng-repeat="t in tabs" ng-click="go(t.route, t.params)" ng-class="{active: t.active, closeable: t.closeable}" style="max-width: calc((100% - 128px) / {{tabs.length}});">
<a href class="nav-tab-text">
<button ng-show="t.closeable" ng-click="removeTab($event, t)" class="close" type="button">×</button>
<span>{{t.heading}}</span>
</a>
</li>
</ul>
</div>
</div>
<div class="row-fluid">
<div class="tab-content" ui-view></div>
</div>
</div>
It seems to me that what I ask is pretty standard, but I sadly couldn't find any usefull thing on the Internet
The basic idea is to store state (i.e. list of tickets) in a service as opposed to a controller. Services hang around for the life of the application. There are some articles on this. I'm still developing my approach but here is an example:
var RefereeRepository = function(resource)
{
this.resource = resource; // angular-resource
this.items = []; // cache of items i.e. tickets
this.findAll = function(reload)
{
if (!reload) return this.items;
return this.items = this.resource.findAll(); // Kicks off actual json request
};
this.findx = function(id)
{
return this.resource.find({ id: id }); // actual json query
};
this.find = function(id) // Uses local cache
{
var itemx = {};
// Needs refining
this.items.every(function(item) {
if (item.id !== id) return true;
itemx = item;
return false;
});
return itemx;
};
this.update = function(item)
{
return this.resource.update(item);
};
};
refereeComponent.factory('refereeRepository', ['$resource',
function($resource)
{
var resource =
$resource('/app_dev.php/referees/:id', { id: '#id' }, {
update: {method: 'PUT'},
findAll: {
method: 'GET' ,
isArray:true,
transformResponse: function(data)
{
var items = angular.fromJson(data);
var referees = [];
items.forEach(function(item) {
var referee = new Referee(item); // Convert json to my object
referees.push(referee);
});
return referees;
}
},
find: {
method: 'GET',
transformResponse: function(data)
{
var item = angular.fromJson(data);
return new Referee(item);
}
}
});
var refereeRepository = new RefereeRepository(resource);
// Load items when service is created
refereeRepository.findAll(true);
return refereeRepository;
}]);
So basically we made a refereeRepository service that queries the web server for a list of referees and then caches the result. The controller would then use the cache.
refereeComponent.controller('RefereeListController',
['$scope', 'refereeRepository',
function($scope, refereeRepository)
{
$scope.referees = refereeRepository.findAll();
}
]);
Still very new to MeanJS and Angular, but am trying to get a repeater to use a custom node service that i created
Here is the Angular Template
<section data-ng-controller="AppController">
<section data-ng-controller="GroupsController" data-ng-init="findMyItems()">
<div class="page-header">
<h1>My Groups</h1>
</div>
<div class="list-group">
<a data-ng-repeat="group in groups" data-ng-href="#!/groups/{{group._id}}" class="list-group-item">
<small class="pull-right" data-ng-bind="group.shortId"></small>
<h4 class="list-group-item-heading" data-ng-bind="group.name"></h4>
<small class="list-group-item-text">
Posted on
<span data-ng-bind="group.created | date:'medium'"></span>
by
<span data-ng-bind="group.user.displayName"></span>
</small>
</a>
</div>
<div class="alert alert-warning text-center" data-ng-hide="!groups.$resolved || groups.length">
No Groups yet, why don't you create one?
</div>
</section>
</section>
Here is an array of JSON objects returned from localhost:3000/users/me/groups
[
{
_id: "5407dd31594e810000af4fa0",
user: "5407c78f9ef3025bbf0440f7",
description: "Activating....",
__v: 0,
projects: [ ],
created: "2014-09-04T03:32:01.825Z",
shortId: "bkXtE746M",
name: "Wonder Twins"
},
{
_id: "5407dc49a34a610000af6896",
user: "5407c78f9ef3025bbf0440f7",
description: "Loved watching this one",
__v: 0,
projects: [ ],
created: "2014-09-04T03:28:09.480Z",
shortId: "WJejxZorTz",
name: "Fantastic Four"
},
{
_id: "5407d71839c7de000008cf6b",
user: "5407c78f9ef3025bbf0440f7",
description: "Great group",
__v: 0,
projects: [ ],
created: "2014-09-04T03:06:00.098Z",
shortId: "ZJfKDyN6f",
name: "Leaders of the New School"
}
]
Controller
'use strict';
// Groups controller
angular.module('groups').controller('GroupsController', ['$scope', '$stateParams', '$location', 'Authentication', 'Groups', 'GroupsAPI',
function($scope, $stateParams, $location, Authentication, Groups, GroupsAPI ) {
$scope.authentication = Authentication;
$scope.findMyItems = function() {
GroupsAPI.getGroupsByCurrentUser()
.success(function (groups) {
$scope.groups = groups;
})
.error(function (error) {
$scope.status = 'Unable to load group data: ' + error.message;
});
};
}
]);
I'm not exactly sure what the service is doing in MeanJS
'use strict';
//Groups service used to communicate Groups REST endpoints
angular.module('groups').factory('Groups', ['$resource',
function($resource) {
return $resource('groups/:groupId', { groupId: '#_id'
}, {
update: {
method: 'PUT'
}
});
}
]);
What I'd like to do is something like to do is something like bellow, but not sure if there is a better way
'use strict';
//Groups service used to communicate Groups REST endpoints
angular.module('groups').factory('Groups', ['$resource',
function($resource) {
return $resource('groups/:groupId', { groupId: '#_id'
}, {
update: {
method: 'PUT'
}
});
}
]);
angular.module('groups')
.factory('GroupsAPI', ['$http', function($http) {
var GroupsAPI = {};
GroupsAPI.getGroupsByCurrentUser = function () {
return $http.get('users/me/groups');
};
return GroupsAPI;
}]);
Is there a better way of doing this the MeanJS way?
It's been a long time since you posted this, so you likely figured out a solution already, but for the sake of future readers I'll toss an answer in here.
If I'm understanding what you're trying to do, it looks like you are trying to create a custom factory that functions similar to the existing Groups factory (the one you mentioned you didn't know what it was doing). That's what I'll be answering...
To begin with, you'll want to read the Angular documentation on $resource: https://docs.angularjs.org/api/ngResource/service/$resource. This is what makes the Groups factory work.
To summarize, Angular's $resource allows you to make AJAX requests very easily, by allowing you to create a variable in your controller which has access to REST functions. So basically, you would do something like this:
// Groups controller
angular.module('groups').controller('GroupsController', ['$scope', '$stateParams', '$location', 'Authentication', 'Groups', 'GroupsAPI',
function($scope, $stateParams, $location, Authentication, Groups, GroupsAPI ) {
$scope.authentication = Authentication;
// Since you've added 'Groups' as a dependency, you can now use this "resource" in a new variable.
var groups = new Groups({
// Set any object data you need here, or leave empty.
});
// Now that you have a new instance of your 'Groups' resource (i.e. 'groups'), you can use standard REST calls.
groups.get({
user: currentUser
}, function(results) {
// Do something with results here, e.g.
if(results) {
$scope.groups = results;
} else {
$scope.status = 'Unable to load group data.';
}
});
]);
Note: I haven't tested this code, but this is the idea.