Message-ID: <126448291.3718.1485855002315.JavaMail.confluence@ip-10-127-227-164> Subject: Exported From Confluence MIME-Version: 1.0 Content-Type: multipart/related; boundary="----=_Part_3717_1238345936.1485855002315" ------=_Part_3717_1238345936.1485855002315 Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: quoted-printable Content-Location: file:///C:/exported.html
As written in the P= latformUI technical introduction, pages in PlatformUI can be generated = either by the browser based on the REST API (or any other API) responses or= by doing part of the rendering on the server side for instance with some T= wig templates called by a Symfony controller. Both options are perfectly va= lid and choosing one or the other is mainly a matter of taste. This step wi= ll examine both strategies even if the later steps will be based on the ser= ver-side rendering.
In this case, the browser uses the REST API to fetch the necessary objec= ts/structures and then the logic of transforming that to an HTML page is wr= itten in JavaScript and executed by the browser.
The first thing to do is to create a view service. A view service is a c=
omponent extending Y.eZ.ViewService
. So we first need to decla=
re and create a module and then this module will create the minimal view se=
rvice class:
ezconf-listviewservice: requires: ['ez-viewservice'] path: %extending_platformui.public_dir%/js/views/services/ezconf-listv= iewservice.js=20
Then in ezconf-listviewservice.js
we can write the minimal =
view service:
YUI.add('ezconf-listviewservice', function (Y) { Y.namespace('eZConf'); Y.eZConf.ListViewService =3D Y.Base.create('ezconfListViewService', Y.e= Z.ViewService, [], { initializer: function () { console.log("Hey, I'm the ListViewService"); }, }); });=20
This is the minimal view service, it only writes a "hello world" message= in the console when instantiated but for now it's not used anywhere in the= application.
To really use our view service in the application, we have to change the=
route so that the PlatformUI application instantiates and uses the view se=
rvice when building the page. To do that, we have to add the route se=
rvice
property to hold the constructor function of the view service =
so the application plugin that adds the route will also have to require the=
ezconf-listviewservice
module:
ezconf-listapplugin: requires: ['ez-pluginregistry', 'plugin', 'base', 'ezconf-listview', '= ezconf-listviewservice'] # the view module has been added dependencyOf: ['ez-platformuiapp']=20
After doing that, Y.eZConf.ListViewService
becomes availabl=
e in the application plugin code and we can change the eZConfList route to:
app.route({ name: "eZConfList", path: "/ezconf/list", view: "ezconfListView", service: Y.eZConf.ListViewService, // constructor function to use to in= stantiate the view service sideViews: {'navigationHub': true, 'discoveryBar': false}, callbacks: ['open', 'checkUser', 'handleSideViews', 'handleMainView'], });=20
After this change, the Y.eZConf.ListViewService
is used whe=
n a user reaches the eZConfList
route.
A view service is responsible for fetching data so it can be rendered. F=
or a given route, the view service is instantiated the first time the route=
is accessed and then the same instance is reused. On that instance, the =
capi
attribute of the view service.
In this tutorial, we want to display the Content in a flat list and filt= er this list by Content Types. For now, let's fetch everything; to do that,= the view service will create a REST view to search for every Location in t= he repository:
YUI.add('ezconf-listviewservice', function (Y) { Y.namespace('eZConf'); Y.eZConf.ListViewService =3D Y.Base.create('ezconfListViewService', Y.e= Z.ViewService, [], { initializer: function () { console.log("Hey, I'm the ListViewService"); }, // _load is automatically called when the view service is configure= d for // a route. callback should be executed when everything is finished _load: function (callback) { var capi =3D this.get('capi'), // REST API JavaScript client contentService =3D capi.getContentService(), query =3D contentService.newViewCreateStruct('ezconf-list',= 'LocationQuery'); // searching for "everything" query.body.ViewInput.LocationQuery.Criteria =3D {SubtreeCriteri= on: "/1/"}; contentService.createView(query, Y.bind(function (err, response= ) { // parsing the response and storing the location list in th= e "location" attribute var locations; locations =3D Y.Array.map(response.document.View.Result.sea= rchHits.searchHit, function (hit) { var loc =3D new Y.eZ.Location({id: hit.value.Location._= href}); loc.loadFromHash(hit.value.Location); return loc; }); this.set('locations', locations); callback(); }, this)); }, }, { ATTRS: { locations: { value: [], } } }); });=20
At this point, if you refresh the PlatformUI application and follow the =
link added in the p=
revious step, you should see a new REST API request to /api/ezp/v=
2/views
in the network panel of the browser:
The Locations are built in the application but not yet used anywhere.
Now that we have the Locations, we have to give them to the view. For th=
at, we have to implement the _getViewParameters
method in the =
view service. This method is automatically called when view service loading=
is finished, it should return an object that will be used as a configurati=
on object for the main view.
In our case, we just want to give the Location list to the view, so the =
_getViewParameters
method is quite simple:
_getViewParameters: function () { return { locations: this.get('locations'), }; },=20
With that code, the view will receive the Location list as an attribute =
under the name locations
.
Why implement _load and _getViewParameters and not load = and getViewParameters?
When implementing a custom view service, you should always implement the=
protected _load
and _getViewParameters
methods, =
not their public counterparts load
and getViewParameters=
. By implementing the protected versions, you keep the opportunity f=
or a developer to write a plugin to enhance your view service.
The view now receives the Location list in the locations attribute so we have to change the view to take that into account. For n=
ow, let's change it to just display the Location it receives as an unordere=
d HTML list of links to those Locations. To do that, we have to:
locations
attribute in the viewPoint 2 is required because Handlebars is not able to understand the com= plex model objects generated by YUI. So we have to transform those complex = object into plain JavaScript objects. After doing the changes in steps 1 an= d 2, the view looks like this:
YUI.add('ezconf-listview', function (Y) { Y.namespace('eZConf'); Y.eZConf.ListView =3D Y.Base.create('ezconfListView', Y.eZ.TemplateBase= dView, [], { initializer: function () { console.log("Hey, I'm the list view"); }, render: function () { this.get('container').setHTML( this.template({ locations: this._getJsonifiedLocations(), }) ); return this; }, _getJsonifiedLocations: function () { // to get usable objects in the template return Y.Array.map(this.get('locations'), function (loc) { return loc.toJSON(); }); }, }, { ATTRS: { locations: { value: [], } } }); });=20
Then the template has to be changed to something like:
<h1 class=3D"ezconf-list-title">List view</h1> <ul class=3D"ezconf-list"> {{#each locations}} <li><a href=3D"{{path 'viewLocation' id=3Did languageCode=3Dco= ntentInfo.mainLanguageCode}}">{{ contentInfo.name }}</a></li>= ; {{/each}} </ul>=20
PlatformUI provides a path
template helper that allows you =
to generate a route URL in PlatformUI. It expects a route name and the rout=
e parameters.
The resulting code can be been in the 6_1_list_client
tag on=
GitHub, this step result can also be viewed as a diff between t=
ags 5_navigation
and 6_1_list_client
.
The rest of this tutorial is focused on the server side rendering strate= gy. Completing the browser side rendering strategy to get the expected feat= ures is left as an exercise.
In this case a part of the rendering is delegated to the server. When bu= ilding a PlatformUI page this way, the application will just do one or more= AJAX request(s) and inject the result in the UI. The PlatformUI Admin = part is built this way. To be easily usable in the JavaScript applicat= ion, the server response has to be structured so that the application can r= etrieve and set the page title, the potential notifications to issue and th= e actual page content. This is done by generating an HTML fragment, in the = following way:
<div data-name=3D"title">Title to set in the application</= div> <div data-name=3D"html"> <p>Page content</p> </div> <ul data-name=3D"notification"> <li data-state=3D"done">I'm a "done" notification to display in t= he application</li> </ul>=20
In the case, the minimal view service is exactly the same as the one pro= duced in the previous Minimal view service paragraph.
The eZConfList route also has to be configured exactly in the same way a= s in the previous Configure the route to use the view servic= e paragraph.
This will be done by a Symfony Controller that will use the Search Servi=
ce and a Twig template to generate the HTML code. As you can see in this Github commit, it=
's a very basic Symfony Controller that extends EzSystems\PlatformUI=
Bundle\Controller\Controller
.
Extending EzSystems\PlatformUIBundle\Controller\Controller=
span>
is not strictly required. By doing that, the act=
ions provided by the controller are automatically restricted to authenticat=
ed users. This base controller also provides the base API to handle notific=
ations if needed.
To learn how to write Symfony controllers, please read the Symfony Controller documentation.
The list
action in ListController
uses the fol=
lowing Twig template:
{% extends "eZPlatformUIBundle::pjax_admin.html.twig" %} {% block header_title %} <h1 class=3D"ezconf-list-title">List view</h1> {% endblock %} {% block content %} <ul class=3D"ezconf-list"> {% for value in results.searchHits %} <li><a href=3D"">{{ value.valueObject.contentInfo.name }}&l= t;/a></li> {% endfor %} </ul> {% endblock %} {% block title %}List{% endblock %}=20
Again, it's a quite a regular template but to ease the generation of the=
expected structured HTML fragment, this template extends eZPlatformUIBundle::pjax_admin.html.twig
=
and redefines a few blocks, the main one being content
where =
the actual page content is supposed to be generated.
We now have a Symfony Controller able to generate our Location list but =
this list is not yet available in the application. As in Fetc=
hing Locations from the view service and =
Passing the Location to the view=
, we have to add the code in the view service. But in the case o=
f a server side rendering, we can reuse ServerSideViewService
=
which provides the base API to parse an HTML fragment which also provides a=
ready to use _getViewParameters
method. All we have to do the=
n is to implement the loading logic in _load
:
YUI.add('ezconf-listviewservice', function (Y) { Y.namespace('eZConf'); Y.eZConf.ListViewService =3D Y.Base.create('ezconfListViewService', Y.e= Z.ServerSideViewService, [], { initializer: function () { console.log("Hey, I'm the ListViewService"); }, _load: function (callback) { var uri =3D this.get('app').get('apiRoot') + 'list'; Y.io(uri, { // YUI helper to do AJAX request, see http://yuilib= rary.com/yui/docs/io/ method: 'GET', on: { success: function (tId, response) { this._parseResponse(response); // provided by Y.eZ.= ServerSideViewService callback(this); }, failure: this._handleLoadFailure, // provided by Y.eZ.S= erverSideViewService }, context: this, }); }, }); });=20
The resulting _load
method will just do an AJAX request to =
the action provided by our Symfony controller.
At this point, if you refresh your browser, nothing should have changed = but you should see the AJAX request in the network panel of your browser.= p>
To have a visual change, we now have to change the ListView
to be=
a server side view. This operations involves removing some code added in <=
a href=3D"/display/DEVELOPER/Define+a+View">the Define a View step. Bas=
ically, the View does not need any template but it will inherit from =
Y.eZ.ServerSideView
. As a result, the view module definition becomes=
:
ezconf-listview: requires: ['ez-serversideview'] path: %extending_platformui.public_dir%/js/views/ezconf-listview.js=20
And the View component also has to be simplified to:
YUI.add('ezconf-listview', function (Y) { Y.namespace('eZConf'); Y.eZConf.ListView =3D Y.Base.create('ezconfListView', Y.eZ.ServerSideVi= ew, [], { initializer: function () { console.log("Hey, I'm the list view"); this.containerTemplate =3D '<div class=3D"ez-view-ezconflist= view"/>'; // make sure we keep the same class on the container }, }); });=20
At this point, you should see the same list as the one that was generate= d in Browser side rendering section. The only difference lies in the non= -working links being generated in the server side solution.
There are several ways to fix the link issue. In this step we are going = to add some metadata to the generated HTML links, then we'll change the vie= w to recognize the enhanced links and finally we'll change the server side = view to achieve the navigation.
The server side code has no knowledge of the JavaScript application rout=
ing mechanism as a result, it can not directly generate any PlatformUI Appl=
ication URI, but we know while generating the HTML fragment that we want ea=
ch link to allow the navigation to the viewLocation
route for =
the Location being displayed. We can then change the Twig template to add t=
he necessary metadata on each link so that the application has a way of gue=
ssing where the user is supposed to go when clicking on the link:
{% block content %} <ul class=3D"ezconf-list"> {% for value in results.searchHits %} <li><a class=3D"ezconf-list-location" href=3D"" data-route-name=3D"viewLocation" data-route-id=3D"{{ path('ezpublish_rest_loadLocation', {locationPa= th: value.valueObject.pathString|trim('/')}) }}" data-route-languageCode=3D"{{ value.valueObject.contentInfo.mainLan= guageCode }}" >{{ value.valueObject.contentInfo.name }}</a></li> {% endfor %} </ul> {% endblock %}=20
For each link we are basically saying to the application that the user s=
hould directed to the viewLocation
route for the given Locatio=
n id and the given language code.
In PlatformUI code, the Locations, Content items and Content Types are i=
dentified by their REST id, that is the REST resource URL which allows you =
to fetch the object in the REST API. That's why we are using the path=
Twig template function to build the Location id.
Then we have to change the view to add a special behavior when the user =
clicks on them. The view can not directly trigger the navigation to the exp=
ected route. So in this case, we are firing an application level event with=
all the data we have on the link and we'll let the view service handle thi=
s application level event to take the user to the expected page. So, we hav=
e to configure our view to recognize the click on the links and to fire the=
navigateTo
custom event:
YUI.add('ezconf-listview', function (Y) { Y.namespace('eZConf'); Y.eZConf.ListView =3D Y.Base.create('ezconfListView', Y.eZ.ServerSideVi= ew, [], { // this is YUI View mechanic to subscribe to DOM events (click, sub= mit, // ...) and synthetic event (some custom event provided by YUI) lik= e // 'tap' here. events: { '.ezconf-list-location': { // tap is 'fast click' (touch friendly) 'tap': '_navigateToLocation' } }, initializer: function () { console.log("Hey, I'm the list view"); this.containerTemplate =3D '<div class=3D"ez-view-ezconflist= view"/>'; }, _navigateToLocation: function (e) { var link =3D e.target; e.preventDefault(); // don't want the normal link behavior // tell the view service we want to navigate somewhere // it's a custom event that will be bubble up to the view servi= ce // (and the app) // the second parameter is the data to add in the event facade,= this // can be used by any event handler function bound to this even= t. this.fire('navigateTo', { routeName: link.getData('route-name'), routeParams: { id: link.getData('route-id'), languageCode: link.getData('route-languagecode'), } }); }, }); });=20
The DOM event handling is one of the main YUI View features. It is docum= ented in the YUI View guide.
Now the click on the Location link is transformed in a navigateTo<=
/code> event, we have to subscribe to that event in the view service to tri=
gger the expected navigation:
YUI.add('ezconf-listviewservice', function (Y) { Y.namespace('eZConf'); Y.eZConf.ListViewService =3D Y.Base.create('ezconfListViewService', Y.e= Z.ServerSideViewService, [], { initializer: function () { console.log("Hey, I'm the ListViewService"); // we catch the `navigateTo` event no matter from where it come= s // when bubbling, the event is prefixed with the name of the // component which fired the event first. // so in this case we could also write // this.on('ezconflistview:navigateTo', function (e) {}); // `e` is the event facade. It contains various informations ab= out // the event and if any the custom data passed to fire(). this.on('*:navigateTo', function (e) { this.get('app').navigateTo( e.routeName, e.routeParams ); }); }, _load: function (callback) { // [...] skip to keep the example short }, }); });=20
With that in place and after a refresh, the Location list should allow y= ou to the navigate to the expected Location in PlatformUI.
Application level events
PlatformUI uses a lot of custom application level events thanks to the EventTarget YUI component. Those events are very = similar to DOM events but they are attached to the appl= ication components instead of the DOM elements. Like for DOM events, there = is a bubbling mechanism. For instance,= here the view is firing an event and unless the propagation of the event i= s stopped, it will bubble to the view service and then to the application. = The event basically follows the components tree until the application. An application= level event is way for a deeply nested component to communicate with a hig= her level component.
The resulting code can be seen in the 6_2_list_server
tag on=
GitHub, this step result can also be viewed as a diff between t=
ags 5_navigation
and 6_2_list_server
.
The next step is then to= add the pagination.