Tvorba moderního e-shopu: nahrávání obrázků k produktu

E-shop-1

HTML5 přináší velké množství novinek, které významně zpříjemňují úkoly jako například nahrávání souborů do aplikace. HTML5 element progress, input typu file či XmlHttpRequest 2 patří mezi témata dnešního dílu.

Seriál: E-shop pomocí moderních technologií (15 dílů)

  1. Úvodní analýza pro moderní e-shop 4.1.2013
  2. Návrh uživatelské části e-shopu 11.1.2013
  3. Tvorba uživatelské části e-shopu 18.1.2013
  4. Nákupní košík pomocí HTML5 Web Storage 25.1.2013
  5. Tvorba moderního eshopu: kategorie a parametrické hledání 1.2.2013
  6. Tvorba moderního e-shopu: dokončení uživatelské části 8.2.2013
  7. Tvorba moderního e-shopu: plánování administrace 15.2.2013
  8. Tvorba moderního e-shopu: správa objednávek 22.2.2013
  9. Tvorba moderního e-shopu: nahrávání obrázků k produktu 1.3.2013
  10. Tvorba moderního e-shopu: Bower, Yeoman a Gemnasium 15.7.2013
  11. Tvorba moderního e-shopu: HTML5 drag & drop a kategorie 29.7.2013
  12. Tvorba moderního e-shopu: zpracování chyb 12.8.2013
  13. Tvorba moderního e-shopu: Rich-Text Editing a dokončení administrace 26.8.2013
  14. Autentizace v single-page aplikacích 9.9.2013
  15. Autentizace v single-page aplikacích – serverová část 7.10.2013

Tvorbu správy produktů rozdělíme na dvě části. V dnešním díle se budeme věnovat nahrávání obrázků k produktům. Jak je již zvykem, k dispozici je demo verze aktuálního stavu aplikace. Zdrojové kódy aplikace jsou v tagu eshop09.

HTML5 element progress

Element progress se používá pro vyjádření průběhu nějaké činnosti. Je podporován všemi moderními prohlížeči. Internet Explorer však zavádí jeho podporu až v 10. verzi (to platí ostatně pro celý zbytek textu, vše funguje až od IE 10). K dispozici máme dva atributy: value udává aktuální hodnotu (např. 60% obrázků již bylo nahráno) a max logicky maximální hodnotu.

Všechny prohlížeče ho vykreslují jako klasický progress bar. Firefox navíc umožňuje určit CSS vlastnost orient s hodnotami „horizontal“ či „vertical“, které říkají, zda chceme progressbar vykreslit horizontálně či vertikálně.

<progress value="60" max="100">60%</progress>
ProgressBar

ProgressBar

XmlHttpRequest Level 2 umožňuje registrovat callback pro událost progress, přes který můžeme snadno aktualizovat hodnotu value, takže celá implementace ukazatele stavu nahrávání souborů do aplikace je v HTML5 velmi jednoduchá, jak bude patrné z dalšího textu.

HTML5 novinky u elementu pro nahrávání souborů

Zásadní novinky v HTML5 se týká elementu input typu file. Nově můžeme použít atribut accept, ve kterém specifikujeme, jaký typ souboru chceme nechat uživatele nahrávat. Jako hodnotu můžeme uvést buď validní MIME typ nebo hodnoty „audio/*“ pro omezení na audio soubory „video/*“ pro omezení na video soubory a „image/*“ pro výběr obrázků.

Dále můžeme uvést atribut multiple. Ten umožní uživateli vybrat více než jeden soubor. Kromě elementu input typu file je možné atribut ještě použít u typu email.

Jakmile uživatel vybere dané soubory, máme k dispozici jejich seznam v kolekci FileList. Za předpokladu, že uživatel právě vybral nějaké soubory elementem s ID „obrazky“, můžeme jejich seznam získat takto:

var obrazky = document.getElementById('obrazky').files;

Kolekce FileList obsahuje instance třídy File, kde máme k dispozici např. informace o názvu, MIME typu či velikosti. Chceme-li např. získat název prvního vybraného souboru, pak to lze udělat takto:

var nazev = document.getElementById('obrazky').files[0].name;

Vyvolání události onchange po výběru souborů v AngularJS

Když budeme v naší aplikaci vybírat obrázky, nebudeme k ním dodávat žádné další informace. Uživatel je zkrátka jen vybere. To znamená, že ani nepotřebujeme žádné tlačítko pro spuštění nahrávání, ale můžeme soubory nahrát ihned po výběru.

K tomu se hodí standardní událost onchange, která zavolá zaregistrovaný callback po výběru souborů. Bohužel v AngularJS zatím není pro input typu file implementovaná direktiva ngChange, takže si musíme vytvořit direktivu vlastní:

module.directive('upload', function upload(){
  var config = {
    restrict: 'A',
    scope: {
      upload: '='
    },
    link: function(scope, element) {
      element.bind('change', function(){
        scope.$apply(function(){
          scope.upload(element[0]);
        });
      })  
    }
  }
  return config;
});

Nyní stačí přidat k elementu input atribut upload a v něm uvést název metody controlleru, kterou poté budeme chtít zavolat.

Zobrazení náhledu vybraných obrázků

Jakmile začneme obrázky nahrávat, budeme uživateli chtít zobrazit jejich náhledy. To není nic těžkého, stačí projít FileList a zavolat metodu window.URL.createObjectURL(File file). Ta vrátí textovou hodnotu URL, kterou můžeme použít v atributu src elementu img a ukázat nahraný obrázek:

<img ng-repeat="img in imgs" ng-src="{{img}}" width="100">
$scope.imgs = [];
$scope.upload = function(element) {
  for (var i = 0; i < element.files.length; ++i) {
    var src = $window.URL.createObjectURL(element.files[i]);
    $scope.imgs.push(src);
  }
}

Za zmínku stojí použití služby $window. To je pouze obal nad globálním objektem window. Doporučuje se používat tuto službu kvůli jednoduššímu testování.

XmlHttpRequest Level 2

Takže nyní máme obrázky nahrané v aplikaci, ale potřebujeme je také poslat na server a trvale je uložit. Naštěstí opět nejde o nic složitého.

K dispozici totiž nově máme třídu FormData, kam vkládáme data z formuláře. Jako hodnotu také můžeme vložit instanci třídy File:

var xhr = new XmlHttpRequest();
var fd = new FormData();
fd.append('file', document.getElementById('obrazky').files[0]);
xhr.open('POST', '/example');
xhr.send(fd);

Kompletní implementace nahrávání obrázků na server

Jak tedy vypadá kompletní nejjednodušší implementace nahrávání obrázků na server přímo v AngularJS? Nejprve kompletní HTML:

<h3>Obrázky</h3>
<form enctype="multipart/form-data">
  <input id="file" ng-model=”file” type="file" upload="upload" multiple accept=”image/*”>
</form>
<p ng-show="pbar"><progress value="{{progress}}" max="100"></progress> {{progress}}%</p>
<div ng-repeat="img in imgs">
  <img ng-src="{{img}}" width="100"> 
</div>

Dále si vytvoříme vlastní třídu UploadFile, která se postará o nahrání souborů na server. V konstruktoru se předává instance XmlHttpRequest a FormData kvůli snazší testovatelnosti. V metodě upload() se předává informace o tom, kam se soubory budou posílat a registrují se také callbacky pro několik událostí:

  1. completeFn se zavolá po dokončení nahrání,
  2. errorFn se zavolá v případě chyby,
  3. cancelFn se zavolá při zastavení nahrávání uživatelem,
  4. progressFn se bude volat v průběhu nahrání a bude informovat uživatele o tom, v jaké fázi nahrávání je.

Její implementace vypadá takto:

function Upload(xhr, fd) {
  this._xhr = xhr;
  this._fd = fd;
  this._files = [];
}

Upload.prototype.getFiles = function() {
  return this._files;
};

Upload.prototype.setFiles = function(files) {
  this._files = [];
  for (var i in files) {
    this.addFile(files[i]);
  }
};

Upload.prototype.addFile = function(file) {
  this._fd.append('file', file);
  this._files.push(file);
};

Upload.prototype.upload = function(method, url, completeFn, errorFn, cancelFn, progressFn) {
  if (progressFn) this._xhr.upload.addEventListener('progress', progressFn, false);
  if (completeFn) this._xhr.addEventListener('load', completeFn, false);
  if (errorFn) this._xhr.addEventListener('error', errorFn, false);
  if (cancelFn) this._xhr.addEventListener('abort', cancelFn, false);
  this._xhr.open(method, url);
  this._xhr.send(this._fd);
};

Nyní potřebujeme vytvořit službu uploadFile, která vytvoří zmíněné instance XmlHttpRequest a FormData, předá je instanci třídy UploadData, kterou pak vrátí:

module.factory('uploadFile', function(){ 
  return function() {
    return new Upload(new XMLHttpRequest(), new FormData());
  }
});

Službu také doplníme do služby api, abychom komunikovali se serverem jednotným způsobem:

api.product.upload = function(params, completeFn, errorFn, cancelFn, progressFn) {
  params.upload.upload(
    'PUT',
    url + 'products/' + params.id + ‘/images’,
    completeFn,
    errorFn,
    cancelFn,
    progressFn
  );
}

A konečně implementujeme controller, ve kterém registrujeme callbacky pro výše zmíněné události:

module.controller('ProductDetailCtrl', ['$scope', '$routeParams', '$window', 'api', 'uploadFile', function($scope, $routeParams, $window, api, uploadFile) {
  $scope.imgs = [];
  $scope.progress = 0;
  $scope.product = api.product.show({id: $routeParams.id});

  var error = function() {
    $scope.$apply(function(){
      $scope.pbar = false; 
    });
  }

  var cancel = function() {
    $scope.$apply(function(){
      $scope.pbar = false; 
    });
  }

  var complete = function() {
    $scope.$apply(function(){
      $scope.file.value = '';
      $scope.pbar = false; 
    });
  }

  var progress = function(evt) {
    $scope.$apply(function(){
      if (evt.lengthComputable) {
        $scope.progress = Math.round(evt.loaded * 100 / evt.total)
      }
    })
  }

  $scope.upload = function(element) {
    var upload = uploadFile();
    upload.setFiles(element.files);
    for (var i = 0; i < element.files.length; ++i) {
      var src = $window.URL.createObjectURL(element.files[i]);
      $scope.imgs.push(src);
    }

    $scope.pbar = true;
    api.product.upload({id: $routeParams.id, upload: upload}, complete, error, cancel, progress);
  }

}]);

Co dále

Ukázali jsme si implementaci jednoduchého nahrávání obrázků na server přes AngularJS s využitím novinek HTML5. Později ještě naši implementaci doplníme o možnost přetahování obrázků přímo do okna prohlížeče pomocí HTML5 drag & drop. To nás čeká později, jakmile budeme pracovat s kategoriemi. Příště dokončíme sekci produkty.

Jakub pracoval na několika zajímavých projektech, za nejvýznamnější považuje vytvoření e-commerce řešení Shopio.cz. Poslední rok se plně věnuje Node.js, frameworku AngularJS a NoSQL databázím.

Komentáře: 9

Přehled komentářů

Medvěd
Martin Kučera
Lukas Svoboda super clanek
Jakub Mrozek Re: super clanek
Martin Další díly článku
Martin Hassman Re: Další díly článku
Jakub Mrozek Re: Další díly článku
Petr Kučera Podpora prohlížečů a vypnutý javascript
Lukas Svoboda zdrojaky celeho shopu
Zdroj: http://www.zdrojak.cz/?p=7259