11.04.2014

Pilot 1.5 / Пример одностраничного приложения (SAP)

1.5

Pilot

Синтаксис

Синтаксис описания маршрута

			var Ivan = new Pilot;
			#!+ // Реагирует на любой url: ^/(.+)/?$
			#!- Ivan.route("/:page", function (evt, req) { });
			#!+ // ^/(.+)/?.*?$
			#!- Ivan.route("/:page*", function (evt, req) { });
			#!+ // ^/(.+)/?.+$
			#!- Ivan.route("/:page+", function (evt, req) { });
		

Синтаксис описания маршрута

			Ivan.route("/:page/:details?/", function (evt, req) { });
			// Реагирует на:
			//   /foo/
			//   /foo/bar/
			// Не реагирует
			//   /
			//   /foo/bar/baz
		

Синтаксис описания маршрута

			"/search/"                 // строгое совпадание
			"/user/:id(\\d+)"          // только цифры
			"/search/(result/:page/)?" // группировка

			// Сложный пример:
			"/:mode(show|link)?/:storage(home|links|shared)"
		

Pilot.Request

Pilot.Request

Pilot.Request: пример

			var Moses = new Pilot;
			#! Moses.route("/deserts/", ctrl); // используется GET
			#! Moses.route("/desert/:name/:coords", ctrl);

			#!+ function ctrl(evt, req) {
				#! var name = req.params.name || req.query.name;
				#! var coords = req.params.coords || req.query.coords;
			#!- }
		

Pilot.Request.fn: пример

			// Получить зачение по имени параметра
			// в независимости от его нахождения
			Pilot.fn.get = function (name) {
				return this.params[name] || this.query[name];
			};
		

Pilot.Request: пример

			var Moses = new Pilot;
			Moses.route("/deserts/", ctrl); // используется GET
			Moses.route("/desert/:name/:coords", ctrl);

			function ctrl(evt, req) {
				#! var name = req.get("name");
				#! var coords = req.get("coords");
			}
		

События

События

			router.route("path/:to", function (evt, req) {
				console.log(evt.type + ": " + req.path");
			});
			#! router.nav("/path/");     // (ничего)
			#! router.nav("/path/foo/"); // routestart: /path/foo/
			#! router.nav("/path/bar/"); // routechange: /path/bar/
			#! router.nav("/path/baz/"); // routechange: /path/baz/
			#! router.nav("/path/");     // (ничего)
		

События

			router.route("path/:to", {
				#! onRouteStart: function (evt, req) { },
				#!+ onRoute: function (evt, req) {
					// routeStart + routeChange
				#!- },
				#! onRouteEnd: function (evt, req) { }
			});
		

Pilot.create

Pilot.create

Чтобы показать всю мощь этого метода, расмотрим пример создания одностраничного приложения «галерея».

Pilot.create: Шаг первый (diff)

Создаем структуру приложения:
Для прототипа используем Ratchet (css framwork)

Pilot.create: Шаг первый.html

			<div id="app">
				#!+ <!-- App screen -->
				<div style="z-index: 1; display: none;">
					#!+ <header class="bar bar-nav">
						<h1 class="js-title title"></h1>
					#!- </header>
					#! <div class="js-content content"></div>
				</div>
				#!- <!-- /App screen -->
			</div>
			#!+ <!-- vendors: -->
			<script src="http://code.jquery.com/jquery-2.1.0.min.js"></script>
			#!- <script src="../../Pilot.js"></script>
			#!+ <!-- app:code -->
			#!- <script src="app.js"></script>
		

Pilot.create: Шаг первый.js

			(function ($, Pilot) {

				/* app code */

			})(jQuery, Pilot);
		

Home screen

Home screen.js (diff)

			(function ($, Pilot) {
				var App = Pilot.create({
					#! el: "#app", // корневой элемент
					#!+ "/": { // ключь — описание маршура
						id: "home", // id маршрута
						#!+ loadData: function (req) {
							return $.getJSON("./data/galleries.json"); // взял с WiKi
						#!- },
						#!+ init: function () {
							#! var galleries = this.getLoadedData();
							#! this.$(".js-title").text("List of art movements");
							#!+ this.$(".js-list").append(galleries.map(function (name) {
								var url = this.getUrl("gallery", { name: name });
								return "<li class='table-view-cell'><a>"+name+"</a></li>";
							#!- }, this));
						#!- }
					#!- }
				});
			})(jQuery, Pilot);
		

Home screen.html

			<div id="app">

				<!-- Home -->
				<div data-view-id="home" style="z-index: 1; display: none;">
					<header class="bar bar-nav">
						<h1 class="js-title title"></h1>
					</header>

					<div class="js-content content">
						<ul class="js-list table-view"></ul>
					</div>
				</div>
				<!-- /Home -->

			</div>
		

Gallery screen

Gallery screen.js (diff)

			// Gallery screen
			"/gallery/:name/": {
				id: "gallery",
				onRoute: function (evt, req) {
					this.$(".js-title").text(req.params.name);
				}
			}
		

Первая проблема

При возвращении на «Home» отправляется запрос за список категорий. Это происходит из-за того, что функция loadData вызывается каждый раз перед сменой маршрута, если нужно получить данные только один раз, замените loadData на loadDataOnce.

Решение проблемы

			"/": { // Home screen
				id: "home",
			-	loadData: function (req) {
			+	loadDataOnce: function (req) {
					return $.getJSON("./data/galleries.json");
				},
				init: function () {
					/* ... */
				}
			}
		

Gallery screen.js (diff)

			"/gallery/:name/:page?": {
				id: "gallery",
				#!+ loadData: function (req) {
					return $.flickr("flickr.photos.search", {
						#! tags: req.params.name,
						#! page: req.params.page|0,
						#! per_page: 50
					}).then(function (result) {
					#!-	return result.photos;
					});
				#!- },
				onRoute: function (evt, req) { /* .. */ }
			}
		

Gallery screen.js (diff)

			"/gallery/:name/:page?": {
				id: "gallery",
				loadData: function (req) { return $.flickr({ /*...*/ }); },
				onRoute: function (evt, req) {
					#!+ var name = req.params.name,
					    photos = this.getLoadedData()
					#!- ;
					#! this.$(".js-title").text(name);
					#!+ this.$(".js-photos").html(photos.photo.map(function (photo) {
						#! var url = this.getUrl("artwork", { name: name, id: photo.id });
						#! return "<a href='" + url + "'><img src='" + photo.url_q + "' /></a>";
					#!- }, this));
				}
			}
		

Home + Gallery
DEMO

Subviews

Subviews: loading (diff)

			App = Pilot.create({
				el: "#app",
				subviews: {
					#!+ loading: { // id подвида и [data-subview-id="loading"]
						#! loadData: function () { this.$el.show(); },
						#! onRoute: function () { this.$el.hide(); }
					#!- }
				},
				/* Home, Gallery, etc. */
			});
		

Effects

Effects (diff)

			Pilot.View.toggleEffect("transition", function ($el, state) {
				#!+ $el.css({
						display: "",
						opacity: +!state,
						transition: "",
						transform: "translate3d(" + (!state ? "0" : "100%") + ",0,0)"
					})
					#! .delay(1)
					#!+ .queue(function (){
						$el.css({
								opacity: +state,
								transition: "all .2s ease-in-out",
								transform: "translate3d(" + (state ? "0" : "100%") + ",0,0)"
							})
							.dequeue()
						;
					#!- })
				#!- ;
			});

		

Effects: применяем (diff)

			App = Pilot.create({
				el: "#app",
				subviews: {},
				"/": {
					id: "home",
					#! toggleEffect: "fadeIn"
				},
				"/gallery/:name/:page?": {
					id: "gallery",
					#! toggleEffect: "transition"
				},
				"/artwork/:id/": {
					id: "artwork",
					#! toggleEffect: "transition"
				}
			});
		

Effects: проблема / DEMO

Попробовав демо, можно быстро заметить, что при переходе с Gallery на Artwork, один экран уезжает, другой приезжает, что выглядит не очень красиво.

Чтобы избавиться от такого эффекта, можно объеденить Gallery и Artwork в группу.

Gallery & Artwork group (diff)

			<!-- GalleryGroup -->
			<div data-view-id="gallery-group" style="z-index: 2; display: none;">
				#!+ <!-- Gallery -->
				<div data-view-id="gallery" style="z-index: 2; display: none;">
					<!--...-->
				</div>
				#!- <!-- /Gallery -->
				#!+ <!-- Artwork -->
				<div data-view-id="artwork" style="z-index: 3; display: none;">
					<!--...-->
				</div>
				#!- <!-- /Artwork -->
			</div>
			<!-- /GalleryGroup -->
		

Gallery & Artwork group (diff)

			App = Pilot.create({
				/* el, subview, home */
				#!+ "/gallery/": { // Группа
					#! id: "gallery-group", // [data-view-id="gallery-group"]
					#! toggleEffect: "transition",

					#!+ "/:name/:page?": { // Gallery screen
						id: "gallery",
						toggleEffect: "show", // просто показать
						#!+ paramsRules: { // валидация параметров
							#! name: function (val) { return val != "artwork"; }
						#!- }
					#!- },
					#!+ "/artwork/:id": { // Artwork screen
						id: "artwork",
						toggleEffect: "transition"
					#!- }
				#!- }
			});
		

Готово?

:(

Pilot.View

Pilot.View: DefaultView (diff)

			var DefaultView = Pilot.View.extend({
				#!+ $$: function (name) {
					return this.$(".js-" + name);
				#!- },
				#!+ setTitle: function (val) {
					this.$$("title").text(val);
				#!- },
				#!+ setBackUrl: function (url) {
					this.$$("back").prop('href', url);
				#!- },
				#!+ setHtml: function (val) {
					this.$$("content").html( val );
				#!- },
			});
		

Pilot.View: HomeView (diff)

			var HomeView = DefaultView.extend({
				#!+ loadDataOnce: function (req) {
					return $.getJSON("./data/galleries.json");
				#!- },
				#!+ init: function () {
					var galleries = this.getLoadedData();
					#! this.setTitle("List of art movements");
					#!+ this.setHtml("list", galleries.map(function (name) {
						#!+ var url = this.getUrl('gallery', { name: name });
						#!- return "<li class='table-view-cell'><a href='" + url + "'>" + name + "</a></li>";
					#!- }, this));
				#!- }
			});
		

И это не конец!

RequireJS

И тут я устал

RequireJS и все шаги создания этого приложения вы можете посмотреть на github: