If you don't already have it, install node.js.
Install the package dependencies:
(/web) $ npm install
Before starting this app, start the API
(/web) $ npm run dev
Then navigate to the app in your browser: http://localhost:3001
NOTE:
npm run dev
uses nodemon, which monitors the filesystem, and restarts the app when you make changes. This is not a tool we would use in production.npm start
runs the app without monitoring for changes, which is how we would start the app in production.
If you ran npm run seed
in the api directory, the following users should exist: shopper1@95729.com
, shopper2@95729.com
, shopper3@95729.com
, shopper4@95729.com
. All you need is the email address to sign in.
- node_modules (third party js libs)
- images (book covers, product images, etc.)
- scripts (javascript)
- common
- environment.js (mock nconf style configuration accessor)
- module-shim (shim that adds support for authoring modules using
module.exports
) - Repo.js (fetch abstraction to simplify / proxy server requests)
- router.js (page.js abstraction to enhance req.context)
- storage.js (session/local storage abstraction)
- home (the home component)
- products (the products component)
- books (the books component)
- users (the users component)
- index.js (composition root / app startup)
- common
- styles (CSS)
- index.html (SPA markup / HTML)
- server.js (The HTTP host to make this available on port 3001)
This application uses a specific module pattern that works with hilary to meet the Dependency Inversion Principle. The module-shim
allows us to define new modules so other modules can begin to depend on them in two steps:
First, define a new module:
// greeter.js
module.exports = {
scope: 'heinz', // usually the same for all of your modules
// (hilary supports multiple scopes)
name: 'greeter', // The name other modules will depend on
dependencies: ['router'], // The names of the modules this depends on
factory: (router) => { // The factory that returns this module,
'use strict' // after it's dependencies are resolved
return {
sayHello: () => { console.log('hello world!') }
}
}
}
Second, add the new file in a script tag in index.html:
<script src="/scripts/greeter.js"></script>
And that's it - your modules can now depend on 'greeter', and call greeter.sayHello()
.
Each component is made up of:
- Component (the HTML, and the state)
- Model (the data, also referred to as a ViewModel)
- Controller (the route bindings, and behaviors)
- Repository (communications with services/APIs)
We use models to enforce a schema for the data we represent in our components, and to add behaviors/event handlers. Since the component depends on a model, it's a good place to start. If you're more comfortable starting with the HTML, that's fine too - skip to Defining a Component. Our app presents products, and we have a specific component model for books. Let's add movies.
module.exports = {
scope: 'heinz',
name: 'Movie',
dependencies: ['router', 'Product'],
factory: (router, Product) => {
'use strict'
return function Movie (movie) {
const self = new Product(movie)
movie = Object.assign({}, movie)
// Add actors to the product model
self.actors = movie.metadata && Array.isArray(movie.metadata.actors)
? movie.metadata.actors
: []
// override product's `viewDetails` function to redirect to movies
self.viewDetails = (event) => {
if (self.uid) {
router.navigate(`/movies/${self.uid}`)
}
}
return self
}
}
}
The component is where we define our HTML, and where we keep/mutate the state of our component.
The following conventions are required for our components to work:
- The name of the module MUST have the word, "component" in it.
- The module's factory must return an object with the an instance of
Vue.component
set to thecomponent
property
The composition root finds all modules with "component" in their name (case insensitive), and registers them in Vue.
module.exports = {
scope: 'heinz',
name: 'movieComponent',
dependencies: ['Vue', 'Movie'],
factory: (Vue, Movie) => {
'use strict'
var state = new Movie()
const component = Vue.component('movie', {
template: `
<div class="movie-component details">
<h1>{{title}}</h1>
<div v-for="actor in actors">
<span>{{actor.name}}</span>
</div>
<div>{{description}}</div>
<img v-if="showThumbnail" :src="thumbnailLink" :alt="thumbnailAlt">
<a class="btn" :href="detailsLink">READ MORE</a>
<div class="purchase">
<button class="btn btn-success btn-buy" v-on:click="addToCart">{{price}}</button>
</div>
</div>`,
data: () => {
return state
}
})
const setMovie = (movie) => {
state = movie
}
return { component, setMovie }
}
}
NOTE if you are using Visual Studio Code, you can install an extension that adds syntax highlighting to HTML in string literals in JavaScript files:
code --install-extension natewallace.angular2-inline
We're using the repository pattern to perform data interactions with our API. This is a fractal pattern: the API uses the repository pattern to read/write data from/to the database, and the web app uses the repository pattern to read/write data from/to the API.
You can write your repository from scratch if you need to (i.e. using fetch). This example leverages the base repository in our common directory.
NOTE that some teams refer to repositories such as these, as "clients", or "consumers".
module.exports = {
scope: 'heinz',
name: 'moviesRepo',
dependencies: ['Repo'],
factory: (Repo) => {
'use strict'
const repo = new Repo()
const get = (uid, callback) => {
repo.get({ path: `/movies/${uid}` }, callback)
}
return { get }
}
}
We also use a fractal pattern to bind activities to routes: the API uses controllers to bind activities to a given route, and the web app uses controllers, as well.
The following conventions are required for our controllers to work:
- The name of the module MUST have the word, "controller" in it.
- The module's factory must return an object with the function,
registerRoutes
on it.
The composition root finds all modules with "controller" in their name (case insensitive), and executes
registerRoutes
. The instance of Vue will be passed as the first argument toregisterRoutes
. Given that you name this argument,app
, you can tell Vue which component to display by calling:app.currentView('[COMPONENT_NAME]')
module.exports = {
scope: 'heinz',
name: 'moviesController',
dependencies: ['router', 'movieComponent', 'Movie', 'moviesRepo'],
factory: (router, movieComponent, Movie, repo) => {
'use strict'
/**
* Route binding (controller)
* @param {Vue} app - the main Vue instance (not the header)
*/
function registerRoutes (app) {
router('/movies/:uid', (context) => {
repo.get(context.params.uid, (err, movie) => {
if (err) {
console.log(err)
// TODO: render error view
}
if (movie) {
movieComponent.setMovie(new Movie(movie))
app.currentView = 'movie'
} else {
// TODO: route to a "none found" page
router.navigate('/')
}
})
})
}
return { registerRoutes }
}
}
All we need to do to add the component we just built to our app, is add it to index.html:
<script src="/scripts/movies/Movie.js"></script>
<script src="/scripts/movies/movieComponent.js"></script>
<script src="/scripts/movies/moviesController.js"></script>
<script src="/scripts/movies/moviesRepo.js"></script>