Loading...
nakov avatar nakov SoftUni Team Trainer 5295 Точки

SPA with AngularJS: курсови проекти

Колеги, готови сме с условията на курсовите проекти за курса "SPA with AngularJS":

Както знаете, този курс ще се завършва с практически проект, който всеки си прави вкъщи. Ще има защита на живо и по Skype за онлайн курсистите.

Какво представлява AngularJS Ads проектът?

  • Дадено вие как изглеждат екраните от система за онлайн обяви (UI prototype)
  • Дадена е REST услуга, която пази данните и имплементира всички необходини действия
  • Посетителите на сайта без регистрация могат да разглеждат обявите, да се регистрират и логват
  • Логнатите потребители могат да си редактират обявите
  • Администраторът (поребител с по-високи права) може да редактира обяви, потребители, категории и градове - тази част е бонус, защото е по-трудна и по-оебмна за имплементация

За да ви помогнем по изграждането на практическия курсов проект добавяме още учебни занятия към курса:

  • 5 януари от 12 до 20 часа - колегата Владо ще пише проекта на живо пред всички, от нулата стъпка по стъпка. Ще запишем видео за онлайн курсистите. Няма да ви дадем сорс кода, за да си го напишете сами. Това е важно условие в заданието.
  • 6 януари - от 16 до 22 часа - ще правим Lab, отново работа по практическия проект.
  • 11 януари (неделя) - защита на проектите - ще направим записване и разпределение по часове.

В понеделник и вторник остават планираните учебни занятия:

  • Services, ruting, directives
  • Работа по практическия проект

Обръщам внимание че има важно изискане да работите с Git и да commit-вате редовно в GitHub. Ако не го правите, ще загубите половината от точките за проекта. Трябва да покажете, че сте работили здраво по този проект и не сте го преписали от ваш колега.

Проектите са индивидуални. Няма да има разпределение на отбори.

Ще запиша и видео разяснения по проекта до няколко дни.

Започвайте работа! Имате право да обсъждате публично и да споделяте кой до къде е стигнал.

За REST услугите админската парола е admin / admin.

Препоръчам ви да си пуснете услугите локално (показано е във видеото как става с [Ctrl+F5] във Visual Studio).

19
JavaScript Applications 26/12/2014 22:39:07
borislavml avatar borislavml 368 Точки

Някой успя ли да си синхронизира pagination-a на сървъра с клиента. Аз направих paging-a с тази ЦЪК библиотека. Става за 10 минути. Но....логиката ми се случва на клиента, в смисъл pаging-a работи с това, което е взел от сървъра в слуачая http://localhost:1337/api/ads , което ми връща по дефолт само първите десет обяви и броя на страниците. Какво ме грее мене този номер на страниците като дефакто аз нямам обявите от следващата страница при такъв рекуест. Варианта да си ги взема всички е да направя една груба заявка http://localhost:1337/api/ads?pagesize=1000&startpage=1 за първите хиляда(като се надявам да няма повече от 1000 обяви) и да си ги обработвам. Другия вариант е да си вържа пейджинга със рекуестите и фактически да ми прави нова заявка за следващите обяви при клик на следваща страница, което обаче не виждам как може да стане. И какво правим ако решим да не правим пейджинг. Как подяволите да си взема всички обяви като тоя сървиз за getAll ми връща само първите 10. Да речем, че разбирам от ASP.NET и си го настроя в сървър-кода, с който разполагам. Ами после... като вържа приложението към azure  cloude-а, там не мога да пипам. Изобщо целия тоя pagination ми се струва доста омазан. В момента съм го подкарал с тая грозна заявка pagesize=1000&startpage=1, но никак не ми харесва така. Ще се радвам, ако някой сподели как го е имплементирал.smile

0
ttitto avatar ttitto 1153 Точки

Аз успях да си взема обявите за всяка страница. Използвах $resource, на който за params обект подавам обект съдържащ параметрите за адс заявката (?CategoryId={CategoryId}&TownId={TownId}&StartPage={StartPage}&PageSize={PageSize}). Pagesize ми е константа в апа, startpage го взимам от клик върху бутон от pagination-а. Като направиш заявка с някакъв pageSize автоматично ти връща колко са numPages и може да го използваш за чертане на pagination-a

Може да погледнеш в гитхъба ми или ако не разбираш как точно го обясних ми пиши в скайп

1
nakov avatar nakov SoftUni Team Trainer 5295 Точки

По принцип трябва да направиш server-side paging, т.е. следното:

  • Викаш REST услугата с AJAX заявка да ти върне първата страница с данни
  • Рисуваш първата страница с данни на екрана (най-лесно е с binding)
  • Рисуваш пейджера с брай страници: numPages (колкото сървърът ти е подал, че има при него)
  • При промяна на текущата страница (клик върху пейджера), зареждаш съответната страница с нова REST заявка, и рисуваш данните на екрана (най-лесно е с binding)

Ето тук има примерче, което може да адаптираш за нашия сценарий: https://github.com/michaelbromley/angularUtils/tree/master/src/directives/pagination#working-with-asynchronous-data

Не е нужно да буташ никакви други параметри към REST услугата, освен ?startpage=page

5
borislavml avatar borislavml 368 Точки

Да, аз ползвам точно тази библиотека, ама като не съм си прочел цялата документация да видя, че има солюшън и за нашия сцернарий... Pagination-a заспа smile 

1
externo avatar externo 119 Точки

Във видеото казахте, че екипа ще отговаря на въпроси относно проекта неотнасящи се до админската част:

зациклих на визуализирането на категории и градове. Самите обяви ми се показват, но градовете и категориите не. При дебъгване source връща заявки с масиви съответно с градове и категории, но не знам защо не мога да ги байндна. Код, демо. Ако някой друг има желание - също може да помогне.

0
petrovaliev95 avatar petrovaliev95 358 Точки

Здравей, колега. Мисля, че проблема ти е споменат по-надолу. Обясних как се решава.

1
29/12/2014 12:24:14
externo avatar externo 119 Точки

благодаря, просто трябва да се чете максимално в дебъгера :)

0
n.velchev95 avatar n.velchev95 79 Точки

Здравейте, натъкнах се на проблем при извличането на градовете.

Грешката е следната: Error in resource configuration. Expected response to contain an object but got an array

Използвам метода с $resource за реализирането на заявката но сървъра ми връща масив а не обект и цялата работа се чупи. При извличането на данните с $q и $http нямам проблем но бих желал да използвам метода с $resource  - просто ми изглежда по-чисто и приятно. Някой може ли да ме посъветва какво да направя. Мерси предварително и весело изкарване на празниците. :)

0
Samuil.Petrow avatar Samuil.Petrow 1550 Точки

Не съм го пробвал и в момента не мога, но -> цъкв секция returns, това isArray като се сетне на false няма ли да се получи?

0
29/12/2014 09:11:12
petrovaliev95 avatar petrovaliev95 358 Точки

Колега имам същия проблем. Реших го като използвам "$http". "$resource" както ти си казал очаква обект а не масив от обекти. Незнам дали това е грешка в сървисите.

EDIT: Намерих решение на проблема. Когато правиш заявка с "$resource" извиквай функцията ".query()", а не ".get()". Така няма да хвърля грешка.

4
29/12/2014 12:25:07
ludmildev avatar ludmildev 13 Точки

Здравейте и от мен,

 

аз се справих с paging-a, с градовете, категорийте и филтрирането по тях, но зациклих след логин-а (успешен) как да рефрешна хедъра (без F5) понеже той не е в контролерите. Използвам $cookie и бутам в $rootScope username, access_token и isAdmin. Проблема е че трябва да рефрешна страницата за да се визоализират нещата. А все пак трябва да има по интелигентен начин за решаването на проблема (рефреш на целия апп). Провах доста неща, но не ми се получиха.

1
Shade avatar Shade 33 Точки

Изполвзвай $broadcast да пратиш event към $rootScope и закачи един MainAppController някъде в началото на апп-а(боди-то е добър вариант), в който слушаш за този event( $rootScope.$on('event name', fn()) );

3
ludmildev avatar ludmildev 13 Точки

Благодаря за отговора, звучи логично, още не сме толкова на "ти" с angular-a ;)

0
nakov avatar nakov SoftUni Team Trainer 5295 Точки

Да, стандартният начин два контролера да си "говорят" в AngularJS е чрез events. Аз ползвам нещо такова за връзка между десния sidebar с филтрите по град и категория (RightSideBarController) и централния контролер, който показва обявите (HomeController):

app.controller('RightSideBarController', ['$scope', '$rootScope', 'categoriesService', 'townsService',
function RightSideBarController($scope, $rootScope, categoriesService, townsService) {
$scope.categories = categoriesService.getCategories();
$scope.towns = townsService.getTowns();

$scope.categoryClicked = function(event) {
var clickedCategoryId = $(event.target).data("id");
$rootScope.$broadcast("categorySelectionChanged", clickedCategoryId);
};

$scope.townClicked = function() {
var clickedTownId = $(event.target).data("id");
$rootScope.$broadcast("townSelectionChanged", clickedTownId);
};
}
]);

app.controller('HomeController', ['$scope', '$rootScope', 'adsService', 'pageSize',
    function HomeController($scope, $rootScope, adsService, pageSize) {
// This event is sent by RightSideBarController when the current category is changed
$scope.$on("categorySelectionChanged", function(event, selectedCategoryId) {
$scope.adsParams.categoryId = selectedCategoryId;
$scope.reloadAds();
});

// This event is sent by RightSideBarController when the current town is changed
$scope.$on("townSelectionChanged", function(event, selectedTownId) {
$scope.adsParams.townId = selectedTownId;
$scope.reloadAds();
});
}
]);

Това работи стабилно и е архитектурно правилно. Запазва decoupling между контролерите, но позволява да си говорят чрез съобщения.

5
29/12/2014 22:06:44
Shade avatar Shade 33 Точки

Искам да попитам филтрирането по градове и категории, чрез нова AJAX заявка ли трябва да стане? В момента съм го направил да филтрира обявите client-side, но сега видях че има такъв тип заявка в 'Postman' колекцията.

0
nakov avatar nakov SoftUni Team Trainer 5295 Точки

Филтрацията и пейджирането се правят сървърно, т.е. при клик върху пейджера или смяна на филтъра по град / категория, се пуск нова AJAX заявка към сървъра, която взима филтрираните данни.

Ако не успееш, направи го client-side, но така ще работи много бавно при голямброй обяви и не е добра практика.

4
ttitto avatar ttitto 1153 Точки

Колеги, имам проблем с ауторизацията при logout. Правя пост заявка с $http.post(serviceUrl, headers)..., която ми връща грешка при ауторизацията. Пробвах въобще да не подавам headers, пробвах да подам обект със следното съдържание {Authorization: "Bearer nfLHbpR0w55qBVGKuaaWajkkw7fpf4PAsib0abT3ulLvjtGCXDvhDvdOENa3-W6xKVo8etdCuu7f4ZTT0AMaQ4VJup7CoGDhHoRo4tHnYo_V3rcCsPYghA0OpdByV8svW7dp6lhg5X2j80pTBuunjfq_ViUpEUSTdlQTEtyQlkPxawb3TGG5vPulk6ojopKoXQSXA-9ef20mGp9FphJ23lKgIrW9itwPcG_Ns1pNxv8XSdRB-4TlK_Mvou-qY2AzSwMC-XF0Tj6Ok0c7yUl9Uo-rFOnUGAStvAYDLSnzixHIfgj7pUhpXSATYpraZPp9pNMOIIG_iQtzXymHkz_6e12z7Ffkt9g3w01z5TDYJnugTm2X6cd26uo8-V_fRSQir1OUieVJaaZeV75Gk9-29ZzymyO9DtJkDtIUSKKJCoPUfqMbMWpeESzZT95BzsZiE4LQTzdfqOXkxxssVxe-UJnB2K5f10iKcKP3L447iK4"}

пробвах да подам хедърите и като json. И в трите случая не ми приема заявката с една и съща грешка.

В док файла няма упътване какво се подава на logout и не знам откъде да разбера. Някой успя ли и дали може да ме насочи?!

0
Shade avatar Shade 33 Точки
Пробвай с това:

logout: function(successCallback, errorCallback){
$http({method: 'POST', url: url + 'logout', headers: {
"Authorization": "Bearer " + session.get().access_token
}})
.success(function(data, status, headers, config){
successCallback(data);
})
.error(function(data, status, headers, config){
errorCallback(data);
});
}

при мен работи.
4
ttitto avatar ttitto 1153 Точки

Ами да, то вторият параметър на $http.post e data, а не хедъри. Ама кой да гледа!

Благодаря ти много! Твоят вариант сработи при мен.

0
borislavml avatar borislavml 368 Точки

Колеги, къде я намерихте тази заявка за logout. Сега пулл-нах от JS-apps репото и не виждам да има нови заявки в postman колекцията.... Аз си мислех да правя logout-a само с един clear na sessionStorage-a (понеже там си пазая authorization data-та). 

Защо ми е да се аутентикирам при логаут и къде е тази заявка?!

EDIT: Сега видях, че има такава в help page-a на клауда  ЦЪК, но там в body parameters пише, че няма параметри...

0
30/12/2014 20:59:19
nakov avatar nakov SoftUni Team Trainer 5295 Точки

Подарък за нова година: ето пример как можете да направите качването на нова обява (с работещо live preview за картинката):

View:

<div class="box">
<h2>Publish New Ad</h2>
<p>
<label for="title">Title:</label>
<input type="text" id="title" ng-model="adData.title" required />
</p>
<p>
<label for="text">Text:</label>
<textarea id="text" ng-model="adData.text" required></textarea>
</p>
<p>
<label for="image">Image:</label>
<input type="file" id="image"
onchange="angular.element(this).scope().fileSelected(this)" />
<div class="image-box">
<p>Image Preview</p>
</div>
</p>
<p>
<label for="category">Category:</label>
<select id="category" ng-model="adData.categoryId">
<option value="null">(None)</option>
<option ng-repeat="c in categories" value="{{c.id}}">{{c.name}}</option>
</select>
</p>
<p>
<label for="town">Town:</label>
<select id="town" ng-model="adData.townId">
<option value="null">(None)</option>
<option ng-repeat="town in towns" value="{{town.id}}">{{town.name}}</option>
</select>
</p>
<p>
<a ng-click="publishAd(adData)" class="button">Publish</a>
<a href="#/user/ads" class="button">Cancel</a>
<p>
</div>

Controller:

app.controller('UserPublishNewAdController',
function ($scope, $rootScope, $location, townsService, categoriesService,
userService, notifyService) {
$scope.adData = {townId: null, categoryId: null};
$scope.categories = categoriesService.getCategories();
$scope.towns = townsService.getTowns();

$scope.fileSelected = function(fileInputField) {
delete $scope.adData.imageDataUrl;
var file = fileInputField.files[0];
if (file.type.match(/image\/.*/)) {
var reader = new FileReader();
reader.onload = function() {
$scope.adData.imageDataUrl = reader.result;
$(".image-box").html("<img src='" + reader.result + "'>");
};
reader.readAsDataURL(file);
} else {
$(".image-box").html("<p>File type not supported!</p>");
}
};

$scope.publishAd = function(adData) {
userService.createNewAd(adData,
function success() {
notifyService.showInfo("Advertisement submitted for approval. Once approved, it will be published.");
$location.path("/user/ads");
},
function error(err) {
notifyService.showError("Publish ad failed", err);
}
);
};
}
);
10
ttitto avatar ttitto 1153 Точки

Това ще работи само ако на един екран има един browse бутон, нали? В противен случай всеки брауз бутон ще показва картинката във всички .image-box.

Вчера загубих 4 часа в търсене на начин да го направя с директива така че да не се месят една на друга, когато имаме да избираме две или повече снимки на един екран и се отказах крайно отчаян. Има ли начин това да стане?

0
31/12/2014 11:34:48
nakov avatar nakov SoftUni Team Trainer 5295 Точки

Според мен правилният начин е с директива. Така няма да се разчита на конкретни id- та и css класове в кода и ще е reusable. Обаче, щеше да отнеме повече време и го изкодих така.

Би могло да се ходи по DOM дървото по .parent и надолу и така няма да се ползват фиксирани класове.

Всичко е възможно, а това ре само пример.

1
ttitto avatar ttitto 1153 Точки

Стана чудесно с директивата

browse.js

 

"use strict";

adsApp.directive('browse', [function () {
return{
restrict: 'A',
templateUrl: './app/templates/directives/browse.html',
link: function (scope, element, attrs) {
scope.toDataUrl = function (inputEl) {
var file = inputEl.files[0];
if (file.type.match(/image\/.*/)) {
var reader = new FileReader();

reader.onload = function () {
var base64Str = reader.result;
$(attrs.preview).attr('src', base64Str);
$('#base64-string').val(base64Str);

scope.getBase64(base64Str, attrs);
};
reader.readAsDataURL(file);
} else {
$(attrs.preview).attr('alt', 'File type not supported!');
}
}
}
}
}]);

 

browse template

<input id="base64-string" placeholder="Choose File" disabled="disabled"/>
<div class="fileUpload btn form-btn">
<span>Browse ...</span>
<input type="file" class="upload-input" onchange="angular.element(this).scope().toDataUrl(this)"/>
</div>

usage in html:

<div browse preview=".image-box"/>

в preview се подава селектор за img елемент, който да визуализира избраната картинка

контролер:

Ако в контролерът се изисква да се прави нещо просто му се извиква функция getBase64, която получава енкоднатия стринг и атрибутите: 

$scope.getBase64 = function (base64String, attrs) {
$scope.ad.imageDataUrl = base64String;
};

 

2
31/12/2014 20:26:38
nakov avatar nakov SoftUni Team Trainer 5295 Точки

Не задължително да спазвате непременно рутовете, описани в условието. Аз примерно нямам logout route, а го правя с бутонче в auth контролера.

2
arsoman avatar arsoman 419 Точки

Благодарим за уточнението, аз късно го видях и вчера пренастройвах рутовете около час, но за другите е добре, че внесохте светлина, макар и в навечерието на Нова Година! Весело посрещане на всички от екипа и колегите-учащи! Бъдете здрави, другото се купува! Наздраве!

1
nakov avatar nakov SoftUni Team Trainer 5295 Точки

По въпроса за logout: по принцип след като е издаден един token, той си остава валиден, дори и след logout. Това, разбира се е грешно, но произтича от дизайн съображения, дълбоко стъпили в Microsoft OWIN автентикацията, която идва с ASP.NET MVC Web API. Той ползва OAuth протокола, който служи за оторизация на потребител с access token като се ползва оторизационен сървър (локален или външен, например Facebook, Twitter или Google). При OAuth няма logout, защото login-ът е във външна система. И така by design при тази автентикция token-ите не умират при logout. Имат си expiration само.

За Ads проекта при logout е достатъчно да изтриеш / забравиш получения от сървъра access-token. Не е нужно да викаш logout услугата.

3
31/12/2014 19:38:03
kgerov avatar kgerov 49 Точки

Здравейте! Когато се опитвам да създам обява ми изникна следната грешка: XMLHttpRequest cannot load http://softuni-ads.azurewebsites.net/user/ads. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost' is therefore not allowed access.

Използвам следният ресурс: var resource = $resource('http://softuni-ads.azurewebsites.net/user/ads'); и му викам метода .save()

 

Мерси предварително (:

0
crazy7 avatar crazy7 177 Точки

Може да е защото URL е грешен забравяш api.

http://softuni-ads.azurewebsites.net/api/user/ads

 

1
kgerov avatar kgerov 49 Точки

Tова е било, мерси много. Колко време си изхабих... не знам откъде съм го копнал този URL.

0
Можем ли да използваме бисквитки?
Ние използваме бисквитки и подобни технологии, за да предоставим нашите услуги. Можете да се съгласите с всички или част от тях.
Назад
Функционални
Използваме бисквитки и подобни технологии, за да предоставим нашите услуги. Използваме „сесийни“ бисквитки, за да Ви идентифицираме временно. Те се пазят само по време на активната употреба на услугите ни. След излизане от приложението, затваряне на браузъра или мобилното устройство, данните се трият. Използваме бисквитки, за да предоставим опцията „Запомни Ме“, която Ви позволява да използвате нашите услуги без да предоставяте потребителско име и парола. Допълнително е възможно да използваме бисквитки за да съхраняваме различни малки настройки, като избор на езика, позиции на менюта и персонализирано съдържание. Използваме бисквитки и за измерване на маркетинговите ни усилия.
Рекламни
Използваме бисквитки, за да измерваме маркетинг ефективността ни, броене на посещения, както и за проследяването дали дадено електронно писмо е било отворено.