Building Apps with Aurelia: #5 Using compose Element to Implement MVVM
We talked about MVVM pattern, its benefits and the ways we can implement it in Aurelia in the previous post. In this post we will look in to how to implement MVVM using the compose element in Aurelia.
We will start from where we left off from the last demo. To this I have made few changes. First I have added bootstrap theme from bootswatch to add some style in to the app and make it easy to place elements on pages. The bootstrap.min.css file is lined on the head. Then I renamed the home module (home.js and home.html) to shell. Now they are shell.js and shell.html and the exported class in shell.js is changed from Home to Shell. This is done to make the naming more sensible since I am going to add 2 new views called Home and About to the app. Then I changed the main.js file and renamed the home to shell in the a.setRoot() method to point to the renamed module. Finally, I added bootstrap navigation bar to the shell.html file. You can see how the changed file look like bellow.
Index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello Aurelia</title>
<link rel="stylesheet" href="css/bootstrap.min.css">
</head>
<body aurelia-app="main">
<div id="app-container"></div>
<script src="jspm\_packages/system.js"></script>
<script src="config.js"></script>
<script>
System.import('aurelia-bootstrapper');
</script>
</body>
</html>
ViewModel (Shell.js)
export class Shell {
constructor() {
this.helloMessage = 'Hello Aurelia!';
}
}
View (Shell.html)
<template>
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">${helloMessage}</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active">
<a href="#">Home <span class="sr-only">(current)</span></a>
</li>
</ul>
</div>
</div>
</nav>
</template>
From this starting point we will now add the new views to the app. J Now we will add 2 views and associated viewmodels to the app. In your src folder add home.js and about.js for viewmodels and home.html and about.html for the views. Now the project structure should look something like this.
Now add the following lines of code to the corresponding files. First add the following code to home.js and home.html
ViewModel (Home.js)
export class Home {
constructor() {
this.message = '';
}
}
View (home.html)
<template>
<h1>Home</h1>
<hr>
<div class="form-group">
<input type="text" class="form-control" value.bind="message">
</div>
${message}
</template>
Then add the following code to the about.js and about.html
ViewModel (about.js)
export class About {
constructor() {
this.aboutMessage = 'This is the Hello Aurelia app used in Building Apps with Aurelia blog series by The KVK Blog';
}
}
View (about.html)
<template>
<h1>About</h1>
<hr>
${aboutMessage}
</template>
Ok. Now we have the views and viewmodels completed for the app. Nothing complicated. J Next step is to add the compose element and drop in those views in to the shell.html file. To do that add the following lines to the shell.html file just about the closing template tag.
<div class="container-fluid">
<div class="row">
<div class="col-md-8">
<compose view-model="home"></compose>
</div>
<div class="col-md-4">
<compose view-model="about"></compose>
</div>
</div>
</div>
Now we have defined our Views using the compose element and it will associate the appropriate ViewModels ‘Automagically’. Run the app and see. You will see an output like this.
Here we create a bootstrap container and add a row. Then I divide the screen vertically and in each side add the compose element to bring in the view for home and about. In the compose element we have view-model attribute.
<compose view-model="about"></compose>
Here we need to put in the name of the view we need to bring in as the value for the view-model attribute. Aurelia follow some conventions here. Aurelia assumes that the matching ViewModel for the view you brought in has the same name as the View and will be in the same directory as the View. This is really important to keep in mind.
You can optionally define view and view-model separately. Look at the code sample bellow.
<compose view="about.html" view-model="about"></compose>
Here you can see I have explicitly defined the view attribute and the view-model attribute. You can omit the .html extension from the view attribute if you want. BUT**, DO NOT** add the .js extension when you define the view-model attribute. That will throw an error since Aurelia will append the .js extension to the view-model attribute value and if you have an extension the file will have 2 extensions appended and that will throw an 404 error. In the above example, it behaves the same as the previous code sample.
But when should you use this? This is useful when you have your view and view-models defined in different directories. Remember, Aurelia is convention based. It assumes that the View is also at the directory where the ViewModel is located. So by using this, you can explicitly define the relative path for both View and ViewModel that are located in different locations. See the example bellow.
<compose view="views/about.html" view-model="about"></compose>
<compose view="about.html" view-model="viewmodels/about"></compose>
In the first line, the about.html View is in a directory called views, but the about.js ViewModel is in the root src folder. Since we explicitly define it, that works fine. In the second example, the about.html View is in the root src folder, but the about.js ViewModel is in a directory called viewmodels. And this works too.
We can also define a View without a ViewModel associated with it. Look at the code sample bellow.
<compose view="about.html"></compose>
I only define the View as the view.html, and do not define the view-model attribute. And in the project I have deleted the about.js file so the ViewModels is nowhere to be found. If you run this, you will see an output like this.
Can you spot the difference? :D Should be easy. You will notice that about message that you saw previously is now not there. Why did this happen?
This is because the about message defined as a property in the ViewModel in the earlier implementation. Since the ViewModel is no longer there the message will not be displayed. To refresh your memory this is the HTML in the about.html file.
<template>
<h1>About</h1>
<hr>
<p class="lead">${aboutMessage}</p>
</template>
But since we do not create a ViewModel, does that mean that about view has no ViewModel or a binding context attached to it? No. it does. When the ViewModel is not defined for a particular View, it is associated with the parent ViewModel. Since the about.html is rendered in the shell.html the parent ViewModels is Shell. So Shell becomes the binding context of the about.html View.
To prove this, let’s do a change to about.html and shell.js files. I will add a new binding to about.html. See the code bellow.
<template>
<h1>About</h1>
<hr>
<p class="lead">${aboutMessage}</p>
<p class="lead">${fromParent}</p>
</template>
I added a new binding that binds to a property called fromParent . Now I will add that property to the shell.js file. Look at the code bellow. This is the modified shell.js file.
export class Shell {
constructor() {
this.helloMessage = 'Hello Aurelia!';
this.fromParent = 'This message is coming from the parent';
}
}
You can see I have added the fromParent property to the Shell ViewModel. Now run this and see. You will see an output like this. ;)
Now it’s clear, the message from the parent ViewModel which is Shell is now displayed in about view. So if you don’t have a ViewModel for your View and you want to use the View just to bring in some UI elements, you still have the parent ViewModel associated with it.
Compose element is not limited to this implementation. Compose elements can be used to create nested Views and create Composite Views. Let’s look at this now.
Creating Nested Views with compose Element
To demonstrate this, we will add some team member details in the about view. I will add 3 team member profiles to the about view. To do this, first we need some data. I will create an array called profiles in the about ViewModel. After that the about.js would look like this.
export class About {
constructor() {
this.aboutMessage = 'This is the Hello Aurelia app used in Building Apps with Aurelia blog series by The KVK Blog';
this.profiles = [
{
id: 1,
name: 'John Doe',
role: 'Lead Developer',
image: 'https://api.adorable.io/avatars/120/john-doe'
},
{
id: 2,
name: 'Jason Smith',
role: 'Lead Designer',
image: 'https://api.adorable.io/avatars/120/jason-smith'
},
{
id: 3,
name: 'Jane Doe',
role: 'Designer',
image: 'https://api.adorable.io/avatars/120/jane-doe'
}
];
}
}
Next I am going to add a new View and a ViewModel to the app to display the profile of an individual team member. I will call the View, profile.html and the ViewModel profile.js. Create them inside the src folder. The profile.html file should look like this.
<template>
<h1>About</h1>
<hr>
<p class="lead">${aboutMessage}</p>
<p class="lead">${fromParent}</p>
</template>
I will display a profile image (randomly generated by adorable avatars service), a Name and Role in this view. Notice that I am binding to the item property coming from the corresponding profile.js ViewModel. This item property is set when we pass each item of the profiles array in to the compose elements binding context. We can get this passed binding context from the profile ViewModel and assign it to the item property. Then we can bind the item properties values to the View.
Next we need to add the code to about.html to use this view. Add the following lines of code to the about.html file.
<div repeat.for="profile of profiles">
<compose model.bind="profile" view-model="profile"></compose>
</div>
This has a repeat.for attribute coming from Aurelia that iterates over the profiles array and get each profile out of it. Then we have a compose element inside the div which has a model.bind attribute. I set the profile taken by the repeat.for and pass it as the value of model.bind attribute in the compose element. I also set the view-model attribute to the profile module I created earlier. Now the about.html file should look like this.
<template>
<h1>About</h1>
<hr>
<p class="lead">${aboutMessage}</p>
<hr>
<h2>Our Team</h2>
<div repeat.for="profile of profiles">
<compose model.bind="profile" view-model="profile"></compose>
</div>
</template>
Now what is left to do is add code to the profile.js View to make everything connect. To get the profile object passed to the profile view model as the binding context, we need to create a method called ‘activate’ inside the profile ViewModel. This activate method is something that Aurelia looks for as a convention. There are some other methods like this that a ViewModel has when a we create, activate, deactivate and removing of our Views and ViewModels. These are lifecycle methods that ViewModels has in Aurelia by conventions. So in this case, when the View is loaded and the ViewModel gets activated the binding context is passed to this activate method. Then we can access it from the ViewModel.
Take a look at the following code in the profile ViewModel
export class Profile {
activate(bindingContext) {
this.item = bindingContext;
}
}
Here in the activate method, I am assigning the passed in bindingContext to the item property of the ViewModel. Now we can access this property from the View and bind to its values. Now we are done. Let’s run it. You will see an output like this one.
Great :D Nice profile images BTW, Weird but works ;) Soo… that is using compose element to create nested views. You can use multiple compose elements with different views to create composite views as well.
But, that’s not all :D We can do this in another way as well. Remember, earlier we talked about using compose element without defining the view-model attribute and only using view attribute? We can do the same here as well. Since we don’t have any manipulation or processing of the data we pass through the binding context to the ViewModel, we can just remove the ViewModel altogether and just bind to the binding context passed via model.bind attribute of the compose element that was used to render the profile view.
All we need to do is, to modify the profile.html view a little bit. We need to remove the binding to the item property of the ViewModel and just bind to the binding context passed in, which is the profile object. So instead of item, we bind to profile object. Look at the modified profile.html view.
<template>
<div class="row" style="margin-bottom: 10px;">
<div class="col-md-3">
<img src.bind="profile.image" alt.bind="profile.name" class="img img-responsive img-circle" style>
</div>
<div class="col-md-8">
<h2>${profile.name}</h2>
<h4>${profile.role}</h4>
</div>
</div>
</template>
All I did was replace the item property with the profile binding context. Just run the application and you will see the same output that we saw earlier ;) Wonderful.
Soo guys, this was a long article.. But we covered most of the stuff we need to know about the compose element and how we can use it to create MVVM hierarchies in our Aurelia app.
You can download the source code from here.
Building Apps with Aurelia: All Articles
- Building Apps with Aurelia: #1 Series Introduction
- Building Apps with Aurelia: #2 Getting Started – Hello Aurelia
- Building Apps with Aurelia: #3 Customizing Aurelia App Startup
- Building Apps with Aurelia: #4 MVVM in Aurelia
- Building Apps with Aurelia: #5 Using compose Element to Implement MVVM (This Article)
- Building Apps with Aurelia: #6 Separating Views and ViewModels into Different Directories & Overriding View Resolution
- Building Apps with Aurelia: #7 Dependency Injection in Aurelia - Part 1
Tags:
You Might Also Like
← Previous Post
Building Apps with Aurelia: #4 MVVM in Aurelia
September 19, 2016
Next Post →