Rails, rails, everywhere
What you will learn
In this lesson, you will use ScrollView
components to create a more complex screen with vertical and horizontal navigation.
This should be the final result :
Retrieve categories name and contents
Before creating new views, you should retrieve categories name and content in prepareData
of CatalogScreen
. For the moment, always retrieve first catalog to get the categories and their contents.
export default $AppScreen.declare("CatalogScreen", {
methods: {
prepareData: function (context) {
const result = {};
return this.vodService.getCatalogs()
.then(catalogs => {
result.menu = catalogs;
return this._getCatalogContents(catalogs[0]);
})
.then(datas => {
result.datas = datas;
return result;
});
},
_getCatalogContents: function (catalog) {
let promises = [];
const datas = [];
catalog.children.forEach(category => {
const promise = this.vodService.getCategory(catalog.id, category.id).then(category => {
return this.vodService.getContents(category.id).then((contents) => {
datas.push({
railName: category.name,
listData: contents
});
});
});
promises.push(promise);
});
return Promise.all(promises).then(_ => {
return datas;
});
},
}
});
ScrollView usage
A ScrollView
is a vertical RecyclingListView
of ScrollViewItem
. A ScrollViewItem
is a View
with horizontal navigation. In our case, we will use a RecyclingListView
with horizontal navigation to create a rail. By default, a children named list
inheriting from RecyclingListView
exist in ScrollViewItem
.
Create a file CatalogScrollItemView.js
to create CatalogScrollItemView
inheriting of ScrollItemView
. Then, add a TextPrimitive
as a children to display category name. Finally, implement setData
to assign data to the TextPrimitive
.
import $ScrollItemView from "@ScrollItemView";
import $TextPrimitive from "@TextPrimitive";
import $Theme from "@Theme";
/**
*
* @name CatalogScrollItemView
* @class
* @extends ScrollItemView
*/
export default $ScrollItemView.declare("CatalogScrollItemView", {
statics: /** @lends CatalogScrollItemView */ {
TITLE_SIZE: 45,
MARGIN: 20
},
children: {
title: {class: $TextPrimitive}
},
methods: /** @lends CatalogScrollItemView.prototype */ {
/**
* @override
*/
_setData: function (data, options) {
this.title.text = data.railName;
}
},
style: {
height: ({statics, list}) => list.height + statics.TITLE_SIZE + statics.MARGIN,
width: $Theme.FULL_SCREEN_WIDTH - $Theme.SCREEN_HORIZONTAL_MARGIN * 2,
title: {
height: ({parent}) => parent.statics.TITLE_SIZE,
width: $Theme.FULL_SCREEN_WIDTH - $Theme.SCREEN_HORIZONTAL_MARGIN * 2,
color: $Theme.COLOR_BLACK,
size: $Theme.H2_FONT_SIZE
}
}
});
To see this item, you need to create a new children in your CatalogScreen
. Name it scrollView
and make it use the new CatalogScrollItemView
to populate. Send datas from the screen to the scrollView
.
import $ScrollView from "@ScrollView";
import $CatalogScrollItemView from "@CatalogScrollItemView";
export default $AppScreen.declare("CatalogScreen", {
children: /** @lends CatalogScreen.prototype */ {
menu: {class: $MenuListView},
scrollView: {
class: $ScrollView,
itemViewClass: () => $CatalogScrollItemView,
itemMargin: 70
}
},
style: {
scrollView: {
x: $Theme.SCREEN_HORIZONTAL_MARGIN,
y: 100
}
},
methods: /** @lends CatalogScreen.prototype */ {
connectData: function (context) {
this.menu.setData(this.data.menu);
this.scrollView.setData(this.data.datas);
}
}
});
Refresh your page to see the different titles being displayed below the menu.
To complete integration of the scroll view, you should handle navigation. Even if scroll view is coming with a navigation behaviour, you should manage the “out-of-bounds” use cases. For example, when pressing down
will focused on a ScrollView
, two cases can happend :
- There is more items to focused on the scroll view, and you should let internal navigation occurred,
- There is no more items to focus on the scroll view, and you may want to change the focus to another view of the screen.
Caution
Navigation methods, like up
, down
, left
or right
, returns a boolean to know if the event as been consumed. If methods return true
, it means that an action has already been taken for this event.
For your screen use case, you will have :
- when pressing down
- call internal navigation
- if consumed, return
true
- if not consumed
- if focus is on menu, focus scroll view and return
true
- otherwise return
false
- if focus is on menu, focus scroll view and return
- if consumed, return
- call internal navigation
- when pressing up
- call internal navigation
- if consumed, return
true
- if not consumed
- if focus is on scroll view, focus menu and return
true
- otherwise return
false
- if focus is on scroll view, focus menu and return
- if consumed, return
- call internal navigation
export default $AppScreen.declare("CatalogScreen", {
methods: /** @lends CatalogScreen.prototype */ {
down: function () {
const res = $AppScreen.prototype.down.apply(this, arguments);
if (res) {
return true;
}
if (this.focusedChild.id === this.menu.id) {
this.scrollView.focus();
return true;
}
return false;
},
up: function () {
const res = $AppScreen.prototype.up.apply(this, arguments);
if (res) {
return true;
}
if (this.focusedChild.id === this.scrollView.id) {
this.menu.focus();
return true;
}
return false;
}
}
});
Caution
There is no support of super
keyword to call parent’s method implementation. Use $SuperClass.prototype.<methodName>.apply(this, arguments)
instead.
You can now navigate vertically between your menu and the scroll view. Also, you already have navigation inside the scroll view. When requesting focus on the second title, view automatically scrolls. Do not hesitate to hide the menu when the scrolling begins:
export default $AppScreen.declare("CatalogScreen", {
style: {
menu: {
opacity: ({parent}) => parent.scrollView.index > 0 ? 0 : 1
}
}
});
Create a rail full of tiles
As for the menu, create a new class inheriting of RecyclingListView
in a file named RailListView
. Also create a new class LandscapeTileItemView
inheriting of RecyclingItemView
that will represent an item of the rail. In a first step, only add a TextPrimitive
as a child to display the content’s title.
import $RecyclingListView from "@RecyclingListView";
import $LandscapeTileItemView from "@LandscapeTileItemView";
import $MHListNavigation from "@MHListNavigation";
import $Theme from "@Theme";
/**
*
* @name RailListView
* @class
* @extends RecyclingListView
* @property {Number} itemMargin Space between items
* @property {LandscapeTileItemView} itemViewClass Class of an item
*/
export default $RecyclingListView.declare("RailListView", {
traits: [$MHListNavigation],
statics: /** @lends RailListView */ {
ITEM_HEIGHT: $LandscapeTileItemView.TILE_CARD_HEIGHT
},
properties: /** @lends RailListView.prototype */ {
itemViewClass: () => $LandscapeTileItemView,
itemMargin: 20
},
style: {
height: ({statics}) => statics.ITEM_HEIGHT,
width: $Theme.FULL_SCREEN_WIDTH - $Theme.SCREEN_HORIZONTAL_MARGIN * 2
}
});
import $RecyclingItemView from "@RecyclingItemView";
import $ImagePrimitive from "@ImagePrimitive";
import $TextPrimitive from "@TextPrimitive";
import $RectanglePrimitive from "@RectanglePrimitive";
import $Theme from "@Theme";
/**
*
* @name LandscapeTileItemView
* @class
* @extends RecyclingItemView
*/
export default $RecyclingItemView.declare("LandscapeTileItemView", {
statics: /** @lends LandscapeTile */ {
TILE_CARD_WIDTH: 320,
TILE_CARD_HEIGHT: 24
},
children: /** @lends LandscapeTile.prototype */ {
title: {
class: $TextPrimitive
}
},
methods: /** @lends LandscapeTile.prototype */ {
/**
* Set properties with current data
* @private
* @override
*/
_setData: function () {
if (this.data != null) {
this.title.text = this.data.title;
}
}
},
style: {
width: ({statics}) => statics.TILE_CARD_WIDTH,
height: ({statics}) => statics.TILE_CARD_HEIGHT,
title: {
width: ({parent}) => parent.statics.TILE_CARD_WIDTH,
height: ({parent}) => parent.statics.TILE_CARD_HEIGHT
}
}
});
To make your scroll list using this list, override list
child by your RailListView
class and set datas in it.
import $RailListView from "@RailListView";
export default $ScrollItemView.declare("CatalogScrollItemView", {
children: {
list: {class: $RailListView}
},
methods: /** @lends CatalogScrollItemView.prototype */ {
/**
* @override
*/
_setData: function (data, options) {
this.list.setData(data.listData, options);
}
},
style: {
list: {
y: ({parent}) => parent.statics.TITLE_SIZE + parent.statics.MARGIN
}
}
});
Add getImageurl in model and dipsplay image
Improve tile design
Current tile is missing a way to inform where focus is. Let’s add an image to each of the contents and add a border over it when focus is on the tile.
First, modify the CustomBEContent
.
Caution
Content
is an ImageModel
and offer an getImageUrl
that requires an implementation of _getImageUrl
methods. It is the only method alowed on Model
Note
Lorem Picsum can generate image of the expected size. Use id
as a seed to always retrieve the same image of a content.
export default $AbstractContent.declare("CustomBEContent", {
methods: /** @lends WiztiviBECmsContent.prototype */ {
/**
* Allows to retrieve an model image from image data and options passed as arguments
* @param {Object} options Options to help construct image url
* @param {number} options.ratio ratio of the image asked
* @param {number} options.width width of the image asked
* @param {number} options.height height of the image asked
* @return {string} Image url
*/
_getImageUrl: function (options) {
return `https://picsum.photos/seed/${this.id}/${options.width}/${options.height}`;
}
}
});
In the LandscapeTileItemView
, add an ImagePrimitive
to display this new image and a RectanglePrimitive
to create a border over it.
import $ImagePrimitive from "@ImagePrimitive";
import $RectanglePrimitive from "@RectanglePrimitive";
import $Theme from "@Theme";
export default $RecyclingItemView.declare("LandscapeTileItemView", {
statics: /** @lends LandscapeTile */ {
TILE_CARD_WIDTH: 320,
IMAGE_CARD_HEIGHT: 160,
TILE_CARD_HEIGHT: 184
},
children: /** @lends LandscapeTile.prototype */ {
image: {
class: $ImagePrimitive
},
title: {
class: $TextPrimitive
},
border: {
class: $RectanglePrimitive
}
},
methods: /** @lends LandscapeTile.prototype */ {
/**
* Set properties with current data
* @private
* @override
*/
_setData: function () {
if (this.data != null) {
this.image.url = this.data.getImageUrl({width: this.statics.TILE_CARD_WIDTH, height: this.statics.IMAGE_CARD_HEIGHT});
this.title.text = this.data.title;
}
}
},
style: {
image: {
width: ({parent}) => parent.statics.TILE_CARD_WIDTH,
height: ({parent}) => parent.statics.IMAGE_CARD_HEIGHT,
borderRadius: 10
},
title: {
height: ({parent}) => parent.statics.TILE_CARD_HEIGHT - parent.statics.IMAGE_CARD_HEIGHT,
y: ({parent}) => parent.statics.IMAGE_CARD_HEIGHT
},
border: {
width: ({parent}) => parent.statics.TILE_CARD_WIDTH,
height: ({parent}) => parent.statics.IMAGE_CARD_HEIGHT,
opacity: ({parent}) => parent.isFocused ? 1 : 0,
borderWidth: 10,
borderColor: $Theme.COLOR_BLUE_LIGHT,
borderRadius: 10
}
}
});
Summary
To summarize, during this lesson you have :
- used
ScrollView
components, - handled a mix of internal and external navigation in a screen
Result of you application architecture is as follows :