chevron-left chevron-right

[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.

Przykładowa struktura widoków i serwisów

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.

  • Od razu widać, że Zakas pracował w Yahoo 😉 YUI jest chyba największym frameworkiem na rynku, który bawi się architekturą sandboxowaną (a przynajmniej próbuje). wszystko to ładnie opisał wspomniany człek w swojej prezentacji: http://www.slideshare.net/nzakas/scalable-javascript-application-architecture

    ciekawe podejście ma także Twitterowy Flight, napędzany de facto samymi eventami. jedynym zgrzytem jest zależność od jQuery…

  • Przyjrzę się temu frameworkowi Flight wolnej chwili