``have their width set to the full width of the page, they cannot both fit onto the same line, thus the browser wraps the second element into a new line. This already looks much better, although the table still requires scrolling, which we will fix a bit later.
We will look at how these two elements should behave for larger screen sizes, first. If you increase the width of the display area, you will see that as we go wider, there is quite a bit of wasted space next to the menu and in the table. This is because any layout defined at the "small" scale is also applied to larger scales. We thus need to add extra CSS classes to override the small layout at larger sizes:
.. code-block:: handlebars
{{page-title 'Modules'}}
By adding the two ``medium-X`` classes, we now provide a slightly different layout for medium-sized screens. The first column of the grid is set to be 3 columns wide (25%), while the second one is 9 columns (75%). As this now fits into a single 12-column row, both will be displayed next to each other, with the respective relative sizes. If you now resize the display window, you will see that when the width hits a certain point, the layout changes from the small vertical layout to the medium-size horizontal one. If you increase the size to the full width, then you will see that again we get excessive white-space, thus add additional CSS classes:
.. code-block:: html
...
For the final large layout we don't use specific widths, we tell the browser that the menu area should take up only as much space as it needs for its content (shrink it down to what is needed) and the table area should take up all remaining space. If you now play with the screen width, you will see that the layout adapts nicely to the full range of screen sizes.
Custom CSS Adaptation
---------------------
However, at the small size, the table still exceeds the available width. To address this we will have to add some custom CSS in the ``app/styles/app.scss``:
.. code-block:: sass
/*
* Responsive Table
*/
@include media-breakpoint-down(md) {
table thead {
display: none;
}
table tbody td {
display: block;
width: 100%;
}
}
When we adapted the layout using the CSS classes, we used three different levels: small, medium, and large. In CSS parlance these are generally known as breakpoints and we can use them when we create our own table-specific CSS. The Bootstrap CSS framework we are using provides some helper functions for dealing with these breakpoints in the form of the ```media-breakpoint-only``, ``media-breakpoint-down`` or ``media-breakpoint-up`` mixin function. This first function takes a single parameter, that defines which breakpoint the CSS rules contained within the mixin are to be applied to. In this case we specify that they should apply to the breakpoints below ``md`` and should not be used for the medium, large or even larger breakpoints. That is because for those sizes we want to show the table normally.
The two modifications we make for the small size are that we first hide the table header and then in the table body we set each table cell (the ``
`` elements) to be displayed as a block element with a width of 100%. The effect of this is that at the small size the table is reformatted from the horizontal layout into a vertical layout which works much better on small devices.
The vertical display is a significant improvement, but on a small display there might still be too much information that is perhaps not needed at that point. To address this we can then add some more CSS classes into the template to hide things that perhaps are not so necessary in the small display:
.. code-block:: html+handlebars
|
{{module.code}}
|
JavaScript Adaptation
---------------------
The final adaptation we want to make to the list of modules is to change the direction of the menu. For the small breakpoint we want the menu to be horizontal, but for all other sizes we want it to be vertical. To implement this we will create our own media query service:
.. code-block:: console
$ yarn ember generate service media-query
We will only build a basic media query service that distinguishes small from not-small breakpoints. Update the code in ``app/services/media-query.js`` to this:
.. code-block:: js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
export default class MediaQueryService extends Service {
@tracked breakpoint = 'sm';
get small() {
return this.breakpoint === 'sm';
}
get notSmall() {
return this.breakpoint !== 'sm';
}
}
The service has only one property, the ``breakpoint`` which we initialise to ``'sm'``, due to the mobile-first approach. Then we define two getters ``small`` and ``notSmall``. The first returns that the breakpoint value is ``'sm'``, the second that it is not.
With that in place, we now need to actually determine what the current ``breakpoint`` is. To do this we will implement the ``init`` function and there use the ``window`` object's ``innerWidth`` property to determine how many pixels wide the browser window is wide. The Bootstrap CSS framework by default uses 768px as the boundary between small and medium and 992px as the boundary between medium and large. Thus we do that as well, by adding the following code:
.. code-block:: js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
export default class MediaQueryService extends Service {
@tracked breakpoint = 'sm';
get small() {
return this.breakpoint === 'sm';
}
get notSmall() {
return this.breakpoint !== 'sm';
}
init() {
super.init(...arguments);
if(window.innerWidth >= 992) {
this.breakpoint = 'lg'
} else if (window.innerWidth >= 768 ) {
this.breakpoint = 'md'
} else {
this.breakpoint = 'sm'
}
}
}
.. warning::
Should you use Visual Studio Code your linter might highlight the entire init-function as an error since the service is not marked as ``classic``. Since this tutorial has been rewritten from an older version of Ember, I left this code mostly as it was and just modified it to the actual Ember version. Despite the thrown error, the code should work.
This will initialise the breakpoint correctly, but will not react to changes in the window's width. To handle these changes, we add an event listener to listen for the resize event. When the event fires, we update the ``breakpoint``:
.. code-block:: js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
export default class MediaQueryService extends Service {
@tracked breakpoint = 'sm';
get small() {
return this.breakpoint === 'sm';
}
get notSmall() {
return this.breakpoint !== 'sm';
}
init() {
super.init(...arguments);
if(window.innerWidth >= 992) {
this.breakpoint = 'lg'
} else if (window.innerWidth >= 768 ) {
this.breakpoint = 'md'
} else {
this.breakpoint = 'sm'
}
window.addEventListener('resize', () => {
if(window.innerWidth >= 992) {
this.breakpoint = 'lg'
} else if (window.innerWidth >= 768 ) {
this.breakpoint = 'md'
} else {
this.breakpoint = 'sm'
}
})
}
}
With the service in place, we can now use our new service in the component class for the modules navbar, that we need to create first by running:
.. code-block:: console
$ yarn ember generate component-class modules/modules-navbar
and then update ``app/components/modules/modules-navbar.js``:
.. code-block:: js
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
export default class ModulesModulesNavbarComponent extends Component {
@service mediaQuery;
get actionMenuDirection() {
if(this.mediaQuery.breakpoint === 'sm') {
return 'horizontal'
} else {
return 'vertical'
}
}
}
The new ``actionMenuDirection`` property now reacts to the value of the ``small`` property on our service and either returns ``'horizontal'`` if we are in the small breakpoint, otherwise it is ``'vertical'``. These classes do not provide anything right now, so we need to update our ``app/styles/app.scss`` file:
.. code-block:: scss
/*
* Responsive menu
*/
.horizontal {
flex-direction: row;
}
.vertical {
flex-direction: column;
}
With that in place, we can now use the computed property in our ``app/components/modules/modules-navbar.hbs`` to change the direction of the menu:
.. code-block:: html+handlebars
...
If you now change the size of the window, you will see that on the small size the menu switches to a horizontal direction.
Responsive Main Navigation
++++++++++++++++++++++++++
The final modification we will do is to add a responsive main application menu. On the small screen the main application menu takes up a lot of space, but is only needed occasionally. We will thus use our service so that when we are in the small size, there is a small title bar with a menu icon to show the full menu. Update the ``app/components/nav-bar.hbs`` to this:
.. code-block:: html+handlebars
{{#if this.mediaQuery.small}}
{{/if}}
{{#if (or this.mediaQuery.notSmall this.menuVisible)}}
{{/if}}
The main changes are the new title bar, the ``menuVisible`` switch around the ``
``, and also the ``this.menuDirection`` for the two menus. The difference to the modules list is that we want the menu to be vertical in the small size and horizontal in all other sizes. Update the ``app/components/nav-bar.js``:
.. code-block:: js
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
export default class NavBarComponent extends Component {
@service intl;
@service mediaQuery;
@tracked menuVisible = false;
get menuDirection() {
if(this.mediaQuery.breakpoint === 'sm') {
return 'vertical'
} else {
return 'horizontal'
}
}
@action
setLocale(primaryLocale) {
let locale = ['en-us'];
if(primaryLocale) {
locale.splice(0, 0, primaryLocale)
}
this.intl.setLocale(locale)
};
@action
toggleMenu() {
this.menuVisible = !this.menuVisible;
}
}
The ``menuDirection`` works the same as in the ``modules/modules-navbar.js`` component class, just that the horizontal/vertical are result are inverted. The other change is the new ``menuVisible`` property (again defaulting to ``false`` - mobile first) and the ``toggleMenu()`` action to show and hide the menu.
Before it will work, you need to update the translations, adding the new ``application.menu`` key:
.. code-block:: yaml
generic:
current_semester: Current Semester
last_semester: Last Semester
application:
athena_study_portal: Athena - Study Portal
my_modules: My Modules
my_exams: My Exams
register: Register
logout: Logout
menu: Menu
modules:
enroll: Enroll in Module
create: Create a new Module
.. code-block:: yaml
generic:
current_semester: Aktuelles Semester
last_semester: Letztes Semester
application:
athena_study_portal: Athena - Studienportal
my_modules: Meine Module
my_exams: Meine Prüfungen
register: Registrieren
logout: Ausloggen
menu: Menü
modules:
enroll: In Modul Einschreiben
create: Neues Modul Erstellen
If you now try this out and change the window size, you will see that in the small size the title bar appears and clicking on the menu icon shows and hides the menu, while for all other sizes, the menu is always visible, but the title bar is hidden.