ugrás a tartalomhoz

Minek nevezzelek (UI komponens)

inf · 2013. Jún. 9. (V), 14.58
Sziasztok!

Hatalmas problémám van :D Csinálok egy olyan container-t, ami az függőségeket tölti be ajax-al aszinkron módon. Amíg tölt, addig kiírja, hogy "Kis türelmet!", vagy valami hasonlót, ha betöltött mindent, akkor kiteszi a tényleges tartalmat, ha meg nem jött össze, akkor meg valamilyen hibaüzenetet. Valahogy így fogom használni:

syncBox.parallel(function (){
	syncBox.fetch(new Role({id: id}));
	syncBox.fetch(new UserSet()),
},
function (role, users){
	var form = new RoleUpdateForm({
		model: role,
		users: users
	});
	form.on("submit", function (){
		syncBox.save(role, function (role){
			controller.read(role.id);
		});
	});
	syncBox.render(form, {persist: false});
});
Egyelőre SyncBox-nak nevezem, mert Backbone.sync-et használ majd ahhoz, hogy pl a Role modelt szinkronban tartsa a szerverrel. Kicsit hajaz a SandBox-ra, mert ő szolgáltatja a környezet egy részét (a model-eket, esetleg további konfigurációs változókat) a benne lévő tartalomnak, meg mert olyan, mint egy doboz, beleszórod a dolgokat, aztán megjeleníti, ha a függőségeket sikerül betölteni...

Az a kérdés, hogy van erre a típusú UI komponensre valami kiforrott név, amit sokan használnak, esetleg van ötletetek valami jobb elnevezésre ennél?
 
1

require?

T.G · 2013. Jún. 9. (V), 15.26
Lehet, hogy nem értem a kódot, de pontosan mit is töltesz be ebben a példában aszinkron módon?
(ha nincs még Role, akkor azt az első függvényben hogyan használhatod, ha meg van, akkor mit is kell még betölteni?)
5

Kimaradtak részek. A Role egy

inf · 2013. Jún. 9. (V), 17.39
Kimaradtak részek. A Role egy Backbone.Model, a UserSet meg egy Backbone.Collection. Ezeknek van fetch, save, destroy metódusa, amik a Backbone.sync-et használják. A fetch küld egy ajax üzit a szerver-nek, hogy ugyanmár adja oda az adatait az adott id-jű role-nak, vagy a teljes felhasználó listát...

Mondjuk a parallel jelen esetben annyit csinál, hogy a két kérést elindítja párhuzamosan, aztán figyeli az eredményüket. Amikor megjött az eredmény, akkor hívja meg a második függvényt, ami gyakorlatilag a done-nak felel meg. Az rajzolja ki az űrlapot...
2

ajax

Hidvégi Gábor · 2013. Jún. 9. (V), 16.55
Hacsak nem helyi hálóról működik, érzésem szerint sokkal gyorsabb lenne, ha egy fájlba pakolnád az összes komponenst, és nem szórakoznál az ajaxos betöltéssel. Még akkor is, ha sokszor változik a forrás.
3

Gondolom nem szkripteket tölt

MadBence · 2013. Jún. 9. (V), 17.17
Gondolom nem szkripteket tölt be aszinkron módon, hanem a modell adatokat (ezek a „függőségek”). Nekem az a problémám, hogy gyakorlatilag semmit nem írsz arról a komponensről, amit el akarsz nevezni :)
4

Mindegy

Hidvégi Gábor · 2013. Jún. 9. (V), 17.19
A lényegen nem változtat.
6

Okés, akkor írok kicsit

inf · 2013. Jún. 9. (V), 17.50
Okés, akkor írok kicsit többet.

Vannak a backbone-ban sync-et használó metódusok. Ezek tipikusan xhr-t küldenek, és aszinkron kommunikálnak a szerverrel. Ezzel kapcsolatban probléma, hogy vagy parallel, vagy series, de csináljunk több kérésből egy egységet. Mondjuk jelen esetben a két model-nek kell betölteni az adatait, és az űrlap csak akkor működőképes, ha mindkét model-nek megvannak az adatai. Mondjuk a Role-hoz, mint szerepkörhöz tartoznak felhaasználók, így kell a teljes UserSet ahhoz, hogy ki lehessen rajzolni egy TwoListSelection-t, aztán oda-vissza pakolni, hogy melyik felhasználó tartozik a role-ba, és melyik nem. Nyilván az, hogy jelenleg melyik tartozik bele, az megjön a Role.fetch-ből...

Azt csinálja a box, hogy összefog különböző aszinkron metódusokat, és amíg vagy series vagy parallel (vagy bármilyen tetszőleges úton módon) az összes le nem fut, és meg nem érkezik a válasz, addig kitesz valamilyen loading feliratot (esetleg akár %-al is). Ha közben valamelyik kéréssel gond van, akkor leállítja az összes többit, és kitesz egy hibaüzenetet. Ha minden kérés sikeres volt, akkor meghívja a paraméterben megadott callback-et, ami képes átírni a syncBox tartalmát. Jelen esetben egy űrlapot szórtam bele. Ez nem a végállapot, szóval ezek után is lehet új kérést indítani, azt meg meg lehet adni, hogy az előzőleg beleírt tartalom ilyenkor eltűnjön, vagy sem. Pl a form.submit-ra a role.save fut le, és mivel nincs persist-re rakva a form, ezért a submit-ra kattintáskor eltűnik. Mondjuk ez a része még nem annyira kidolgozott, több tapasztalatra lesz szükség azzal kapcsolatban, hogy itt pontosan mire van szükség, és mire nincs...

Nyilván gyorsabb lenne, ha egy fájlban menne minden kérés, viszont akkor már nem REST service-ről beszélnénk szerver oldalon. Kliens oldalon sem tartom annyira fontosnak az optimalizálást, mert úgyis csak egyszer fog betöltődni, onnantól meg benne lesz minden a böngésző cache-ében statikus fájlok formájában. Ha mégis nagyon lassú lenne elsőre, akkor tolok rá egy r.js-t, aztán leoptimalizálom egy fájlba.
7

Azt hiszem a kezdeti kérdések

inf · 2013. Jún. 11. (K), 06.37
Azt hiszem a kezdeti kérdések azok jogosak voltak, egyáltalán nem egyértelmű ez az interface, át fogom írni. Névre nem jutott eszembe jobb, nyilván, amit írtam, azt nem mind ez a box csinálja. Végülis csak annyit fog ő maga tudni, hogy lesz benne egy status rész az aktuális kérésekről, aztán ha lefutottak a kérések, akkor megjelenít valamilyen tartalmat. Gyakorlatilag container/panel with request status.

Szerk:

Ez lett a vége:

	var role = new Role({id: id});
	var userSet = new UserSet();
	
	var syncBox = new RequestStateDecorator({
		appendTo: "body",
		states: {
			updateForm: {
				trigger: function (done){
					new ParallelRequestAggregator({
						models: [role, userSet],
						run: function (){
							role.fetch();  
							userSet.fetch();
						},
						sync: done,
						error: done
					});
				},
				content: function (){
					var form = new RoleUpdateForm({  
						model: role,  
						userSet: userSet  
					});  
					form.on("submit", function (){
						this.render("updated");
					}, this);  
					form.render();
					return form.$el;
				}
			},
			updated: {
				trigger: function (){
					role.save();  
				},
				content: function (){  
					controller.read(role.id);  
				};
			}
		}
	});
	syncBox.render("updateForm");
Még ez sem jó igazán... Alapvetően az a probléma, hogy aszinkron kérésektől függően kell tartalmat kiírni.

Szét fogom darabolni több állapotú kijelzőre és aszinkron állapot dekorátoros panelre. Végülis ez utóbbi is több állapotú kijelző, szóval két ilyen kijelző van egyben ^^ Az egyiknek automatikusan változik az állapota attól függően, hogy pending, error vagy success van az aszinkron kéréseknél, amik a szerver felé mennek, a másiknak meg manuálisan változik az állapota attól függően, hogy éppen melyik kéréseknél volt success. Jobb lenne, ha a több állapotú GUI komponensek között körülnéznék, biztosan van valami best practice erre...
8

Szerintem...

T.G · 2013. Jún. 11. (K), 09.02
Szerintem ez már sokkal jobban néz ki, mint az első verzió, de valami nekem attól még nagyon furcsa.
(elöljáróban ez a komment, inkább csak brainstorming jellegű, mivel a körülményeket nem ismerem:)

Leírom az ExtJS-es megközelítést, sok hasonlóság van, de lényeges különbségek is vannak.

Nincs erre külön komponens, hanem minden komponens ősével végezhetjük el ezeket a fajta feladatokat. Minden komponensnek kell tudnia eseményt kezelni, illetve akár esemény sorozatot lekezelni.

A render(...) függvény elnevezés szerintem félrevezető, ExtJS-nél fireEvent(eventName) van, már csak azért is zavaró, mert használod a render() függvényt, gondolom az valóban a form kirajzolása.

A feladat tehát az, hogy az esemény előtt lefutassunk egy másik eseményt és az hatással legyen a valódi eseményre. ExtJs-ben ez mindenhol beforeEventName elnevezésű esemény. (ez csak szabvány) Pl. van egy load esemény, ekkor először meghívódik a beforeLoad esemény, amennyiben az nem false értékkel tér vissza, akkor meghívódik a load esemény.

Szerintem szebb lenne, ha a RoleUpdateForm a fentieket lekezelné magában, és amikor használod ezt a komponenst, akkor ezekkel egyáltalán nem kellene foglalkoznod. Az zavar ebben a kódban, hogy túl sok olyat kell leírni, ami nem biztos, hogy változik, ha máshol is használnám ezeket az osztályokat. Illetve jelenleg ezek nélkül nem is használható. (ha nem tévedek)

Azaz a RoleUpdateForm esetében az onBeforeUpdateForm privát függvényben minden esetben egy Ajax Queue-nek átadod a két fetch()-et, amikor ez végez, akkor meghívja az onUpdateForm szintén privát függvényt.

Persze mondom ezt úgy, hogy nem ismerem a körülményeket, tehát lehetséges, hogy egyáltalán nem illik ez oda, de gondoltam, hátha ötletet ad a továbblépésre. :)

new RoleUpdateForm({    
    role: new Role({id: id}),
    userSet: new UserSet(),
    renderTo: 'body',
    listeners: {
        submit: function (form) {
            controller.read(form.role.id);
        }
    }
});
9

Semmi gond nincs a

inf · 2013. Jún. 11. (K), 17.38
Semmi gond nincs a brainstorming-gal, általában véve ilyesmiket várok, hogy itt és itt így oldották meg ugyanezt a problémát, mert nem szeretem feltalálni a spanyol viaszt...

Pl. van egy load esemény, ekkor először meghívódik a beforeLoad esemény, amennyiben az nem false értékkel tér vissza, akkor meghívódik a load esemény.


Itt a lényegi különbség a két dolog között, itt nincs visszatérő érték, mert nem szinkron, hanem aszinkron dolgok történnek a cucc megjelenítése előtt.

Több olyan komponens is van, ami adatok lerántására várhat, de ez nem kötelező jellegű. Mondjuk van egy SelectManyInput vagy TwoListSelection, aminek be kell töltenie a szerepkörhöz tartozó felhasználókat, és a teljes felhasználó listát, amiből ezeket kiválasztjuk. Aztán van a teljes űrlap, ami akár még több ilyen komponenst is tartalmazhat. Nyilván az ember nem akarja mindegyikbe betenni, hogy

if (this.options.autoFetch)
    this.model.fetch();
meg hasonlókat. Jobb inkább betölteni egy külön erre a célre szolgáló komponenssel az összes függőséget, aztán csak utána megjeleníteni a tényleges tartalmat, mondjuk űrlapot, táblázatot, vagy bármi mást. Szerintem így könnyebben újra lehet hasznosítani a kódot, mintha minden komponensnek lenne ilyen feature-je. Általában maga a komponens nem is kell, hogy tudja, hogy milyen forrásból jönnek az adatok, mert nem az ő dolga...

A mostani megoldásom sem jó egyébként, mert ez az állapot megjelenítő komponens nem ismeri előre az összes állapotát.
10

No körülbelül erről van szó a

inf · 2013. Jún. 11. (K), 18.15
No körülbelül erről van szó spagetti kóddal a jelenlegi eszközökkel:

	var content = new Backbone.View({
		appendTo: "body"
	});
	content.render();

	var role = new Role({id: id});
	var userSet = new UserSet();
		
	Backbone.syncParallel({
		models: [role, userSet],
		run: function (){
			role.fetch();  
			userSet.fetch();
		},
		listeners: {
			request: function (){
				content.$el.html("Kis türelmet!");
			},
			error: function (){
				content.$el.html("Sajnos nem sikerült letölteni az adatokat.");
			},
			sync: function (){
				var form = new RoleUpdateForm({  
					model: role,  
					userSet: userSet  
				});  
				form.on("submit", function (){
					content.$el.html("Kis türelmet!");
					role.save({
						error: function (){
							content.$el.html("Sajnos nem sikerült menteni a változásokat, kérem próbálja újra!");
							content.$el.append(new Backbone.UI.Button({
								content: "Vissza az űrlaphoz",
								onClick: function (){
									content.$el.html(form.$el);
								}
							}));
						},
						success: function (){
							content.$el.html("Mentettük az adatokat. Átirányítjuk az imént mentett szerepkör oldalára, kérem várjon!");
							setTimeout(function (){
								controller.read(role.id); 
							}, 2000);
						}
					});
				}, this);  
				form.render();
				content.$el.html(form.$el);
			}
		}
	});
Ebből egyedül a Backbone.syncParallel nem létezik a content meg egy sima div-es panel. Azt hiszem átjön, hogy ez így nem túl szép, mert a fetch és save figyelése, a hibaüzenetek kiírása teljesen automatizálható lenne, illetve mert rengeteg blokk van egymásban. Pl az onSubmit már egy totál más állapotba rakja a content-et, tehát akár lehetne egy szinten is annak a tartalma a syncParallel-el, mint ahogy a fentebbi megoldásban van. Ami probléma, hogy nem lehet előre tudni az összes állapotát a content-nek, mert azt a controller-ben állítom be. Pl ha rámennek a menüben, hogy szerepkörök, akkor listázza az összes szerepkört, aztán meg ki lehet választani közülük konkrétat, stb... Mindegyik állapotváltozással jár a content-en.
11

(Nincs téma)

Hidvégi Gábor · 2013. Jún. 11. (K), 19.03
12

:D :D :D

inf · 2013. Jún. 11. (K), 19.29
:D :D :D
13

Másik példa?

T.G · 2013. Jún. 11. (K), 20.31
Hülyeséget kérdezek, de ebben a kódban hogy oldod meg, hogy a run-ban lévő két fetch végét elkapod? Illetve a models-ben lehet más, mint amit a run-ban elindítasz?

Érdekes, én pont arra gondoltam, hogy olyanokat írnék a komponens kódjába, amire azt írtad, hogy nyilván nem. :)

Engem nem zavar, hogy a komponenseimnek van egy rakás nem kötelező mezője, amit inicializáláskor egyesével ellenőrzök. Inkább a komponensben írok ilyeneket, mint ott, ahol használom a komponenst.

Tudnál egy másik példát is írni? Ahol kijönne ennek a konténernek az előnye.
14

Hülyeséget kérdezek, de ebben

inf · 2013. Jún. 12. (Sze), 05.12
Hülyeséget kérdezek, de ebben a kódban hogy oldod meg, hogy a run-ban lévő két fetch végét elkapod? Illetve a models-ben lehet más, mint amit a run-ban elindítasz?


Van request, sync és error event a model-en, azokra lehet bindelni figyelőket... A model-t azért kell megadni, hogy tudjon bindelni ezekre, a fetch-et meg azért nem string-ben adom meg, mert úgy nem valami szép. Arra gondoltam, hogy amíg az első request le nem zárul, addig lehet majd újakat hozzáadni a task list-hez, így nem kell kézzel lezárni valamilyen függvénnyel a listát...

Tudnál egy másik példát is írni? Ahol kijönne ennek a konténernek az előnye.


Na hát ez jó kérdés... Ez a minta rendeszeresen megjelenik a kódomban. A mostani oldalam vázában van header, content, footer, stb... Arra gondoltam, hogy a content egy az egyben lehetne ez a container, aztán ha valamelyik aloldalt töltöm be, akkor automatikusan kiteheti mindig a loading feliratot, meg esetleg a hibaüzeneteket. Eléggé céleszköz, nem hiszem, hogy másra használható ilyesmiken kívül...
15

Így már értem...

T.G · 2013. Jún. 12. (Sze), 08.12
Így már értem, ötletes, de biztos, hogy minden fetch egy Ajax hívás? Nem fordulhat elő, hogy a role-t egyszer már lekérted, és második lekérésnél már syncron mód visszaadja?

Érdekes, én nagyon ritkán használok Ajax Queue-t, azaz ritkán kérek le úgy két Ajax hívást, hogy azok így összekapcsolódnak. Amikor ilyen van, akkor mindig azt érzem, hogy valami rosszat csinálok. :)
16

Amíg saját gépen/helyi

Hidvégi Gábor · 2013. Jún. 12. (Sze), 10.07
Amíg saját gépen/helyi hálózaton teszteli, és biztosítva van, hogy a kérések sorrendjében érkeznek meg a válaszok (a PHP beépített munkamenetkezelője ebből a szempontból megfelelő), addig nem lesz gond.
17

Ha jól értem...

T.G · 2013. Jún. 12. (Sze), 10.17
Ha jól értem, akkor a sorrend nem is számít, hanem az első befejezéséig el kell indulnia az összes kérésnek. Ami akkor oké, ha tényleg van első kérés. Tekintettel arra, hogy nem tudom, hogy mi van a role.fetch()-ben, de lehet akár egy olyan valami is, ami cache-el, azaz előfordulhat olyan, hogy az első befejezése előtt a második még el sem indul.
18

A role.fetch-et belövöm majd,

inf · 2013. Jún. 12. (Sze), 15.21
A role.fetch-et belövöm majd, hogy ne kesselje a szerver. Amúgy sima jquery ajax-ot használ... A cache-et max úgy lehetne kijátszani, ha úgy használod, mint a tranzakciókat, és beteszel egy commit szerű függvényt a végére... Mondjuk olyan szempontból jogos ez a megközelítés, hogy a rollback-el vissza lehetne hozni az előző tartalmat, mondjuk jelen esetben a hibaüzenetnél a kötöltött űrlapot... Azt hiszem akkor inkább tranzakció szerűen fogom csinálni, mert szeretném felszórni majd github-ra ezeket az osztályokat...

Na de legalább alakul a kép... Egyébként ha egyetlen config object-ben adok meg mindent, az is kb azonos az egyenkénti task hozzáadással. Még nem tudom, hogy a kettő közül melyik legyen... A rollback használat miatt a tranzakciós interface szimpatikusabb... Annyi a para vele, hogy mivel már van jelentése, ezért sokakban zavart kelthet...

Szerk: közben belegondoltam, hogy úgyis a komponens fogja vezérelni az egészet, úgyhogy elég config objectet megadni, vagy valami hasonlót...

Ez lett a vége:

	var role = new Role({id: id});
	var userSet = new UserSet();
		
	var updateForm = {
		tasks: {
			fetch: [role, userSet],
		},
		sync: function (){
			var form = new RoleUpdateForm({  
				model: role,  
				userSet: userSet  
			});  
			form.on("submit", function (){
				this.load(updateRequest);
			}, this);  
			form.render();
			content.$el.html(form.$el);
		}
	};
	
	var updateRequest = {
		tasks: {
			save: role
		},
		error: function (){
			this.$el.append(new Backbone.UI.Button({
				content: "Vissza az űrlaphoz",
				onClick: function (){
					this.rollBack();
				}.bind(this)
			}));
		},
		sync: function (){
			this.$el.html("Mentettük az adatokat. Átirányítjuk az imént mentett szerepkör oldalára, kérem várjon!");
			setTimeout(function (){
				controller.read(role.id); 
			}, 2000);
		}
	};
	
	
	var content = new Backbone.UI.SyncDecorator({
		appendTo: "body",
		messages: {
			request: "Kérem várjon!",
			error: {
				fetch: "Sajnos nem sikerült lekérni az adatokat!",
				save: "Sajnos nem sikerült menteni az adatokat!",
				destroy: "Sajnos nem sikerült a törlés!"
			}
		},
		load: updateForm
	});
	content.render();
Szerintem elég rendezett, és megfelel az elvárásaimnak...
19

Írtam egy kezdetleges

inf · 2013. Jún. 12. (Sze), 18.38
Írtam egy kezdetleges megvalósítást:

    Backbone.UI.SyncLabelDecorator = Backbone.View.extend({
        options: {
            pendingMessage: "Sending request. Please wait ...",
            errorMessage: "An unexpected error occured, we could not process your request!",
            load: null
        },
        supported: ["fetch", "save", "destroy"],
        render: function () {
            if (this.options.load)
                this.load();
        },
        load: function (load) {
            if (load)
                this.options.load = load;
            this._reset();
            if (_.isFunction(this.options.load)) {
                this.$el.html("");
                this.options.load.call(this);
                return;
            }
            _(this.options.load.tasks).each(function (models, method) {
                if (_.isArray(models))
                    _(models).each(function (model) {
                        this._addTask(model, method);
                    }, this);
                else
                    this._addTask(models, method);
            }, this);
            this._onRun();
            _(this.tasks).each(function (task) {
                var model = task.model;
                var method = task.method;
                var options = {
                    beforeSend: function (xhr, options) {
                        this._onRequest(task, xhr);
                    }.bind(this),
                    error: function (xhr, statusText, error) {
                        this._onError(task, xhr);
                    }.bind(this),
                    success: function (data, statusText, xhr) {
                        this._onSync(task, xhr);
                    }.bind(this)
                };
                if (model instanceof Backbone.Model) {
                    if (method == "save")
                        model[method](null, options);
                    else
                        model[method](options);
                }
                else {
                    if (method in model)
                        model[method](options);
                    else
                        model.sync(method == "fetch" ? "read" : (method == "save" ? "update" : "delete"), model, options);
                }
            }, this);
        },
        _addTask: function (model, method) {
            if (!_(this.supported).contains(method))
                throw new Error("Method " + method + " is not supported!");
            this.tasks.push({
                method: method,
                model: model
            });
        },
        _onRun: function () {
            this.$el.html(this.options.pendingMessage);
            if (this.options.load.request)
                this.options.load.request.call(this);
        },
        _onRequest: function (task, xhr) {
            task.abort = function () {
                xhr.abort();
            };
        },
        _onError: function (task, xhr) {
            this._abort();
            this.$el.html(this.options.errorMessage);
            if (this.options.load.error)
                this.options.load.error.call(this);
        },
        _onSync: function (task, xhr) {
            ++this.complete;
            if (this.complete == this.tasks.length)
                this._onEnd();
        },
        _onEnd: function () {
            this.$el.html("");
            if (this.options.load.sync)
                this.options.load.sync.call(this);
        },
        _reset: function () {
            this._abort();
            this.tasks = [];
            this.complete = 0;
        },
        _abort: function () {
            _(this.tasks).each(function (task) {
                if (task.abort)
                    task.abort();
            });
        }
    });
Ami még hiányzik belőle:
  • a loading felirat késleltetése (addig át se írja az előző tartalmat, amíg el nem telik egy másodperc)
  • tegyen az előző tartalom fölé egy áttetsző vagy átlátszó réteget, amíg tölt, hogy ne lehessen beleírni az űrlapba, vagy újraküldeni...
  • rollback az előző tartalomra (hogy újra lehessen küldeni az űrlapot, ha nem ment el)
  • késleltetett eredmény kiírás, mondjuk ha adatokat mentek, akkor írja ki egy másodpercig, hogy sikeresen mentve, aztán fusson csak le a callback


Példa a használatára:

var content = new Backbone.UI.SyncLabelDecorator({
	appendTo: "body",
});

content.load(function (){
	this.$el.append("normal data without preload");
});

var User = Backbone.Model.extend({
	urlRoot: "/users"
});
var UserSet = Backbone.Collection.extend({
	url: "/users",
	model: User
});
var Role = Backbone.RelationalModel.extend({
	relations: [{
		type: Backbone.HasMany,
		key: 'members',
		relatedModel: User
	}]
});

var administrator = new Role({id :1});
var users = new UserSet();

content.load({
	fetch: [role, users],
	sync: function (){
		var form = new Form({
			title: "Update role",
			model: role,
			fields: {
				id: {
					type: "HiddenInput"
				},
				name: {
					type: "TextInput"
				},
				members: {
					type: "TwoListSelection",
					alternatives: users
				}
			},
			submit: function (){
				content.load({
					tasks: {
						save: role
					},
					sync: function (){
						this.$el.html("Role is successfully saved.");
					}
				});
			}
		});
		this.$el.append(form.render().$el);
	}
});