[JS] Jak nasłuchiwać zdarzeń z innych widoków w YUI3?
W związku z rozpoczęciem pracy w nowej firmie przyszło mi się spotkać z frameworkiem YUI3. Z początku ten wybór wydawał mi się dziwny, ponieważ co chwilę można przeczytać informacje o frameworkach takich jak Angular, Backbone, React, Ember czy też wielu innych. Ba, sam pisałem o Backbone.js na swoim blogu!
Czym jest YUI3?
YUI3 jest frameworkiem JS, ale innym niż wymienione przeze mnie wcześniej frameworki. Dlaczego?
Ponieważ jest to prawdziwy kombajn, który:
- Jest frameworkiem JS do pisania aplikacji internetowych - zupełnie jak Backbone.js,
- Jest biblioteką do manipulacji drzewem DOM - zupełnie jak jQuery,
- Ma budowę modułową (myślę tu AMD) - niepotrzebne jest korzystanie z require.js,
- Zawiera zestaw narzędzi usprawniających pisanie kodu - niepotrzebne jest już korzystanie z lodash.js lub underscore.js,
- Jest zintegrowany z własnym systemem testów jednostkowych (YUI Tests) - nie trzeba już korzystać z QUnit, Jasmine czy innych podobnych,
- Posiada zestaw widgetów, które można z powodzeniem stosować na stronach WWW - nie ma potrzeby korzystania jQuery UI.
Nie minę się za bardzo z prawdą, jeśli napiszę że korzystając z tej biblioteki nie jesteś już zmuszony do korzystania z dodatkowych "wspomagaczy" przy budowaniu swojej aplikacji internetowej czy też przy tworzeniu różnego rodzaju efektów a'la jQuery na stronie internetowej.
Nasłuchiwanie zdarzeń (eventów) z innych widoków/serwisów w YUI3 - koncept
Pora teraz przejść do sedna tego wpisu, mianowicie do sposobu obsługi zdarzeń w widokach utworzonych za pomocą YUI3.
Wyobraź sobie taką sytuację: masz panel administracyjny, który jest głównym widokiem. Tenże panel administracyjny składa się z osobnych widgetów. Każdy widget jest osobnym widokiem - dzieckiem widoku panelu administracyjnego i może zawierać elementarne, jednostkowe widoki dla elementów swojej zawartości. Do widoku panelu administracyjnego przypisany jest serwis, którego zadaniem jest pobieranie danych potrzebnych do wyświetlenia w panelu. Poniżej można się zapoznać ze schematem.
Naszym celem jest takie ogłaszanie zdarzeń, aby zdarzenia były emitowane do elementów, które chcą je nasłuchiwać. Bowiem, częstym błędem jest emitowanie zdarzeń do globalnej przestrzeni nazw YUI, co nie jest optymalnym rozwiązaniem.
Nasłuchiwanie zdarzeń (eventów) z innych widoków/serwisów w YUI3 - implementacja
Mając przed oczami plan działania, pora przystąpić do tworzenia kodu. Utworzymy 4 kawałki kodu. Pierwszy dla serwisu aplikacji, drugi dla widoku panelu administracyjnego, trzeci dla widoku widgetu (niech to będzie tabela) a czwarty dla pojedynczego elementu zawartego w widgecie (niech to będzie wiersz tabeli).
1. Kod serwisu
Serwis jest inicjalizowany przez aplikację. Serwis został przez aplikację powiązany z widokiem panelu administracyjnego przy ustalaniu reguł routingu aplikacji.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | // app-service.js YUI.add('app-service', function (Y) { 'use strict'; Y.namespace('myApp'); Y.myApp.AppService = Y.Base.create('appService', Y.ViewService /* niestandardowa baza dla serwisów */, [], { initializer : function () { // nasłuchujemy zdarzeń z widoku widgetu this.on('get:widgetdata', this._getWidgetData); // nasłuchujemy zdarzeń z widoku elementu widgetu (wiersza) this.on('get:widgetitemdata', this._getWidgetItemData); }, _getWidgetData : function (event) { // pobieramy dane, które są potrzebne do wygenerowania widgetu Y.io('sample/data.php', { on : { success : function (response) { // w tym przypadku, event.target jest to referencja do widoku widgetu // i w tym widoku aktualizujemy wartość własności widgetData event.target.set('widgetData', response); } } }); return this; }, _getWidgetItemData : function (event) { var that = this; // pobieramy dane, które są potrzebne do wygenerowania widgetu Y.io('sample/itemData.php', { on : { success : function (response) { var itemModel = that.get('widgetItemModel'); itemModel.setAttrs(response); // w tym przypadku, event.target jest to referencja do widoku wiersza w widgecie // i w tym widoku aktualizujemy wartość własności model event.target.set('model', itemModel); } } }); return this; } }, { ATTRS : { widgetItemModel : { valueFn : function () { return new Y.myApp.WidgetItemModel(); } } } }); }, '1.0.0', { requires : ['io-base', 'widget-item-model', 'dashboard-view'] }); |
Analizując powyższy kod, możesz zauważyć że serwis rozszerza obiekt Y.ViewService
, lecz nie stanowi on elementu frameworka YUI3. Nie jest to istotny kawałek kodu.
W dalszej części, w funkcji initializer()
jest uruchomione nasłuchiwanie zdarzeń get:widgetdata oraz getwidgetitemdata. Pierwsze ze zdarzeń jest uruchamiane w widoku widgetu, a drugie z nich w widoku elementu widgetu. W ten sposób, serwis nie musi wiedzieć czym są obiekty które wysłały żądanie po dane. Tym samym unikamy problemu zbyt głębokich zależności między różnymi kawałkami kodu.
Metody _getWidgetData()
oraz _getWidgetItemData()
wykonują żadania AJAX i modyfikują własności obiektów, które wysłały żądanie o dane.
2. Kod widoku panelu administracyjnego
Panel administracyjny jest powiązany z serwisem i w nim są umiejscowione widgety, które mogą wysyłać żądania do serwisu.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | // dashboard-view.js YUI.add('dashboard-view', function (Y) { 'use strict'; Y.namespace('myApp'); Y.myApp.DashboardView = Y.Base.create('dashboardView', Y.View, [], { // funkcja renderująca widok panelu administracyjnego render : function () { // renderujemy widok panelu this.get('container').setHTML(this.template()); // uruchamiamy renderowanie widgetu w panelu administracyjnym this._renderWidget(); return this; }, // funkcja renderująca widok widgetu jako część panelu administracyjnego _renderWidget : function () { var widget = this.get('widgetView'); // BARDZO WAŻNE // widok panelu administracyjnego dodaje siebie jako obserwatora zdarzeń w widgecie // dzięki temu zdarzenia będą w stanie dotrzeć do serwisu z którym panel jest powiązany widget.addTarget(this); // wyświetlenie widgetu w panelu administracyjnym this.get('container').append(widget.render().get('container')); return this; } }, { ATTRS : { widgetView : { valueFn : function () { return new Y.myApp.WidgetView(); } } } }); }, '1.0.0', { requires : ['widget-view', 'app-service'] }); |
W widoku panelu administracyjnego nie obserwujemy/nasłuchujemy zdarzeń. Po prostu renderujemy widok panelu administracyjnego i ustawiamy panel jako obserwatora zdarzeń w widgecie.
3. Kod widgetu
Widok widgetu jest widokiem podrzędnym względem panelu administracyjnego.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | // widget-view.js YUI.add('widget-view', function (Y) { 'use strict'; Y.namespace('myApp'); Y.myApp.WidgetView = Y.Base.create('widgetView', Y.View, [], { // funkcja wyświetlająca widok widgetu render : function () { // renderujemy pusty widok i go wyświetlamy this.get('container').empty().setHTML(this.template()); // BARDZO WAŻNE // gdy widok będzie aktywny wtedy uruchamiamy zdarzenie get:widgetdata // jest to spowodowane tym, że widok musi zostać oznaczony jako aktywny // dzieje się to tak poprzez użycie metody addTarget() w widoku panelu administracyjnego this.after('activeChange', function () { this.fire('get:widgetdata', this); }); // obserwujemy zdarzenie zmiany wartości własności widgetData // gdy wartość własności ulegnie zmianie, to uruchamiamy funkcję wyświetlającą // zawartość widgetu this.after('widgetDataChange', function () { this._renderWidgetContent(); }); return this; }, // funkcja wyświetlająca zawartość widgetu _renderWidgetContent : function () { var that = this, // tworzymy pustą tabelę table = Y.Node.create('<table/>'), widgetData = this.get('widgetData'); // dla każdego kawałka informacji mającego zostać wyświetlonym jako wiersz widgetData.rows.each(function (rowData) { // utwórz nową instancję widoku elementu widgetu var row = new Y.myApp.WidgetItemView(); // BARDZO WAŻNE // widok widgetu dodaje siebie jako obserwatora zdarzeń w elemencie widgetu // dzięki temu zdarzenia będą w stanie dotrzeć do serwisu, który znajduje się // na samym szczycie powiązań row.addTarget(that); // zmień własność model, która przechowuje dane do wyświetlenia w elemencie widgetu row.set('model', rowData); // dodaj wiersz do tabeli table.append(row.render().get('container')); }); // wyczyść widok z innych tabel this.get('container').all('table').remove(); // wyświetl nową tabelę z zawartością this.get('container').append(table); return this; } }, { ATTRS : { widgetData : { valueFn : function () { return {}; } } } }); }, '1.0.0', { requires : ['widget-item-view'] }); |
W tym widoku już dzieje się więcej niż w widoku panelu administracyjnego. Uruchamiamy zdarzenie do pobrania danych do wyświetlenia w widgecie (zdarzenie jest obserwowane przez serwis aplikacji), obserwujemy zdarzenia zmiany wartości własności widoku - dzięki czemu wiadomo kiedy wyświetlić zawartość widgetu. Ponadto, uruchamiamy renderowanie pojedynczych elementów widgetu i wyświetlamy je w widgecie.
Pojedyncze elementy widgetu są obserwowane przez cały widget dzięki temu będą również w stanie wysłać żądania po dane do serwisu aplikacji.
4. Widok pojedynczego elementu widgetu
Widok ten jest odpowiedzialny za renderowanie pojedynczego elementu znajdującego się w widgecie i docelowo obsługę jego specyficznych zdarzeń.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | // widget-item-view.js YUI.add('widget-item-view', function (Y) { 'use strict'; Y.namespace('myApp'); Y.myApp.WidgetItemView = Y.Base.create('widgetItemView', Y.View, [], { initializer : function () { // obserwowanie zmian we własności widoku o nazwie model this.after('modelChange', function () { // uruchomienie zdarzenia get:widgetitemdata // które jest obserwowane przez serwis aplikacji this.fire('get:widgetitemdata', this.get('model').get('slug')); }); // nasłuchiwanie zmian we własności widoku o nazwie itemData this.after('itemDataChange', function () { // renderowanie widoku elementu this._renderItem(); }); }, _renderItem : function () { this.get('container').setHTML(this.template({ name : this.get('itemData').name })); return this; } }, { ATTRS : { itemData : { valueFn : function () { return {}; } } } }); }, '1.0.0', { requires : [] }); |
Tutaj sytuacja wygląda podobnie jak w widoku widgetu. Tym razem, nie ma już podrzędnego elementu do którego zostałby dodany obserwator zdarzeń.
Podsumowanie
W ten sposób osiągnięto zamierzony cel. Nawet pojedynczy element widoku może uruchamiać zdarzenia odpowiedzialne za pobieranie elementarnych danych a następnie może nasłuchiwać czy nastąpiły zmiany we właściwościach widoku. Powyższy kod, nie jest pełnym kodem aplikacji. Ma za zadanie tylko zaprezentować sposób nasłuchiwania zdarzeń uruchamianych w różnych elementach widoków, w taki sposób, aby osiągnąć jak najmniejszą zależność między poszczególnymi widokami.