Building Apps with Aurelia: #6 Separating Views and ViewModels into Different Directories & Override View Resolution
Hey Guys, Back with a quick one.. In the last post we talked about how to use the compose element to implement MVVM in our Aurelia application. If you remember correctly we had our Views and ViewModels in the same src folder. And when we reference the ViewModels in the compose elements view-model attribute everything worked fine.
And also I told you that Aurelia is conventions based and by convention Aurelia looks for the View in the same folder that it finds the ViewModels for a particular module. Since we had all the Views and ViewModels in the same src folder, everything worked by convention. But you see the problem here don’t you?
When our application grows, we simply cannot have all the Views and ViewModels in the same directory. And when applications start to have so many modules, this becomes messy and really hard to look for Views and ViewModels. We should organize the code/project structure in a better way.
There are number of ways to do this and it really depends on your personal preference or your team’s conventions or a standard way your team agrees to structure your project. One good way to structure your project is to separate Views and ViewModels in to different folders, by that at least we have some separation between the two and off the top of my head, I feel that it would be much easier to setup a task runners like gulp, grunt to point to where the Views and ViewModels are by giving a pattern to find the files.. Just a thought ;)
So, since we have multiple views in our small sample project ( I know there are only 3, so shut up :P ) we can try this out. So to start off, download the code from the previous post If you have not already and add 2 directories in to your src folder and name them view and viewmoldes. And then move your views and viewmodels in to those 2 folders accordingly. Now your project structure should look like this.
Ok. Just for a second, lets run this and see what happens? It fails.. :D Yes, Aurelia complains that it cannot find the shell.js viewModel. Look at the image bellow, you should see something like this.
So we changed the locations of the Views and ViewModels, so Aurelia cannot find the shell viewmodel. We know where this is coming from right? We explicitly gave shell as the root module in main.js where we customized the startup process. Let’s change it to give the new relative path to shell.js and run the app. So the new main.js should look like this.
import {LogManager} from 'aurelia-framework';
import {ConsoleAppender} from 'aurelia-logging-console';LogManager.addAppender(new ConsoleAppender());
LogManager.setLevel(LogManager.logLevel.debug);export function configure(aurelia) {
aurelia.use.standardConfiguration();
aurelia.start().then(a => a.setRoot('viewmodels/shell', document.getElementById('app-container')));
}
Now if we run this, we again face an error, this time it complains that it cannot find the shell.html which is the view for this ViewModel. Let’s stop here for a while, let’s move the shell.html to the viewmodels folder to make sure both view and viewmodel is at the same location and see what happens next.
So move the shell.html file to viewmodels folder and run the app. So by convention Aurelia should pick up the shell.html view from the viewmodels folder. And it does.. But does the app start without any issues? No. Look at the image bellow.
Now Aurelia picks up the shell.html, but it cannot find home.js and about.js viewmodels. So we know how to fix this right? Yes.. We just update the compose elements in the shell.html and about.html (remember, we have a nested view in our team member profiles as well) with the relative paths to the viewmodels. Like this.
<!-- shell.html -->
<div class="container-fluid">
<div class="row">
<div class="col-md-8">
<compose view-model="viewmodels/home"></compose>
</div>
<div class="col-md-4">
<compose view-model="viewmodels/about"></compose>
</div>
</div>
</div><!-- about.html -->
<div repeat.for="profile of profiles">
<compose model.bind="profile" view-model="viewmodels/profile"></compose>
</div>
Now it cannot find both views for home and about. Obviously we can manually add a view attribute to the compose element and add the relative path to the view and fix this. But we don’t have to. We can override the Default View Resolution and fix this. This is done in the main.js where we customized the Aurelia startup process. Now you can see the use-cases of having a custom app startup. ;)
Let’s see how to do this, By doing this change, we only need to define the relative path to the view-model attribute in compose element, Aurelia figures out where the view is from there. First we import ViewLocator from Aurelia framework. We do this in the top of the file like this.
import {ViewLocator} from 'aurelia-framework';
In the ViewLocator there is a prototype method called convertOriginToViewUrl We need to replace this method with our own implementation. The new implementation looks like this
ViewLocator.prototype.convertOriginToViewUrl = (origin) => {
let moduleId = origin.moduleId;
let id = moduleId.endsWith('.js') ? moduleId.substring(0, moduleId.length - 3) : moduleId;
return `${id.replace('viewmodels', 'views')}.html`;
};
Here the origin that was passed in to the method include the moduleId for each module that Aurelia tries to load. From that moduleId we check to see if it contains .js extension and if so strip the extension out and replace the viewmodels part of the module with views and tack on .html extension to the end. Then with this implementation if we provide the correct relative path to the viewmodel Aurelia will figure out where the view is for that particular viewmodel.
So the modified main.js file should look like this.
import {LogManager} from 'aurelia-framework';
import {ConsoleAppender} from 'aurelia-logging-console';
import {ViewLocator} from 'aurelia-framework';LogManager.addAppender(new ConsoleAppender());
LogManager.setLevel(LogManager.logLevel.debug);export function configure(aurelia) {ViewLocator.prototype.convertOriginToViewUrl = (origin) => {
let moduleId = origin.moduleId;
let id = moduleId.endsWith('.js') ? moduleId.substring(0, moduleId.length - 3) : moduleId;
return `${id.replace('viewmodels', 'views')}.html`;
};aurelia.use.standardConfiguration();
aurelia.start().then(a => a.setRoot('viewmodels/shell', document.getElementById('app-container')));
}
Let’s run this and see if it is working. (NOTE: If you have moved the shell.html to the viewmodels directory as we discussed earlier in the post, move it back to views directory. If now Aurelia won’t find the shell.html view)
Now when you run this, you should see the app loading up and everything works fine.. The output should be like this.
There you have it folks, that’s how you change the project structure and separate views and viewmodels in to different directories and Override the default View Resolution behavior of Aurelia. :D Until I see you guys in the next post, C ya.
You can download source code from here.