Cx Walkthrough Part 3: The App

Marko Stijak
Codaxy
Published in
7 min readNov 22, 2016

--

In this post, I’ll go through the application and explain how things work, mainly layout and routes. If you haven’t done so already, please open the application in the browser and check it out.

There are three pages (routes): home (search) page, employee details page, and about page.

The code for each page can be found inside the client/app/routes folder.

The home page displays a list of employee cards and enables filtering. Clicking on a card will open the details page which loads more information about the employee and enables editing. The about page contains the basic information about the application and links to the source code and this blog post.

Layout

All pages use the common layout with a blue header on top. This layout works well on smaller screens; however, there’s a lot of empty space on bigger screens. That’s why an additional layout is introduced — OverlayLayout, which is used to display the employee profile page on top of the search results when the screen size allows it.

This layout is also more user-friendly because the context is preserved when browsing through multiple profiles.

routes/index.js

This is the main junction point where routes and layout come together.

export default <cx>
<Sandbox key:bind="url" storage:bind="pages">
<div class="b-layout" layout={FirstVisibleChildLayout}>

{/* use overlays on screens larger than 1000px */}
<PureContainer visible={()=>window.innerWidth >= 1000}>
<DefaultPage outerLayout={PageLayout}/>

<Route route="~/about" url:bind="url">
<AboutPage outerLayout={OverlayLayout}/>
</Route>

<Route route="~/emp/:id" url:bind="url">
<EmployeePage outerLayout={OverlayLayout}/>
</Route>
</PureContainer>


{/* default layout */}
<PureContainer>
<Route route="~/about" url:bind="url">
<AboutPage outerLayout={PageLayout}/>
</Route>

<Route route="~/emp/:id" url:bind="url">
<EmployeePage outerLayout={PageLayout}/>
</Route>

<Route route="~/" url:bind="url">
<DefaultPage outerLayout={PageLayout}/>
</Route>
</PureContainer>
</div>
</Sandbox>
</cx>

The first thing to see here is the Sandbox widget which isolates data belonging to different pages. If you’re not familiar with how Sandbox works, please refer to the Cx documentation. Basically, you get to use the $page property which holds the data belonging to that page only. This is a plain JavaScript object, and you can put whatever you like inside it, e.g. $page.profile or $page.results. When you zoom into an employee, $page data will be populated with the data for that employee. If you move to another employee, $page data will be reset and populated from scratch.

Next, you’ll see a div which uses the FirstVisibleChildLayout. Its children are two sets of routes, one for larger screens and one for smaller screens.

In the default layout (smaller screens), there are three routes which correspond to the pages, and each page uses the standard PageLayout.

For larger screens, we first render the default (search) page using the PageLayout and then, depending on the URL, additional pages are rendered on top using the OverlayLayout.

Now that you know how pages and layouts are glued together, let’s visit each page to see how it works.

Home Page

Let’s start with the controller.

import {Controller} from 'cx/ui/Controller';
import {queryEmployees} from 'app/api/index'

export default class extends Controller {
init() {
this.store.init('list.loading', true);
this.addTrigger('load',
['search.query', 'list.version'], ::this.load, true);
}

load() {
var q = this.store.get('search.query');
var options = {
q: q || ''
};
queryEmployees(options)
.then(data=> {
this.store.set('list.data', data);
this.store.set('list.loading', false);
})
}
}

In the init method, first the list.loading flag is set to true. This will allow you to display the loading message the first time you get to this page. Notice that the store.init method is used over store.set. The difference is that init will not overwrite any existing value — this way the loading message will be avoided on code patches (HMR).

Next is the trigger responsible for loading the data. The first parameter is the name of the trigger. The second parameter for the trigger is an array of bindings. If any of listed bindings change, the trigger will execute the load function, specified as the third argument. The fourth argument specifies if the trigger should run immediately or wait for a change. In this case, true indicates that the data should be loaded immediately.

The load method uses the API functions defined in the previous blog post. Employees are queried by passing the search.query value which is bound to the search field. After you receive the search results data object, it’s stored under the list.data, and you can also set the list.loading flag to false to indicate that results should be shown instead of the loading message.

The index.js file holds the view logic (template).

<main controller={Controller} class="b-list">
<header putInto="header">
<SearchField
value:bind="search.query"
placeholder="Search..."
label={{ items: <i class="fa fa-search" /> }}
/>
<Link href="~/emp/new"><i class="fa fa-plus" /></Link>
<Link href="~/about"><i class="fa fa-question" /></Link>
</header>

In the first part, you assign the controller and specify what goes into the header. Notice the putInto=”header” instruction which indicates that this is the content which should be put elsewhere — in this case, in the header (See PageLayout.js).

The SearchField is bound to search.query used in the load trigger, so any change will trigger a server call.

Next, render the cards section:

<div
class="b-cards"
layout={FirstVisibleChildLayout}
>
<div
class="e-cards-empty"
visible:expr="{list.loading}">
Loading...
</div>

<div
class="e-cards-empty"
visible:expr="!{list.data} || {list.data}.length == 0"
>
No records found matching the given search criteria.
</div>

<Repeater
records:bind='list.data'
recordName="$person"
idField="id"
>
<div class="b-card">
<div class="e-card-img">
<figure
text:expr="{$person.firstName}[0]+{$person.lastName}[0]"
/>
<img
visible:expr="!!{$person.pictureUrl}"
src:bind="$person.pictureUrl"
/>
</div>

<div class="e-card-details">
<Link href:tpl="~/emp/{$person.id}">
<h3 text:tpl="{$person.firstName} {$person.lastName}" />
<p text:tpl="{$person.title}" />
</Link>

<div>
<i class="fa fa-globe" />
<Text bind="$person.officeName" />
</div>

<div>
<i class="fa fa-phone" />
<Text
visible:expr="!!{$person.mobilePhone}"
bind="$person.mobilePhone"
/>
<span
class="muted"
visible:expr="!{$person.mobilePhone}"
>
Not provided
</span>
</div>

<div>
<i class="fa fa-envelope-o" />
<Text
visible:expr="!!{$person.primaryEmail}"
bind="$person.primaryEmail"
/>
<span
class="muted"
visible:expr="!{$person.primaryEmail}"
>
Not provided
</span>
</div>
</div>
</div>
</Repeater>
</div>

Using FirstVisibleChildLayout and visible attributes you can render the loading message, no results found or the actual results.

The actual results are represented using contact cards. If a photo is available, it’s displayed on the left — otherwise, person initials are displayed. Person name and title are visible in the card header, and link to the person’s profile page.

Phone, email, and office are also shown as these fields are probably the only information that the user is looking for.

Employee Profile Page

Once again, let’s review the main points in the controller.

export default class extends Controller {
init() {
this.load();
}
load() {
var id = this.store.get('$route.id');

if (id == 'new') {
this.store.set('$page.info', {
status: 'ok',
mode: 'edit',
data: {}
});
}
else {

if (id != this.store.get('$page.info.data.id')) {
this.store.set('$page.info.status', 'loading');
this.store.set('$page.info.mode', 'view');
}

getEmployee(id)
.then(data=> {
this.store.set('$page.info.data', data);
this.store.set('$page.info.status', 'ok');
})
}
}

As soon as the page becomes active, employee data is loaded. As you have previously seen, the employee route has the id parameter which is available as $route.id.

<Route route="~/emp/:id" url:bind="url">
<EmployeePage outerLayout={OverlayLayout}/>
</Route>

The same page will also be used for adding new entries, and, in that scenario, you simply initialize info with an empty object. If you get a valid employee id, you set the status to loading and fetch the data from the server.

Similar logic is also present in the save method.

save(e) {
e.preventDefault();
var data = this.store.get('$page.info.data');
var id = this.store.get('$route.id');
var promise;
if (id == 'new') {
promise = putEmployee(data)
.then(x=> {
this.store.set('$page.info.data', x);
History.replaceState({}, null, `~/emp/${x.id}`);
});
}
else {
promise = patchEmployee(data.id, data);
}

this.store.set('$page.info.mode', 'view');

promise
.then(()=> {
this.store.update('list.version', version => (version || 0) + 1);
})
.catch(e=> {
console.log(e);
this.store.set('$page.info.mode', 'edit');
});
}

The interesting point here is that after you successfully store a new employee in the database and get the id, you need to replace the URL. Another important thing is that on each save, list.version is incremented so that the parent view (search) is aware that it needs to reload.

The view side is also very interesting.

<main class="b-emp" controller={Controller}>
<Rescope bind="$page.info" visible:expr="{status}=='ok'">
<header putInto="header">
<Link href="~/"><i class="fa fa-arrow-left"/></Link>
<h2 text:expr="{mode}=='edit' ? 'Edit Profile' : 'View Profile'"/>
<a href="#" visible:expr="{mode}!='edit'"><i class="fa fa-pencil" onClick="edit"/></a>
<a href="#" visible:expr="{mode}=='edit' && {valid}"><i class="fa fa-check" onClick="save"/></a>
<a href="#" visible:expr="{mode}=='edit'"><i class="fa fa-times" onClick="cancel"/></a>
</header>

This block is not very readable, but it’s important to note the Rescope element. Rescope allows you to zoom into data. Relevant employee data is stored in $page.info.data, $page.info.status and $page.info.mode, but after you rescope the view to $page.info, that becomes just data, status and mode. Even the visible expression on the Rescope element itself is affected by this change.

The rest of the page is just one large form. Cx form widgets support two modes — edit mode and view mode. Edit mode is the default mode where form widgets behave naturally. However, if you switch to view mode, each widget shows only its assigned value. That is very useful for forms and grids which are used to present and edit the data.

Conclusion

In this post, we have reviewed how Cx can offer different routing and layout logic based on the screen size. We also saw how controllers can be used to load the data initially and trigger additional loading on state changes. In the employee profile page, we learned how to save new data and redirect on success. We also learned how the Rescope widget can be used to simplify view bindings and how form widgets can be used to either display or edit the data.

In the next post, we’ll go into more details about how the appearance of Cx widgets can be modified to achieve the desired design guidelines.

--

--