As you can see, we want only specific fields to be shown in the Grid View and we've chosen 3 fields for that view
Next you'd want to define the board view in your view_name.jade file
extends project/card-board block panelBody .panel-body h4 {{item.Your_Entity_Field_1}} p {{item.Your_Entity_Field_2}} p {{item.Your_Entity_Field_5}}
You can note that in the board view, we want 3 other fields to be displayed. It's also important to note the need for a status field for the board view.
Hope this helps you in defining Multiple Views for an Entity in your application.
In case you have further queries on this, please feel free to join us in the Chat Room on Gitter https://gitter.im/allcount/allcountjs
Watch this space for more tid-bits coming your way!
Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
ā Live Streamingā Interactive Chatā Private Showsā HD Qualityā Free Actions
Free to watch ⢠No registration required ⢠HD streaming
As you are already aware that the purpose of AllcountJS is to make life easier for someone with minimal coding skills to get up and running with a POC for their idea.
While the current documentation available is a good enough starting point for anyone with prior AngularJS coding experience, we decided to make it more easier for novice coders to get a jumpstart.
This is going to be a multi-part short "How To" posts that are based on the queries we've been getting on our Community Chat Room on Gitter https://gitter.im/allcount/allcountjs
In the first post of this series, we'll cover How To configure a Child Menu on your AllcountJS Application.
As you can see, defining a Child Menu is as simple as this and all you need to do is to create customView to correspond to the menu item.
Hope this helps you in customizing your application's menu, in case you have further queries on this, please feel free to join us in the Chat Room on Gitter https://gitter.im/allcount/allcountjs
Watch this space for more tid-bits coming your way!
Allcount team wins iHack hackathon contest in 4 nominations: IBM special grant prize for hosting and market place valuated at $24,000, HSE Lab acceleration program, peopleās choice award and grand prize. Allcount team implemented loan assessment conveyor application in 48 hours with AllcountJS framework and won an opportunity to make it live in one of the most biggest Russian banks: VTB24. Itās a big step forward for AllcountJS and Node.js enterprise community.
Making product: a marketplace for Moto parts with AllcountJS
Foreword
You are about to find this interesting as we are going to talk about a live product. A Proof Of Concept we put together as an example of what you could get done with AllcountJS - http://zrum.ru.
You can take a look at the Source Code of the Moto Parts Aggregator & Market Place POC weāve built - https://github.com/allcount/moto-parts-marketplace
āAll moto parts in one placeā, the Google for moto parts.
Introduction
Imagine you came up with a wonderful idea, a brilliant solution, that you think will generate a lot of attention from people.
What should you do to ensure your idea gets shape and form and gets nurtured towards execution?
First and foremost is to defeat the most powerful enemy ever -- Time.
An idea is only worth its execution. A startup should race against time or be wiped out. We believe that the Lean Startup principles could lead you and your team to success. One important concept from Lean Startup is Minimum Viable Product, which basically means that you should spend minimum efforts in minimum time and we take that up for todayās topic: How to built a product faster than you can even imagine?
Weāll not be covering MVP much here, but you can read more about it here.
A few words about this particular case
Assume we recently found that it is hard to search and buy moto parts here in Russia today. And local bikers community is hopefully quite big. So we've decided to build a moto parts aggregator: A place where all the price lists are available and are combined into the one super-list with the user-friendly search. "All moto parts in one place", the Google for moto parts.
MVP
Getting started
If you haven't installed Node.js, MondoDB and AllcountJS yet you should do it. Iāll assume that you already know how to create basic AllcountJS configuration, so I wonāt go in detail about that here.
The Model
We need actual stores to get the parts from. So the providers:
The "referenceName" property is needed to let the framework know how to show references (what field should be used for references labels).
We have 3 providers so far and all of them provide XLS (MS Excel) files containing price lists. Therefore, "parseMethod" indicates what function (by its name) should be used to parse these XLS files. You'll learn about functions later when we'll discuss about how to setup the content.
Moto parts should be connected to providers and information about parts availability should exist:
Now we have data storage. Let's move on to the user's interface.
The View
Everyone likes simple clear interfaces, and so do we! Do you remember Google's search interface? Just one field on the screen. So let's use that valuable experience and build something like that:
include mixins include main-mixins - var hasToolbar = false - var entityTypeId = 'PartAvailability' doctype html html head +defaultHead() block head body(style="padding-top:20px") div(ng-app='allcount', ng-init="viewState = {mode: 'list', filtering: {}}", ng-controller="SearchController") .splash(style="padding: 2em 0 2em; background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url('/images/splash.jpg');background-position-y: -700px;background-position-x: center;") .container .row .col-lg-12(ng-if="!viewState.filtering.textSearch") span.app-logo i(class="fa fa-search fa-fw") h1 zrum.ru h2 All moto parts in one place .col-lg-12(ng-if="viewState.filtering.textSearch").ng-cloak h1 i(class="fa fa-search fa-fw") | zrum.ru .container .row.text-center: .col-xs-12 h1(ng-if="!viewState.filtering.textSearch") Search available moto parts input.form-control.input-lg(style="margin-top: 10px", placeholder="Enter name of the part", ng-model="viewState.filtering.textSearch", ng-model-options="{ debounce: 700 }") div(style="margin-top: 20px", ng-if="viewState.filtering.textSearch") +pagingControl() .screen-container(ng-cloak, ng-if="viewState.filtering.textSearch") +defaultList()(ng-show="viewState.paging && viewState.paging.count > 0") .row(ng-repeat="item in items") .col-lg-9.col-md-8 h3: a(ng-href="{{item.siteUrl}}", target="_blank", ng-click="trackNavigate(item)") {{item.name}} h4 {{item.provider.name}} h4: a(ng-href="tel:{{item.phone}}", ng-click="callItem(item)") {{item.phone}} .col-lg-3.col-md-4.text-right h2 {{(item.price / 100) | currency}} h3.text-success(ng-if="item.isAvailable") Available h3.text-danger(ng-if="!item.isAvailable") Not available +noEntries() +defaultEditAndCreateForms() include core-scripts script. angular.module('allcount').config(["$locationProvider", function ($locationProvider) { $locationProvider.html5Mode({enabled: false}); }]); angular.module('allcount').controller('SearchController', ['$scope', function ($scope) { $scope.$watch('viewState.filtering.textSearch', function (textSearch) { lc.track('search', {query: textSearch}); }) $scope.$watch('viewState.paging.count', function (count) { if (count === 0 && $scope.viewState && $scope.viewState.filtering && $scope.viewState.filtering.textSearch) { lc.track('not-found', {query: $scope.viewState.filtering.textSearch}); } }) $scope.trackNavigate = function (item) { lc.track('navigate', {item: item}); } $scope.callItem = function (item) { lc.track('call', {item: item}); } }]);
Surely we have some CSS (or rather LESS) files under the hood, but it's not so important now. One thing you should definitely know is that these LESS files should be put under ./public/assets/less directory where . is the directory where application's config is.
The content
One of the most interesting questions here is how to fill the price list. There is no way to involve end users into this process. So the solution is to scrape and parse data from the actual moto parts shops. We'll divide this process into two stages:
get data from the shop's XLS price lists
scrape the data (i.e. links) from web sites in order to allow end users to actually buy the parts
Each of this stages we could divide into two subtasks:
service itself (worker's code)
UI invoking that service
Let's implement each service.
Link scrapping
Every moto parts provider has their own web site and their own data formats, hence we need to implement custom functions for each of them to get actual information about moto parts. Let's create ./services/scraper.js file:
var request = require('request'); var cheerio = require('cheerio'); var rp = require('request-promise'); var _ = require('underscore') module.exports = function (Q) { var pool = {maxSockets: 2}; return { parseLbaMoto: function () { var self = this; var lbaRootLink = 'http://www.lbamoto.ru'; return this.cheerioRequest(lbaRootLink).then(function ($) { var categoriesList = $('a', $('ul.level1')).map(function (i, el) { return $(this).attr('href'); }).get(); categoriesList = _.unique(categoriesList); console.log(categoriesList.join("\n")); //categoriesList = _.take(categoriesList, 10); //TODO return Q.all(categoriesList.map(function (categoryLink) { var categoryUrl = lbaRootLink + categoryLink + '/results,0-1000'; return self.cheerioRequest(categoryUrl).then(function ($) { console.log('Scraping: ' + categoryUrl); return _.filter($('.row > .product').map(function () { var link = $('a', $(this)); if (!link.text()) { return; } var priceBlock = $('.product-price__base', $(this)); if (!priceBlock || !priceBlock.text()) { return; } var priceMatch = priceBlock.text().match(/(\d+)\s+ŃŃŠ±/); if (!priceMatch) { return; } return { name: link.text().trim(), siteUrl: lbaRootLink + link.attr('href'), price: parseInt(priceMatch[1]) * 100 }; }).get(), _.identity); }) })).then(_.flatten); }) }, parseMrMoto: function () { var self = this; var entryLink = 'http://www.mr-moto.ru/Netshop/shop/charges/'; var rootLink = 'http://www.mr-moto.ru'; function drillDown(categoriesList, selector) { return Q.all(categoriesList.map(function (categoryUrl) { return self.cheerioRequest(categoryUrl).then(function ($) { var collectHrefs = self.collectHrefs($); return collectHrefs($(selector), rootLink); }) })).then(_.flatten); } return this.cheerioRequest(entryLink).then(function ($) { var collectHrefs = self.collectHrefs($); var categoriesList = collectHrefs($('.divmenu1 a'), rootLink); console.log(categoriesList.join("\n")); return drillDown(categoriesList, '.divmenu2 a').then(function (secondList) { return drillDown(secondList, '.divmenu3 a').then(function (thirdList) { return _.unique(_.union(categoriesList, secondList, thirdList)); }) }).then(function (allCategories) { console.log(allCategories.join("\n")); //allCategories = _.take(allCategories, 10); //TODO return Q.all(allCategories.map(function (categoryLink) { var categoryUrl = categoryLink + '?portion=-1'; return self.cheerioRequest(categoryUrl).then(function ($) { console.log('Scraping: ' + categoryUrl); return _.filter($('.tovblock').map(function () { var link = $($('a', $(this))[1]); var priceBlock = $($('div', link)[1]); if (!priceBlock || !priceBlock.text()) { return; } var priceMatch = priceBlock.text().trim(); if (!priceMatch) { return; } return { name: $($('div', link)[0]).text().trim(), siteUrl: rootLink + link.attr('href'), price: parseInt(priceMatch) * 100 }; }).get(), _.identity); }) })).then(_.flatten); }) }); }, parseDriveBike: function () { var self = this; var entryLink = 'http://www.drivebike.ru/zapchasti'; return this.cheerioRequest(entryLink).then(function ($) { var collectHrefs = self.collectHrefs($); var categoriesList = collectHrefs($('.amshopby-cat-level-1 a')); console.log(categoriesList.join("\n")); return categoriesList; }).then(function (allCategories) { console.log(allCategories.join("\n")); allCategories = _.unique(allCategories); //allCategories = _.take(allCategories, 0); //TODO return Q.all(allCategories.map(function (categoryLink) { var categoryUrl = categoryLink + '?limit=60&mode=grid'; function scrapePage(pageUrl) { return self.cheerioRequest(pageUrl).then(function ($) { console.log('Scraping: ' + pageUrl); var itemsOnPage = _.filter($('li.item').map(function () { var link = $('.product-name a', $(this)); var priceBlock = $('.price-box .regular-price, .price-box .special-price', $(this)); if (!priceBlock || !priceBlock.text()) { return; } var priceMatch = priceBlock.text().trim().replace(/\s/g, '').match(/(\d+)/); if (!priceMatch) { return; } var manufacturer = $('.manufacturer-name', $(this)); return { name: ((manufacturer && manufacturer.text().trim() + ' ') || '') + link.text().trim(), siteUrl: link.attr('href'), price: parseInt(priceMatch[1]) * 100 }; }).get(), _.identity); var nextPage = $('a.i-next'); if (!nextPage || !nextPage.attr('href')) { return itemsOnPage; } return scrapePage(nextPage.attr('href')).then(function (nextPageItems) { return _.union(itemsOnPage, nextPageItems); }) }) } return scrapePage(categoryUrl); })).then(_.flatten).then(function (items) { return _.unique(items, _.property('name')); }); }); }, collectHrefs: function ($) { return function ($el, root) { return _.unique(_.filter($el.map(function (i, el) { if (root && $(this).attr('href').indexOf('/') !== 0) { return; } return (root || '') + $(this).attr('href'); }).get(), _.identity)); } }, cheerioRequest: function (url, retries) { var self = this; return Q(rp({ url: url, transform: function (body) { return cheerio.load(body) }, pool: pool })).catch(function (e) { console.error(e); if (retries === 0) { throw e; } return self.cheerioRequest(url, (retries || 3) - 1); }); } } }
Here you can see "parse <provider's name>" function which actually parses web sites of the shop with name "<provider's name>". I won't stop on technical aspects at this moment, you are very welcome to contact us if you're interested in such details.
Parsing XLS price lists
The same is about parsing the XLS files: every provider has their own quirky price lists. So we need some custom services implementation:
var request = require('request'); var XLSX = require('xlsx'); var _ = require('underscore') var querystring = require("querystring"); module.exports = function (Q) { var qRequest = Q.nfbind(request); return { parsePartAvailabilities: function (provider, url, parseMethod) { var self = this; if (url.indexOf('https://yadi.sk') !== -1) { return qRequest({ method: 'GET', url: 'https://cloud-api.yandex.net/v1/disk/public/resources?public_key=' + url, json: true }).then(function (resourceUrl) { console.log(resourceUrl[1]); var item = _.find(resourceUrl[1]._embedded.items, function (item) { return item.name.indexOf('xls') !== -1; }) console.log(item); var getDownloadUrl = 'https://cloud-api.yandex.net/v1/disk/public/resources/download?' + querystring.stringify({ public_key: item.public_key, path: item.path }); console.log(getDownloadUrl); return qRequest({ method: 'GET', url: getDownloadUrl, json: true }).then(function (downloadUrl) { console.log(downloadUrl[1]); return qRequest({ method: 'GET', url: downloadUrl[1].href, encoding: null }).then(function (res) { return self[parseMethod](provider, res[1]); }) }) }) } if (url.indexOf('mr-moto.ru') !== -1) { return self.extractMrMoto(provider, url, parseMethod); } return Q.nfcall(request, { method: 'GET', url: url, encoding: null }).then(function (res) { console.log(res[0]); return self[parseMethod](provider, res[1]); }) }, extractMrMoto: function (provider, url, parseMethod) { var self = this; return qRequest({method: 'GET', url: url}).then(function (res) { var fileUrl = res[1].match(/\<a\s+href\=\'(.*?)\'(.*?)\>оŃŃŠ°ŃŠŗŠø(.*?)\<\/a\>/)[1]; console.log(fileUrl); return qRequest({ method: 'GET', url: 'http://www.mr-moto.ru' + fileUrl, encoding: null }).then(function (res) { console.log(res[0]); return self[parseMethod](provider, res[1]); }) }) }, parseLbaMoto: function (provider, xlsxBinary) { var workbook = XLSX.read(xlsxBinary); var first_sheet_name = workbook.SheetNames[0]; var worksheet = workbook.Sheets[first_sheet_name]; function cellValue(c, r) { var cell = worksheet[XLSX.utils.encode_cell({c: c, r: r})]; //console.log(cell); return cell && cell.v; } var result = []; for (var i = 10; cellValue(3, i); i++) { if (!cellValue(23, i) || !cellValue(0, i)) { continue; } result.push({ sku: cellValue(0, i), name: cellValue(3, i), price: cellValue(23, i) * 100, provider: {id: provider.id, name: provider.name}, isAvailable: !!cellValue(21, i) }) } return result; }, parseMrMoto: function (provider, xlsxBinary) { var workbook = XLSX.read(xlsxBinary); var first_sheet_name = workbook.SheetNames[0]; var worksheet = workbook.Sheets[first_sheet_name]; function cellValue(c, r) { var cell = worksheet[XLSX.utils.encode_cell({c: c, r: r})]; //console.log(cell); return cell && cell.v; } var result = []; for (var i = 10; cellValue(0, i); i++) { if (!cellValue(5, i)) { continue; } result.push({ name: cellValue(0, i), price: cellValue(5, i) * 100, provider: provider, isAvailable: !!cellValue(8, i) }) } return result; }, parseDriveBike: function (provider, xlsxBinary) { var workbook = XLSX.read(xlsxBinary); var first_sheet_name = workbook.SheetNames[0]; var worksheet = workbook.Sheets[first_sheet_name]; function cellValue(c, r) { var cell = worksheet[XLSX.utils.encode_cell({c: c, r: r})]; //console.log(cell); return cell && cell.v; } var result = []; for (var i = 1; cellValue(0, i); i++) { if (!cellValue(5, i)) { continue; } result.push({ name: cellValue(5, i).replace("[", "").replace("]", ""), price: cellValue(7, i) * 100, provider: provider, isAvailable: true }) } return result; } } }
While this implementation looks mysterious, giant and overall scary, trust me, it is much more concise than it was in times of Java EE applications' prosperity!
Service usage
It's time to use our new services within our application's config. Basically we need to implement actions that would trigger our services. Let's do it! Here is the final config:
Labels are in Russian here. As you can see, we've pasted actions properties to the entities descriptions. AllcountJS will automatically place buttons to the appropriate interfaces for us. Nice! But it still won't work because we've injected ExcelParser and Scraper services into the actions, but AllcountJS knows nothing about where to get this services. In order to let it know that we should do a little bit customization in application's start up. Let's write app.js file right in the project's root directory:
var injection = require('allcountjs'); injection.bindFactory('port', process.env.PORT || 9080); injection.bindFactory('dbUrl', process.env.MONGOLAB_URI || 'mongodb://localhost:27017/motochast'); injection.bindFactory('gitRepoUrl', 'app-config'); injection.bindFactory('ExcelParser', require('./services/excel-parser')); injection.bindFactory('Scraper', require('./services/scraper')); var server = injection.inject('allcountServerStartup'); server.startup(function (errors) { if (errors) { throw new Error(errors.join('\n')); } });
You should run this by node app.js command as any usual Node.js application. Our MVP is ready to see the world now!
What's next?
Now we've got the MVP. Is that all you can do with AllcountJS? The answer is surely no. For instance, if you get an insight about demand for a mobile app -- you can easily get it. Do you need for some awesome custom user interfaces? No problem. Next you can customize your app however you want -- the only restriction is Node.js itself - Which means almost, sky is the limit and you donāt have to stop with the MVP but can go ahead and continue building the complete product. Our team would be more than happy to help you on your way. Welcome to our chat room!
It is unnecessary to introduce point of sales (POS) concept - it is everywhere in our life, not only in markets and places related to sales directly. Also it is tightly coupled with CRMs, inventory, warehousing, etc. With AllcountJS you can rapidly create all of this stuff. Letās see how we can construct POS POC.
Considering model
First things first so letās start with the essential thing -- items which we are going to sale.
The most interesting thing here is the stock field. It should be computed according to quantities of this items involved into transactions. So that's what we exactly wrote in our configuration: integer field stock is computed as sum of transactions' quantity - but with parentheses, dots and quotes. And the ātransactionsā here is the field defined as ārelationā with currently absent entity. Relation is defined by itās name, related entity type and the fieldās name of the related entity which reference to this item. So letās add the Transaction entity type:
As you can see, field āitemā is referencing to the Item and maintaining the relation previously defined. So nothing special here except āshowInGridā option -- it tells the view to show only listed fields by its identifiers. Next letās define the central part of our POS:
I called it ācentralā because this is the part where we define our main view: the āPoint Of Saleā as a custom view located in file āpos.jadeā which we will tackle with later. Also youāve surely noticed ābeforeSaveā and ābeforeDeleteā functions. They are handlers which triggers when the appropriate event happens. More about such as functions you can learn from our documentation. Iāll just mention that we need this functions here to update orderās counter when order is completed and clear up all related items from this order when itās removed. You can also see relational and computed fields here. The latter is to show up total price of particular order and the former is the relation with entities which should be considered as items containing this order:
Now you might noticed that every entity type which participate in the relational fields have its own āshowInGridā configuration. This is because usually we donāt want to show user the owner entity -- he get himself there through it. But certainly you have an ability to customize this behaviour as youād like. Here we have the CRUD-hooks too: we need to update total price according to ordered quantity before save this particular orderās item, we need to remove transaction and create a new one when editing of orderās items is successfully completed and we need to remove the transaction made with ordered item if it exists when this orderās item is going to be removed. Also we mark the āfinal priceā field as a total-row-participant by calling the āadd to total rowā function on it. By the way, we worship of DRY principle, hence weāve reused function removing transation:
function removeTransaction(Crud, Entity) { var crud = Crud.crudForEntityType('Transaction'); return crud.find({filtering: {orderItem: Entity.id}}).then(function (transactions) { if (transactions.length) { return crud.deleteEntity(transactions[0].id); } }); }
Just in case of the consecutive numbering of orders weāve got singleton counter of orders:
Recently weāve mentioned this view as an essential thing for such as applications. Remember yourself tackling horrible and overloaded userās interfaces. And how happy you might be pushing just a few big colored buttons and getting your job done by it. Thatās why we emphasize our customizability of UI. The bottom line is: if you have an outstanding UX, you can easily express it in the UI! And this is what we exactly going to do now -- create a file called āpos.jadeā with the following content:
We are not going to stop on its content this time, but you can see it in action by running the demo!
Putting all things together
Alright, so far weāve defined our nice and nifty view, decided about model and some business logic. If you follow our blog you should be familiar with starting up AllcountJS. Iāll be brief with it:
Create directory somewhere on your computer (e.g. mkdir pos-example)
Create the app.js file inside it with the following content (as you can see weāve added menus there and some sample data just for the sake of presentability):
Place recently introduced pos.jade file right behind app.js
Install allcountjs-cli if you haven't done it yet.
Run the server by command allcountjs -c app.js (Note: you should already have Mongo DB service up and running)
Afterword
Hope you enjoyed reading this case study. The feedback would be very appreciated! Don't forget to follow our blog just not to miss something interesting! Also you are very welcome to our gitter chat -- you can ask there whatever you want regarding every Node.js-related aspect or development in general.
Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
ā Live Streamingā Interactive Chatā Private Showsā HD Qualityā Free Actions
Free to watch ⢠No registration required ⢠HD streaming
Kanban board mobile application with AllcountJS and Ionic
In previous blog post we discussed how to bootstrap your mobile AllcountJS application with Ionic. Today weāre going to go further and discover how to do some sort of meaningful application from user standpoint. Weāre going to build kanban board view for one of our demos: customer development CRM.
You can run CusDev CRM Demo here.
After all our Kanban board should look like following
So letās start. If you missed the Blog post about how to bootstrap your mobile app you could check it out for detailed explantation of how to do it. But for now Iāll go pretty quick with it.
Tools:
$ npm install -g allcountjs-cli cordova ionic
Bootstrapping and starting CusDev CRM AllcountJS application:
$ allcountjs init --template cusdev-crm
Iām using crm-app-allcountjs name for my newly created application.
$ cd crm-app-allcountjs && npm install $ allcountjs run
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width"> <title></title> <link href="lib/ionic/css/ionic.css" rel="stylesheet"> <link href="css/style.css" rel="stylesheet"> <!-- IF using Sass (run gulp sass first), then uncomment below and remove the CSS includes above <link href="css/ionic.app.css" rel="stylesheet"> --> <!-- ionic/angularjs js --> <script src="lib/ionic/js/ionic.bundle.js"></script> <script src="lib/underscore/underscore.js"></script> <script src="lib/jquery/dist/jquery.js"></script> <script src="lib/jquery.inputmask/dist/jquery.inputmask.bundle.js"></script> <script src="lib/allcountjs-angular-base/allcount-base.js"></script> <script src="lib/allcountjs-ionic/allcount-mobile.js"></script> <!-- cordova script (this will be a 404 during development) --> <script src="cordova.js"></script> <!-- your app's js --> <script src="js/app.js"></script> </head> <body ng-app="starter"> <ion-nav-view></ion-nav-view> </body> </html>
www/js/app.js
angular.module('starter', ['ionic', 'allcount-mobile']) .run(function($ionicPlatform, lcApiConfig) { lcApiConfig.setServerUrl('http://localhost:9080'); $ionicPlatform.ready(function() { // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard // for form inputs) if(window.cordova && window.cordova.plugins.Keyboard) { cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true); } if(window.StatusBar) { StatusBar.styleDefault(); } }); }) .config(function ($stateProvider, $urlRouterProvider) { $stateProvider .setupStandardAllcountMainState('app', 'lib/allcountjs-ionic/templates') .setupStandardAllcountStates('app', 'lib/allcountjs-ionic/templates'); $urlRouterProvider.otherwise('/app/main'); });
Now run ionic serve and you should see your AllcountJS mobile app is showing auth modal for your AllcountJS backend.
Cool! Project bootstrap part has been done and now weāre going to add our custom view. Letās start with minimum viable setup and make 3 changes.
What was done so far? We use lc-list directive in our view to load FlowBoard entity type items, ng-repeat to render these items as cards (please note card class for item) and ion-infinite-scroll along with FlowBoardController logic to fetch items while scrolling. The most tricky moment here is app.flowBoard state overrides standard AllcountJS app.entity state and allows us to replace view for this entity flawlessly. Now if you run your ionic app youāll see card view of your Board menu item.
Nice! But it isnāt kanban board actually. So what weāre going to do is to load our statuses, wrap our lc-list with ion-slide repeated for each status in ion-slide-box in order to slide between status columns and filter items for each lc-list with corresponding status id.
So our modified www/flow-board.html will look like
angular.module('starter', ['ionic', 'allcount-mobile']) .run(function($ionicPlatform, lcApiConfig) { lcApiConfig.setServerUrl('http://localhost:9080'); $ionicPlatform.ready(function() { // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard // for form inputs) if(window.cordova && window.cordova.plugins.Keyboard) { cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true); } if(window.StatusBar) { StatusBar.styleDefault(); } }); }) .config(function ($stateProvider, $urlRouterProvider) { $stateProvider .setupStandardAllcountMainState('app', 'lib/allcountjs-ionic/templates') .state('app.flowBoard', { url: "/entity/FlowBoard", views: { 'menuContent': { templateUrl: "flow-board.html", controller: 'FlowBoardController' } } }) .setupStandardAllcountStates('app', 'lib/allcountjs-ionic/templates'); $urlRouterProvider.otherwise('/app/main'); }) .controller('FlowBoardController', function ($scope, lcApi, $ionicSlideBoxDelegate) { $scope.mainEntityTypeId = 'FlowBoard'; $scope.$on('$ionicView.enter',function () { $scope.boardColumns && $scope.boardColumns.forEach(function (column) { column.gridMethods.updateGrid(); }) }); lcApi.getFieldDescriptions($scope.mainEntityTypeId).then(function (descriptions) { var statusFieldDescription = _.find(descriptions, function (d) { return d.field === 'status'; }); return lcApi.referenceValues(statusFieldDescription.fieldType.referenceEntityTypeId); }).then(function (statusReferenceValues) { $scope.boardColumns = statusReferenceValues; $scope.boardColumns = _.filter($scope.boardColumns, function (obj) { return !!obj.id; }) $scope.boardColumns.forEach(function (column) { column.loadNextItems = function () { column.gridMethods.infiniteScrollLoadNextItems().then(function () { $scope.$broadcast('scroll.infiniteScrollComplete'); }) }; }) $ionicSlideBoxDelegate.update(); $scope.updateTitle(0); }); $scope.updateTitle = function (columnIndex) { $scope.title = $scope.boardColumns[columnIndex].name; } $scope.columnFiltering = function (column) { return {filtering: {status: column.id}}; } });
Letās build it and see the result. Iām using iOS but you can use Android as well. Donāt forget to override your server url to accessible one for emulator and to set up security policy properly (See previous blog post)
Mobile application and backend with Ionic and AllcountJS in less than a hour
Nowadays is the time when people expect both web UI and mobile client application in their phones for almost every project. And itās actually pretty tough and time consuming problem to ship all of this stuff at the same time. In this tutorial weāre going to build backend, web UI and hybrid mobile application using AllcountJS and ionic in less than a hour.
Letās start with backend and web UI. For this example I'll use twenty two lines hello world application template. If you havenāt set up AllcountJS develeopment environment yet please visit our Getting Started or watch Video tutorial from one of our previous blog posts.
Letās init our backend project repository with
$ allcountjs init
Iām using default name helloworld-app for my project. The next step is to install dependencies as CLI suggests:
$ cd helloworld-app && npm install
And to get our server up and running we should execute following command at the project dir (donāt forget to startup your local MongoDB instance)
$ allcountjs run
If all went ok we should see something like
Using db url: mongodb://localhost:27017/helloworld-app Failed to fetch "app-config". Trying to use as regular directory. Application server for "app-config" listening on port 9080
So when you open http://localhost:9080/ youād see your application up and running.
First part is finished here and weāre going to create mobile application as our next.
If you havenāt installed ionic itās time to do that: Getting Started. Actually itās as simple as
$ npm install -g cordova ionic
Now letās change directory to parent of our helloworld-app and run init for ionic:
$ ionic start helloworld-app-mobile blank
Now letās change current directory to newly created ionic appās directory and run ionic serve in order to check what we have now:
$ cd helloworld-app-mobile && ionic serve
If all went nice youād see blank ionic starter app. Now letās install allcountjs-ionic with (if you havenāt installed bower yet please run npm install -g bower)
$ bower install allcountjs-ionic --save
Then open helloworld-app-mobile/www/index.html and add AllcountJS dependencies just right after ionic.bundle.js declaration
angular.module('starter', ['ionic', 'allcount-mobile']) .run(function($ionicPlatform, lcApiConfig) { lcApiConfig.setServerUrl('http://localhost:9080'); $ionicPlatform.ready(function() { // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard // for form inputs) if(window.cordova && window.cordova.plugins.Keyboard) { cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true); } if(window.StatusBar) { StatusBar.styleDefault(); } }); }) .config(function ($stateProvider, $urlRouterProvider) { $stateProvider .setupStandardAllcountMainState('app', 'lib/allcountjs-ionic/templates') .setupStandardAllcountStates('app', 'lib/allcountjs-ionic/templates'); $urlRouterProvider.otherwise('/app/main'); });
Letās check our result by running ionic serve from helloworld-app-mobile directory. You should see login dialog where you could just enter admin/admin. After login youād see your app menu described in AllcountJS app config.
Cool! But letās go further and build and run native iOS application then. In order to do this weād need to add cordova ios platform
$ ionic platform add ios
But before youāll run app on emulator you should change your app server url to something accessible for it. For myself I used my IP on local network. Letās change it in helloworld-app-mobile/www/js/app.js
Another one thing I definitely need to mention is the new Apple security policy for iOS 9. So in order to use insecure http endpoints you should add this to your platforms/ios/helloworld-app-mobile/helloworld-app-mobile-Info.plist just after first <dict> tag before run (see http://stackoverflow.com/questions/30731785/how-do-i-load-an-http-url-with-app-transport-security-enabled-in-ios-9 for details):