📜 ⬆️ ⬇️

One-page store on Phalcon PHP + AngularJS. Bug work

image

Introduction

Hello! Not so long ago, I wrote a publication “One-page store with a basket on the Phalcon + AngularJS + Zurb Foundation” , which had an ambiguous effect to say the least. Or rather, I received many negative comments, some were objective and constructive, some were not, and they made me think about why it happened, because I wanted to make a useful manual that would be useful to me and others who are starting to write on AngularJS.

Confession

Yes, the manual was useful for me, for me old, the one who was denied work in the local web studio in 2009, and to this day he has never worked in a team, has never worked in a hired job, but relied only for myself, and the main criterion for the effectiveness of project implementation was one - the main thing that works. But this is me - old, after writing that article, and many comments, for the first time I decided to try to do everything according to the rules of good form, at least for the sake of interest.

Bibliography


Usually, the list of references is given at the end, but on the other hand, as the article is written, questions will arise as to why such an implementation was chosen. And in order not to leave you without answers to your questions, I will indicate in brackets [?] The source from which it was taken. So, here is the literature on which I relied, correcting my mistakes.
')
  1. Comments on the article One-page store with a basket on the Phalcon + AngularJS + Zurb Foundation
  2. Bold AngularJS for Team Development [1/2]
  3. Bold AngularJS for team development [2/2]
  4. angular js: ng-repeat no longer allowing duplicates
  5. Using local storage
  6. AngularJS $ http not sending X-Requested-With header
  7. ngRoute preloader example


Bug work



In the course of the one-page food delivery project, the following shortcomings were unintentionally made:



In this article I want to share how I was looking for a solution, and what came of it. Now let's go through each item.

Downloading all the products on one page at once using php


The products in the store turned out to be slightly more than calculated, and considering our Kamchatka Internet, we calculated:
100 products (images), each picture 100-300 kb, in the end, only the goods ate up the precious traffic of our clients in the amount of 10 mb, plus almost 10 mb weighed the site itself (pictures, styles, scripts). Now I was able to optimize everything, and the site weighs 3.9 mb during the first download, and no more than 1 mb in the next, as the browser caches everything you need.

This optimization was achieved by compressing large background images without losing quality, as well as sprites for icons and small graphics, as well as blocks that did not play an important role on the page (reviews, partners, certificates, why we open the reverse form) were removed. links replaced by jivosite). All the same, the download speed for the site with food is much more important than our laurels and awards, which nobody needs.

And of course, the optimization was due to the refusal to render all the goods on the page at once using php, and was replaced by ajax loading json data by clicking on the link like / #! / Menu / 7, but I finally had a chance to learn how angularjs routing works, to my shame, I had never worked with js routing before.

And it turned out not as difficult as I expected, in fact I had only one route:

function config($routeProvider, $locationProvider) { $routeProvider .when('/menu/:id', {templateUrl: '/app/views/products.html', controller: ProductsController}); $locationProvider.hashPrefix('!'); $locationProvider.html5Mode(true); } angular.module('rollShop').config(config); 


He then loaded json data from the server and rendered it in a template:

 <div class="large-12 columns" ng-view></div> 


In general, I heard that to index this approach, the server should return html instead of json, but then it is not clear how to avoid inline inserts of PHP scripts? This question has remained a mystery to me. And this approach has created another problem, if the user clicking on the link / #! / Menu / 7 presses the button “Refresh page”, then his eyes will be filled with blood from the type of json data he will see instead of the normal page just json data. And here the Phalcon PHP Framework comes to help us, in fact, it was possible without him, but since I work with him, I do everything in his style. To correct this error, I decided that I would give json data only with ajax requests, and with normal requests I will redirect the user via the link / #! / Menu / $ id.

 public function menuAction($id) { if($id != 'undefined') { if ($this->request->isAjax() == true) { //   $category = Category::findFirst($id); //    $sub_category = Category::find("pid = '" . $id . "'"); if (count($sub_category) > 0) { $products['category'] = $category->name; foreach ($sub_category as $key => $val) { $products['subcategory'][$key] = array( 'name' => $val->name, 'products' => Products::find("category = '" . $val->id . "'")->toArray() ); } } else { $products = array( 'category' => $category->name, 'products' => Products::find("category = '" . $category->id . "'")->toArray() ); } $this->response->setContent(json_encode($products)); return $this->response->send(); } else { $this->response->redirect('/#!/menu/' . $id); return false; } } else { return false; } } 


Check for AJAX request in Phalcon, using the condition:

 if ($this->request->isAjax() == true) { //This is Ajax } 


But there is a problem, by default AngularJS does not send a special X-Requested-With header to the server [6], which means that without the help of AngularJS, this function will not work, so I had to add one line to the Angular script.

 var app = angular.module('rollShop', ['ngRoute', 'mm.foundation'], function ($httpProvider) { $httpProvider.defaults.headers.common["X-Requested-With"] = 'XMLHttpRequest'; } 


Now everything works, and the user will never see bare JSON.

By the way, for indexing, I thought I could make a check on the bot, and give the bot pure html, without js code, without buttons, just the goods. True, I don’t know if it will work or if it’s correct, can someone tell me in the comments.

Add to cart through the insertion of inline PHP scripts


Due to the correction of the previous bug, the bug was automatically corrected with the insertion of inline PHP scripts into the js function. Let's compare how the goods were added to the cart before, and how it became now.

To (fu):

 <div class="add-cart"> <input type="number" ng-model="num<?=$p['id']?> value="1" min="1" max="50"> <button type="button" ng-click="addCart(<?=$p['id']?>, num<?=$p['id']?>, '<?=$p['title']?>', <?=$p['price']?>)"></button> </div> 


 $scope.addCart = function(id, num, title, price) { var nums = num || 1; $scope.carts.push({ id : id, num : nums, title : title, price : price }); }; 


After:

 <div class="add-cart"> <input type="number" ng-model="item.quality" min="1" max="50" placeholder="1"> <button type="button" ng-click="addCart(item)"></button> </div> 


 $scope.addCart = function (item) { CartsService.addCart(item); }; 


Honestly, I like this approach much more, although the customer will not appreciate it, and will not pay more, but somehow it became proud of itself, and it was pleasant to my soul.

The lack of a uniform style of writing code


While correcting the previous two shortcomings, I automatically had to correct two more - “use of logic in controllers”, as you see, now the service does everything, and “the lack of a single style of writing code”.

This is my eternal problem, but again, if you look from the point of view of economics, and the “most important thing - it works” approach, everything is quite effective here, not bothering with the style guides, I saved a lot of my time, and time, as you know, money. By the way, the budgets of my clients range from 50 to 100 thousand rubles, and they don't care how the code is written. But these are all excuses, because I want to correct the situation and make the code beautiful, readable and logical, in which two articles on Habré [2] and [3] helped me.

As a result, I got 2 controllers, 3 factories, 1 view and one common app.js. It looks like this now:



Actually, as recommended to do in those stylgaydah. And now let's look at the controllers, and can compare with the previous article.

ProductsController.js

 function ProductsController($scope, $routeParams, ProductsService, CartsService) { $scope.items = ''; ProductsService.getData($routeParams.id).success(function(data){ $scope.items = data; }); $scope.addCart = function (item) { CartsService.addCart(item); }; } angular.module('rollShop').controller('ProductsController', ProductsController); 


CartsController.js

 function CartController($scope, CartsService) { $scope.carts = CartsService.getItemsCart(); $scope.total = function(){ return CartsService.summary($scope.delivery) }; $scope.removeItem = function (carts, item) { CartsService.removeItem(carts, item); }; } angular.module('rollShop').controller('CartController', CartController); 


Everything is cool, as it seems to me, even most like more than what was before, more precisely, what was happening earlier in my controllers.

Storing the recycle bin in the js object, which was reset after reloading the page


The next drawback, to which readers first of all paid attention, is the storage of the goods in the js object in the basket. Why I chose this approach, I explained in the last article, because the store is one-page. In any case, it is easy to implement storage in localStorage, and I think it will come in handy later.

I started looking for a convenient module for AngularJS that would work with localStorage, but at the same time it should be simple and easy. I found several implementation options, but they were complex and large in my opinion, unfortunately I don’t remember the links now, and then I came across the option that was just for me [5].

 angular.module('ionic.utils', []) .factory('$localstorage', ['$window', function($window) { return { set: function(key, value) { $window.localStorage[key] = value; }, get: function(key, defaultValue) { return $window.localStorage[key] || defaultValue; }, setObject: function(key, value) { $window.localStorage[key] = JSON.stringify(value); }, getObject: function(key) { return JSON.parse($window.localStorage[key] || '{}'); } } }]); 


True use of it in this form did not give 100% proper operation. When adding one product to the cart, everything was fine, but as soon as the second different product or product from another category got there, ng-repeat stopped displaying the goods in the basket, and an error occurred in the console:

Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys.


The solution to this error, I found in the source [4]. The problem was due to adding to the $$ hashKey array, I never figured out what it was, where it came from, and why, but I had to get rid of it so that everything worked, and the final version of the factory working with localStorage looks like this:

 function LocalStorageFactory($window) { return { set: function(key, value) { $window.localStorage[key] = value; }, get: function(key, defaultValue) { return $window.localStorage[key] || defaultValue; }, setObject: function(key, value) { $window.localStorage[key] = JSON.stringify(value, function (key, val) { if (key == '$$hashKey') { return undefined; } return val; }); }, getObject: function(key) { return JSON.parse($window.localStorage[key] || '{}'); }, remove: function(key){ $window.localStorage.removeItem(key); }, clear : function() { $window.localStorage.clear(); } } } angular.module('rollShop').factory('LocalStorageFactory', LocalStorageFactory); 


I think the code is small, and it will not be difficult to find 2 differences; checking for $$ hashKey helped me fix the duplicate error. Now the basket service worked properly and well, remembering positions, recalculating the sum, etc. I give the code below:

 function CartsService(LocalStorageFactory) { var CartsService = {}; var CartData; if(LocalStorageFactory.getObject('carts').length > 0) { CartData = LocalStorageFactory.getObject('carts'); } else { CartData = []; } CartsService.addCart = function (item) { CartData.push({ id: item.id, num: item.quality || 1, title: item.title, price: item.price }); CartsService.update(); }; CartsService.update = function() { if(LocalStorageFactory.getObject('carts').length > 0) { LocalStorageFactory.remove('carts'); } LocalStorageFactory.setObject('carts',CartData); }; CartsService.getItemsCart = function () { return CartData; }; CartsService.removeItem = function (items, id) { items.splice(id, 1); CartsService.update(); }; CartsService.summary = function(dispatch) { var total = 0; var delivery = 0; angular.forEach(CartsService.getItemsCart(), function (item) { total += item.num * item.price; }); //      return total + delivery; }; return CartsService; } angular.module('rollShop').factory('CartsService', CartsService); 


Not perfect, but still better than it was.

Responsiveness of the interface in the course of loading new categories


JSON data coming from the server weighs no more than 10 kb, plus the server upload time, plus js response time, generally in the case of the great Kamchatka Internet with a huge ping, it makes sense to show the user a preloader on every request, and this is anyway good .

Of course, this can be given to the controller, but I wanted a more universal solution, and again at the same time, a simple and easy one. At first, I thought of using the nProgress lite plugin for AngularJS, but still decided to make it even easier, and found an example [7].

 function run($rootScope, $timeout) { $rootScope.layout = {}; $rootScope.layout.loading = false; $rootScope.$on('$routeChangeStart', function () { $timeout(function(){ $rootScope.layout.loading = true; }); }); $rootScope.$on('$routeChangeSuccess', function () { $timeout(function(){ $rootScope.layout.loading = false; }, 500); }); $rootScope.$on('$routeChangeError', function () { $timeout(function(){ $rootScope.layout.loading = false; }, 500); }); } angular.module('rollShop').run(run); 


Now when one of the events comes (I think everything is clear from the name of the event), the preloader is shown / hiding.

 <div class="large-12 columns" ng-hide="!layout.loading"> <!--  ,  gif ,    ,     --> </div> 


Now the client will not click on the category 5 times, not knowing whether to load it or not.

Conclusion


I tried to correct the shortcomings made during the development, but the main goal was to accustom myself to the competent use of technology, even despite the lack of a command. Honestly, I got a lot of pleasure, although I didn’t spend too much time working, and didn’t receive any compensation for it. For myself, I concluded that I will do all subsequent projects with a more competent approach, because such an approach makes us appreciate not only the money received for the project, but also the knowledge and experience gained while working on it. Experience and knowledge give pride in their work, improve the quality of work, love for what you do. And all this will not leave you without money in your pocket.

Source: https://habr.com/ru/post/246733/


All Articles