Introduction
In the last chapter, we blocked out the first view in our app, the Edit Hike view. This was a simple view that displays a single hike, as well as lets us edit that hike. This is a great start, but for our app, we need to be able to select one of many hikes and edit them individually.
In this chapter, that's exactly what we're going to do. To make sure things will be easy to manage, we'll still be keeping everything in our MainView.ux
file. We'll add "selector" at the top of the view that allows us to select which hike we want to edit, and then our edit hike view from the last chapter will be populated with this data. Changes will not be persistent yet - we'll simply be loading data from our model into our view model, but not going the other way. We'll solve that in a later chapter about mocking our backend.
The final code for this chapter is available here.
Creating our List of Hikes
In order for us to display a list of hikes to choose from, we first need a list of hikes. This is where we'll start building something that looks like a model for our app. At first, we'll just use a simple array of hikes. Near the top of the JavaScript portion of our MainView.ux
file, we'll place the following array:
var hikes = [
{
id: 0,
name: "Tricky Trails",
location: "Lakebed, Utah",
distance: 10.4,
rating: 4,
comments: "This hike was nice and hike-like. Glad I didn't bring a bike."
},
{
id: 1,
name: "Mondo Mountains",
location: "Black Hills, South Dakota",
distance: 20.86,
rating: 3,
comments: "Not the best, but would probably do again. Note to self: don't forget the sandwiches next time."
},
{
id: 2,
name: "Pesky Peaks",
location: "Bergenhagen, Norway",
distance: 8.2,
rating: 5,
comments: "Short but SO sweet!!"
},
{
id: 3,
name: "Rad Rivers",
location: "Moriyama, Japan",
distance: 12.3,
rating: 4,
comments: "Took my time with this one. Great view!"
},
{
id: 4,
name: "Dangerous Dirt",
location: "Cactus, Arizona",
distance: 19.34,
rating: 2,
comments: "Too long, too hot. Also that snakebite wasn't very fun."
}
];
You'll notice each item in this array has the same fields our edit hike view had, with the exception of an added id
field, which is just a unique identifier for each of our items.
Displaying our List of Hikes
Now that we've got our list of hikes, we'll make a simple view to display them.
The first thing we'll do is export our hikes
variable to make it available from UX:
module.exports = {
hikes: hikes,
name: name,
location: location,
distance: distance,
rating: rating,
comments: comments
};
Since we want to be able to select one of the hikes we're displaying, we'll display each hike as a button that, when pressed, will select that specific hike and populate the edit hike view.
First, we'll just display our array of hikes as buttons. But how do we do that in UX? For these kinds of scenarios, UX provides a very helpful mechanism called Each:
<ScrollView>
<StackPanel>
<Each Items="{hikes}">
<Text Value="{name}" />
</Each>
Each is a very powerful UX feature. What Each does is take the collection specified by its Items
property and project each item into a copy of the visual subtree inside the Each tag. We can think of it kind of like copying and pasting the code inside Each for each item in Items
.
In our case, we'll use Each to create a Button for each of our hikes whose Text will be set to that hike's name
. That will look like this:
<Each Items="{hikes}">
<Button Text="{name}" />
</Each>
If we save here, we can see that now Each has created a Button for each item in hikes
, just like we expected. Cool! This works for pretty much all code we can put inside Each, but in our case one Button for each hike is all we need.
Notice how we also databound each Button's Text property to name
, but we never exposed any variable called name
. One of the awesome things about Each is that for each of our Items
, Each will "narrow down" the data context that we're binding to to be the current item. So when we bind to name
, we're not binding to a variable called name
exported from JS; we're binding to the name
property of the current item, which in this case is the current hike. It's that easy!
Selecting Hikes
Now that we've got a button for each of the hikes in our model, we want to be able to select one by clicking one of these buttons.
Currently, our view model consists of many separate Observable's for the various fields we can edit. Let's also add an Observable whose value
represents the hike we're currently editing. Initially, this will be empty, as we haven't yet selected a hike to edit:
var hike = Observable();
Now we need to get the data from hike
to our other Observable's, and this is where things get interesting. On one hand, we could imperatively populate all of our field Observable's whenever hike
is populated, but this would mean we'd have to subscribe to the Observable and is frankly a bit too imperative for my taste. We'd much prefer doing something declarative instead!
Rather, what we'll do is transform, or map, the value of the hike
Observable into the value of its various properties. For example, this is what that might look like for name
:
var name = hike.map(function(x) { return x.name; });
This might look a bit daunting at first if you're not yet used to functional reactive programming, but not to worry - it can be quite intuitive once you get a feel for it.
If we break down this code into parts, you'll notice a couple things. First of all, we're calling the map
function on hike
. map
itself will return a new Observable that acts just like any other Observable, with the added benefit that each value that goes inside our hike
Observable will be propagated to our new Observable. In other words, all of the values in name
will be based on, or mapped from, values in hike
.
You'll also notice we're passing a function into map
. This function is (unsurprisingly) called a mapping function. A mapping function is just like any other function - it takes an argument and returns a new value based on that argument. Sometimes, these functions have side-effects, which means they can affect the world around them. But typically, these functions are pure functions, which means they only return values based on their arguments, and nothing more.
But what does this mapping function do, exactly? Great question! Simply put, the mapping function will be called whenever there is a new value in hike
. This new value is what gets passed to the function as its argument. The function will then return a new value based on this argument, which represents the value in the Observable we're creating.
So, altogether, this means that we're taking hike
and creating a new Observable that represents the name
property of each value that gets put into hike
. Pretty cool, huh?
Note: We can find out more information on Observables in our Observable Crash Course video, as well as our Observable docs.
Now, let's refactor the other Observable's to also be based on hike
like we did with name
:
var name = hike.map(function(x) { return x.name; });
var location = hike.map(function(x) { return x.location; });
var distance = hike.map(function(x) { return x.distance; });
var rating = hike.map(function(x) { return x.rating; });
var comments = hike.map(function(x) { return x.comments; });
Finally, we need to hook up each of our buttons to a function that will populate our hike
Observable (which in turn will populate all of the Observable's for individual fields). We'll start by creating an empty function in JavaScript, which we'll fill in in a moment:
function chooseHike() {
}
Next, let's add it to our exports so that UX can see it:
module.exports = {
hikes: hikes,
name: name,
location: location,
distance: distance,
rating: rating,
comments: comments,
chooseHike: chooseHike
};
And we'll go ahead and hook up all of our buttons to it like this:
<Button Text="{name}" Clicked="{chooseHike}" />
Now it's time to fill in our function. The basic idea is that we'll fill in the value of our hike
Observable:
function chooseHike() {
hike.value = ???
}
But what will we set it to, exactly? As it turns out, when we databind a function to Clicked
on a Button, that function can receive an argument. This argument contains a data
field, which will represent the current data context for the Button. And because of the way we've used Each, that means that data
will actually be the hike
we're after. Cool! So, let's update our function to accept that argument and put its data
property into our hike
Observable:
function chooseHike(arg) {
hike.value = arg.data;
}
And now when we save this, we can see that our hike selectors work as we expect! When we click on one of them, the edit hike view gets populated properly and we can edit the individual fields. Cool!
Our progress so far
So now we've started to get a basic model made up of many hikes, a view to select one of these hikes, and a view to edit the selected hike. Altogether, it looks like this:
The code hasn't grown too much, either. It looks like this:
<App>
<ClientPanel>
<JavaScript>
var Observable = require("FuseJS/Observable");
var hikes = [
{
id: 0,
name: "Tricky Trails",
location: "Lakebed, Utah",
distance: 10.4,
rating: 4,
comments: "This hike was nice and hike-like. Glad I didn't bring a bike."
},
{
id: 1,
name: "Mondo Mountains",
location: "Black Hills, South Dakota",
distance: 20.86,
rating: 3,
comments: "Not the best, but would probably do again. Note to self: don't forget the sandwiches next time."
},
{
id: 2,
name: "Pesky Peaks",
location: "Bergenhagen, Norway",
distance: 8.2,
rating: 5,
comments: "Short but SO sweet!!"
},
{
id: 3,
name: "Rad Rivers",
location: "Moriyama, Japan",
distance: 12.3,
rating: 4,
comments: "Took my time with this one. Great view!"
},
{
id: 4,
name: "Dangerous Dirt",
location: "Cactus, Arizona",
distance: 19.34,
rating: 2,
comments: "Too long, too hot. Also that snakebite wasn't very fun."
}
];
var hike = Observable();
var name = hike.map(function(x) { return x.name; });
var location = hike.map(function(x) { return x.location; });
var distance = hike.map(function(x) { return x.distance; });
var rating = hike.map(function(x) { return x.rating; });
var comments = hike.map(function(x) { return x.comments; });
function chooseHike(arg) {
hike.value = arg.data;
}
module.exports = {
hikes: hikes,
name: name,
location: location,
distance: distance,
rating: rating,
comments: comments,
chooseHike: chooseHike
};
</JavaScript>
<ScrollView>
<StackPanel>
<Each Items="{hikes}">
<Button Text="{name}" Clicked="{chooseHike}" />
</Each>
<Text Value="{name}" />
<Text>Name:</Text>
<TextBox Value="{name}" />
<Text>Location:</Text>
<TextBox Value="{location}" />
<Text>Distance (km):</Text>
<TextBox Value="{distance}" InputHint="Decimal" />
<Text>Rating:</Text>
<TextBox Value="{rating}" InputHint="Integer" />
<Text>Comments:</Text>
<TextView Value="{comments}" TextWrapping="Wrap" />
</StackPanel>
</ScrollView>
</ClientPanel>
</App>
Now we should be getting a feel for how data might flow through our Fuse app and how different views will start to interact.
What's Next
While being able to select from our list of hikes is really helpful, we'll ideally want to separate the parts of this view into separate views entirely. In the next chapter, that's exactly what we'll do - we'll separate our views, view models, and model into organized, isolated components. Let's dig in!
The final code for this chapter is available here.