Comment on page
17. Relationships
Let's look at further requirements for our Punch List app:
App Design: Construction Punch List
When more and more punches are captured in the punch list app, it will become problematic to browse all the punches in a single list. We want to allow users to organize punches into categories, and then to filter the lists of punches by category.
Therefore, we want to make it possible for categories to be created in our database, and then each punch list item must be assigned to a particular category. In order to implement this functionality, we'll make use of Relationships in our Data Model. Relationships allow us to define how our different object types relate to each other. If we create a new Category object type, we'll say that each category has many punch list items that belong to that category. This is called a one-to-many relationship.
Let's head over to the Data Model workspace and update the
schema.xml
to include a new Category model
. This model will only have one field for now, a field that will store the 'name' of the category - we will also use this field as the default display value. Like this.<model name="category" label="Category">
<field name="name" label="Name" type="text" />
<display>{name}</display>
</model>
Next, we'll create the one-to-many relationship between the two object types. This is done by adding a
belongs-to
tag to the Item object type, and a has-many
to the Category object type as shown below. Relationship tags should be listed after all the fields in a model but before the <display>
tag. Note that the name=""
property on the has-many
allows you to specify a name for the whole collection of items that belong to a category. Using the pluralized form of the object type (in this case, "items") is a good convention to use for the collection name. On the belongs-to
side you can also specify a name=""
for the relationship, but if you don't the system will default the name to the name of the associated model
- in our case category
.<model name="item" label="Item">
<field name="comments" label="Comments" type="text" />
<field name="photo" label="Photo" type="photo" />
<field name="status" label="Status" type="single-choice">
<option key="Open">Open</option>
<option key="Closed">Closed</option>
</field>
<field name="gps_location" label="Gps Location" type="location" />
<field name="created_at" label="Created At" type="datetime" />
<belongs-to model="category" />
<display>{status} - {comments}</display>
</model>
<model name="category" label="Category">
<field name="name" label="Name" type="text" />
<has-many model="item" name="items" />
<display>{name}</display>
</model>
Your ERD should also visualize these changes, like this.

While we are at it, let's also add a relationship between user and item to signify which user created which punch items. To do this we add another
belongs-to
tag on our item
model, this time pointing to the user
model, and we add the has-many
tag to the user
model. For this belongs-to
relationship let's not use the system default name, and instead specify it as name="created_by"
. At this point your schema.xml
file should look like this.schema.xml
<?xml version="1.0" encoding="UTF-8"?>
<data-model>
<!-- Do not remove this - it is used to store information about your app's users: -->
<model name="user" label="User">
<field name="name" label="Name" type="text:name" />
<has-many model="item" name="items" />
<display>{name}</display>
</model>
<!-- Used for Push Notifications - to send a push notification, create a 'push_notification' object -->
<!-- and specify the recipient in the 'belongs to user' relationship -->
<!-- For more details, refer to: http://resources.journeyapps.com/v4/push-notifications -->
<model name="push_notification" label="Push Notification">
<field name="message" label="Message" type="text" />
<field name="received_at" label="Received At" type="datetime" />
<field name="created_at" label="Created At" type="datetime" />
<belongs-to model="user" />
<display>{message} ({user})</display>
<notify-user message="{message}" received-field="received_at" recipient-field="user" />
</model>
<!-- ADD MODELS HERE: -->
<model name="item" label="Item">
<field name="comments" label="Comments" type="text" />
<field name="photo" label="Photo" type="photo" />
<field name="status" label="Status" type="single-choice">
<option key="Open">Open</option>
<option key="Closed">Closed</option>
</field>
<field name="gps_location" label="Gps Location" type="location" />
<field name="created_at" label="Created At" type="datetime" />
<belongs-to model="category" />
<belongs-to model="user" name="created_by" />
<display>{status} - {comments}</display>
</model>
<model name="category" label="Category">
<field name="name" label="Name" type="text" />
<has-many model="item" name="items" />
<display>{name}</display>
</model>
</data-model>
If you go to the Data Browser for the "Testing" environment, you'll notice that there is a new Category object type shown on the left, and that clicking on an Item shows a Belongs to Category field and a Belongs to User - created_by field at the bottom (if your Data Browser was already open in a different tab then you will need to first refresh that browser tab before you will see the changes):

Click on the Category tab on the left and let's create a couple of categories:

Now let's add the category selection to the Add New Item view. Under the "Variables go here" section of the
add_new.view.xml
file, add a new variable called all_categories
which will be of type query:category
. Like this.<!-- Variables go here: -->
<var name="item" type="item" />
<var name="all_categories" type="query:category" />
Then change the
init()
function to populate the categories using a Query and also populate the relationship to the user
model. To set the belongs-to
relationship in JS we use a helper method that is created for us on the model in question, this method will use the value of name
that is specified in the in the Data Model in the <belongs-to>
tag. So in our case, we specified the relationship as <belongs-to model="user" name="created_by" />
and so we will use the created_by
method to set the relationship to the user model, like this.// This function is called when the app navigates to this view (using a link)
function init() {
// initialize any data here that should be available when the view is shown
view.all_categories = DB.category.all().orderBy("name");
view.item = DB.item.create();
view.item.status = "Open";
view.item.created_at = new Date();
view.item.created_by(user);
}
Finally, add a
<object-dropdown>
View Component, above the <text-input>
component, allowing the user to select the category. Note that:- We specify
query="all_categories"
so that the list will show all the categories that we've pulled using our Query. - We specify
bind="item.category"
so that the category that the user selects will be stored in the category relationship on our item variable.
<?xml version="1.0" encoding="UTF-8"?>
<view title="Add New Punch List Item">
<!-- Parameters go here: -->
<!-- Variables go here: -->
<var name="item" type="item" />
<var name="all_categories" type="query:category" />
<!-- Components go here: -->
<columns>
<column>
<capture-photo bind="item.photo" required="true" />
</column>
<column>
<object-dropdown query="all_categories" bind="item.category" label="Category" empty-message="Your items will appear here" required="false" />
<text-input bind="item.comments" required="true" />
<button label="Save Item" on-press="saveItem" validate="true" style="solid" />
</column>
</columns>
<capture-coordinates bind="item.gps_location" />
</view>
At "Add New Item", you should now see a drop-down list to select a category:


After you've added a new item, if you open up the Data Browser for the "Testing" environment again, you'll see "Belongs to Category" for the item shows the category that you've selected. Go ahead and update the other punch items and make each of them
belong-to
a category
as well. Like this:
Let's update the
list
component on our Main view to display the category
inside of the <content>
tag, and while we are at it, let's also add the created_at
timestamp in the <footer>
tag. So, in your main.view.xml
, update the list component like this.<list empty-message="Your items will appear here">
<list-item query="open_punches">
<header>{comments}</header>
<content>{category}</content>
<footer>{created_at}</footer>
<accent label="{status}" color="info" />
<asset src="{photo}" />
<action on-press="$:selectItem($selection)" />
</list-item>
</list>
If we want to show the category on the View punch Item view, we could adjust our table component there as follows:
<info-table>
<row label="Item Comments" bind="item.comments" />
<row label="Category" bind="item.category" />
<row label="Item Status" bind="item.status" />
<row label="Current Time" value="{$:showTime()}" />
</info-table>


Lastly, we want the user to be able to browse punch items by category. We will do this in a new view called
browse_items
, but first let's add a button on our Main view to allow the user to navigate to the Browse Items view. So, on your main.view.xml
, add a new button to the <button-group>
below the 'Add New Item' button, and hook it up like this:<button-group>
<button label="Add New Item" icon="fa-plus" on-press="goToNew" validate="false" style="solid" />
<button label="Browse by Category" on-press="$:navigate.link('browse_items')" icon="fa-filter" validate="false" style="solid" />
</button-group>
You will notice we have included the necessary JS
navigation
logic directly in the on-press
attribute by using the $:
notation to execute raw JS from our View XML.Next, let's create a new View with the name
browse_items
(remember, you can create views using the command palette, ctrl+shift+p
/ cmd+shift+p
to open the command palette and then start typing create and choose the Create view
action), and then once created update the <view title="Browse Punch Items by Category">
as before.
Now we can add some components to our new view. Firstly, we are going to need to present the user with a list of all categories (we will need a
query
for this), let the user select a category (we will need a category
variable for this), and then show the user all the punch items that belong to the selected category (we will need another query
for this). So, in your newly created browse_items.view.xml
specify all these variables, like this.<?xml version="1.0" encoding="UTF-8"?>
<view title="Browse Punch Items by Category">
<!-- Parameters go here: -->
<!-- Variables go here: -->
<var name="all_categories" type="query:category" />
<var name="selected_category" type="category" />
<var name="filtered_items" type="query:item" />
<!-- Components go here: -->
</view>
Now let's add the necessary view components. We want to display all the the categories to the user and then once they select one, show all the related punch items for the selected category. A nice way to do this is to make use of the JourneyApps
show-if
and hide-if
logic available to any view component (syntax reference for show-if
and hide-if
available here). We will display an <object-list>
for all the categories but once the user selects one we will hide the <object-list>
and display a <list>
of related items
instead (re-using the list we defined on our main view). So, in your browse_items.view.xml
, add the following code:<!-- Components go here: -->
<object-list hide-if="selected_category" query="all_categories" bind="selected_category" label="Please select a category to view related punch items" empty-message="Your items will appear here" required="false" />
<heading show-if="selected_category">{selected_category.name}: Open Punch Items</heading>
<list show-if="selected_category" empty-message="Your items will appear here">
<list-item query="filtered_items">
<header>{comments}</header>
<content>{category}</content>
<footer>{created_at}</footer>
<accent label="{status}" color="info" />
<asset src="{photo}" />
<action on-press="$:selectItem($selection)" />
</list-item>
</list>
Next we need to add a way for the user to clear their selection (we will use a button for that - using
show-if
to only display the button if the user has already made a selection). After that we also need to populate the various queries from our JS. Populating the all_categories
variable can be done from the init()
function, but we can only populate filtered_items
once the user makes a selection, and we need to re-populate it every time the user makes a new selection. To do this we will make use of the on-change=""
attribute of our <object-list>
component. (Note, the on-change=""
attribute is available on all input
components, but note that for capture-photo
and scan-barcode
it is referenced as on-capture
and on-scan
instead). So, update your browse_items.view.xml
to the following:<?xml version="1.0" encoding="UTF-8"?>
<view title="Browse Punch Items by Category">
<!-- Parameters go here: -->
<!-- Variables go here: -->
<var name="all_categories" type="query:category" />
<var name="selected_category" type="category" />
<var name="filtered_items" type="query:item" />
<!-- Components go here: -->
<object-list on-change="filterCategories" hide-if="selected_category" query="all_categories" bind="selected_category" label="Please select a category to view related punch items" empty-message="Your items will appear here" required="false" />
<heading show-if="selected_category">{selected_category.name}: Open Punch Items</heading>
<list show-if="selected_category" empty-message="Your items will appear here">
<list-item query="filtered_items">
<header>{comments}</header>
<content>{category}</content>
<footer>{created_at}</footer>
<accent label="{status}" color="info" />
<asset src="{photo}" />
<action on-press="$:selectItem($selection)" />
</list-item>
</list>
<button-group>
<button label="Back" on-press="dismiss" icon="fa-arrow-left" validate="false" style="outline" />
<button show-if="selected_category" label="Clear Selection" on-press="clearSelection" icon="fa-times" validate="false" style="solid" />
</button-group>
</view>
Let's update our
browse_items.js
file next to populate our queries and handle the on-change
event, as well as the clearSelection
and selectItem
function calls (let's reuse the selectItem functions from the main view). Like thisbrowse_items.js
// This function is called when the app navigates to this view (using a link)
function init() {
// initialize any data here that should be available when the view is shown
view.all_categories = DB.category.all().orderBy("name");
}
// This function is called when the user returns to this view from another view
function resume(from) {
// from.back (true/false) if true, the user pressed the "Back" button to return to this view
// from.dismissed (true/false) if true, the app dismissed to return to this view
// from.path contains the path of the view that the user returned from
// if any data needs to be refreshed when the user returns to this view, you can do that here:
}
function filterCategories(selectedCategory) {
if (selectedCategory) { // we add the null check to catch the situation where the user 'clears' their selection - which will trigger the on-change
view.filtered_items = selectedCategory.items;
}
}
function clearSelection() {
view.selected_category = null;
}
function selectItem(selectedItem) {
var userOptions = ["View Item Details", "Delete Selected Item"];
var userSelection = optionList(userOptions);
if (userSelection === 0) { // user selected 'View Item Details'
viewItem(selectedItem);
} else if (userSelection === 1) { // user selected 'Delete Selected Item'
deleteItem(selectedItem);
} else { // user cancelled or clicked away
return; // exit the function pro-actively
}
}
function viewItem(item) {
navigate.link('view_item', item);
}
function deleteItem(item) {
if (confirmDialog("Delete Item?", "Are you sure?", "YES", "NO")) {
item.destroy();
}
}
Let's test these changes. Your app should now look like this:
Desktop
Mobile






Well done, you have effectively completed the Punch List App for the scope we originally envisioned. A user can view open punches, capture new punches, view details for existing punches and mark them as closed. Next up we are going to add a new user role to our app, an App Admin, that should be able to easily manage all the data in the application.
Last modified 1yr ago