├── app ├── initialize.js ├── components │ └── recipe │ │ ├── list │ │ ├── table-item.jst │ │ ├── table.jst │ │ └── index.js │ │ ├── form │ │ ├── template.jst │ │ └── index.js │ │ ├── tests │ │ └── detail.js │ │ ├── detail │ │ ├── template.jst │ │ └── index.js │ │ └── models.js ├── theme │ ├── application.css │ ├── layout.js │ └── index.html └── routes │ └── recipes │ ├── recipes.js │ ├── recipe.js │ └── recipe-edit.js ├── webpack-prod.config.js ├── webpack-dev.config.js ├── .tmp └── mocha-webpack │ ├── a623dfc6e4489b534e6b83f665e3b973-entry.js │ └── a623dfc6e4489b534e6b83f665e3b973 │ └── index.html ├── .gitignore ├── core ├── app_finder.js ├── router.js ├── extract_routes.js └── init.js ├── karma.conf.js ├── .eslintrc ├── package.json ├── README.md └── webpack.config-helper.js /app/initialize.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | export default async function resolveInitialData() { 3 | return; 4 | } 5 | -------------------------------------------------------------------------------- /webpack-prod.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./webpack.config-helper')({ 2 | isProduction: true, 3 | devtool: 'cheap-module-source-map' 4 | }); 5 | -------------------------------------------------------------------------------- /app/components/recipe/list/table-item.jst: -------------------------------------------------------------------------------- 1 | 2 | <%- title %> 3 | 4 | <%- cook %> 5 | <%- prep %> 6 | -------------------------------------------------------------------------------- /webpack-dev.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./webpack.config-helper')({ 2 | isProduction: false, 3 | devtool: 'cheap-eval-source-map', 4 | port: 1337 5 | }); 6 | -------------------------------------------------------------------------------- /.tmp/mocha-webpack/a623dfc6e4489b534e6b83f665e3b973-entry.js: -------------------------------------------------------------------------------- 1 | 2 | var testsContext = require.context("../../app", false); 3 | 4 | var runnable = testsContext.keys(); 5 | 6 | runnable.forEach(testsContext); 7 | -------------------------------------------------------------------------------- /app/theme/application.css: -------------------------------------------------------------------------------- 1 | .fullwidth { 2 | width: 100%; 3 | } 4 | 5 | .navigation { 6 | background: #f4f5f6; 7 | } 8 | 9 | form { 10 | width: 100%; 11 | } 12 | 13 | .is-hidden { 14 | display: none; 15 | } 16 | -------------------------------------------------------------------------------- /app/components/recipe/form/template.jst: -------------------------------------------------------------------------------- 1 | <% if(!loading) { %> 2 |
3 |
4 | 5 |
6 | <% } else { %> 7 | Loading... 8 | <% } %> 9 | -------------------------------------------------------------------------------- /app/components/recipe/tests/detail.js: -------------------------------------------------------------------------------- 1 | import DetailView from '../detail/index.js'; 2 | import { expect } from 'chai'; 3 | 4 | 5 | describe('Detail View', () => { 6 | it('should exist', () => { 7 | expect(DetailView).to.exist; 8 | }); 9 | it('and it should provide a constructor', () => { 10 | expect(new DetailView()).to.exist; 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /app/theme/layout.js: -------------------------------------------------------------------------------- 1 | import 'milligram'; 2 | import './application.css'; 3 | 4 | 5 | export default Mn.View.extend({ 6 | el: 'body', 7 | regions: { 8 | 'region1': '#js-region-1', 9 | 'region2': '#js-region-2', 10 | 'region3': '#js-region-3', 11 | 'region4': '#js-region-4', 12 | 'region5': '#js-region-5', 13 | }, 14 | template: false 15 | }) 16 | -------------------------------------------------------------------------------- /app/routes/recipes/recipes.js: -------------------------------------------------------------------------------- 1 | import RecipeList from 'recipe/list/index.js'; 2 | 3 | //BEGIN ROUTES 4 | var routes = { 5 | '': 'recipes' 6 | } 7 | //END ROUTES 8 | 9 | export default (layout) => Backbone.Router.extend({ 10 | routes, 11 | initialize(options) { 12 | }, 13 | recipes(params) { 14 | layout.showChildView('region1', new RecipeList()); 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Numerous always-ignore extensions 2 | *.diff 3 | *.err 4 | *.orig 5 | *.log 6 | *.rej 7 | *.swo 8 | *.swp 9 | *.vi 10 | *~ 11 | *.sass-cache 12 | 13 | # OS or Editor folders 14 | .DS_Store 15 | .cache 16 | .project 17 | .settings 18 | .tmproj 19 | nbproject 20 | Thumbs.db 21 | 22 | # NPM packages folder. 23 | node_modules 24 | 25 | # Output folder. 26 | public/ 27 | 28 | dist 29 | -------------------------------------------------------------------------------- /app/routes/recipes/recipe.js: -------------------------------------------------------------------------------- 1 | import RecipeDetail from 'recipe/detail/index.js'; 2 | 3 | //BEGIN ROUTES 4 | var routes = { 5 | 'recipe/:id': 'recipe' 6 | } 7 | //END ROUTES 8 | 9 | export default (layout) => Backbone.Router.extend({ 10 | routes, 11 | initialize(options) {}, 12 | recipe(params) { 13 | layout.showChildView('region1', new RecipeDetail({id: params})) 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /app/components/recipe/detail/template.jst: -------------------------------------------------------------------------------- 1 |
2 | <% if(!loading) { %> 3 |
4 |

<%- title %>

5 |

Directions

6 | <%= marked(directions) %> 7 |
8 |
9 | EDIT 10 | Prep time: <%- prep %> 11 | Cook time: <%- cook %> 12 |

Ingredients

13 | <%= marked(ingredients) %> 14 |
15 | <% } else { %> 16 | Loading... 17 | <% } %> 18 |
19 | -------------------------------------------------------------------------------- /app/routes/recipes/recipe-edit.js: -------------------------------------------------------------------------------- 1 | import RecipeForm from 'recipe/form/index.js'; 2 | 3 | //BEGIN ROUTES 4 | var routes = { 5 | 'recipe/:id/edit': 'recipeEdit', 6 | 'recipe/new': 'recipeNew' 7 | } 8 | //END ROUTES 9 | 10 | export default (layout) => Backbone.Router.extend({ 11 | routes, 12 | recipeEdit(params) { 13 | layout.showChildView('region1', new RecipeForm({id: params})) 14 | }, 15 | recipeNew(params) { 16 | layout.showChildView('region1', new RecipeForm()) 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /app/components/recipe/detail/index.js: -------------------------------------------------------------------------------- 1 | import template from './template.jst'; 2 | import models from '../models.js'; 3 | import marked from 'marked'; 4 | 5 | export default Mn.View.extend({ 6 | template: template, 7 | initialize: function() { 8 | this.model = new models.RecipeModel({id: this.options.id}); 9 | this.model.set('loading', true); 10 | this.model.fetch().then(() => this.model.set('loading', false)); 11 | }, 12 | modelEvents: { 13 | 'change': 'render', 14 | }, 15 | templateContext: { 16 | marked: (text) => marked(text) 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /core/app_finder.js: -------------------------------------------------------------------------------- 1 | const customAppRoutes = {}; 2 | 3 | // we need to convert Backbone routes to plain RegExps 4 | function routeToRegExp(route) { 5 | return Backbone.Router.prototype._routeToRegExp.call(null, route); 6 | } 7 | 8 | // Creating the index of routes' regexes 9 | _.each(__ROUTES__, (value, key) => { 10 | customAppRoutes[value.appName] = Object.keys(value.routes).map(routeToRegExp); 11 | }); 12 | 13 | export default path => { 14 | if (!path) { path = ''; } 15 | const matcher = route => route.test(path); 16 | return _.findKey(customAppRoutes, routes => _.some(routes, matcher)); 17 | } 18 | -------------------------------------------------------------------------------- /core/router.js: -------------------------------------------------------------------------------- 1 | import appFinder from './app_finder.js'; 2 | 3 | export default (layout) => Backbone.Router.extend({ 4 | routes: { 5 | '*handleMissingRoute': 'handle404', 6 | }, 7 | handle404(path) { 8 | const miniApp = appFinder(path); 9 | if (miniApp) { 10 | const handler = require('bundle!./../app/routes/' + miniApp); 11 | handler(bundle => { 12 | new (bundle.default(layout)); 13 | Backbone.history.loadUrl(); // just refreshing the current path, because we've added new paths that we can handle 14 | }); 15 | } else { 16 | console.log('404'); 17 | } 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /core/extract_routes.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var path = require('path'); 3 | var glob = require('glob'); 4 | var fs = require('fs'); 5 | 6 | module.exports = _.map( 7 | glob.sync(path.join(__dirname, '../app/routes/**/*.js')), function(file) { 8 | let appName = path.join( 9 | path.relative( 10 | path.join(__dirname, '..', 'app', 'routes'), 11 | path.dirname(file) 12 | ), 13 | path.basename(file) 14 | ); 15 | let contents = fs 16 | .readFileSync(file) 17 | .toString() 18 | .match(/\/\/BEGIN\ ROUTES\nvar\ routes\ =\ ([\s\S]*)\n\/\/END\ ROUTES/m) 19 | return { 20 | appName, 21 | routes: JSON.parse(contents[1].replace(/\'/g,'\"')) 22 | } 23 | } 24 | ) 25 | -------------------------------------------------------------------------------- /app/components/recipe/list/table.jst: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
TitlePrep timeCook time
18 |
19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 | -------------------------------------------------------------------------------- /core/init.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import Router from './router'; 3 | import Initial from './../app/initialize.js'; 4 | import Layout from './../app/theme/layout.js'; 5 | 6 | var App = new Marionette.Application({ 7 | onStart: function(options) { 8 | Initial().then(() => { 9 | let layout = new Layout({el: 'body', regions: Initial.regions}) 10 | layout.render(); 11 | let router = new (Router(layout)); 12 | Backbone.history.start(); 13 | }); 14 | } 15 | }); 16 | 17 | $(document).ready(() => App.start()); 18 | $(document).on('click', 'a[data-sref]', function(e) { 19 | e.preventDefault(); 20 | Backbone.history.navigate( 21 | Bb.history.fragment + '/' + 22 | $(this).attr('data-sref'), 23 | {trigger: true} 24 | ); 25 | }); 26 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var webpackConf = require('./webpack-dev.config.js'); 2 | module.exports = function(config) { 3 | config.set({ 4 | basePath: '', 5 | frameworks: ['mocha', 'chai', 'sinon'], 6 | files: [ 7 | 'app/components/**/tests/*.js' 8 | ], 9 | exclude: [ 10 | ], 11 | preprocessors: { 12 | 'app/components/**/tests/*.js': ['webpack'] 13 | }, 14 | webpack: { 15 | module: webpackConf.module, 16 | resolve: webpackConf.resolve, 17 | plugins: webpackConf.plugins 18 | }, 19 | webpackMiddleware: { 20 | stats: 'errors-only' 21 | }, 22 | reporters: ['spec'], 23 | port: 9876, 24 | colors: true, 25 | logLevel: config.LOG_INFO, 26 | autoWatch: true, 27 | browsers: ['PhantomJS'], 28 | singleRun: false, 29 | concurrency: Infinity 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /app/theme/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Webpack with MarionetteJS 8 | 9 | 10 |
11 | 16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/components/recipe/form/index.js: -------------------------------------------------------------------------------- 1 | import 'backbone-forms'; 2 | import template from './template.jst'; 3 | import models from '../models.js'; 4 | 5 | export default Mn.View.extend({ 6 | template, 7 | ui: { 8 | 'save': '.js-save', 9 | }, 10 | events: { 11 | 'click @ui.save': 'onSave', 12 | }, 13 | regions: { 14 | form: '.js-form', 15 | }, 16 | modelEvents: { 17 | 'change:loading': 'render', 18 | }, 19 | initialize: function() { 20 | window.probe = this; 21 | this.model = new models.RecipeModel(this.options); 22 | this.model.set('loading', true); 23 | 24 | this.model.fetch().then(() => { 25 | this.form = new Backbone.Form({ 26 | model: this.model 27 | }); 28 | this.model.set('loading', false); 29 | }); 30 | }, 31 | onRender: function() { 32 | if (this.model.get('loading')) return; 33 | this.showChildView('form', this.form); 34 | }, 35 | onSave: function() { 36 | this.form.commit(); 37 | this.model.set('loading', true); 38 | this.model.save().then(() => { 39 | this.form = new Backbone.Form({ 40 | model: this.model 41 | }); 42 | this.model.set('loading', false); 43 | }); 44 | } 45 | }); 46 | -------------------------------------------------------------------------------- /app/components/recipe/models.js: -------------------------------------------------------------------------------- 1 | import PageableCollection from 'backbone.paginator'; 2 | 3 | let RecipeModel = Bb.Model.extend({ 4 | urlRoot: 'http://localhost:3000/recipes', 5 | //url: 'http://localhost:3000/recipes', 6 | defaults: { 7 | prep: '-', 8 | cook: '-', 9 | portions: '-', 10 | }, 11 | schema: { 12 | title: { type: 'Text', validators: ['required'] }, 13 | prep: 'Text', 14 | cook: 'Text', 15 | directions: { type: 'TextArea', validators: ['required'] }, 16 | ingredients: { type: 'TextArea', validators: ['required'] }, 17 | }, 18 | }); 19 | 20 | let RecipesCollection = PageableCollection.extend({ 21 | url: 'http://localhost:3000/recipes', 22 | model: RecipeModel, 23 | mode: 'infinite', 24 | state: { 25 | pageSize: 20, 26 | }, 27 | queryParams: { 28 | currentPage: '_page', 29 | pageSize: '_limit' 30 | }, 31 | parseState: function(response) { 32 | return { totalRecords: this.totalRecords || 0}; 33 | }, 34 | fetch: function(options) { 35 | var jqXHR = PageableCollection.prototype.fetch.call(this, options); 36 | jqXHR.done(() => { 37 | this.totalRecords = parseInt( 38 | jqXHR.getResponseHeader('X-Total-Count') 39 | ); 40 | }); 41 | return jqXHR; 42 | } 43 | }); 44 | 45 | export default { 46 | RecipeModel, 47 | RecipesCollection, 48 | } 49 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es6": true 6 | }, 7 | "globals": { 8 | "$": true, 9 | "_": true, 10 | "Bb": true, 11 | "Backbone": true, 12 | "Mn": true, 13 | "Marionette": true, 14 | "__ROUTES__": true, 15 | "describe": true, 16 | "it": true, 17 | "expect": true 18 | }, 19 | "parserOptions": { 20 | "sourceType": "module" 21 | }, 22 | "rules": { 23 | "array-bracket-spacing": [ 2, "never" ], 24 | "block-scoped-var": 2, 25 | "brace-style": [ 2, "1tbs", { "allowSingleLine": true } ], 26 | "camelcase": [ 2, { "properties": "always" } ], 27 | "curly": [ 2, "all" ], 28 | "dot-notation": [ 2, { "allowKeywords": true } ], 29 | "eol-last": 2, 30 | "eqeqeq": [ 2, "allow-null" ], 31 | "guard-for-in": 2, 32 | "indent": [ 2, 2, { "SwitchCase": 1 } ], 33 | "key-spacing": [ 2, 34 | { 35 | "beforeColon": false, 36 | "afterColon": true 37 | } 38 | ], 39 | "keyword-spacing": [ 2 ], 40 | "new-cap": 2, 41 | "no-bitwise": 2, 42 | "no-caller": 2, 43 | "no-cond-assign": [ 2, "except-parens" ], 44 | "no-debugger": 2, 45 | "no-empty": 2, 46 | "no-eval": 2, 47 | "no-extend-native": 2, 48 | "no-irregular-whitespace": 2, 49 | "no-iterator": 2, 50 | "no-loop-func": 2, 51 | "no-mixed-spaces-and-tabs": 2, 52 | "no-multi-str": 2, 53 | "no-multiple-empty-lines": 2, 54 | "no-new": 2, 55 | "no-proto": 2, 56 | "no-script-url": 2, 57 | "no-sequences": 2, 58 | "no-shadow": 2, 59 | "no-spaced-func": 2, 60 | "no-trailing-spaces": 2, 61 | "no-undef": 2, 62 | "no-unused-vars": [ 1, { "args": "none" } ], 63 | "no-with": 2, 64 | "one-var": [ 2, "never" ], 65 | "operator-linebreak": [ 2, "after" ], 66 | "quotes": [ 2, "single" ], 67 | "semi": [ 0, "never" ], 68 | "space-before-blocks": [ 2, "always" ], 69 | "space-before-function-paren": [ 2, "never" ], 70 | "space-in-parens": [ 2, "never" ], 71 | "space-infix-ops": 2, 72 | "space-unary-ops": [ 2, 73 | { 74 | "nonwords": false, 75 | "overrides": {} 76 | } 77 | ], 78 | "strict": 0, 79 | "valid-jsdoc": 2, 80 | "valid-typeof": 2, 81 | "wrap-iife": [ 2, "inside" ] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-marionette", 3 | "description": "A small boilerplate introducing webpack and es6 features to a Marionette/Backbone application", 4 | "author": "alexpsi", 5 | "version": "0.4.1", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/alexpsi/webpack-marionette" 9 | }, 10 | "scripts": { 11 | "build": "rimraf dist && webpack --config webpack-prod.config.js --colors", 12 | "analyze": "webpack --config webpack-prod.config.js --json | webpack-bundle-size-analyzer", 13 | "dev": "webpack-dev-server --config webpack-dev.config.js --watch --colors", 14 | "test": "karma start --single-run", 15 | "tdd": "karma start", 16 | "json": "json-server --watch ./app/db.json", 17 | "eject": "rimraf components/* && rimraf routes/*", 18 | "dash": "webpack-dashboard -- webpack-dev-server --config webpack-dev.config.js" 19 | }, 20 | "dependencies": { 21 | "axios": "^0.15.2", 22 | "babel-polyfill": "^6.9.0", 23 | "backbone": "^1.3.3", 24 | "backbone-forms": "^0.14.1", 25 | "backbone.marionette": "^3.1.0", 26 | "backbone.paginator": "^2.0.5", 27 | "jquery": "^3.1.1", 28 | "marked": "^0.3.6", 29 | "milligram": "^1.2.3" 30 | }, 31 | "devDependencies": { 32 | "babel-core": "^6.7.6", 33 | "babel-loader": "^6.2.4", 34 | "babel-plugin-syntax-async-functions": "^6.13.0", 35 | "babel-plugin-transform-regenerator": "^6.16.1", 36 | "babel-plugin-transform-runtime": "^6.15.0", 37 | "babel-preset-es2015": "^6.6.0", 38 | "bundle-loader": "^0.5.4", 39 | "chai": "^3.5.0", 40 | "compression-webpack-plugin": "^0.3.2", 41 | "copy-webpack-plugin": "^1.1.1", 42 | "css-loader": "^0.23.1", 43 | "eslint": "^2.11.1", 44 | "extract-text-webpack-plugin": "^1.0.1", 45 | "glob": "^7.1.1", 46 | "html-webpack-plugin": "^2.24.1", 47 | "isparta": "^4.0.0", 48 | "karma": "^1.3.0", 49 | "karma-chai": "^0.1.0", 50 | "karma-mocha": "^1.3.0", 51 | "karma-phantomjs-launcher": "^1.0.2", 52 | "karma-sinon": "^1.0.5", 53 | "karma-spec-reporter": "0.0.26", 54 | "karma-webpack": "^1.8.0", 55 | "mocha": "^2.5.3", 56 | "mocha-webpack": "^0.7.0", 57 | "phantomjs-prebuilt": "^2.1.14", 58 | "rimraf": "^2.4.3", 59 | "sinon": "^1.17.6", 60 | "sinon-chai": "^2.8.0", 61 | "style-loader": "^0.13.1", 62 | "underscore-template-loader": "^0.7.2", 63 | "webpack": "^1.14.0", 64 | "webpack-dashboard": "^0.2.0", 65 | "webpack-dev-server": "^1.16.2", 66 | "webpack-merge": "^0.15.0" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Webpack and MarionetteJS 2 | 3 | A small boilerplate introducing [Webpack](https://webpack.github.io/) and es6 features through [Babel](https://babel.github.io/) to a CRUD Marionette/Backbone application. 4 | 5 | ## Getting started 6 | 7 | * Install 8 | * Clone the repository: `git clone https://github.com/alexpsi/webpack-marionette` 9 | * Inside this folder run: `npm install` 10 | 11 | * Build 12 | * `npm run build` - builds you project inside the `/dist` directory, notice that each route has a separate bundle thus allowing for lazy loading. 13 | * `npm run analyze` - creates a size report for bundled libraries 14 | 15 | * Development 16 | * `npm run dev` - launches the project through webpack-dev-server utilizing 17 | the configuration from webpack-dev.config and hotreload for css assets. 18 | * `npm run dash` - As above but uses webpack-dashboard 19 | * `npm run eject` - Deletes sample application leaving only the core files. 20 | 21 | * Test 22 | * `npm test` - Searches inside the tests folder of each component directory for 23 | .js files and runs them with mocha and chai over Karma. 24 | * `npm run tdd` - As above but watches files for changes and reruns tests. 25 | 26 | ## Features 27 | 28 | * Utilizes Backbone router along with Webpack requireContext so additional libraries utilized by a certain route are lazily added when the route loads. 29 | The routes are defined in the routes directory and a custom webpack plugin collects routes definition from the comments inside the routes folder. 30 | 31 | * ES6 async/await syntax 32 | 33 | * A basic structure, consisting of 3 folders, `components` which is used as a 34 | place to store your views, `routes` which is where you define your routes and 35 | route initializations (check example app for sample route definition), `theme` which stores tha application layout as well as global stylesheets. 36 | 37 | ## Example app 38 | The example app is an editable Cookbook, it utilizes a list of recipes taken 39 | from https://github.com/mikeizbicki/ucr-cs100 and packaged as a json file which 40 | is server by json-server, to run the json-server run `npm run json`, then in another terminal run `npm run dev` and navigate to localhost:1337. Inside the example app 41 | you can find a full collection of views and models allowing to do a full set of CRUD operations on a REST resource. The example app uses [Backbone.paginator](https://github.com/backbone-paginator/backbone.paginator) and 42 | [Backbone.forms](https://github.com/backbone-paginator/backbone.paginator) for creating forms based on the schemas in the models. 43 | -------------------------------------------------------------------------------- /app/components/recipe/list/index.js: -------------------------------------------------------------------------------- 1 | import template from './table.jst'; 2 | import templateItem from './table-item.jst'; 3 | import models from '../models.js'; 4 | 5 | let TableItem = Mn.View.extend({ 6 | tagName: 'tr', 7 | template: templateItem, 8 | ui: { 9 | 'item': '.js-item', 10 | }, 11 | events: { 12 | 'click @ui.item': 'onClick', 13 | }, 14 | onClick: function(e) { 15 | e.preventDefault(); 16 | Backbone.history.navigate( 17 | `recipe/${this.model.get('id')}`, 18 | {trigger: true} 19 | ); 20 | } 21 | }); 22 | 23 | let TableBody = Mn.CollectionView.extend({ 24 | tagName: 'tbody', 25 | childView: TableItem, 26 | onRender: function() { 27 | this.listenTo(this.collection, 'change', this.render); 28 | } 29 | }) 30 | 31 | export default Mn.View.extend({ 32 | className: 'fullwidth', 33 | template, 34 | regions: { 35 | body: { 36 | el: 'tbody', 37 | replaceElement: true 38 | } 39 | }, 40 | ui: { 41 | 'previous': '.js-previous', 42 | 'next': '.js-next', 43 | 'search': '.js-search', 44 | }, 45 | events: { 46 | 'keyup @ui.search': 'onSearch', 47 | 'click @ui.previous': 'onPrevious', 48 | 'click @ui.next': 'onNext', 49 | }, 50 | initialize: function() { 51 | this.collection = new models.RecipesCollection(); 52 | this._search = _.debounce(_.bind(this.search, this), 2000); 53 | window.probe = this.collection; 54 | this.collection.fetch(); 55 | this.listenTo(this.collection, 'reset', () => { 56 | if (this.collection.hasPreviousPage()) { 57 | this.ui.previous.removeClass('is-hidden') 58 | } else { 59 | this.ui.previous.addClass('is-hidden') 60 | } 61 | if (this.collection.hasNextPage()) { 62 | this.ui.next.removeClass('is-hidden') 63 | } else { 64 | this.ui.next.addClass('is-hidden') 65 | } 66 | }); 67 | }, 68 | onSearch: function(e) { 69 | var query = e.target.value; 70 | this._search(query); 71 | }, 72 | search: function(query) { 73 | this.ui.search.attr('disabled', true); 74 | if (query && query.length > 0) { 75 | this.collection.switchMode('infinite'); 76 | this.collection.fetch({ data: { 77 | q: query 78 | }}).then(() => { 79 | this.ui.search.attr('disabled', false); 80 | }); 81 | } else { 82 | this.collection.switchMode('client'); 83 | this.collection.fetch().then(() => { 84 | this.ui.search.attr('disabled', false); 85 | }); 86 | } 87 | }, 88 | onRender: function() { 89 | this.showChildView('body', new TableBody({ 90 | collection: this.collection 91 | })); 92 | }, 93 | onNext: function() { 94 | this.collection.getNextPage(); 95 | }, 96 | onPrevious: function() { 97 | this.collection.getPreviousPage(); 98 | }, 99 | }); 100 | -------------------------------------------------------------------------------- /webpack.config-helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const ROUTES = require('./core/extract_routes.js'); 3 | const Path = require('path'); 4 | const Webpack = require('webpack'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | var DashboardPlugin = require('webpack-dashboard/plugin'); 7 | 8 | 9 | module.exports = (options) => { 10 | 11 | let webpackConfig = { 12 | devtool: options.devtool, 13 | output: { 14 | path: Path.join(__dirname, 'dist'), 15 | filename: 'bundle.js' 16 | }, 17 | plugins: [ 18 | new Webpack.DefinePlugin({ 19 | 'process.env': { 20 | NODE_ENV: JSON.stringify(options.isProduction ? 'production' : 'development') 21 | }, 22 | '__ROUTES__': JSON.stringify(ROUTES) 23 | }), 24 | new HtmlWebpackPlugin({ 25 | template: './app/theme/index.html' 26 | }), 27 | new Webpack.ProvidePlugin({ 28 | _: 'underscore', 29 | $: 'jquery', 30 | jQuery: 'jquery', 31 | Backbone: 'backbone', 32 | Bb: 'backbone', 33 | Marionette: 'backbone.marionette', 34 | Mn: 'backbone.marionette', 35 | }), 36 | ], 37 | module: { 38 | loaders: [{ 39 | test: /\.js$/, 40 | exclude: /(node_modules|bower_components)/, 41 | loader: 'babel', 42 | query: { 43 | presets: ['es2015'], 44 | plugins: ['syntax-async-functions', 'transform-regenerator'], 45 | }, 46 | }, { 47 | test: /\.jst$/, 48 | loader: 'underscore-template-loader', 49 | }, { 50 | test: /\.s?css$/i, 51 | loaders: ['style', 'css'] 52 | }], 53 | }, 54 | resolve: { 55 | //fallback: path.resolve(__dirname, 'node_modules'), 56 | root: Path.join(__dirname, './app/components'), 57 | }, 58 | resolveLoader: { 59 | root: Path.join(__dirname, './node_modules'), 60 | } 61 | }; 62 | 63 | if (options.isProduction) { 64 | 65 | webpackConfig.plugins.push( 66 | new Webpack.optimize.DedupePlugin(), 67 | new Webpack.optimize.UglifyJsPlugin({ 68 | compressor: {screw_ie8: true, keep_fnames: true, warnings: false}, 69 | mangle: {screw_ie8: true, keep_fnames: true} 70 | }), 71 | new Webpack.optimize.OccurenceOrderPlugin(), 72 | new Webpack.optimize.AggressiveMergingPlugin() 73 | ); 74 | 75 | /* 76 | webpackConfig.module.loaders.push({ 77 | test: /\.scss$/i, 78 | loader: ExtractSASS.extract(['css', 'sass']) 79 | });*/ 80 | 81 | } else { 82 | webpackConfig.plugins.push( 83 | new Webpack.HotModuleReplacementPlugin(), 84 | new DashboardPlugin() 85 | ); 86 | 87 | webpackConfig.devServer = { 88 | contentBase: './dist', 89 | hot: true, 90 | port: options.port, 91 | inline: true, 92 | progress: true 93 | }; 94 | } 95 | 96 | webpackConfig.entry = []; 97 | if (!options.isProduction) { 98 | webpackConfig.entry.push(`webpack-dev-server/client?http://localhost:${options.port}`); 99 | webpackConfig.entry.push('webpack/hot/dev-server'); 100 | } 101 | webpackConfig.entry.push('./core/init'); 102 | 103 | return webpackConfig; 104 | 105 | }; 106 | -------------------------------------------------------------------------------- /.tmp/mocha-webpack/a623dfc6e4489b534e6b83f665e3b973/index.html: -------------------------------------------------------------------------------- 1 | Html Webpack Plugin: 2 |
  3 |   Error: Child compilation failed:
  4 |   Entry module not found: Error: Cannot resolve 'file' or 'd  irectory' /Users/alexpsi/Desktop/stuff/repos/m6/.tmp/mocha  -webpack/app/theme/index.html in /Users/alexpsi/Desktop/st  uff/repos/m6/.tmp/mocha-webpack:
  5 |   Error: Cannot resolve 'file' or 'directory' /Users/alexpsi  /Desktop/stuff/repos/m6/.tmp/mocha-webpack/app/theme/index  .html in /Users/alexpsi/Desktop/stuff/repos/m6/.tmp/mocha-  webpack
  6 |   
  7 |   - compiler.js:76 
  8 |     [m6]/[html-webpack-plugin]/lib/compiler.js:76:16
  9 |   
 10 |   - Compiler.js:214 Compiler.
 11 |     [m6]/[webpack]/lib/Compiler.js:214:10
 12 |   
 13 |   - Compiler.js:403 
 14 |     [m6]/[webpack]/lib/Compiler.js:403:12
 15 |   
 16 |   - Tapable.js:67 Compiler.next
 17 |     [m6]/[tapable]/lib/Tapable.js:67:11
 18 |   
 19 |   - CachePlugin.js:40 Compiler.
 20 |     [m6]/[webpack]/lib/CachePlugin.js:40:4
 21 |   
 22 |   - Tapable.js:71 Compiler.applyPluginsAsync
 23 |     [m6]/[tapable]/lib/Tapable.js:71:13
 24 |   
 25 |   - Compiler.js:400 Compiler.
 26 |     [m6]/[webpack]/lib/Compiler.js:400:9
 27 |   
 28 |   - Compilation.js:577 Compilation.
 29 |     [m6]/[webpack]/lib/Compilation.js:577:13
 30 |   
 31 |   - Tapable.js:60 Compilation.applyPluginsAsync
 32 |     [m6]/[tapable]/lib/Tapable.js:60:69
 33 |   
 34 |   - Compilation.js:572 Compilation.
 35 |     [m6]/[webpack]/lib/Compilation.js:572:10
 36 |   
 37 |   - Tapable.js:60 Compilation.applyPluginsAsync
 38 |     [m6]/[tapable]/lib/Tapable.js:60:69
 39 |   
 40 |   - Compilation.js:567 Compilation.
 41 |     [m6]/[webpack]/lib/Compilation.js:567:9
 42 |   
 43 |   - Tapable.js:60 Compilation.applyPluginsAsync
 44 |     [m6]/[tapable]/lib/Tapable.js:60:69
 45 |   
 46 |   - Compilation.js:563 Compilation.
 47 |     [m6]/[webpack]/lib/Compilation.js:563:8
 48 |   
 49 |   - Tapable.js:60 Compilation.applyPluginsAsync
 50 |     [m6]/[tapable]/lib/Tapable.js:60:69
 51 |   
 52 |   - Compilation.js:525 Compilation.seal
 53 |     [m6]/[webpack]/lib/Compilation.js:525:7
 54 |   
 55 |   - Compiler.js:397 Compiler.
 56 |     [m6]/[webpack]/lib/Compiler.js:397:15
 57 |   
 58 |   - Tapable.js:103 
 59 |     [m6]/[tapable]/lib/Tapable.js:103:11
 60 |   
 61 |   - Compilation.js:445 Compilation.
 62 |     [m6]/[webpack]/lib/Compilation.js:445:10
 63 |   
 64 |   - Compilation.js:344 Compilation.errorAndCallback
 65 |     [m6]/[webpack]/lib/Compilation.js:344:3
 66 |   
 67 |   - Compilation.js:358 Compilation.
 68 |     [m6]/[webpack]/lib/Compilation.js:358:11
 69 |   
 70 |   - NormalModuleFactory.js:29 onDoneResolving
 71 |     [m6]/[webpack]/lib/NormalModuleFactory.js:29:20
 72 |   
 73 |   - NormalModuleFactory.js:85 
 74 |     [m6]/[webpack]/lib/NormalModuleFactory.js:85:20
 75 |   
 76 |   - async.js:726 
 77 |     [m6]/[webpack]/[async]/lib/async.js:726:13
 78 |   
 79 |   - async.js:52 
 80 |     [m6]/[webpack]/[async]/lib/async.js:52:16
 81 |   
 82 |   - async.js:241 done
 83 |     [m6]/[webpack]/[async]/lib/async.js:241:17
 84 |   
 85 |   - async.js:44 
 86 |     [m6]/[webpack]/[async]/lib/async.js:44:16
 87 |   
 88 |   - async.js:723 
 89 |     [m6]/[webpack]/[async]/lib/async.js:723:17
 90 |   
 91 |   - async.js:167 
 92 |     [m6]/[webpack]/[async]/lib/async.js:167:37
 93 |   
 94 |   - UnsafeCachePlugin.js:24 
 95 |     [m6]/[enhanced-resolve]/lib/UnsafeCachePlugin.js:24:19
 96 |   
 97 |   - Resolver.js:38 onResolved
 98 |     [m6]/[enhanced-resolve]/lib/Resolver.js:38:18
 99 |   
100 |   - Resolver.js:127 
101 |     [m6]/[enhanced-resolve]/lib/Resolver.js:127:10
102 |   
103 |   - Resolver.js:191 
104 |     [m6]/[enhanced-resolve]/lib/Resolver.js:191:15
105 |   
106 |   - Resolver.js:110 applyPluginsParallelBailResult.createInn    erCallback.log
107 |     [m6]/[enhanced-resolve]/lib/Resolver.js:110:4
108 |   
109 |   - createInnerCallback.js:21 loggingCallbackWrapper
110 |     [m6]/[enhanced-resolve]/lib/createInnerCallback.js:21:19  
111 |   - Tapable.js:134 
112 |     [m6]/[tapable]/lib/Tapable.js:134:6
113 |   
114 |   - DirectoryDescriptionFilePlugin.js:24 Tapable.    [m6]/[enhanced-resolve]/lib/DirectoryDescriptionFilePlug    in.js:24:12
115 |   
116 |   - CachedInputFileSystem.js:38 Storage.finished
117 |     [m6]/[enhanced-resolve]/lib/CachedInputFileSystem.js:38:    16
118 |   
119 |   - graceful-fs.js:78 ReadFileContext.callback
120 |     [m6]/[graceful-fs]/graceful-fs.js:78:16
121 |   
122 | 
--------------------------------------------------------------------------------