Přejít k navigační liště

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

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

Články JavaScript

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.

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.

Komentáře

Subscribe
Upozornit na
guest
9 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Medvěd

Ahoj, narazil jsem na jednu chybku ( i když v demo to funguje :-) ).

//TODO nahravani obrazku, pouze docasne reseni
app.put(‚/api/v1/products/*/images‘, function (req, res) {
res.send(204);
})

, aby vše fungovalo, mělo by být app.post …

Martin Kučera

Zajímavé, ve firefoxu se mi progress bar vůbec nezobrazí. V chromu sice ano, ale když dám nahrávat víc souborů najednou, tak se to začne chovat celkem zajímavě :).

Lukas Svoboda

Chtel bych moc podekovat autorovi za tento serial – je skvelej.
Take bych chtel pozadat, o uvolneni zdrojovych kodu s nejakou dostatecne free licenci – alespon ted na githubu zadna licence specifikovana neni – takze jestli to chapu spravne, nemuzu pouzit copy&paste ani na jedinou radku, coz je skoda. Pokud bych nekdy zacal AnguarJS urcite by se hodilo – a musim rict, ze me serial o pouziti Angularu v produkci dost presvedcuje.

Martin

Dobrý den,
tento seriál je hodně zajímavý, ale už 14dní nevyšel další díl. Bude seriál pokračovat?
Díky za odpověď.

Martin Hassman

Ano bude, jen autor vzhledem k nedostatku času další díly na čas odložil.

Petr Kučera

Dobrý den,
jelikož e-shop je celkem speciální oblast webů (žádný technický blog pro nadšence), jak je řešena podpora mezi prohlížeči – tedy klidně i v nějaké korporaci zastaralé IE6, nebo IE7 ?
Zároveň jelikož chceme prodávat, jak se přistupuje ke klientům, kteří mají vypnutý javascript?
HTML5, CSS3, tuna javascriptu, AngularJS… Je to pěkné, chtěl bych v tom dělat, ale co výše zmíněné příklady?
Jasně, u zájmového webu, blogu apod. je to asi jedno, ale když děláte aplikaci klientovi na míru, tak vám dřív, nebo později začne hlásit chyby ve smyslu, že mu to nefunguje v práci, kde mají politiku na IE6, nebo mu někteří jeho klienti hlásí, že jim to nefunguje a zjistíte, že musí mít vypnutý javascript…
Tomu klientovi prostě nevysvětlíte, že se nejedná o chybu, ale o zastaralost, on si koupil aplikaci a ta má fungovat všude, je to chyba a chce to spravit, samozřejmě zadarmo…
Má otázka tedy je – jak se v tomto konkrétním tutoriálu řeší staré prohlížeče, různé verze, vypnutý javascript apod…?
A jak se to má řešit obecně?

Děkuji

Lukas Svoboda

Na http://weblog.ronnieweb.net/2013/03/pokracovani-serial-na-zdrojaku-o-node-jsangularjshtml5/ jsem si precetl ze „Za pár dní si dokonce budete moci vyzkoušet nejen demo verzi, ale normální e-shop, který je na tom ze Zdrojáku postaven“.
Protoze serial velmi hltam a netrpelive cekam na pokracovani, rad bych alespon v mezicase nastudoval zdorjaky celeho shopu. Jsou/budou nekde k dispozici? (obzvlast se tesim na authentication a implementaci service pro platebni branu).

Zaroven chci autorovi podekovat za jeden z nejkvalitnejsich clanku a to nejen na zdrojaku (a stejne tak za zverejeni soucasnych zdrojovych kodu k serialu pod MIT licenci).

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.